diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e680c9e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,137 @@ +# CLAUDE.md + +## Project Overview + +**Zero-State** is a Chrome Extension (Manifest V3) that replaces the New Tab page with a customizable link and notes organizer. Users can create hierarchical lists of links and notes, organized as trees. Built with TypeScript and VanJS. + +- **Chrome Web Store**: [zero-state](https://chromewebstore.google.com/detail/zero-state/diloncejoolaamlldicgmhimamhecipj) +- **Repository**: https://github.com/nmai/zero-state + +## Build & Development Commands + +```bash +npm run build # Bundle with esbuild → dist/app.js (with sourcemaps) +npm run build-min # Bundle minified → dist/app.js +npm run watch # Watch mode for development +npm run pkg # Copy manifest, static assets, and dist into pkg/ for publishing +``` + +There is no test suite, linter, or CI/CD pipeline configured. + +## Architecture + +### Tech Stack + +- **TypeScript** 5.8 with strict mode, targeting ES2019 +- **VanJS** 1.5.3 — ultralight (~5kb) reactive UI framework +- **esbuild** — bundler +- **Chrome APIs** — `chrome.storage.sync`, `chrome.permissions`, `chrome.runtime` + +### Source Layout + +``` +ts/ +├── app.ts # Entry point: initialization, keyboard shortcuts, top-level render +├── core/ +│ ├── types.ts # Interfaces (LinkNodeFlat, LinkNode, Settings) and FaviconProvider enum +│ ├── constants.ts # Storage keys, DOM classes, text icons, SVG loader, default settings +│ └── van.ts # VanJS re-exports (dependency isolation) +├── state/ +│ ├── ui.state.ts # UI-only state (editMode, settingsMode, editingNode, footerMessages) +│ └── data.state.ts # Persistent data state (rawList, names, root tree, settings) +├── services/ +│ ├── command.service.ts # Orchestrates state mutations + persistence (the main action layer) +│ ├── storage.service.ts # Chrome storage sync API wrapper (load/save) +│ ├── tree.service.ts # Builds tree structure from flat node array +│ ├── favicon.service.ts # Favicon provider management and caching +│ ├── theme.service.ts # Theme application (light/dark/system) +│ └── validator.service.ts # URL validation +└── components/ + ├── tree.component.ts # Tree rendering, node content, move/delete buttons + ├── edit-form.component.ts # Add/edit form with validation + ├── settings.component.ts # Settings modal + └── footer.component.ts # Footer with messaging/prompts +``` + +### Static Assets + +``` +static/ +├── index.html # Single-page entry point +├── css/ +│ ├── variables.css # Theme CSS custom properties (light/dark) +│ ├── base.css # Body, links, utility classes +│ ├── layout.css # Flexbox layout (row, col, main-content) +│ ├── buttons.css # Toggle, move, edit, close buttons +│ ├── tree.css # Tree lines, node styles, favicons, editable highlights +│ ├── modal.css # Settings modal and overlay +│ ├── forms.css # Edit form inputs, selects, actions +│ └── footer.css # Fixed footer bar +├── icons/ +│ ├── svg/ # SVG icon files (edit, close, settings, link) +│ └── *.png # Extension icons at various sizes +└── json/ + └── initial-data-2.0.0.json # Default data for new installs +``` + +### Data Flow + +``` +Chrome Storage ↔ StorageService ↔ CommandService ↔ DataState/UIState ↔ Components ↔ DOM +``` + +- Data is stored as a **flat array** of `LinkNodeFlat` objects with parent references +- `TreeService.buildTree()` converts flat data into a nested `LinkNode` tree for rendering +- `CommandService` is the single orchestration layer — components call it instead of directly touching storage or rebuilding the tree +- `UIState` holds transient UI state (edit mode, modals); `DataState` holds persistent data (list, settings) +- Changes sync across devices via `chrome.storage.sync` + +### Chrome Storage Constraints + +See `notes.md` for details. Key limits: +- 512 items max +- 100kb total across all keys +- 8kb per key +- All node data is stored under a single key (`links-v1`) + +**Important**: The `FaviconProvider` enum string values (`'chrome'`, `'duck'`, `'gen'`, `'none'`) are persisted in storage. Never change these values — it would break settings for existing users. + +## Code Conventions + +### Naming + +- **PascalCase** for classes: `StorageService`, `EditForm`, `TreeComponent` +- **camelCase** for methods and variables +- **UPPER_SNAKE_CASE** for constants: `CURRENT_LIST_VERSION`, `DOM_CLASSES` + +### File Naming + +- `.service.ts` — services (data/logic/orchestration layer) +- `.component.ts` — UI components +- `.state.ts` — state management + +### Patterns + +- Service classes use **static methods** (no instantiation) +- Components call `CommandService` for any action that mutates state + persists (never call `StorageService` directly from a component) +- Immutable state updates (spread operator for arrays/objects) +- `nameToIndexMap` cache in `DataState` for O(1) lookups by item name +- Optimistic updates with revert-on-failure pattern in `CommandService` +- SVG icons are loaded from files at init time via `loadSvgIcons()`, stored in `SVG_ICONS` map + +### CSS + +- CSS custom properties for theming (defined in `variables.css`) +- `light-dark()` CSS function with `prefers-color-scheme` media query +- Lowercase hyphenated class names (`tree-list`, `edit-node-btn`) +- One CSS file per concern (variables, layout, buttons, tree, modal, forms, footer) + +## Key Features in Code + +- **Tree visualization**: hierarchical lists with parent-child tree lines +- **Edit mode**: toggled via `[+]` button or backtick key +- **Task completion**: right-click to strikethrough (optional setting) +- **Favicon support**: multiple providers (Chrome cache, DuckDuckGo, generic icon) +- **Theme support**: light, dark, or system preference +- **Keyboard shortcuts**: Escape (close/exit), backtick (toggle edit mode) +- **Cross-device sync**: via Chrome storage sync API when user is signed in diff --git a/package-lock.json b/package-lock.json index 275fa3e..814b61b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "zero-state", - "version": "1.0.0", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "zero-state", - "version": "1.0.0", + "version": "2.0.0", "license": "ISC", "dependencies": { "typescript": "^5.8.2", diff --git a/static/css/base.css b/static/css/base.css new file mode 100644 index 0000000..41a5e57 --- /dev/null +++ b/static/css/base.css @@ -0,0 +1,18 @@ +/* ===== BASE STYLES ===== */ +body { + color: var(--text-color); + background-color: var(--background-color); + font-size: 14px; + margin-left: 0; + margin-right: 0; +} + +a { + text-decoration: none; + color: var(--link-color); +} + +/* ===== UTILITY CLASSES ===== */ +.display-none { + display: none; +} diff --git a/static/css/buttons.css b/static/css/buttons.css new file mode 100644 index 0000000..7335fcc --- /dev/null +++ b/static/css/buttons.css @@ -0,0 +1,91 @@ +/* ===== TOGGLE & SETTINGS BUTTONS ===== */ +#toggle-form-btn { + float: right; +} + +#toggle-form-btn:hover, #settings-btn:hover { + color: var(--primary-button-hover-color); + transform: translateY(-1px); +} + +#toggle-form-btn:active, #settings-btn:active { + transform: translateY(0); +} + +/* ===== MOVE BUTTONS ===== */ +.move-controls { + display: inline-block; + margin-right: 5px; + vertical-align: middle; +} + +.move-btn { + display: block; + text-align: center; + width: 16px; + height: 16px; + line-height: 14px; + font-size: 12px; + margin: 1px 0; + border: 1px solid var(--border-color); + border-radius: 2px; + background-color: var(--secondary-bg-color); +} + +.move-btn:hover { + background-color: var(--hover-bg-color); +} + +.move-up { + margin-bottom: 0; +} + +.move-down { + margin-top: 0; +} + +/* ===== NODE EDIT BUTTON ===== */ +.edit-node-btn { + margin-left: 8px; + display: inline-flex; + align-items: center; + opacity: 0.7; + transition: opacity 0.2s, transform 0.1s; +} + +.edit-node-btn:hover { + opacity: 1; + transform: translateY(-1px); +} + +/* ===== CLOSE BUTTONS ===== */ +.close-settings-btn { + display: flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + border-radius: 50%; + background-color: var(--secondary-bg-color); + transition: background-color 0.2s; +} + +.close-settings-btn:hover { + background-color: var(--hover-bg-color); +} + +.close-form-btn { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 50%; + background-color: var(--hover-bg-color); + transition: background-color 0.2s, transform 0.1s; +} + +.close-form-btn:hover { + background-color: var(--border-color); + transform: translateY(-1px); +} diff --git a/static/css/custom.css b/static/css/custom.css deleted file mode 100644 index 922d01c..0000000 --- a/static/css/custom.css +++ /dev/null @@ -1,570 +0,0 @@ -/* ===== THEME VARIABLES ===== */ -:root { - color-scheme: light dark; - - /* Base colors */ - --background-color: white; - --text-color: #24292e; - --link-color: #0366d6; - - /* UI elements */ - --primary-button-color: #4285f4; - --primary-button-hover-color: #1a73e8; - --border-color: #d1d5da; - --secondary-bg-color: #f6f8fa; - --hover-bg-color: #e1e4e8; - --footer-link-color: rgba(0,0,0,.5); -} - -/* Dark mode variables */ -@media (prefers-color-scheme: dark) { - :root { - /* Base colors */ - --background-color: #101010; - /* --background-color: #000000; */ - --text-color: #bdc1c6; - --link-color: #4D99FA; - - /* UI elements */ - --primary-button-color: #8ab4f8; - --primary-button-hover-color: #aecbfa; - --border-color: #313438; - --secondary-bg-color: #202124; - --hover-bg-color: #303134; - --footer-link-color: rgba(255,255,255,.5); - } -} - -/* Explicit theme classes */ -.theme-light { - /* Base colors */ - --background-color: white; - --text-color: #24292e; - --link-color: #0366d6; - - /* UI elements */ - --primary-button-color: #4285f4; - --primary-button-hover-color: #1a73e8; - --border-color: #d1d5da; - --secondary-bg-color: #f6f8fa; - --hover-bg-color: #e1e4e8; - --footer-link-color: rgba(0,0,0,.5); -} - -.theme-dark { - /* Base colors */ - --background-color: #101010; - --text-color: #bdc1c6; - --link-color: #4D99FA; - - /* UI elements */ - --primary-button-color: #8ab4f8; - --primary-button-hover-color: #aecbfa; - --border-color: #313438; - --secondary-bg-color: #202124; - --hover-bg-color: #303134; - --footer-link-color: rgba(255,255,255,.5); -} - -/* ===== BASE STYLES ===== */ -body { - color: var(--text-color); - background-color: var(--background-color); - font-size: 14px; - margin-left: 0; - margin-right: 0; -} - -a { - text-decoration: none; - color: var(--link-color); -} - -/* ===== LAYOUT COMPONENTS ===== */ -.container { - margin: 0 auto; - max-width: 800px; -} - -.row { - display: flex; - min-height: calc(100vh - 80px); /* Account for footer height + some margin */ -} - -.main-content { - flex-grow: 1; - display: flex; - align-items: flex-start; -} - -.row-side-panel { - flex-grow: 0; -} - -.col, .col-md-3, .col-md-9 { - float: left; - position: relative; - min-height: 1px; - padding: 0 15px; -} - -.col { - max-width: 400px; -} - -.col-md-3 { - width: 25%; -} - -.col-md-9 { - width: 75%; -} - -/* ===== BUTTON COMPONENTS ===== */ -.button-group { - display: flex; - flex-direction: column; - gap: 10px; - margin-bottom: 16px; -} - -/* #toggle-form-btn, #settings-btn { - display: flex; - justify-content: center; - align-items: center; - width: 28px; - height: 28px; - background-color: transparent; - border-radius: 4px; - text-decoration: none; - color: var(--primary-button-color); - transition: color 0.2s, transform 0.1s; -} */ - -#toggle-form-btn { - float: right; -} - -#toggle-form-btn:hover, #settings-btn:hover { - color: var(--primary-button-hover-color); - transform: translateY(-1px); -} - -#toggle-form-btn:active, #settings-btn:active { - transform: translateY(0); -} - -/* Move buttons */ -.move-controls { - display: inline-block; - margin-right: 5px; - vertical-align: middle; -} - -.move-btn { - display: block; - text-align: center; - width: 16px; - height: 16px; - line-height: 14px; - font-size: 12px; - margin: 1px 0; - border: 1px solid var(--border-color); - border-radius: 2px; - background-color: var(--secondary-bg-color); -} - -.move-btn:hover { - background-color: var(--hover-bg-color); -} - -.move-up { - margin-bottom: 0; -} - -.move-down { - margin-top: 0; -} - -/* ===== TEXT STYLING ===== */ -.text-parent { - font-size: 16px; - font-weight: 600; -} - -.text-child { - font-weight: normal; - font-size: 14px; -} - -.text-linethrough { - text-decoration: line-through; -} - -/* ===== COMPONENT SPECIFIC STYLES ===== */ -/* Favicon */ -.favicon { - max-height: 18px; - float: left; - padding: 5px; - padding-right: 6px; -} - -.border-effect { - filter: - drop-shadow(1px 0 0 white) - drop-shadow(0 1px 0 white) - drop-shadow(-1px 0 0 white) - drop-shadow(0 -1px 0 white); -} - -/* -.old-favicon-container { - background-color: light-dark(white, #bdc1c6); - border: 1px solid #9aa0a6; - border-radius: 50%; - display: inline-flex; - justify-content: center; - align-items: center; - height: 1.8em; - width: 1.8em; - margin-right: 0.3em; - flex-shrink: 0; - vertical-align: middle; -} -*/ - -#overlay-container { - position: fixed; - width: 100%; - z-index: 1000; -} - -/* ===== ANIMATIONS ===== */ -@keyframes fadeInUp { - from { - opacity: 0; - transform: translateY(20px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - -/* Settings modal */ -.modal { - display: flex; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.5); - justify-content: center; - align-items: center; -} - -/* Settings page */ -.settings-page { - position: relative; - width: 100%; - max-width: 500px; - margin: 20px; - padding: 20px; - background-color: var(--background-color); - border: 1px solid var(--border-color); - border-radius: 8px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); - /* animation: fadeIn 0.2s ease-out; */ -} - -.settings-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 20px; - padding-bottom: 10px; - border-bottom: 1px solid var(--border-color); -} - -.settings-group { - margin-bottom: 24px; -} - -.settings-group h3 { - margin-bottom: 12px; -} - -.setting-item { - margin-left: 10px; -} - -.setting-description { - font-size: 0.9em; - color: var(--footer-link-color); - margin-top: 5px; - margin-bottom: 15px; -} - -.select-wrapper { - position: relative; - width: 100%; - margin-bottom: 10px; -} - -.select-wrapper select { - width: 100%; - padding: 8px 10px; - border: 1px solid var(--border-color); - border-radius: 4px; - background-color: var(--background-color); - color: var(--text-color); - box-sizing: border-box; - appearance: none; - cursor: pointer; -} - -.select-wrapper::after { - /* content: "▼"; */ - position: absolute; - right: 10px; - top: 50%; - transform: translateY(-50%); - pointer-events: none; - font-size: 12px; - color: var(--text-color); -} - -.radio-group { - display: flex; - flex-direction: column; - gap: 8px; -} - -.close-settings-btn { - display: flex; - align-items: center; - justify-content: center; - width: 30px; - height: 30px; - border-radius: 50%; - background-color: var(--secondary-bg-color); - transition: background-color 0.2s; -} - -.close-settings-btn:hover { - background-color: var(--hover-bg-color); -} - -/* Footer */ -.footer { - position: fixed; - bottom: 0; - left: 0; - width: 100%; - display: flex; - /* justify-content: flex-end; */ - justify-content: space-between; - align-items: center; - height: 30px; - font-size: 12px; - /* border-top: 1px solid var(--border-color); */ - z-index: 100; -} - -.footer a { - color: var(--footer-link-color); - transition: color 0.2s, transform 0.1s; - padding: 5px 5px; -} - -.footer a:hover { - color: var(--primary-button-color); - transform: translateY(-1px); -} - -/* ===== UTILITY CLASSES ===== */ -.display-none { - display: none; -} - -/* Form controls in settings */ -.settings-page input[type="checkbox"] { - margin-right: 8px; - cursor: pointer; - width: 16px; - height: 16px; - vertical-align: middle; -} - -.settings-page label { - display: flex; - align-items: center; - cursor: pointer; - margin-bottom: 5px; -} - -.settings-page input[type="radio"] { - margin-right: 8px; - cursor: pointer; - width: 16px; - height: 16px; - vertical-align: middle; -} - -.settings-page h2 { - color: var(--text-color); - font-size: 1.5em; - font-weight: 500; - margin-top: 0; -} - -.settings-page h3 { - color: var(--text-color); - font-size: 1.2em; - font-weight: 500; -} - -/* Node edit button */ -.edit-node-btn { - margin-left: 8px; - display: inline-flex; - align-items: center; - opacity: 0.7; - transition: opacity 0.2s, transform 0.1s; -} - -.edit-node-btn:hover { - opacity: 1; - transform: translateY(-1px); -} - -/* Editable node styling */ -.editable-node { - position: relative; - padding: 2px 4px; - margin: -2px -4px; - border-radius: 3px; - transition: background-color 0.2s; - background-color: rgba(255, 235, 59, 0.25); /* Brighter yellow with opacity */ - border: 1px dashed rgba(255, 215, 0, 0.5); /* Gold border for better contrast */ -} - -.editable-node:hover { - background-color: rgba(255, 235, 59, 0.4); /* More intense yellow on hover */ - cursor: pointer; -} - -.editable-node:hover::after { - /* content: "✎"; */ - position: absolute; - right: -20px; - top: 50%; - transform: translateY(-50%); - font-size: 12px; - opacity: 0.7; - color: var(--primary-button-color); -} - -/* Button group behavior in edit mode */ -.row-side-panel .button-group { - display: flex; - flex-direction: column; - gap: 10px; - margin-bottom: 16px; -} - -.form-header { - font-size: 16px; - font-weight: 500; - margin-bottom: 15px; - color: var(--text-color); - display: flex; - justify-content: space-between; - align-items: center; - padding-bottom: 10px; - border-bottom: 1px solid var(--border-color); -} - -.close-form-btn { - display: flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - border-radius: 50%; - background-color: var(--hover-bg-color); - transition: background-color 0.2s, transform 0.1s; -} - -.close-form-btn:hover { - background-color: var(--border-color); - transform: translateY(-1px); -} - -.form-actions { - display: flex; - gap: 10px; - margin-top: 10px; -} - -#newlink-form input[type="text"] { - width: 100%; - padding: 8px 10px; - border: 1px solid var(--border-color); - border-radius: 4px; - margin-bottom: 10px; - background-color: var(--background-color); - color: var(--text-color); - box-sizing: border-box; -} - -#newlink-form input[type="submit"], -#newlink-form input[type="button"] { - padding: 6px 12px; - border: none; - border-radius: 4px; - cursor: pointer; - transition: background-color 0.2s; -} - -#newlink-form input[type="submit"] { - background-color: transparent; - color: var(--primary-button-color); - border: 1px solid var(--primary-button-color); - font-weight: 500; -} - -#newlink-form input[type="submit"]:hover { - background-color: rgba(66, 133, 244, 0.1); - color: var(--primary-button-hover-color); -} - -#newlink-form input[type="button"] { - background-color: var(--hover-bg-color); - color: var(--text-color); -} - -#newlink-form input[type="button"]:hover { - background-color: var(--border-color); -} - -.form-hint { - margin-top: 15px; - font-size: 12px; - color: var(--footer-link-color); - font-style: italic; - /* text-align: center; */ - padding: 5px; - border-top: 1px solid var(--border-color); -} diff --git a/static/css/footer.css b/static/css/footer.css new file mode 100644 index 0000000..24510dd --- /dev/null +++ b/static/css/footer.css @@ -0,0 +1,24 @@ +/* ===== FOOTER ===== */ +.footer { + position: fixed; + bottom: 0; + left: 0; + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + height: 30px; + font-size: 12px; + z-index: 100; +} + +.footer a { + color: var(--footer-link-color); + transition: color 0.2s, transform 0.1s; + padding: 5px 5px; +} + +.footer a:hover { + color: var(--primary-button-color); + transform: translateY(-1px); +} diff --git a/static/css/forms.css b/static/css/forms.css new file mode 100644 index 0000000..8b8938a --- /dev/null +++ b/static/css/forms.css @@ -0,0 +1,88 @@ +/* ===== FORM LAYOUT ===== */ +.form-header { + font-size: 16px; + font-weight: 500; + margin-bottom: 15px; + color: var(--text-color); + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: 10px; + border-bottom: 1px solid var(--border-color); +} + +.form-actions { + display: flex; + gap: 10px; + margin-top: 10px; +} + +.form-hint { + margin-top: 15px; + font-size: 12px; + color: var(--footer-link-color); + font-style: italic; + padding: 5px; + border-top: 1px solid var(--border-color); +} + +/* ===== FORM INPUTS ===== */ +#newlink-form input[type="text"] { + width: 100%; + padding: 8px 10px; + border: 1px solid var(--border-color); + border-radius: 4px; + margin-bottom: 10px; + background-color: var(--background-color); + color: var(--text-color); + box-sizing: border-box; +} + +#newlink-form input[type="submit"], +#newlink-form input[type="button"] { + padding: 6px 12px; + border: none; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.2s; +} + +#newlink-form input[type="submit"] { + background-color: transparent; + color: var(--primary-button-color); + border: 1px solid var(--primary-button-color); + font-weight: 500; +} + +#newlink-form input[type="submit"]:hover { + background-color: rgba(66, 133, 244, 0.1); + color: var(--primary-button-hover-color); +} + +#newlink-form input[type="button"] { + background-color: var(--hover-bg-color); + color: var(--text-color); +} + +#newlink-form input[type="button"]:hover { + background-color: var(--border-color); +} + +/* ===== SELECT DROPDOWNS ===== */ +.select-wrapper { + position: relative; + width: 100%; + margin-bottom: 10px; +} + +.select-wrapper select { + width: 100%; + padding: 8px 10px; + border: 1px solid var(--border-color); + border-radius: 4px; + background-color: var(--background-color); + color: var(--text-color); + box-sizing: border-box; + appearance: none; + cursor: pointer; +} diff --git a/static/css/layout.css b/static/css/layout.css new file mode 100644 index 0000000..9a364bc --- /dev/null +++ b/static/css/layout.css @@ -0,0 +1,23 @@ +/* ===== LAYOUT ===== */ +.row { + display: flex; + min-height: calc(100vh - 80px); +} + +.main-content { + flex-grow: 1; + display: flex; + align-items: flex-start; +} + +.row-side-panel { + flex-grow: 0; +} + +.col { + float: left; + position: relative; + min-height: 1px; + padding: 0 15px; + max-width: 400px; +} diff --git a/static/css/modal.css b/static/css/modal.css new file mode 100644 index 0000000..3b53097 --- /dev/null +++ b/static/css/modal.css @@ -0,0 +1,94 @@ +/* ===== MODAL ===== */ +.modal { + display: flex; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + justify-content: center; + align-items: center; +} + +#overlay-container { + position: fixed; + width: 100%; + z-index: 1000; +} + +/* ===== SETTINGS PAGE ===== */ +.settings-page { + position: relative; + width: 100%; + max-width: 500px; + margin: 20px; + padding: 20px; + background-color: var(--background-color); + border: 1px solid var(--border-color); + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); +} + +.settings-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + padding-bottom: 10px; + border-bottom: 1px solid var(--border-color); +} + +.settings-group { + margin-bottom: 24px; +} + +.settings-group h3 { + margin-bottom: 12px; +} + +.setting-item { + margin-left: 10px; +} + +.setting-description { + font-size: 0.9em; + color: var(--footer-link-color); + margin-top: 5px; + margin-bottom: 15px; +} + +.radio-group { + display: flex; + flex-direction: column; + gap: 8px; +} + +.settings-page input[type="checkbox"], +.settings-page input[type="radio"] { + margin-right: 8px; + cursor: pointer; + width: 16px; + height: 16px; + vertical-align: middle; +} + +.settings-page label { + display: flex; + align-items: center; + cursor: pointer; + margin-bottom: 5px; +} + +.settings-page h2 { + color: var(--text-color); + font-size: 1.5em; + font-weight: 500; + margin-top: 0; +} + +.settings-page h3 { + color: var(--text-color); + font-size: 1.2em; + font-weight: 500; +} diff --git a/static/css/tree-list.css b/static/css/tree-list.css deleted file mode 100644 index a4b319a..0000000 --- a/static/css/tree-list.css +++ /dev/null @@ -1,65 +0,0 @@ - -.tree-wrapper { - padding-top: 10px; -} -.tree-list { - list-style: none; - padding: 0; - margin: 0; -} -.tree-list .tree-item { - position: relative; - display: block; - min-height: 2em; - line-height: 2em; - margin-bottom: 10px; - padding-left: 21px; -} -.tree-list .tree-item:before, .tree-list .tree-item:after { - content: ''; - position: absolute; - display: block; - background-color: light-dark(#d1d5da,#313438); -} -.tree-list .tree-item:before { - top: 0; - left: 10px; - width: 1px; - height: calc(100% + 10px); -} -.tree-list .tree-item:after { - top: 1em; - left: 10px; - width: 11px; - height: 1px; -} -.tree-list .tree-item:last-child:not(:first-child) { - margin-bottom: 0; -} -.tree-list .tree-item:last-child:before { - height: 1em; -} -.tree-list .tree-item:first-child:before { - top: -10px; - /* height: calc(50% + 10px); */ - height: 25px; -} -.tree-list .tree-item:first-child:not(:last-child):before { - top: -10px; - height: calc(100% + 20px); -} -.tree-list .tree-item:last-child:not(:first-child):before { - height: 1em; -} -.tree-list .tree-item > span { - display: inline-block; - padding: 0 5px; - border: 1px solid light-dark(#d1d5da,#313438); -} -.tree-list .tree-item > span:hover { - background-color: lightgray; -} -.tree-list .tree-item > .tree-list { - padding-top: 10px; -} - diff --git a/static/css/tree.css b/static/css/tree.css new file mode 100644 index 0000000..bfb825c --- /dev/null +++ b/static/css/tree.css @@ -0,0 +1,120 @@ +/* ===== TREE LIST ===== */ +.tree-list { + list-style: none; + padding: 0; + margin: 0; +} + +.tree-list .tree-item { + position: relative; + display: block; + min-height: 2em; + line-height: 2em; + margin-bottom: 10px; + padding-left: 21px; +} + +.tree-list .tree-item:before, +.tree-list .tree-item:after { + content: ''; + position: absolute; + display: block; + background-color: light-dark(#d1d5da, #313438); +} + +.tree-list .tree-item:before { + top: 0; + left: 10px; + width: 1px; + height: calc(100% + 10px); +} + +.tree-list .tree-item:after { + top: 1em; + left: 10px; + width: 11px; + height: 1px; +} + +.tree-list .tree-item:last-child:not(:first-child) { + margin-bottom: 0; +} + +.tree-list .tree-item:last-child:before { + height: 1em; +} + +.tree-list .tree-item:first-child:before { + top: -10px; + height: 25px; +} + +.tree-list .tree-item:first-child:not(:last-child):before { + top: -10px; + height: calc(100% + 20px); +} + +.tree-list .tree-item:last-child:not(:first-child):before { + height: 1em; +} + +.tree-list .tree-item > span { + display: inline-block; + padding: 0 5px; + border: 1px solid light-dark(#d1d5da, #313438); +} + +.tree-list .tree-item > span:hover { + background-color: var(--hover-bg-color); +} + +.tree-list .tree-item > .tree-list { + padding-top: 10px; +} + +/* ===== TEXT STYLING ===== */ +.text-parent { + font-size: 16px; + font-weight: 600; +} + +.text-child { + font-weight: normal; + font-size: 14px; +} + +.text-linethrough { + text-decoration: line-through; +} + +/* ===== EDITABLE NODE ===== */ +.editable-node { + position: relative; + padding: 2px 4px; + margin: -2px -4px; + border-radius: 3px; + transition: background-color 0.2s; + background-color: rgba(255, 235, 59, 0.25); + border: 1px dashed rgba(255, 215, 0, 0.5); +} + +.editable-node:hover { + background-color: rgba(255, 235, 59, 0.4); + cursor: pointer; +} + +/* ===== FAVICON ===== */ +.favicon { + max-height: 18px; + float: left; + padding: 5px; + padding-right: 6px; +} + +.border-effect { + filter: + drop-shadow(1px 0 0 white) + drop-shadow(0 1px 0 white) + drop-shadow(-1px 0 0 white) + drop-shadow(0 -1px 0 white); +} diff --git a/static/css/variables.css b/static/css/variables.css new file mode 100644 index 0000000..5103c8f --- /dev/null +++ b/static/css/variables.css @@ -0,0 +1,60 @@ +/* ===== THEME VARIABLES ===== */ +:root { + color-scheme: light dark; + + /* Base colors */ + --background-color: white; + --text-color: #24292e; + --link-color: #0366d6; + + /* UI elements */ + --primary-button-color: #4285f4; + --primary-button-hover-color: #1a73e8; + --border-color: #d1d5da; + --secondary-bg-color: #f6f8fa; + --hover-bg-color: #e1e4e8; + --footer-link-color: rgba(0,0,0,.5); +} + +/* Dark mode variables */ +@media (prefers-color-scheme: dark) { + :root { + --background-color: #101010; + --text-color: #bdc1c6; + --link-color: #4D99FA; + + --primary-button-color: #8ab4f8; + --primary-button-hover-color: #aecbfa; + --border-color: #313438; + --secondary-bg-color: #202124; + --hover-bg-color: #303134; + --footer-link-color: rgba(255,255,255,.5); + } +} + +/* Explicit theme classes */ +.theme-light { + --background-color: white; + --text-color: #24292e; + --link-color: #0366d6; + + --primary-button-color: #4285f4; + --primary-button-hover-color: #1a73e8; + --border-color: #d1d5da; + --secondary-bg-color: #f6f8fa; + --hover-bg-color: #e1e4e8; + --footer-link-color: rgba(0,0,0,.5); +} + +.theme-dark { + --background-color: #101010; + --text-color: #bdc1c6; + --link-color: #4D99FA; + + --primary-button-color: #8ab4f8; + --primary-button-hover-color: #aecbfa; + --border-color: #313438; + --secondary-bg-color: #202124; + --hover-bg-color: #303134; + --footer-link-color: rgba(255,255,255,.5); +} diff --git a/static/icons/svg/close.svg b/static/icons/svg/close.svg new file mode 100644 index 0000000..24a1feb --- /dev/null +++ b/static/icons/svg/close.svg @@ -0,0 +1,4 @@ + diff --git a/static/icons/svg/edit.svg b/static/icons/svg/edit.svg new file mode 100644 index 0000000..756a1bb --- /dev/null +++ b/static/icons/svg/edit.svg @@ -0,0 +1,3 @@ + diff --git a/static/icons/svg/link.svg b/static/icons/svg/link.svg new file mode 100644 index 0000000..427b7cf --- /dev/null +++ b/static/icons/svg/link.svg @@ -0,0 +1,4 @@ + diff --git a/static/icons/svg/settings.svg b/static/icons/svg/settings.svg new file mode 100644 index 0000000..9c67616 --- /dev/null +++ b/static/icons/svg/settings.svg @@ -0,0 +1,4 @@ + diff --git a/static/index.html b/static/index.html index ed022f1..a49b2ca 100644 --- a/static/index.html +++ b/static/index.html @@ -2,12 +2,16 @@