Feat v0.6.12 css isolation (#47)
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-frontend (push) Successful in 6s
CI/CD / test-go-pg (push) Successful in 2m44s
CI/CD / test-sqlite (push) Successful in 2m47s
CI/CD / build-and-deploy (push) Successful in 1m46s

Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #47.
This commit is contained in:
2026-04-01 11:58:39 +00:00
committed by xcaliber
parent 786bc92768
commit 221ae94f4f
33 changed files with 1385 additions and 1074 deletions

View File

@@ -2,6 +2,37 @@
All notable changes to Armature are documented here. All notable changes to Armature are documented here.
## v0.6.12 — Extension CSS Isolation
Prefix enforcement prevents extension CSS from leaking into the kernel or
sibling extensions. All 12 in-tree packages migrated to `.ext-{slug}-*`
naming convention.
### Added
- **`data-ext` attribute** on extension mount container — enables scoped
selectors like `[data-ext="chat"] .ext-chat-app`.
- **CSS linter** (`scripts/lint-package-css.sh`) — validates that the first
class selector in every extension CSS rule starts with `.ext-{slug}`.
Exempts `:root`, `@keyframes`, `@font-face`, `@media`, kernel `.sw-*`
classes, and CodeMirror `.cm-*` classes.
- **Kernel CSS contract** (`docs/EXTENSION-CSS.md`) — documents stable
public classes and CSS variables that extensions may reference. Everything
else is internal kernel CSS.
### Changed
- **12 packages migrated** — all class selectors renamed to `.ext-{slug}-*`:
chat, dashboard, editor, git-board, hello-dashboard, icd-test-runner,
notes, schedules, sdk-test-runner, tasks, team-activity-log, workflow-demo.
CSS and JS files updated in lockstep.
- **`icd-test-runner`** — ID selectors (`#extension-mount`) converted to
class-based selectors with proper prefix.
- **`editor` cross-references** — compound selectors referencing notes
classes updated to new `.ext-notes-*` names.
- **`chat` kernel overrides** — `.sw-dialog:has(...)` override scoped under
`[data-ext="chat"]` instead of global.
## v0.6.11 — CSS Deduplication ## v0.6.11 — CSS Deduplication
One class per concept. The old `primitives.css` button, toast, popup-menu, One class per concept. The old `primitives.css` button, toast, popup-menu,

View File

@@ -63,16 +63,17 @@ Shipped. Old primitive system retired. One class per concept. See CHANGELOG.md.
| Mark deprecated classes | Any remaining old classes get a `/* DEPRECATED v0.6.11 — use .sw-btn--* */` comment and a 1-version grace period for package authors. | | Mark deprecated classes | Any remaining old classes get a `/* DEPRECATED v0.6.11 — use .sw-btn--* */` comment and a 1-version grace period for package authors. |
| Package CSS audit | Scan all `packages/*/css/main.css` for references to deprecated kernel classes. Fix in-tree packages. | | Package CSS audit | Scan all `packages/*/css/main.css` for references to deprecated kernel classes. Fix in-tree packages. |
### v0.6.12 — Extension CSS Isolation ### v0.6.12 — Extension CSS Isolation
Prevent extension CSS from leaking into the kernel or sibling extensions. Shipped. Prefix enforcement via linter. All 12 in-tree packages migrated.
See CHANGELOG.md.
| Step | Description | | Step | Description |
|------|-------------| |------|-------------|
| Scoping strategy | Two options: (A) `@scope (.extension-mount[data-ext="slug"])` — CSS `@scope` is supported in Chrome 118+, Firefox 128+, Safari 17.4+. (B) Prefix enforcement — package CSS linter rejects selectors that don't start with `.ext-{slug}` or `[data-ext="{slug}"]`. **Recommendation**: (B) prefix enforcement. `@scope` support is still patchy; prefix enforcement works everywhere and is trivially lintable. | | Scoping strategy | Prefix enforcement (option B). `@scope` support still patchy; prefix works everywhere, trivially lintable. |
| Linter | `scripts/lint-package-css.sh`shell script using `grep`/`awk`. Runs in CI. Accepts `.ext-{slug}` or `[data-ext="{slug}"]` prefixed selectors only. Exempts `:root`, `@keyframes`, `@font-face`, and `@media` wrappers. | | Linter | `scripts/lint-package-css.sh`validates first class selector in every rule starts with `.ext-{slug}`. Exempts `:root`, `@keyframes`, `@font-face`, `@media`, `.sw-*` kernel classes, `.cm-*` CodeMirror classes. |
| Kernel CSS contract | Document which kernel classes are stable public API for extensions to reference (`.sw-btn--*`, `.sw-input`, `.sw-field`, `.sw-dialog`, `.sw-toast`, `.sw-menu`, `.sw-tabs`, CSS variables). Everything else is internal. Publish in `docs/EXTENSION-CSS.md`. | | Kernel CSS contract | `docs/EXTENSION-CSS.md` — stable public classes (`.sw-btn--*`, `.sw-input`, `.sw-field`, `.sw-dialog`, `.sw-toast`, `.sw-menu`, `.sw-tabs`, etc.) and all CSS variables. Everything else internal. |
| Migrate in-tree packages | Add `data-ext` attribute to extension mount container. Prefix all in-tree package CSS. Verify no visual regressions. | | Migrate in-tree packages | `data-ext="{{.Surface}}"` on extension mount. All 12 packages prefixed to `.ext-{slug}-*`. CSS + JS updated in lockstep. |
### v0.6.13 — Responsive & Spacing ### v0.6.13 — Responsive & Spacing
@@ -120,7 +121,7 @@ v0.6.10 Viewport Foundation ✅ SHIPPED
v0.6.11 CSS Deduplication ✅ SHIPPED v0.6.11 CSS Deduplication ✅ SHIPPED
v0.6.12 Extension CSS Isolation Scoping requires the kernel CSS to be stable first v0.6.12 Extension CSS Isolation SHIPPED
v0.6.13 Responsive & Spacing ← Spacing tokens need stable class names to attach to v0.6.13 Responsive & Spacing ← Spacing tokens need stable class names to attach to

View File

@@ -1,6 +1,6 @@
# Armature — Roadmap # Armature — Roadmap
## Current: v0.6.11CSS Deduplication ## Current: v0.6.12Extension CSS Isolation
Self-hosted extensible platform. Auth, identity, packages, Starlark sandbox, Self-hosted extensible platform. Auth, identity, packages, Starlark sandbox,
storage, realtime, and ops are kernel primitives. Everything else is an extension. storage, realtime, and ops are kernel primitives. Everything else is an extension.

View File

@@ -1 +1 @@
0.6.11 0.6.12

149
docs/EXTENSION-CSS.md Normal file
View File

@@ -0,0 +1,149 @@
# Extension CSS Contract
> **Version**: v0.6.12 — Extension CSS Isolation
This document defines the CSS contract between the Armature kernel and extension
packages. Extensions **must** follow these rules; the kernel guarantees the listed
classes and variables are stable public API.
---
## Naming Rule
All class selectors in extension CSS (`packages/{slug}/css/main.css`) must start
with `.ext-{slug}-`. The `{slug}` is the package directory name.
```css
/* Good */
.ext-my-app-sidebar { ... }
.ext-my-app-card { ... }
/* Bad — will be rejected by the linter */
.sidebar { ... }
.my-sidebar { ... }
```
**Compound selectors**: Descendant classes scoped under your `.ext-{slug}` root
are allowed to reference kernel classes or state modifiers:
```css
/* Allowed — kernel class scoped under extension namespace */
.ext-my-app .sw-btn { margin-top: 8px; }
/* Allowed — state modifier on an extension element */
.ext-my-app-item.active { ... }
```
Run `bash scripts/lint-package-css.sh` to validate. The linter checks that the
**first** class selector in every rule starts with `.ext-{slug}`.
---
## Stable Kernel Classes
Extensions may reference these classes in compound selectors. They are part of
the public API and will not change without a major version bump.
### Components (from `sw-primitives.css`)
| Class pattern | Component |
|---------------|-----------|
| `.sw-btn`, `.sw-btn--{variant}`, `.sw-btn--{size}` | Buttons |
| `.sw-input` | Text inputs |
| `.sw-field`, `.sw-field__label`, `.sw-field__hint` | Form fields |
| `.sw-dialog`, `.sw-dialog__header`, `.sw-dialog__body`, `.sw-dialog__footer` | Dialogs |
| `.sw-toast`, `.sw-toast-container` | Toast notifications |
| `.sw-menu`, `.sw-menu-item` | Context menus |
| `.sw-tabs`, `.sw-tab-btn` | Tab strips |
| `.sw-dropdown` | Custom dropdowns |
| `.sw-spinner` | Loading spinners |
| `.sw-avatar` | User avatars |
| `.sw-drawer` | Slide-out drawers |
| `.sw-banner` | Banner bars |
| `.sw-tooltip` | Tooltips |
### Extension Mount
The extension surface container has a `data-ext` attribute set to the package
slug. Use this for scoping if needed:
```css
[data-ext="my-app"] .ext-my-app-sidebar { ... }
```
---
## Stable CSS Variables
All variables from `variables.css` are public API. Extensions should use these
instead of hardcoded colors to respect the user's theme.
### Colors
| Variable | Purpose |
|----------|---------|
| `--bg` | Page background |
| `--bg-secondary` | Secondary/darker background |
| `--bg-elevated` | Elevated surface background |
| `--bg-raised` | Raised card background |
| `--bg-surface` | Surface-level background |
| `--bg-hover` | Hover state background |
| `--bg-active` | Active/pressed state background |
| `--bg-code` | Code block background |
| `--text` | Primary text color |
| `--text-2` | Secondary text color |
| `--text-3` | Tertiary/muted text color |
| `--text-on-color` | Text on colored backgrounds |
| `--accent` | Primary accent color |
| `--accent-dim` | Dimmed accent for backgrounds |
| `--accent-hover` | Accent hover state |
| `--accent-light` | Light accent variant |
| `--border` | Default border color |
| `--border-light` | Light border variant |
| `--border-elevated` | Border for elevated surfaces |
| `--danger` | Error/destructive color |
| `--danger-dim` | Dimmed danger background |
| `--danger-light` | Light danger variant |
| `--success` | Success/positive color |
| `--success-dim` | Dimmed success background |
| `--success-light` | Light success variant |
| `--warning` | Warning/caution color |
| `--warning-dim` | Dimmed warning background |
| `--warning-light` | Light warning variant |
| `--purple` | Purple accent |
| `--purple-dim` | Dimmed purple background |
### Layout & Typography
| Variable | Purpose |
|----------|---------|
| `--font` | Primary font family |
| `--mono` | Monospace font family |
| `--radius` | Default border-radius (8px) |
| `--radius-lg` | Large border-radius (12px) |
| `--transition` | Default transition timing |
| `--shadow-lg` | Large elevation shadow |
| `--overlay` | Modal overlay color |
| `--glass` | Glassmorphism backdrop |
| `--input-bg` | Form input background |
| `--sidebar-w` | Sidebar width |
---
## What Is Internal
Everything not listed above is **internal kernel CSS** and may change between
minor versions. Extensions must not depend on:
- Kernel layout classes (`.admin-*`, `.surface-*`, `.sidebar`, etc.)
- Kernel CSS file load order
- Specific HTML structure of the shell or topbar
- Undocumented CSS variables
---
## Enforcement
The linter script `scripts/lint-package-css.sh` runs against all
`packages/*/css/main.css` files. It exits non-zero if any rule's first class
selector does not start with `.ext-{slug}`.

View File

@@ -9,7 +9,7 @@
/* ── Layout ─────────────────────────────── */ /* ── Layout ─────────────────────────────── */
.chat-app { .ext-chat-app {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
@@ -18,20 +18,20 @@
color: var(--text); color: var(--text);
} }
.chat-loading { .ext-chat-loading {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 100%; height: 100%;
} }
.chat-body { .ext-chat-body {
display: flex; display: flex;
flex: 1; flex: 1;
min-height: 0; min-height: 0;
} }
.chat-main { .ext-chat-main {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
@@ -40,7 +40,7 @@
/* ── Topbar extras ──────────────────────── */ /* ── Topbar extras ──────────────────────── */
.chat-topbar__thread-title { .ext-chat-topbar__thread-title {
font-weight: 600; font-weight: 600;
font-size: 14px; font-size: 14px;
margin-right: 8px; margin-right: 8px;
@@ -49,7 +49,7 @@
/* ── Sidebar ────────────────────────────── */ /* ── Sidebar ────────────────────────────── */
.chat-sidebar { .ext-chat-sidebar {
width: 280px; width: 280px;
min-width: 280px; min-width: 280px;
border-right: 1px solid var(--border); border-right: 1px solid var(--border);
@@ -58,7 +58,7 @@
background: var(--bg-secondary); background: var(--bg-secondary);
} }
.chat-sidebar__header { .ext-chat-sidebar__header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
@@ -66,46 +66,46 @@
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
} }
.chat-sidebar__title { .ext-chat-sidebar__title {
font-weight: 600; font-weight: 600;
font-size: 14px; font-size: 14px;
} }
.chat-sidebar__list { .ext-chat-sidebar__list {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
} }
.chat-sidebar__empty { .ext-chat-sidebar__empty {
padding: 24px 16px; padding: 24px 16px;
text-align: center; text-align: center;
color: var(--text-3); color: var(--text-3);
font-size: 13px; font-size: 13px;
} }
.chat-sidebar__item { .ext-chat-sidebar__item {
padding: 10px 16px; padding: 10px 16px;
cursor: pointer; cursor: pointer;
border-bottom: 1px solid var(--border-light); border-bottom: 1px solid var(--border-light);
transition: background 0.1s; transition: background 0.1s;
} }
.chat-sidebar__item:hover { .ext-chat-sidebar__item:hover {
background: var(--bg-hover); background: var(--bg-hover);
} }
.chat-sidebar__item--active { .ext-chat-sidebar__item--active {
background: var(--accent-dim); background: var(--accent-dim);
} }
.chat-sidebar__item-top { .ext-chat-sidebar__item-top {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: baseline; align-items: baseline;
margin-bottom: 2px; margin-bottom: 2px;
} }
.chat-sidebar__item-title { .ext-chat-sidebar__item-title {
font-weight: 600; font-weight: 600;
font-size: 13px; font-size: 13px;
white-space: nowrap; white-space: nowrap;
@@ -115,19 +115,19 @@
margin-right: 8px; margin-right: 8px;
} }
.chat-sidebar__item-time { .ext-chat-sidebar__item-time {
font-size: 11px; font-size: 11px;
color: var(--text-3); color: var(--text-3);
white-space: nowrap; white-space: nowrap;
} }
.chat-sidebar__item-bottom { .ext-chat-sidebar__item-bottom {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
} }
.chat-sidebar__item-preview { .ext-chat-sidebar__item-preview {
font-size: 12px; font-size: 12px;
color: var(--text-2); color: var(--text-2);
white-space: nowrap; white-space: nowrap;
@@ -136,7 +136,7 @@
flex: 1; flex: 1;
} }
.chat-sidebar__badge { .ext-chat-sidebar__badge {
background: var(--accent); background: var(--accent);
color: var(--text-on-color); color: var(--text-on-color);
font-size: 11px; font-size: 11px;
@@ -153,13 +153,13 @@
/* ── Sidebar Search ────────────────────── */ /* ── Sidebar Search ────────────────────── */
.chat-sidebar__search { .ext-chat-sidebar__search {
position: relative; position: relative;
padding: 8px 16px; padding: 8px 16px;
border-bottom: 1px solid var(--border-light); border-bottom: 1px solid var(--border-light);
} }
.chat-sidebar__search-input { .ext-chat-sidebar__search-input {
width: 100%; width: 100%;
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 6px; border-radius: 6px;
@@ -171,12 +171,12 @@
box-sizing: border-box; box-sizing: border-box;
} }
.chat-sidebar__search-input:focus { .ext-chat-sidebar__search-input:focus {
outline: none; outline: none;
border-color: var(--accent); border-color: var(--accent);
} }
.chat-sidebar__search-clear { .ext-chat-sidebar__search-clear {
position: absolute; position: absolute;
right: 22px; right: 22px;
top: 50%; top: 50%;
@@ -190,16 +190,16 @@
line-height: 1; line-height: 1;
} }
.chat-sidebar__search-clear:hover { .ext-chat-sidebar__search-clear:hover {
color: var(--text); color: var(--text);
} }
.chat-sidebar__search-results { .ext-chat-sidebar__search-results {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
} }
.chat-sidebar__search-section { .ext-chat-sidebar__search-section {
padding: 8px 16px 4px; padding: 8px 16px 4px;
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 600;
@@ -208,13 +208,13 @@
color: var(--text-3); color: var(--text-3);
} }
.chat-sidebar__search-loading { .ext-chat-sidebar__search-loading {
display: flex; display: flex;
justify-content: center; justify-content: center;
padding: 16px; padding: 16px;
} }
.chat-sidebar__item--search-msg .chat-sidebar__item-preview { .ext-chat-sidebar__item--search-msg .ext-chat-sidebar__item-preview {
font-size: 13px; font-size: 13px;
white-space: normal; white-space: normal;
display: -webkit-box; display: -webkit-box;
@@ -225,20 +225,20 @@
/* ── Message Thread ─────────────────────── */ /* ── Message Thread ─────────────────────── */
.chat-thread { .ext-chat-thread {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 0; min-height: 0;
} }
.chat-thread--empty { .ext-chat-thread--empty {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: var(--text-3); color: var(--text-3);
} }
.chat-thread__messages { .ext-chat-thread__messages {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding: 16px; padding: 16px;
@@ -247,19 +247,19 @@
gap: 4px; gap: 4px;
} }
.chat-thread__loading { .ext-chat-thread__loading {
display: flex; display: flex;
justify-content: center; justify-content: center;
padding: 24px; padding: 24px;
} }
.chat-thread__loading-more { .ext-chat-thread__loading-more {
display: flex; display: flex;
justify-content: center; justify-content: center;
padding: 8px; padding: 8px;
} }
.chat-thread__load-more { .ext-chat-thread__load-more {
align-self: center; align-self: center;
background: none; background: none;
border: 1px solid var(--border); border: 1px solid var(--border);
@@ -271,11 +271,11 @@
margin-bottom: 8px; margin-bottom: 8px;
} }
.chat-thread__load-more:hover { .ext-chat-thread__load-more:hover {
background: var(--bg-hover); background: var(--bg-hover);
} }
.chat-thread__typing { .ext-chat-thread__typing {
padding: 4px 16px 8px; padding: 4px 16px 8px;
font-size: 12px; font-size: 12px;
color: var(--text-3); color: var(--text-3);
@@ -284,7 +284,7 @@
/* ── Message Bubble ─────────────────────── */ /* ── Message Bubble ─────────────────────── */
.chat-msg { .ext-chat-msg {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
gap: 8px; gap: 8px;
@@ -292,44 +292,44 @@
position: relative; position: relative;
} }
.chat-msg--own { .ext-chat-msg--own {
flex-direction: row-reverse; flex-direction: row-reverse;
} }
.chat-msg--system { .ext-chat-msg--system {
justify-content: center; justify-content: center;
padding: 2px 0; padding: 2px 0;
} }
.chat-msg--system span { .ext-chat-msg--system span {
font-size: 12px; font-size: 12px;
color: var(--text-3); color: var(--text-3);
font-style: italic; font-style: italic;
} }
.chat-msg--deleted { .ext-chat-msg--deleted {
justify-content: center; justify-content: center;
padding: 2px 0; padding: 2px 0;
} }
.chat-msg--deleted em { .ext-chat-msg--deleted em {
font-size: 12px; font-size: 12px;
color: var(--text-3); color: var(--text-3);
} }
.chat-msg__body { .ext-chat-msg__body {
max-width: 65%; max-width: 65%;
background: var(--bg-raised); background: var(--bg-raised);
border-radius: 12px; border-radius: 12px;
padding: 8px 12px; padding: 8px 12px;
} }
.chat-msg--own .chat-msg__body { .ext-chat-msg--own .ext-chat-msg__body {
background: var(--accent); background: var(--accent);
color: var(--text-on-color); color: var(--text-on-color);
} }
.chat-msg__name { .ext-chat-msg__name {
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 600;
color: var(--text-2); color: var(--text-2);
@@ -337,42 +337,42 @@
margin-bottom: 2px; margin-bottom: 2px;
} }
.chat-msg__content { .ext-chat-msg__content {
font-size: 14px; font-size: 14px;
line-height: 1.4; line-height: 1.4;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-word; word-break: break-word;
} }
.chat-msg__meta { .ext-chat-msg__meta {
display: flex; display: flex;
gap: 6px; gap: 6px;
align-items: center; align-items: center;
margin-top: 2px; margin-top: 2px;
} }
.chat-msg__time { .ext-chat-msg__time {
font-size: 10px; font-size: 10px;
color: var(--text-3); color: var(--text-3);
} }
.chat-msg--own .chat-msg__time { .ext-chat-msg--own .ext-chat-msg__time {
color: rgba(255, 255, 255, 0.7); color: rgba(255, 255, 255, 0.7);
} }
.chat-msg__edited { .ext-chat-msg__edited {
font-size: 10px; font-size: 10px;
color: var(--text-3); color: var(--text-3);
font-style: italic; font-style: italic;
} }
.chat-msg--own .chat-msg__edited { .ext-chat-msg--own .ext-chat-msg__edited {
color: rgba(255, 255, 255, 0.7); color: rgba(255, 255, 255, 0.7);
} }
/* ── Message Actions ────────────────────── */ /* ── Message Actions ────────────────────── */
.chat-msg__actions { .ext-chat-msg__actions {
display: flex; display: flex;
gap: 2px; gap: 2px;
position: absolute; position: absolute;
@@ -385,12 +385,12 @@
padding: 2px; padding: 2px;
} }
.chat-msg--own .chat-msg__actions { .ext-chat-msg--own .ext-chat-msg__actions {
right: auto; right: auto;
left: 0; left: 0;
} }
.chat-msg__action { .ext-chat-msg__action {
background: none; background: none;
border: none; border: none;
padding: 4px 6px; padding: 4px 6px;
@@ -401,24 +401,24 @@
color: var(--text-2); color: var(--text-2);
} }
.chat-msg__action:hover { .ext-chat-msg__action:hover {
background: var(--bg-hover); background: var(--bg-hover);
} }
.chat-msg__action--danger:hover { .ext-chat-msg__action--danger:hover {
background: var(--danger-bg); background: var(--danger-bg);
color: var(--danger); color: var(--danger);
} }
/* ── Message Edit ───────────────────────── */ /* ── Message Edit ───────────────────────── */
.chat-msg__edit { .ext-chat-msg__edit {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 6px; gap: 6px;
} }
.chat-msg__edit-input { .ext-chat-msg__edit-input {
width: 100%; width: 100%;
min-width: 200px; min-width: 200px;
border: 1px solid var(--border); border: 1px solid var(--border);
@@ -431,7 +431,7 @@
color: var(--text); color: var(--text);
} }
.chat-msg__edit-actions { .ext-chat-msg__edit-actions {
display: flex; display: flex;
gap: 6px; gap: 6px;
justify-content: flex-end; justify-content: flex-end;
@@ -439,7 +439,7 @@
/* ── Compose Bar ────────────────────────── */ /* ── Compose Bar ────────────────────────── */
.chat-compose { .ext-chat-compose {
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
gap: 8px; gap: 8px;
@@ -448,7 +448,7 @@
background: var(--bg); background: var(--bg);
} }
.chat-compose__input { .ext-chat-compose__input {
flex: 1; flex: 1;
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 8px; border-radius: 8px;
@@ -462,7 +462,7 @@
color: var(--text); color: var(--text);
} }
.chat-compose__input:focus { .ext-chat-compose__input:focus {
outline: none; outline: none;
border-color: var(--accent); border-color: var(--accent);
box-shadow: 0 0 0 2px var(--accent-dim); box-shadow: 0 0 0 2px var(--accent-dim);
@@ -470,7 +470,7 @@
/* ── Participant Sidebar ────────────────── */ /* ── Participant Sidebar ────────────────── */
.chat-participants { .ext-chat-participants {
width: 240px; width: 240px;
min-width: 240px; min-width: 240px;
border-left: 1px solid var(--border); border-left: 1px solid var(--border);
@@ -479,7 +479,7 @@
background: var(--bg-secondary); background: var(--bg-secondary);
} }
.chat-participants__header { .ext-chat-participants__header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
@@ -489,20 +489,20 @@
font-size: 13px; font-size: 13px;
} }
.chat-participants__list { .ext-chat-participants__list {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding: 8px 0; padding: 8px 0;
} }
.chat-participants__item { .ext-chat-participants__item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
padding: 6px 16px; padding: 6px 16px;
} }
.chat-participants__name { .ext-chat-participants__name {
flex: 1; flex: 1;
font-size: 13px; font-size: 13px;
white-space: nowrap; white-space: nowrap;
@@ -510,14 +510,14 @@
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.chat-participants__badge { .ext-chat-participants__badge {
font-size: 10px; font-size: 10px;
color: var(--accent); color: var(--accent);
font-weight: 600; font-weight: 600;
margin-left: 4px; margin-left: 4px;
} }
.chat-participants__status { .ext-chat-participants__status {
width: 8px; width: 8px;
height: 8px; height: 8px;
border-radius: 50%; border-radius: 50%;
@@ -525,11 +525,11 @@
flex-shrink: 0; flex-shrink: 0;
} }
.chat-participants__status--online { .ext-chat-participants__status--online {
background: var(--success); background: var(--success);
} }
.chat-participants__remove { .ext-chat-participants__remove {
background: none; background: none;
border: none; border: none;
color: var(--text-3); color: var(--text-3);
@@ -539,25 +539,25 @@
line-height: 1; line-height: 1;
} }
.chat-participants__remove:hover { .ext-chat-participants__remove:hover {
color: var(--danger); color: var(--danger);
} }
/* ── New Conversation Dialog ────────────── */ /* ── New Conversation Dialog ────────────── */
.chat-new { .ext-chat-new {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 12px;
min-width: 320px; min-width: 320px;
} }
.chat-new__type { .ext-chat-new__type {
display: flex; display: flex;
gap: 16px; gap: 16px;
} }
.chat-new__type label { .ext-chat-new__type label {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
@@ -565,7 +565,7 @@
cursor: pointer; cursor: pointer;
} }
.chat-new__title { .ext-chat-new__title {
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 6px; border-radius: 6px;
padding: 8px 10px; padding: 8px 10px;
@@ -575,13 +575,13 @@
color: var(--text); color: var(--text);
} }
.chat-new__selected { .ext-chat-new__selected {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 6px; gap: 6px;
} }
.chat-new__chip { .ext-chat-new__chip {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
@@ -593,7 +593,7 @@
border-radius: 12px; border-radius: 12px;
} }
.chat-new__chip button { .ext-chat-new__chip button {
background: none; background: none;
border: none; border: none;
color: inherit; color: inherit;
@@ -607,10 +607,10 @@
/* Allow the autocomplete dropdown to overflow the dialog body. /* Allow the autocomplete dropdown to overflow the dialog body.
Applies to both New Conversation and Add Participant dialogs. */ Applies to both New Conversation and Add Participant dialogs. */
.sw-dialog__body:has(.sw-user-picker) { [data-ext="chat"] .sw-dialog__body:has(.sw-user-picker) {
overflow: visible; overflow: visible;
} }
.sw-dialog:has(.sw-user-picker) { [data-ext="chat"] .sw-dialog:has(.sw-user-picker) {
overflow: visible; overflow: visible;
} }

View File

@@ -123,60 +123,60 @@
var sMsgs = showSearch ? (searchResults.messages || []) : []; var sMsgs = showSearch ? (searchResults.messages || []) : [];
return html` return html`
<div class="chat-sidebar"> <div class="ext-chat-sidebar">
<div class="chat-sidebar__header"> <div class="ext-chat-sidebar__header">
<span class="chat-sidebar__title">Conversations</span> <span class="ext-chat-sidebar__title">Conversations</span>
<${Button} size="sm" onClick=${onNew}>New<//> <${Button} size="sm" onClick=${onNew}>New<//>
</div> </div>
<div class="chat-sidebar__search"> <div class="ext-chat-sidebar__search">
<input class="chat-sidebar__search-input" <input class="ext-chat-sidebar__search-input"
type="text" type="text"
value=${searchQuery} value=${searchQuery}
placeholder="Search\u2026" placeholder="Search\u2026"
onInput=${handleSearchInput} /> onInput=${handleSearchInput} />
${searchQuery && html` ${searchQuery && html`
<button class="chat-sidebar__search-clear" onClick=${clearSearch}>\u00d7</button>`} <button class="ext-chat-sidebar__search-clear" onClick=${clearSearch}>\u00d7</button>`}
</div> </div>
${showSearch ? html` ${showSearch ? html`
<div class="chat-sidebar__search-results"> <div class="ext-chat-sidebar__search-results">
${searching && html`<div class="chat-sidebar__search-loading"><${Spinner} size="sm" /></div>`} ${searching && html`<div class="ext-chat-sidebar__search-loading"><${Spinner} size="sm" /></div>`}
${!searching && sConvs.length === 0 && sMsgs.length === 0 && html` ${!searching && sConvs.length === 0 && sMsgs.length === 0 && html`
<div class="chat-sidebar__empty">No results</div>`} <div class="ext-chat-sidebar__empty">No results</div>`}
${sConvs.length > 0 && html` ${sConvs.length > 0 && html`
<div class="chat-sidebar__search-section">Conversations</div> <div class="ext-chat-sidebar__search-section">Conversations</div>
${sConvs.map(c => html` ${sConvs.map(c => html`
<div key=${c.id} class="chat-sidebar__item" onClick=${() => selectFromSearch(c.id)}> <div key=${c.id} class="ext-chat-sidebar__item" onClick=${() => selectFromSearch(c.id)}>
<div class="chat-sidebar__item-top"> <div class="ext-chat-sidebar__item-top">
<span class="chat-sidebar__item-title">${c.title || 'Untitled'}</span> <span class="ext-chat-sidebar__item-title">${c.title || 'Untitled'}</span>
<span class="chat-sidebar__item-time">${timeAgo(c.updated_at || c.created_at)}</span> <span class="ext-chat-sidebar__item-time">${timeAgo(c.updated_at || c.created_at)}</span>
</div> </div>
</div>`)}`} </div>`)}`}
${sMsgs.length > 0 && html` ${sMsgs.length > 0 && html`
<div class="chat-sidebar__search-section">Messages</div> <div class="ext-chat-sidebar__search-section">Messages</div>
${sMsgs.map(m => html` ${sMsgs.map(m => html`
<div key=${m.id} class="chat-sidebar__item chat-sidebar__item--search-msg" onClick=${() => selectFromSearch(m.conversation_id)}> <div key=${m.id} class="ext-chat-sidebar__item ext-chat-sidebar__item--search-msg" onClick=${() => selectFromSearch(m.conversation_id)}>
<div class="chat-sidebar__item-top"> <div class="ext-chat-sidebar__item-top">
<span class="chat-sidebar__item-preview">${truncate(m.content, 80)}</span> <span class="ext-chat-sidebar__item-preview">${truncate(m.content, 80)}</span>
</div> </div>
<div class="chat-sidebar__item-bottom"> <div class="ext-chat-sidebar__item-bottom">
<span class="chat-sidebar__item-time">${timeAgo(m.created_at)}</span> <span class="ext-chat-sidebar__item-time">${timeAgo(m.created_at)}</span>
</div> </div>
</div>`)}`} </div>`)}`}
</div> </div>
` : html` ` : html`
<div class="chat-sidebar__list"> <div class="ext-chat-sidebar__list">
${conversations.length === 0 && html` ${conversations.length === 0 && html`
<div class="chat-sidebar__empty">No conversations yet</div>`} <div class="ext-chat-sidebar__empty">No conversations yet</div>`}
${conversations.map(c => html` ${conversations.map(c => html`
<div key=${c.id} <div key=${c.id}
class=${'chat-sidebar__item' + (selected === c.id ? ' chat-sidebar__item--active' : '')} class=${'ext-chat-sidebar__item' + (selected === c.id ? ' ext-chat-sidebar__item--active' : '')}
onClick=${() => onSelect(c.id)}> onClick=${() => onSelect(c.id)}>
<div class="chat-sidebar__item-top"> <div class="ext-chat-sidebar__item-top">
<span class="chat-sidebar__item-title">${c.title || 'Untitled'}</span> <span class="ext-chat-sidebar__item-title">${c.title || 'Untitled'}</span>
<span class="chat-sidebar__item-time">${timeAgo(c.updated_at || c.created_at)}</span> <span class="ext-chat-sidebar__item-time">${timeAgo(c.updated_at || c.created_at)}</span>
</div> </div>
<div class="chat-sidebar__item-bottom"> <div class="ext-chat-sidebar__item-bottom">
<span class="chat-sidebar__item-preview"> <span class="ext-chat-sidebar__item-preview">
${c.last_message ${c.last_message
? truncate(c.last_message.content_type === 'system' ? truncate(c.last_message.content_type === 'system'
? '\u2022 ' + c.last_message.content ? '\u2022 ' + c.last_message.content
@@ -184,7 +184,7 @@
: 'No messages yet'} : 'No messages yet'}
</span> </span>
${(unread[c.id] || 0) > 0 && html` ${(unread[c.id] || 0) > 0 && html`
<span class="chat-sidebar__badge">${unread[c.id]}</span>`} <span class="ext-chat-sidebar__badge">${unread[c.id]}</span>`}
</div> </div>
</div>`)} </div>`)}
</div> </div>
@@ -204,14 +204,14 @@
if (msg._deleted) { if (msg._deleted) {
return html` return html`
<div class="chat-msg chat-msg--deleted"> <div class="ext-chat-msg ext-chat-msg--deleted">
<em>This message was deleted</em> <em>This message was deleted</em>
</div>`; </div>`;
} }
if (msg.content_type === 'system') { if (msg.content_type === 'system') {
return html` return html`
<div class="chat-msg chat-msg--system"> <div class="ext-chat-msg ext-chat-msg--system">
<span>${msg.content}</span> <span>${msg.content}</span>
</div>`; </div>`;
} }
@@ -239,37 +239,37 @@
} }
return html` return html`
<div class=${'chat-msg' + (isOwn ? ' chat-msg--own' : '')} <div class=${'ext-chat-msg' + (isOwn ? ' ext-chat-msg--own' : '')}
onMouseEnter=${() => setHover(true)} onMouseEnter=${() => setHover(true)}
onMouseLeave=${() => setHover(false)}> onMouseLeave=${() => setHover(false)}>
${!isOwn && html` ${!isOwn && html`
<${Avatar} name=${msg._display_name || msg.participant_id} size="sm" />`} <${Avatar} name=${msg._display_name || msg.participant_id} size="sm" />`}
<div class="chat-msg__body"> <div class="ext-chat-msg__body">
${!isOwn && html`<span class="chat-msg__name">${msg._display_name || msg.participant_id}</span>`} ${!isOwn && html`<span class="ext-chat-msg__name">${msg._display_name || msg.participant_id}</span>`}
${editing ? html` ${editing ? html`
<div class="chat-msg__edit"> <div class="ext-chat-msg__edit">
<textarea class="chat-msg__edit-input" <textarea class="ext-chat-msg__edit-input"
value=${editText} value=${editText}
onInput=${e => setEditText(e.target.value)} onInput=${e => setEditText(e.target.value)}
onKeyDown=${onEditKeyDown} onKeyDown=${onEditKeyDown}
rows="2" /> rows="2" />
<div class="chat-msg__edit-actions"> <div class="ext-chat-msg__edit-actions">
<${Button} size="sm" variant="secondary" onClick=${cancelEdit}>Cancel<//> <${Button} size="sm" variant="secondary" onClick=${cancelEdit}>Cancel<//>
<${Button} size="sm" onClick=${saveEdit}>Save<//> <${Button} size="sm" onClick=${saveEdit}>Save<//>
</div> </div>
</div> </div>
` : msg.content_type === 'markdown' && sw?.markdown?.ready ? html` ` : msg.content_type === 'markdown' && sw?.markdown?.ready ? html`
<div class="chat-msg__content" dangerouslySetInnerHTML=${{ __html: sw.markdown.renderSync(msg.content, { sanitize: true }) }} />` : html` <div class="ext-chat-msg__content" dangerouslySetInnerHTML=${{ __html: sw.markdown.renderSync(msg.content, { sanitize: true }) }} />` : html`
<div class="chat-msg__content">${msg.content}</div>`} <div class="ext-chat-msg__content">${msg.content}</div>`}
<div class="chat-msg__meta"> <div class="ext-chat-msg__meta">
<span class="chat-msg__time">${timeAgo(msg.created_at)}</span> <span class="ext-chat-msg__time">${timeAgo(msg.created_at)}</span>
${msg.edited_at && html`<span class="chat-msg__edited">(edited)</span>`} ${msg.edited_at && html`<span class="ext-chat-msg__edited">(edited)</span>`}
</div> </div>
</div> </div>
${hover && isOwn && !editing && html` ${hover && isOwn && !editing && html`
<div class="chat-msg__actions"> <div class="ext-chat-msg__actions">
<button class="chat-msg__action" onClick=${startEdit} title="Edit">&#9998;</button> <button class="ext-chat-msg__action" onClick=${startEdit} title="Edit">&#9998;</button>
<button class="chat-msg__action chat-msg__action--danger" onClick=${() => onDelete(msg.id)} title="Delete">&#128465;</button> <button class="ext-chat-msg__action ext-chat-msg__action--danger" onClick=${() => onDelete(msg.id)} title="Delete">&#128465;</button>
</div>`} </div>`}
</div>`; </div>`;
} }
@@ -442,18 +442,18 @@
if (!conversationId) { if (!conversationId) {
return html` return html`
<div class="chat-thread chat-thread--empty"> <div class="ext-chat-thread chat-thread--empty">
<p>Select a conversation or start a new one</p> <p>Select a conversation or start a new one</p>
</div>`; </div>`;
} }
return html` return html`
<div class="chat-thread"> <div class="ext-chat-thread">
<div class="chat-thread__messages" ref=${listRef}> <div class="ext-chat-thread__messages" ref=${listRef}>
${loading && messages.length === 0 && html`<div class="chat-thread__loading"><${Spinner} /></div>`} ${loading && messages.length === 0 && html`<div class="ext-chat-thread__loading"><${Spinner} /></div>`}
${loading && messages.length > 0 && html`<div class="chat-thread__loading-more"><${Spinner} size="sm" /></div>`} ${loading && messages.length > 0 && html`<div class="ext-chat-thread__loading-more"><${Spinner} size="sm" /></div>`}
${hasMore && !loading && html` ${hasMore && !loading && html`
<button class="chat-thread__load-more" onClick=${loadMore}> <button class="ext-chat-thread__load-more" onClick=${loadMore}>
Load older messages Load older messages
</button>`} </button>`}
${messages.map(m => html` ${messages.map(m => html`
@@ -466,7 +466,7 @@
/>`)} />`)}
<div ref=${bottomRef} /> <div ref=${bottomRef} />
</div> </div>
${typingText && html`<div class="chat-thread__typing">${typingText}</div>`} ${typingText && html`<div class="ext-chat-thread__typing">${typingText}</div>`}
</div>`; </div>`;
} }
@@ -533,8 +533,8 @@
if (!conversationId) return null; if (!conversationId) return null;
return html` return html`
<div class="chat-compose"> <div class="ext-chat-compose">
<textarea class="chat-compose__input" <textarea class="ext-chat-compose__input"
ref=${textareaRef} ref=${textareaRef}
value=${text} value=${text}
placeholder="Type a message\u2026" placeholder="Type a message\u2026"
@@ -580,22 +580,22 @@
} }
return html` return html`
<div class="chat-participants"> <div class="ext-chat-participants">
<div class="chat-participants__header"> <div class="ext-chat-participants__header">
<span>Participants (${(participants || []).length})</span> <span>Participants (${(participants || []).length})</span>
${isAdmin && html`<${Button} size="sm" onClick=${() => setAddOpen(true)}>Add<//>` } ${isAdmin && html`<${Button} size="sm" onClick=${() => setAddOpen(true)}>Add<//>` }
</div> </div>
<div class="chat-participants__list"> <div class="ext-chat-participants__list">
${(participants || []).map(p => html` ${(participants || []).map(p => html`
<div key=${p.participant_id} class="chat-participants__item"> <div key=${p.participant_id} class="ext-chat-participants__item">
<${Avatar} name=${p.display_name || p.participant_id} size="sm" /> <${Avatar} name=${p.display_name || p.participant_id} size="sm" />
<span class="chat-participants__name"> <span class="ext-chat-participants__name">
${p.display_name || p.participant_id} ${p.display_name || p.participant_id}
${p.role === 'admin' && html`<span class="chat-participants__badge">admin</span>`} ${p.role === 'admin' && html`<span class="ext-chat-participants__badge">admin</span>`}
</span> </span>
<span class=${'chat-participants__status' + (presence[p.participant_id] ? ' chat-participants__status--online' : '')} /> <span class=${'chat-participants__status' + (presence[p.participant_id] ? ' ext-chat-participants__status--online' : '')} />
${isAdmin && p.participant_id !== currentUserId() && html` ${isAdmin && p.participant_id !== currentUserId() && html`
<button class="chat-participants__remove" onClick=${() => removeUser(p.participant_id)} title="Remove">\u00d7</button>`} <button class="ext-chat-participants__remove" onClick=${() => removeUser(p.participant_id)} title="Remove">\u00d7</button>`}
</div>`)} </div>`)}
</div> </div>
@@ -671,8 +671,8 @@
return html` return html`
<${Dialog} open=${open} title="New Conversation" onClose=${() => { reset(); onClose(); }} actions=${actions}> <${Dialog} open=${open} title="New Conversation" onClose=${() => { reset(); onClose(); }} actions=${actions}>
<div class="chat-new"> <div class="ext-chat-new">
<div class="chat-new__type"> <div class="ext-chat-new__type">
<label> <label>
<input type="radio" name="convType" value="group" <input type="radio" name="convType" value="group"
checked=${type === 'group'} onChange=${() => { setType('group'); setSelected([]); }} /> checked=${type === 'group'} onChange=${() => { setType('group'); setSelected([]); }} />
@@ -685,14 +685,14 @@
</label> </label>
</div> </div>
${type === 'group' && html` ${type === 'group' && html`
<input class="chat-new__title" type="text" value=${title} <input class="ext-chat-new__title" type="text" value=${title}
placeholder="Conversation title (optional)" placeholder="Conversation title (optional)"
onInput=${e => setTitle(e.target.value)} />`} onInput=${e => setTitle(e.target.value)} />`}
<${UserPicker} onSelect=${addUser} placeholder=${type === 'direct' ? 'Search for a user\u2026' : 'Add participants\u2026'} /> <${UserPicker} onSelect=${addUser} placeholder=${type === 'direct' ? 'Search for a user\u2026' : 'Add participants\u2026'} />
${selected.length > 0 && html` ${selected.length > 0 && html`
<div class="chat-new__selected"> <div class="ext-chat-new__selected">
${selected.map(u => html` ${selected.map(u => html`
<span key=${u.id} class="chat-new__chip"> <span key=${u.id} class="ext-chat-new__chip">
${u.display_name || u.username} ${u.display_name || u.username}
<button onClick=${() => removeSelected(u.id)}>\u00d7</button> <button onClick=${() => removeSelected(u.id)}>\u00d7</button>
</span>`)} </span>`)}
@@ -807,27 +807,27 @@
var threadTitle = selectedConv ? (selectedConv.title || 'Direct Message') : ''; var threadTitle = selectedConv ? (selectedConv.title || 'Direct Message') : '';
if (loading) { if (loading) {
return html`<div class="chat-loading"><${Spinner} /></div>`; return html`<div class="ext-chat-loading"><${Spinner} /></div>`;
} }
return html` return html`
<div class="chat-app"> <div class="ext-chat-app">
<${Topbar} title="Chat"> <${Topbar} title="Chat">
${selectedId && html` ${selectedId && html`
<span class="chat-topbar__thread-title">${threadTitle}</span> <span class="ext-chat-topbar__thread-title">${threadTitle}</span>
<${Button} size="sm" variant="secondary" <${Button} size="sm" variant="secondary"
onClick=${() => setShowParticipants(!showParticipants)}> onClick=${() => setShowParticipants(!showParticipants)}>
${showParticipants ? 'Hide' : 'People'} ${showParticipants ? 'Hide' : 'People'}
<//>`} <//>`}
<//> <//>
<div class="chat-body"> <div class="ext-chat-body">
<${ConversationList} <${ConversationList}
selected=${selectedId} selected=${selectedId}
onSelect=${selectConversation} onSelect=${selectConversation}
onNew=${() => setShowNew(true)} onNew=${() => setShowNew(true)}
conversations=${conversations} conversations=${conversations}
unread=${unread} /> unread=${unread} />
<div class="chat-main"> <div class="ext-chat-main">
<${MessageThread} <${MessageThread}
conversationId=${selectedId} conversationId=${selectedId}
participants=${participants} /> participants=${participants} />

View File

@@ -6,7 +6,7 @@
All SDK components style themselves. All SDK components style themselves.
========================================== */ ========================================== */
.surface-dashboard { .ext-dashboard {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
@@ -15,7 +15,7 @@
/* ── Topbar ──────────────────────────────── */ /* ── Topbar ──────────────────────────────── */
.dashboard-topbar { .ext-dashboard-topbar {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
@@ -26,7 +26,7 @@
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
} }
.dashboard-topbar-back { .ext-dashboard-topbar-back {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
@@ -39,30 +39,30 @@
transition: color 0.15s, background 0.15s; transition: color 0.15s, background 0.15s;
} }
.dashboard-topbar-back:hover { .ext-dashboard-topbar-back:hover {
color: var(--text); color: var(--text);
background: var(--bg-hover); background: var(--bg-hover);
} }
.dashboard-topbar-title { .ext-dashboard-topbar-title {
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
color: var(--text); color: var(--text);
} }
.dashboard-topbar-sep { .ext-dashboard-topbar-sep {
width: 1px; width: 1px;
height: 18px; height: 18px;
background: var(--border); background: var(--border);
} }
.dashboard-topbar-spacer { .ext-dashboard-topbar-spacer {
flex: 1; flex: 1;
} }
/* ── Body ────────────────────────────────── */ /* ── Body ────────────────────────────────── */
.dashboard-body { .ext-dashboard-body {
display: flex; display: flex;
flex: 1; flex: 1;
min-height: 0; min-height: 0;
@@ -70,7 +70,7 @@
/* ── Sidebar ─────────────────────────────── */ /* ── Sidebar ─────────────────────────────── */
.dashboard-sidebar { .ext-dashboard-sidebar {
width: 300px; width: 300px;
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
@@ -81,34 +81,34 @@
/* ── Main Content ────────────────────────── */ /* ── Main Content ────────────────────────── */
.dashboard-main { .ext-dashboard-main {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding: 20px; padding: 20px;
min-width: 0; min-width: 0;
} }
.dashboard-greeting { .ext-dashboard-greeting {
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;
color: var(--text); color: var(--text);
margin-bottom: 4px; margin-bottom: 4px;
} }
.dashboard-subtitle { .ext-dashboard-subtitle {
font-size: 13px; font-size: 13px;
color: var(--text-3); color: var(--text-3);
margin-bottom: 20px; margin-bottom: 20px;
} }
.dashboard-filter-bar { .ext-dashboard-filter-bar {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
margin-bottom: 16px; margin-bottom: 16px;
} }
.dashboard-filter-label { .ext-dashboard-filter-label {
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
color: var(--text-2); color: var(--text-2);
@@ -118,13 +118,13 @@
/* ── Cards Grid ──────────────────────────── */ /* ── Cards Grid ──────────────────────────── */
.dashboard-cards { .ext-dashboard-cards {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 12px; gap: 12px;
} }
.dashboard-card { .ext-dashboard-card {
background: var(--bg-raised); background: var(--bg-raised);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
@@ -135,17 +135,17 @@
transition: border-color 0.15s; transition: border-color 0.15s;
} }
.dashboard-card:hover { .ext-dashboard-card:hover {
border-color: var(--border-elevated); border-color: var(--border-elevated);
} }
.dashboard-card-header { .ext-dashboard-card-header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
} }
.dashboard-card-title { .ext-dashboard-card-title {
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
color: var(--text); color: var(--text);
@@ -155,24 +155,24 @@
white-space: nowrap; white-space: nowrap;
} }
.dashboard-card-meta { .ext-dashboard-card-meta {
font-size: 11px; font-size: 11px;
color: var(--text-3); color: var(--text-3);
} }
.dashboard-card-desc { .ext-dashboard-card-desc {
font-size: 12px; font-size: 12px;
color: var(--text-2); color: var(--text-2);
line-height: 1.4; line-height: 1.4;
} }
.dashboard-card-actions { .ext-dashboard-card-actions {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
margin-top: 4px; margin-top: 4px;
} }
.dashboard-empty { .ext-dashboard-empty {
text-align: center; text-align: center;
color: var(--text-3); color: var(--text-3);
font-size: 13px; font-size: 13px;
@@ -181,13 +181,13 @@
/* ── Admin Section ───────────────────────── */ /* ── Admin Section ───────────────────────── */
.dashboard-admin-section { .ext-dashboard-admin-section {
margin-top: 24px; margin-top: 24px;
padding-top: 16px; padding-top: 16px;
border-top: 1px solid var(--border); border-top: 1px solid var(--border);
} }
.dashboard-section-title { .ext-dashboard-section-title {
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
color: var(--text-2); color: var(--text-2);

View File

@@ -28,7 +28,7 @@
if (!mount) return; if (!mount) return;
const surface = document.createElement('div'); const surface = document.createElement('div');
surface.className = 'surface-dashboard'; surface.className = 'ext-dashboard';
surface.id = 'dashboardSurface'; surface.id = 'dashboardSurface';
mount.appendChild(surface); mount.appendChild(surface);
@@ -41,12 +41,12 @@
// ── Body ── // ── Body ──
const body = document.createElement('div'); const body = document.createElement('div');
body.className = 'dashboard-body'; body.className = 'ext-dashboard-body';
surface.appendChild(body); surface.appendChild(body);
// ── Sidebar (tabs: activity + notes) ── // ── Sidebar (tabs: activity + notes) ──
const sidebar = document.createElement('div'); const sidebar = document.createElement('div');
sidebar.className = 'dashboard-sidebar'; sidebar.className = 'ext-dashboard-sidebar';
body.appendChild(sidebar); body.appendChild(sidebar);
// sw.tabs — two tabs // sw.tabs — two tabs
@@ -83,14 +83,14 @@
// ── Main content area ── // ── Main content area ──
const main = document.createElement('div'); const main = document.createElement('div');
main.className = 'dashboard-main'; main.className = 'ext-dashboard-main';
body.appendChild(main); body.appendChild(main);
_buildMainContent(main); _buildMainContent(main);
// ── Theme reactivity ── // ── Theme reactivity ──
sw.theme.on('change', function (resolved) { sw.theme.on('change', function (resolved) {
const cards = main.querySelectorAll('.dashboard-card'); const cards = main.querySelectorAll('.ext-dashboard-card');
cards.forEach(function (c) { cards.forEach(function (c) {
c.style.borderColor = ''; // reset to CSS default for new theme c.style.borderColor = ''; // reset to CSS default for new theme
}); });
@@ -98,7 +98,7 @@
// ── Cross-component events ── // ── Cross-component events ──
sw.on('dashboard.filter.changed', function (payload) { sw.on('dashboard.filter.changed', function (payload) {
_loadChannels(main.querySelector('.dashboard-cards'), payload.value); _loadChannels(main.querySelector('.ext-dashboard-cards'), payload.value);
}); });
console.log('[DashboardPkg] Mounted'); console.log('[DashboardPkg] Mounted');
@@ -108,15 +108,15 @@
function _buildTopbar() { function _buildTopbar() {
const el = document.createElement('div'); const el = document.createElement('div');
el.className = 'dashboard-topbar'; el.className = 'ext-dashboard-topbar';
el.innerHTML = el.innerHTML =
'<a href="' + esc(base) + '/" class="dashboard-topbar-back" title="Back to chat">' + '<a href="' + esc(base) + '/" class="ext-dashboard-topbar-back" title="Back to chat">' +
'<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>' + '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>' +
'Back' + 'Back' +
'</a>' + '</a>' +
'<div class="dashboard-topbar-sep"></div>' + '<div class="ext-dashboard-topbar-sep"></div>' +
'<span class="dashboard-topbar-title">Dashboard</span>' + '<span class="ext-dashboard-topbar-title">Dashboard</span>' +
'<div class="dashboard-topbar-spacer"></div>'; '<div class="ext-dashboard-topbar-spacer"></div>';
// sw.toolbar — action buttons // sw.toolbar — action buttons
const toolbarItems = [ const toolbarItems = [
@@ -125,7 +125,7 @@
icon: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>', icon: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>',
title: 'Refresh', title: 'Refresh',
onClick: function () { onClick: function () {
const cards = document.querySelector('.dashboard-cards'); const cards = document.querySelector('.ext-dashboard-cards');
if (cards) _loadChannels(cards, _currentFilter); if (cards) _loadChannels(cards, _currentFilter);
sw.toast('Refreshed', 'success'); sw.toast('Refreshed', 'success');
}, },
@@ -155,22 +155,22 @@
// sw.user — greeting // sw.user — greeting
const user = sw.user; const user = sw.user;
const greeting = document.createElement('div'); const greeting = document.createElement('div');
greeting.className = 'dashboard-greeting'; greeting.className = 'ext-dashboard-greeting';
greeting.textContent = 'Welcome back' + (user ? ', ' + (user.display_name || user.username) : ''); greeting.textContent = 'Welcome back' + (user ? ', ' + (user.display_name || user.username) : '');
main.appendChild(greeting); main.appendChild(greeting);
const subtitle = document.createElement('div'); const subtitle = document.createElement('div');
subtitle.className = 'dashboard-subtitle'; subtitle.className = 'ext-dashboard-subtitle';
subtitle.textContent = 'Your recent conversations' + (sw.isAdmin ? ' \u00b7 Admin' : ''); subtitle.textContent = 'Your recent conversations' + (sw.isAdmin ? ' \u00b7 Admin' : '');
main.appendChild(subtitle); main.appendChild(subtitle);
// ── Filter bar ── // ── Filter bar ──
const filterBar = document.createElement('div'); const filterBar = document.createElement('div');
filterBar.className = 'dashboard-filter-bar'; filterBar.className = 'ext-dashboard-filter-bar';
main.appendChild(filterBar); main.appendChild(filterBar);
const filterLabel = document.createElement('span'); const filterLabel = document.createElement('span');
filterLabel.className = 'dashboard-filter-label'; filterLabel.className = 'ext-dashboard-filter-label';
filterLabel.textContent = 'Filter'; filterLabel.textContent = 'Filter';
filterBar.appendChild(filterLabel); filterBar.appendChild(filterLabel);
@@ -192,7 +192,7 @@
// ── Cards ── // ── Cards ──
const cards = document.createElement('div'); const cards = document.createElement('div');
cards.className = 'dashboard-cards'; cards.className = 'ext-dashboard-cards';
main.appendChild(cards); main.appendChild(cards);
_loadChannels(cards, ''); _loadChannels(cards, '');
@@ -200,16 +200,16 @@
// ── Admin section (sw.isAdmin) ── // ── Admin section (sw.isAdmin) ──
if (sw.isAdmin) { if (sw.isAdmin) {
const adminSection = document.createElement('div'); const adminSection = document.createElement('div');
adminSection.className = 'dashboard-admin-section'; adminSection.className = 'ext-dashboard-admin-section';
main.appendChild(adminSection); main.appendChild(adminSection);
const sectionTitle = document.createElement('div'); const sectionTitle = document.createElement('div');
sectionTitle.className = 'dashboard-section-title'; sectionTitle.className = 'ext-dashboard-section-title';
sectionTitle.textContent = 'Administration'; sectionTitle.textContent = 'Administration';
adminSection.appendChild(sectionTitle); adminSection.appendChild(sectionTitle);
const adminCards = document.createElement('div'); const adminCards = document.createElement('div');
adminCards.className = 'dashboard-cards'; adminCards.className = 'ext-dashboard-cards';
adminSection.appendChild(adminCards); adminSection.appendChild(adminCards);
_loadAdminCards(adminCards); _loadAdminCards(adminCards);
@@ -220,7 +220,7 @@
async function _loadChannels(container, typeFilter) { async function _loadChannels(container, typeFilter) {
if (!container) return; if (!container) return;
container.innerHTML = '<div class="dashboard-empty">Loading\u2026</div>'; container.innerHTML = '<div class="ext-dashboard-empty">Loading\u2026</div>';
try { try {
// sw.api — real API call // sw.api — real API call
@@ -231,7 +231,7 @@
container.innerHTML = ''; container.innerHTML = '';
if (!channels.length) { if (!channels.length) {
container.innerHTML = '<div class="dashboard-empty">No channels found</div>'; container.innerHTML = '<div class="ext-dashboard-empty">No channels found</div>';
return; return;
} }
@@ -239,31 +239,31 @@
container.appendChild(_buildChannelCard(ch)); container.appendChild(_buildChannelCard(ch));
}); });
} catch (e) { } catch (e) {
container.innerHTML = '<div class="dashboard-empty">Failed to load: ' + (typeof esc === 'function' ? esc(e.message) : e.message) + '</div>'; container.innerHTML = '<div class="ext-dashboard-empty">Failed to load: ' + (typeof esc === 'function' ? esc(e.message) : e.message) + '</div>';
} }
} }
function _buildChannelCard(ch) { function _buildChannelCard(ch) {
const card = document.createElement('div'); const card = document.createElement('div');
card.className = 'dashboard-card'; card.className = 'ext-dashboard-card';
const header = document.createElement('div'); const header = document.createElement('div');
header.className = 'dashboard-card-header'; header.className = 'ext-dashboard-card-header';
card.appendChild(header); card.appendChild(header);
const title = document.createElement('div'); const title = document.createElement('div');
title.className = 'dashboard-card-title'; title.className = 'ext-dashboard-card-title';
title.textContent = ch.title || ch.name || 'Untitled'; title.textContent = ch.title || ch.name || 'Untitled';
header.appendChild(title); header.appendChild(title);
const meta = document.createElement('div'); const meta = document.createElement('div');
meta.className = 'dashboard-card-meta'; meta.className = 'ext-dashboard-card-meta';
meta.textContent = ch.type || ''; meta.textContent = ch.type || '';
card.appendChild(meta); card.appendChild(meta);
if (ch.description) { if (ch.description) {
const desc = document.createElement('div'); const desc = document.createElement('div');
desc.className = 'dashboard-card-desc'; desc.className = 'ext-dashboard-card-desc';
desc.textContent = ch.description.slice(0, 120); desc.textContent = ch.description.slice(0, 120);
card.appendChild(desc); card.appendChild(desc);
} }
@@ -271,14 +271,14 @@
const updated = ch.updated_at || ch.created_at; const updated = ch.updated_at || ch.created_at;
if (updated) { if (updated) {
const date = document.createElement('div'); const date = document.createElement('div');
date.className = 'dashboard-card-meta'; date.className = 'ext-dashboard-card-meta';
date.textContent = new Date(updated).toLocaleDateString(); date.textContent = new Date(updated).toLocaleDateString();
card.appendChild(date); card.appendChild(date);
} }
// ── Card action menu ── // ── Card action menu ──
const actions = document.createElement('div'); const actions = document.createElement('div');
actions.className = 'dashboard-card-actions'; actions.className = 'ext-dashboard-card-actions';
card.appendChild(actions); card.appendChild(actions);
const menuBtn = document.createElement('button'); const menuBtn = document.createElement('button');
@@ -372,7 +372,7 @@
// sw.toast — success feedback // sw.toast — success feedback
sw.toast('Channel deleted', 'success'); sw.toast('Channel deleted', 'success');
// Refresh cards // Refresh cards
const cards = document.querySelector('.dashboard-cards'); const cards = document.querySelector('.ext-dashboard-cards');
if (cards) _loadChannels(cards, _currentFilter); if (cards) _loadChannels(cards, _currentFilter);
} catch (e) { } catch (e) {
// sw.toast — error feedback // sw.toast — error feedback
@@ -390,21 +390,21 @@
container.innerHTML = ''; container.innerHTML = '';
packages.forEach(function (pkg) { packages.forEach(function (pkg) {
const card = document.createElement('div'); const card = document.createElement('div');
card.className = 'dashboard-card'; card.className = 'ext-dashboard-card';
card.innerHTML = card.innerHTML =
'<div class="dashboard-card-header">' + '<div class="ext-dashboard-card-header">' +
'<div class="dashboard-card-title">' + (typeof esc === 'function' ? esc(pkg.title || pkg.id) : (pkg.title || pkg.id)) + '</div>' + '<div class="ext-dashboard-card-title">' + (typeof esc === 'function' ? esc(pkg.title || pkg.id) : (pkg.title || pkg.id)) + '</div>' +
'</div>' + '</div>' +
'<div class="dashboard-card-meta">' + (typeof esc === 'function' ? esc(pkg.type || '') : (pkg.type || '')) + ' \u00b7 ' + (typeof esc === 'function' ? esc(pkg.tier || '') : (pkg.tier || '')) + '</div>' + '<div class="ext-dashboard-card-meta">' + (typeof esc === 'function' ? esc(pkg.type || '') : (pkg.type || '')) + ' \u00b7 ' + (typeof esc === 'function' ? esc(pkg.tier || '') : (pkg.tier || '')) + '</div>' +
'<div class="dashboard-card-desc">' + (typeof esc === 'function' ? esc(pkg.description || '') : (pkg.description || '')) + '</div>'; '<div class="ext-dashboard-card-desc">' + (typeof esc === 'function' ? esc(pkg.description || '') : (pkg.description || '')) + '</div>';
container.appendChild(card); container.appendChild(card);
}); });
if (!packages.length) { if (!packages.length) {
container.innerHTML = '<div class="dashboard-empty">No packages installed</div>'; container.innerHTML = '<div class="ext-dashboard-empty">No packages installed</div>';
} }
} catch (_) { } catch (_) {
container.innerHTML = '<div class="dashboard-empty">Failed to load packages</div>'; container.innerHTML = '<div class="ext-dashboard-empty">Failed to load packages</div>';
} }
} }

View File

@@ -8,7 +8,7 @@
/* ── Surface Shell ─────────────────────────── */ /* ── Surface Shell ─────────────────────────── */
.surface-editor { .ext-editor {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
@@ -18,7 +18,7 @@
/* ── Topbar ────────────────────────────────── */ /* ── Topbar ────────────────────────────────── */
.editor-topbar { .ext-editor-topbar {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
@@ -32,7 +32,7 @@
z-index: 20; z-index: 20;
} }
.editor-topbar-back { .ext-editor-topbar-back {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
@@ -45,18 +45,18 @@
transition: color 0.15s, background 0.15s; transition: color 0.15s, background 0.15s;
} }
.editor-topbar-back:hover { .ext-editor-topbar-back:hover {
color: var(--text, #eee); color: var(--text, #eee);
background: var(--bg-hover); background: var(--bg-hover);
} }
.editor-topbar-sep { .ext-editor-topbar-sep {
width: 1px; width: 1px;
height: 18px; height: 18px;
background: var(--border, #2a2a2e); background: var(--border, #2a2a2e);
} }
.editor-topbar-name { .ext-editor-topbar-name {
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 600;
color: var(--text, #eee); color: var(--text, #eee);
@@ -64,11 +64,11 @@
/* ── Workspace Selector ────────────────────── */ /* ── Workspace Selector ────────────────────── */
.editor-ws-selector { .ext-editor-ws-selector {
position: relative; position: relative;
} }
.editor-ws-selector-btn { .ext-editor-ws-selector-btn {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
@@ -84,12 +84,12 @@
transition: border-color 0.15s, background 0.15s; transition: border-color 0.15s, background 0.15s;
} }
.editor-ws-selector-btn:hover { .ext-editor-ws-selector-btn:hover {
border-color: var(--border, #2a2a2e); border-color: var(--border, #2a2a2e);
background: var(--bg-hover); background: var(--bg-hover);
} }
.editor-ws-dropdown { .ext-editor-ws-dropdown {
display: none; display: none;
position: absolute; position: absolute;
top: 100%; top: 100%;
@@ -106,14 +106,14 @@
padding: 4px 0; padding: 4px 0;
} }
.editor-ws-dropdown.open { display: block; } .ext-editor-ws-dropdown.open { display: block; }
.editor-ws-list { .ext-editor-ws-list {
max-height: 240px; max-height: 240px;
overflow-y: auto; overflow-y: auto;
} }
.editor-ws-dropdown-item { .ext-editor-ws-dropdown-item {
display: block; display: block;
width: 100%; width: 100%;
text-align: left; text-align: left;
@@ -127,29 +127,29 @@
transition: background 0.1s; transition: background 0.1s;
} }
.editor-ws-dropdown-item:hover { .ext-editor-ws-dropdown-item:hover {
background: var(--bg-hover); background: var(--bg-hover);
} }
.editor-ws-dropdown-item.active { .ext-editor-ws-dropdown-item.active {
color: var(--accent, #b38a4e); color: var(--accent, #b38a4e);
font-weight: 600; font-weight: 600;
} }
.editor-ws-dropdown-divider { .ext-editor-ws-dropdown-divider {
height: 1px; height: 1px;
background: var(--border, #2a2a2e); background: var(--border, #2a2a2e);
margin: 4px 0; margin: 4px 0;
} }
.editor-ws-new { .ext-editor-ws-new {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
color: var(--accent, #b38a4e); color: var(--accent, #b38a4e);
} }
.editor-topbar-branch { .ext-editor-topbar-branch {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
@@ -158,7 +158,7 @@
border-radius: 4px; border-radius: 4px;
} }
.editor-topbar-branch-text { .ext-editor-topbar-branch-text {
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 600;
color: var(--purple, #a078ff); color: var(--purple, #a078ff);
@@ -167,7 +167,7 @@
/* ── Body ──────────────────────────────────── */ /* ── Body ──────────────────────────────────── */
.editor-body { .ext-editor-body {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
overflow: hidden; overflow: hidden;
@@ -175,14 +175,14 @@
/* ── Bootstrap (no workspace) ──────────────── */ /* ── Bootstrap (no workspace) ──────────────── */
.editor-bootstrap { .ext-editor-bootstrap {
flex: 1; flex: 1;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.editor-bootstrap-card { .ext-editor-bootstrap-card {
text-align: center; text-align: center;
padding: 40px; padding: 40px;
background: var(--bg-secondary, #151517); background: var(--bg-secondary, #151517);
@@ -191,7 +191,7 @@
max-width: 360px; max-width: 360px;
} }
.editor-bootstrap-input { .ext-editor-bootstrap-input {
width: 100%; width: 100%;
padding: 8px 12px; padding: 8px 12px;
background: var(--bg, #0e0e10); background: var(--bg, #0e0e10);
@@ -205,11 +205,11 @@
box-sizing: border-box; box-sizing: border-box;
} }
.editor-bootstrap-input:focus { .ext-editor-bootstrap-input:focus {
border-color: var(--accent, #b38a4e); border-color: var(--accent, #b38a4e);
} }
.editor-bootstrap-btn { .ext-editor-bootstrap-btn {
width: 100%; width: 100%;
padding: 10px 16px; padding: 10px 16px;
background: var(--accent, #b38a4e); background: var(--accent, #b38a4e);
@@ -223,11 +223,11 @@
transition: opacity 0.15s; transition: opacity 0.15s;
} }
.editor-bootstrap-btn:hover { opacity: 0.9; } .ext-editor-bootstrap-btn:hover { opacity: 0.9; }
.editor-bootstrap-btn:disabled { opacity: 0.5; cursor: not-allowed; } .ext-editor-bootstrap-btn:disabled { opacity: 0.5; cursor: not-allowed; }
/* Workspace list in bootstrap */ /* Workspace list in bootstrap */
.editor-bootstrap-ws-item { .ext-editor-bootstrap-ws-item {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
@@ -245,30 +245,30 @@
text-align: left; text-align: left;
} }
.editor-bootstrap-ws-item:hover { .ext-editor-bootstrap-ws-item:hover {
border-color: var(--accent, #b38a4e); border-color: var(--accent, #b38a4e);
background: var(--bg-hover); background: var(--bg-hover);
} }
.editor-bootstrap-ws-name { .ext-editor-bootstrap-ws-name {
font-weight: 600; font-weight: 600;
} }
.editor-bootstrap-ws-date { .ext-editor-bootstrap-ws-date {
font-size: 11px; font-size: 11px;
color: var(--text-3, #777); color: var(--text-3, #777);
} }
/* ── FileTree overrides (in editor context) ── */ /* ── FileTree overrides (in editor context) ── */
.surface-editor .file-tree { .ext-editor .file-tree {
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border-right: 1px solid var(--border, #2a2a2e); border-right: 1px solid var(--border, #2a2a2e);
} }
.surface-editor .file-tree-header { .ext-editor .file-tree-header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
@@ -276,7 +276,7 @@
border-bottom: 1px solid var(--border, #2a2a2e); border-bottom: 1px solid var(--border, #2a2a2e);
} }
.surface-editor .file-tree-title { .ext-editor .file-tree-title {
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 600;
color: var(--text-2, #999); color: var(--text-2, #999);
@@ -285,13 +285,13 @@
flex: 1; flex: 1;
} }
.surface-editor .file-tree-items { .ext-editor .file-tree-items {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding: 4px 0; padding: 4px 0;
} }
.surface-editor .file-tree-row { .ext-editor .file-tree-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
@@ -302,29 +302,29 @@
transition: background 0.1s; transition: background 0.1s;
} }
.surface-editor .file-tree-row:hover { .ext-editor .file-tree-row:hover {
background: var(--bg-hover); background: var(--bg-hover);
} }
.surface-editor .file-tree-row.active { .ext-editor .file-tree-row.active {
background: var(--accent-dim, rgba(179, 138, 78, 0.15)); background: var(--accent-dim, rgba(179, 138, 78, 0.15));
color: var(--text, #eee); color: var(--text, #eee);
} }
.surface-editor .file-tree-arrow { .ext-editor .file-tree-arrow {
width: 12px; width: 12px;
font-size: 10px; font-size: 10px;
color: var(--text-3, #555); color: var(--text-3, #555);
text-align: center; text-align: center;
} }
.surface-editor .file-tree-icon { .ext-editor .file-tree-icon {
font-size: 13px; font-size: 13px;
width: 18px; width: 18px;
text-align: center; text-align: center;
} }
.surface-editor .file-tree-name { .ext-editor .file-tree-name {
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -332,13 +332,13 @@
} }
/* Git status indicators */ /* Git status indicators */
.surface-editor .file-tree-row.git-modified .file-tree-name { color: var(--warning, #e5a842); } .ext-editor .file-tree-row.git-modified .file-tree-name { color: var(--warning, #e5a842); }
.surface-editor .file-tree-row.git-added .file-tree-name { color: var(--success, #4caf50); } .ext-editor .file-tree-row.git-added .file-tree-name { color: var(--success, #4caf50); }
.surface-editor .file-tree-row.git-untracked .file-tree-name { color: var(--text-3, #555); font-style: italic; } .ext-editor .file-tree-row.git-untracked .file-tree-name { color: var(--text-3, #555); font-style: italic; }
.surface-editor .file-tree-row.git-deleted .file-tree-name { color: var(--danger, #f44336); text-decoration: line-through; } .ext-editor .file-tree-row.git-deleted .file-tree-name { color: var(--danger, #f44336); text-decoration: line-through; }
/* Context menu */ /* Context menu */
.file-tree-ctx-menu { .ext-editor-file-tree-ctx-menu {
position: fixed; position: fixed;
background: var(--bg-secondary, #1a1a1e); background: var(--bg-secondary, #1a1a1e);
border: 1px solid var(--border, #2a2a2e); border: 1px solid var(--border, #2a2a2e);
@@ -349,26 +349,26 @@
box-shadow: 0 4px 12px rgba(0,0,0,0.4); box-shadow: 0 4px 12px rgba(0,0,0,0.4);
} }
.file-tree-ctx-item { .ext-editor-file-tree-ctx-item {
padding: 6px 12px; padding: 6px 12px;
font-size: 12px; font-size: 12px;
color: var(--text, #eee); color: var(--text, #eee);
cursor: pointer; cursor: pointer;
} }
.file-tree-ctx-item:hover { .ext-editor-file-tree-ctx-item:hover {
background: var(--bg-hover); background: var(--bg-hover);
} }
/* ── CodeEditor overrides ──────────────────── */ /* ── CodeEditor overrides ──────────────────── */
.surface-editor .code-editor { .ext-editor .code-editor {
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.surface-editor .code-editor-tabs { .ext-editor .code-editor-tabs {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0; gap: 0;
@@ -379,9 +379,9 @@
flex-shrink: 0; flex-shrink: 0;
} }
.surface-editor .code-editor-tabs::-webkit-scrollbar { height: 0; } .ext-editor .code-editor-tabs::-webkit-scrollbar { height: 0; }
.surface-editor .code-editor-tab { .ext-editor .code-editor-tab {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
@@ -395,14 +395,14 @@
white-space: nowrap; white-space: nowrap;
} }
.surface-editor .code-editor-tab:hover { background: var(--bg-hover); } .ext-editor .code-editor-tab:hover { background: var(--bg-hover); }
.surface-editor .code-editor-tab.active { color: var(--text, #eee); background: var(--bg, #0e0e10); } .ext-editor .code-editor-tab.active { color: var(--text, #eee); background: var(--bg, #0e0e10); }
.surface-editor .code-editor-tab.modified .code-editor-tab-modified { color: var(--warning, #e5a842); } .ext-editor .code-editor-tab.modified .code-editor-tab-modified { color: var(--warning, #e5a842); }
.surface-editor .code-editor-tab-icon { font-size: 12px; } .ext-editor .code-editor-tab-icon { font-size: 12px; }
.surface-editor .code-editor-tab-modified { font-size: 10px; color: var(--text-3); } .ext-editor .code-editor-tab-modified { font-size: 10px; color: var(--text-3); }
.surface-editor .code-editor-tab-close { .ext-editor .code-editor-tab-close {
background: none; background: none;
border: none; border: none;
color: var(--text-3, #555); color: var(--text-3, #555);
@@ -414,32 +414,32 @@
line-height: 1; line-height: 1;
} }
.surface-editor .code-editor-tab-close:hover { .ext-editor .code-editor-tab-close:hover {
background: var(--danger-dim, rgba(244, 67, 54, 0.15)); background: var(--danger-dim, rgba(244, 67, 54, 0.15));
color: var(--danger, #f44336); color: var(--danger, #f44336);
} }
.surface-editor .code-editor-content { .ext-editor .code-editor-content {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
} }
.surface-editor .code-editor-welcome { .ext-editor .code-editor-welcome {
height: 100%; height: 100%;
} }
.surface-editor .code-editor-cm-wrap { .ext-editor .code-editor-cm-wrap {
height: 100%; height: 100%;
overflow: auto; overflow: auto;
} }
.surface-editor .code-editor-cm-wrap .cm-editor { .ext-editor .code-editor-cm-wrap .cm-editor {
height: 100%; height: 100%;
} }
.surface-editor .code-editor-statusbar { .ext-editor .code-editor-statusbar {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16px; gap: 16px;
@@ -453,7 +453,7 @@
font-family: var(--mono, 'SF Mono', monospace); font-family: var(--mono, 'SF Mono', monospace);
} }
.surface-editor .code-editor-textarea-fallback { .ext-editor .code-editor-textarea-fallback {
width: 100%; width: 100%;
height: 100%; height: 100%;
background: var(--bg, #0e0e10); background: var(--bg, #0e0e10);
@@ -469,18 +469,18 @@
/* ── Tabbed assist pane overrides ──────────── */ /* ── Tabbed assist pane overrides ──────────── */
.surface-editor .pane-tabbed { .ext-editor .pane-tabbed {
border-left: 1px solid var(--border, #2a2a2e); border-left: 1px solid var(--border, #2a2a2e);
} }
/* ChatPane in editor tabbed pane */ /* ChatPane in editor tabbed pane */
.surface-editor .chat-pane { .ext-editor .chat-pane {
position: absolute; position: absolute;
inset: 0; inset: 0;
} }
/* Notes in editor pane */ /* Notes in editor pane */
.surface-editor .note-editor { .ext-editor .ext-notes-editor {
position: absolute; position: absolute;
inset: 0; inset: 0;
display: flex; display: flex;
@@ -488,20 +488,20 @@
overflow: hidden; overflow: hidden;
} }
.surface-editor .note-editor-list-view { .ext-editor .ext-notes-editor-list-view {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
} }
.surface-editor .notes-list { .ext-editor .ext-notes-list {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
} }
/* Compact notes toolbar for narrow pane */ /* Compact notes toolbar for narrow pane */
.surface-editor .notes-toolbar { .ext-editor .ext-notes-toolbar {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 4px; gap: 4px;
@@ -509,22 +509,22 @@
border-bottom: 1px solid var(--border, #2a2a2e); border-bottom: 1px solid var(--border, #2a2a2e);
} }
.surface-editor .notes-toolbar .sw-btn--sm { .ext-editor .ext-notes-toolbar .sw-btn--sm {
font-size: 11px; font-size: 11px;
padding: 3px 6px; padding: 3px 6px;
} }
.surface-editor .notes-search-row { .ext-editor .ext-notes-search-row {
padding: 4px 8px; padding: 4px 8px;
} }
.surface-editor .notes-filter-row { .ext-editor .ext-notes-filter-row {
padding: 2px 8px 4px; padding: 2px 8px 4px;
display: flex; display: flex;
gap: 4px; gap: 4px;
} }
.surface-editor .notes-filter-select { .ext-editor .ext-notes-filter-select {
font-size: 11px; font-size: 11px;
flex: 1; flex: 1;
min-width: 0; min-width: 0;

View File

@@ -56,7 +56,7 @@
// Wrap in surface container for CSS scoping // Wrap in surface container for CSS scoping
const surface = document.createElement('div'); const surface = document.createElement('div');
surface.className = 'surface-editor'; surface.className = 'ext-editor';
surface.id = 'editorSurface'; surface.id = 'editorSurface';
mount.appendChild(surface); mount.appendChild(surface);
@@ -84,7 +84,7 @@
// Build body + bootstrap // Build body + bootstrap
const body = document.createElement('div'); const body = document.createElement('div');
body.className = 'editor-body'; body.className = 'ext-editor-body';
body.id = 'editorBody'; body.id = 'editorBody';
surface.appendChild(body); surface.appendChild(body);
@@ -113,32 +113,32 @@
function _buildTopbar(wsName) { function _buildTopbar(wsName) {
const el = document.createElement('div'); const el = document.createElement('div');
el.className = 'editor-topbar'; el.className = 'ext-editor-topbar';
el.id = 'editorTopbar'; el.id = 'editorTopbar';
el.innerHTML = el.innerHTML =
'<a href="' + esc(base) + '/" class="editor-topbar-back" title="Back to chat">' + '<a href="' + esc(base) + '/" class="ext-editor-topbar-back" title="Back to chat">' +
'<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>' + '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>' +
'Back' + 'Back' +
'</a>' + '</a>' +
'<div class="editor-topbar-sep"></div>' + '<div class="ext-editor-topbar-sep"></div>' +
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--accent);"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>' + '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--accent);"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>' +
'<div class="editor-ws-selector" id="editorWsSelector">' + '<div class="ext-editor-ws-selector" id="editorWsSelector">' +
'<button class="editor-ws-selector-btn" id="editorWsSelectorBtn">' + '<button class="ext-editor-ws-selector-btn" id="editorWsSelectorBtn">' +
'<span id="editorWorkspaceName">' + esc(wsName || 'Editor') + '</span>' + '<span id="editorWorkspaceName">' + esc(wsName || 'Editor') + '</span>' +
'<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>' + '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>' +
'</button>' + '</button>' +
'<div class="editor-ws-dropdown" id="editorWsDropdown">' + '<div class="ext-editor-ws-dropdown" id="editorWsDropdown">' +
'<div id="editorWsList" class="editor-ws-list"></div>' + '<div id="editorWsList" class="ext-editor-ws-list"></div>' +
'<div class="editor-ws-dropdown-divider"></div>' + '<div class="ext-editor-ws-dropdown-divider"></div>' +
'<button class="editor-ws-dropdown-item editor-ws-new" id="editorWsNewBtn">' + '<button class="ext-editor-ws-dropdown-item ext-editor-ws-new" id="editorWsNewBtn">' +
'<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>' + '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>' +
'New Workspace' + 'New Workspace' +
'</button>' + '</button>' +
'</div>' + '</div>' +
'</div>' + '</div>' +
'<div class="editor-topbar-branch" id="editorBranchBadge" style="display:none;">' + '<div class="ext-editor-topbar-branch" id="editorBranchBadge" style="display:none;">' +
'<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="var(--purple)" stroke-width="2"><circle cx="12" cy="18" r="3"/><circle cx="12" cy="6" r="3"/><line x1="12" y1="9" x2="12" y2="15"/></svg>' + '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="var(--purple)" stroke-width="2"><circle cx="12" cy="18" r="3"/><circle cx="12" cy="6" r="3"/><line x1="12" y1="9" x2="12" y2="15"/></svg>' +
'<span id="editorBranchName" class="editor-topbar-branch-text">main</span>' + '<span id="editorBranchName" class="ext-editor-topbar-branch-text">main</span>' +
'</div>' + '</div>' +
'<div style="flex:1;"></div>' + '<div style="flex:1;"></div>' +
'<button class="icon-btn" id="editorRefreshBtn" title="Refresh files">' + '<button class="icon-btn" id="editorRefreshBtn" title="Refresh files">' +
@@ -151,11 +151,11 @@
function _buildBootstrap() { function _buildBootstrap() {
const el = document.createElement('div'); const el = document.createElement('div');
el.className = 'editor-bootstrap'; el.className = 'ext-editor-bootstrap';
el.id = 'editorBootstrap'; el.id = 'editorBootstrap';
el.style.display = 'none'; el.style.display = 'none';
el.innerHTML = el.innerHTML =
'<div class="editor-bootstrap-card">' + '<div class="ext-editor-bootstrap-card">' +
'<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="1.5" style="opacity:0.6;margin-bottom:12px;">' + '<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="1.5" style="opacity:0.6;margin-bottom:12px;">' +
'<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>' + '<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>' +
'</svg>' + '</svg>' +
@@ -168,8 +168,8 @@
'<span style="font-size:11px;color:var(--text-3);text-transform:uppercase;">or create new</span>' + '<span style="font-size:11px;color:var(--text-3);text-transform:uppercase;">or create new</span>' +
'<div style="flex:1;height:1px;background:var(--border);"></div>' + '<div style="flex:1;height:1px;background:var(--border);"></div>' +
'</div>' + '</div>' +
'<input type="text" id="editorBootstrapName" class="editor-bootstrap-input" placeholder="Workspace name" value="workspace">' + '<input type="text" id="editorBootstrapName" class="ext-editor-bootstrap-input" placeholder="Workspace name" value="workspace">' +
'<button id="editorBootstrapBtn" class="editor-bootstrap-btn">Create Workspace</button>' + '<button id="editorBootstrapBtn" class="ext-editor-bootstrap-btn">Create Workspace</button>' +
'</div>'; '</div>';
return el; return el;
} }
@@ -218,7 +218,7 @@
} }
workspaces.forEach(ws => { workspaces.forEach(ws => {
const item = document.createElement('button'); const item = document.createElement('button');
item.className = 'editor-ws-dropdown-item' + (ws.id === currentWsId ? ' active' : ''); item.className = 'ext-editor-ws-dropdown-item' + (ws.id === currentWsId ? ' active' : '');
item.textContent = ws.name || ws.id?.slice(0, 8); item.textContent = ws.name || ws.id?.slice(0, 8);
item.addEventListener('click', () => { window.location.href = base + '/s/editor?ws=' + ws.id; }); item.addEventListener('click', () => { window.location.href = base + '/s/editor?ws=' + ws.id; });
listEl.appendChild(item); listEl.appendChild(item);
@@ -243,10 +243,10 @@
listEl.innerHTML = ''; listEl.innerHTML = '';
workspaces.forEach(ws => { workspaces.forEach(ws => {
const item = document.createElement('button'); const item = document.createElement('button');
item.className = 'editor-bootstrap-ws-item'; item.className = 'ext-editor-bootstrap-ws-item';
item.innerHTML = item.innerHTML =
'<span class="editor-bootstrap-ws-name">' + esc(ws.name || ws.id?.slice(0, 8)) + '</span>' + '<span class="ext-editor-bootstrap-ws-name">' + esc(ws.name || ws.id?.slice(0, 8)) + '</span>' +
'<span class="editor-bootstrap-ws-date">' + esc(ws.created_at ? new Date(ws.created_at).toLocaleDateString() : '') + '</span>'; '<span class="ext-editor-bootstrap-ws-date">' + esc(ws.created_at ? new Date(ws.created_at).toLocaleDateString() : '') + '</span>';
item.addEventListener('click', () => { window.location.href = base + '/s/editor?ws=' + ws.id; }); item.addEventListener('click', () => { window.location.href = base + '/s/editor?ws=' + ws.id; });
listEl.appendChild(item); listEl.appendChild(item);
}); });

View File

@@ -1,6 +1,6 @@
/* Git Board — Surface Styles */ /* Git Board — Surface Styles */
.gb-shell { .ext-git-board-shell {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
@@ -9,7 +9,7 @@
/* ── Header ──────────────────────────────── */ /* ── Header ──────────────────────────────── */
.gb-header { .ext-git-board-header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
@@ -18,21 +18,21 @@
flex-shrink: 0; flex-shrink: 0;
} }
.gb-header__left, .ext-git-board-header__left,
.gb-header__right { .ext-git-board-header__right {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
} }
.gb-title { .ext-git-board-title {
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;
color: var(--text); color: var(--text);
margin: 0; margin: 0;
} }
.gb-repo-picker { .ext-git-board-repo-picker {
background: var(--bg-raised); background: var(--bg-raised);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--radius); border-radius: var(--radius);
@@ -45,20 +45,20 @@
/* ── Connection Setup ─────────────────────── */ /* ── Connection Setup ─────────────────────── */
.gb-setup { .ext-git-board-setup {
max-width: 480px; max-width: 480px;
margin: 60px auto; margin: 60px auto;
text-align: center; text-align: center;
padding: 0 16px; padding: 0 16px;
} }
.gb-setup h2 { .ext-git-board-setup h2 {
color: var(--text); color: var(--text);
font-size: 20px; font-size: 20px;
margin: 0 0 8px; margin: 0 0 8px;
} }
.gb-setup p { .ext-git-board-setup p {
color: var(--text-2); color: var(--text-2);
font-size: 14px; font-size: 14px;
line-height: 1.5; line-height: 1.5;
@@ -66,14 +66,14 @@
} }
.gb-setup__hint { .ext-git-board-setup__hint {
font-size: 12px; font-size: 12px;
color: var(--text-3); color: var(--text-3);
} }
/* ── Kanban Board ────────────────────────── */ /* ── Kanban Board ────────────────────────── */
.gb-board { .ext-git-board-board {
display: flex; display: flex;
gap: 12px; gap: 12px;
padding: 12px 16px; padding: 12px 16px;
@@ -82,7 +82,7 @@
overflow-y: hidden; overflow-y: hidden;
} }
.gb-column { .ext-git-board-column {
min-width: 260px; min-width: 260px;
max-width: 320px; max-width: 320px;
flex: 1; flex: 1;
@@ -94,7 +94,7 @@
overflow: hidden; overflow: hidden;
} }
.gb-column__header { .ext-git-board-column__header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
@@ -102,7 +102,7 @@
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
} }
.gb-column__title { .ext-git-board-column__title {
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 600;
color: var(--text); color: var(--text);
@@ -110,7 +110,7 @@
letter-spacing: 0.03em; letter-spacing: 0.03em;
} }
.gb-column__count { .ext-git-board-column__count {
background: var(--bg-raised); background: var(--bg-raised);
color: var(--text-2); color: var(--text-2);
font-size: 11px; font-size: 11px;
@@ -119,7 +119,7 @@
border-radius: 10px; border-radius: 10px;
} }
.gb-column__cards { .ext-git-board-column__cards {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding: 8px; padding: 8px;
@@ -130,7 +130,7 @@
/* ── Cards ───────────────────────────────── */ /* ── Cards ───────────────────────────────── */
.gb-card { .ext-git-board-card {
display: block; display: block;
background: var(--bg); background: var(--bg);
border: 1px solid var(--border); border: 1px solid var(--border);
@@ -142,35 +142,35 @@
cursor: pointer; cursor: pointer;
} }
.gb-card:hover { .ext-git-board-card:hover {
border-color: var(--accent); border-color: var(--accent);
background: var(--bg-hover); background: var(--bg-hover);
} }
.gb-card--pr { .ext-git-board-card--pr {
border-left: 3px solid var(--accent); border-left: 3px solid var(--accent);
} }
.gb-card__header { .ext-git-board-card__header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
margin-bottom: 4px; margin-bottom: 4px;
} }
.gb-card__number { .ext-git-board-card__number {
font-family: var(--mono); font-family: var(--mono);
font-size: 12px; font-size: 12px;
color: var(--text-3); color: var(--text-3);
} }
.gb-card__assignee { .ext-git-board-card__assignee {
font-size: 11px; font-size: 11px;
color: var(--accent); color: var(--accent);
margin-left: auto; margin-left: auto;
} }
.gb-card__branch { .ext-git-board-card__branch {
font-family: var(--mono); font-family: var(--mono);
font-size: 11px; font-size: 11px;
color: var(--text-3); color: var(--text-3);
@@ -181,26 +181,26 @@
max-width: 150px; max-width: 150px;
} }
.gb-card__title { .ext-git-board-card__title {
font-size: 13px; font-size: 13px;
font-weight: 500; font-weight: 500;
line-height: 1.4; line-height: 1.4;
color: var(--text); color: var(--text);
} }
.gb-card__labels { .ext-git-board-card__labels {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 4px; gap: 4px;
margin-top: 6px; margin-top: 6px;
} }
.gb-card__labels .badge { .ext-git-board-card__labels .ext-git-board-badge {
font-size: 10px; font-size: 10px;
padding: 1px 6px; padding: 1px 6px;
} }
.gb-card__meta { .ext-git-board-card__meta {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin-top: 6px; margin-top: 6px;
@@ -210,25 +210,25 @@
/* ── DnD States ─────────────────────────── */ /* ── DnD States ─────────────────────────── */
.gb-card[draggable="true"] { .ext-git-board-card[draggable="true"] {
cursor: grab; cursor: grab;
user-select: none; user-select: none;
} }
.gb-card[draggable="true"]:active { .ext-git-board-card[draggable="true"]:active {
cursor: grabbing; cursor: grabbing;
opacity: 0.6; opacity: 0.6;
} }
.gb-column--dragover { .ext-git-board-column--dragover {
border-color: var(--accent); border-color: var(--accent);
background: color-mix(in srgb, var(--accent) 6%, var(--bg-surface)); background: color-mix(in srgb, var(--accent) 6%, var(--bg-surface));
} }
.gb-column--dragover .gb-column__header { .ext-git-board-column--dragover .ext-git-board-column__header {
border-bottom-color: var(--accent); border-bottom-color: var(--accent);
} }
/* ── Empty state ─────────────────────────── */ /* ── Empty state ─────────────────────────── */
.gb-empty { .ext-git-board-empty {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -239,7 +239,7 @@
/* ── Issue Detail Modal ─────────────────── */ /* ── Issue Detail Modal ─────────────────── */
.gb-modal-overlay { .ext-git-board-modal-overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
background: rgba(0,0,0,0.5); background: rgba(0,0,0,0.5);
@@ -251,7 +251,7 @@
overflow-y: auto; overflow-y: auto;
} }
.gb-modal { .ext-git-board-modal {
background: var(--bg); background: var(--bg);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
@@ -264,7 +264,7 @@
box-shadow: 0 8px 32px rgba(0,0,0,0.3); box-shadow: 0 8px 32px rgba(0,0,0,0.3);
} }
.gb-modal__header { .ext-git-board-modal__header {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
justify-content: space-between; justify-content: space-between;
@@ -273,7 +273,7 @@
flex-shrink: 0; flex-shrink: 0;
} }
.gb-modal__title-row { .ext-git-board-modal__title-row {
display: flex; display: flex;
align-items: baseline; align-items: baseline;
gap: 8px; gap: 8px;
@@ -281,7 +281,7 @@
min-width: 0; min-width: 0;
} }
.gb-modal__title { .ext-git-board-modal__title {
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;
color: var(--text); color: var(--text);
@@ -289,7 +289,7 @@
word-break: break-word; word-break: break-word;
} }
.gb-modal__close { .ext-git-board-modal__close {
background: none; background: none;
border: none; border: none;
color: var(--text-3); color: var(--text-3);
@@ -299,19 +299,19 @@
border-radius: var(--radius); border-radius: var(--radius);
flex-shrink: 0; flex-shrink: 0;
} }
.gb-modal__close:hover { .ext-git-board-modal__close:hover {
color: var(--text); color: var(--text);
background: var(--bg-hover); background: var(--bg-hover);
} }
.gb-modal__body { .ext-git-board-modal__body {
overflow-y: auto; overflow-y: auto;
padding: 16px 20px; padding: 16px 20px;
flex: 1; flex: 1;
min-height: 0; min-height: 0;
} }
.gb-modal__meta { .ext-git-board-modal__meta {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
@@ -321,27 +321,27 @@
color: var(--text-2); color: var(--text-2);
} }
.gb-modal__date { .ext-git-board-modal__date {
color: var(--text-3); color: var(--text-3);
} }
.gb-modal__extlink { .ext-git-board-modal__extlink {
margin-left: auto; margin-left: auto;
color: var(--accent); color: var(--accent);
text-decoration: none; text-decoration: none;
font-size: 12px; font-size: 12px;
} }
.gb-modal__extlink:hover { .ext-git-board-modal__extlink:hover {
text-decoration: underline; text-decoration: underline;
} }
.gb-modal__description { .ext-git-board-modal__description {
margin-bottom: 20px; margin-bottom: 20px;
padding-bottom: 16px; padding-bottom: 16px;
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
} }
.gb-modal__body-text { .ext-git-board-modal__body-text {
font-family: var(--font); font-family: var(--font);
font-size: 13px; font-size: 13px;
line-height: 1.6; line-height: 1.6;
@@ -354,14 +354,14 @@
padding: 0; padding: 0;
} }
.gb-modal__empty { .ext-git-board-modal__empty {
color: var(--text-3); color: var(--text-3);
font-size: 13px; font-size: 13px;
font-style: italic; font-style: italic;
margin: 0; margin: 0;
} }
.gb-modal__section-title { .ext-git-board-modal__section-title {
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 600;
color: var(--text-2); color: var(--text-2);
@@ -372,31 +372,31 @@
/* ── Comments ────────────────────────────── */ /* ── Comments ────────────────────────────── */
.gb-comment { .ext-git-board-comment {
padding: 10px 0; padding: 10px 0;
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
} }
.gb-comment:last-child { .ext-git-board-comment:last-child {
border-bottom: none; border-bottom: none;
} }
.gb-comment__header { .ext-git-board-comment__header {
display: flex; display: flex;
align-items: baseline; align-items: baseline;
gap: 8px; gap: 8px;
margin-bottom: 4px; margin-bottom: 4px;
font-size: 12px; font-size: 12px;
} }
.gb-comment__header strong { .ext-git-board-comment__header strong {
color: var(--accent); color: var(--accent);
} }
.gb-comment__date { .ext-git-board-comment__date {
color: var(--text-3); color: var(--text-3);
font-size: 11px; font-size: 11px;
} }
.gb-comment__body { .ext-git-board-comment__body {
font-size: 13px; font-size: 13px;
line-height: 1.5; line-height: 1.5;
color: var(--text); color: var(--text);
@@ -406,13 +406,13 @@
/* ── Add Comment ─────────────────────────── */ /* ── Add Comment ─────────────────────────── */
.gb-modal__add-comment { .ext-git-board-modal__add-comment {
margin-top: 16px; margin-top: 16px;
padding-top: 16px; padding-top: 16px;
border-top: 1px solid var(--border); border-top: 1px solid var(--border);
} }
.gb-modal__textarea { .ext-git-board-modal__textarea {
width: 100%; width: 100%;
background: var(--bg-raised); background: var(--bg-raised);
border: 1px solid var(--border); border: 1px solid var(--border);
@@ -425,11 +425,11 @@
outline: none; outline: none;
box-sizing: border-box; box-sizing: border-box;
} }
.gb-modal__textarea:focus { .ext-git-board-modal__textarea:focus {
border-color: var(--accent); border-color: var(--accent);
} }
.gb-modal__actions { .ext-git-board-modal__actions {
display: flex; display: flex;
gap: 8px; gap: 8px;
margin-top: 8px; margin-top: 8px;
@@ -438,11 +438,11 @@
/* ── Badge variants ──────────────────────── */ /* ── Badge variants ──────────────────────── */
.badge--green { .ext-git-board-badge--green {
background: var(--green); background: var(--green);
color: #fff; color: #fff;
} }
.badge--muted { .ext-git-board-badge--muted {
background: var(--bg-raised); background: var(--bg-raised);
color: var(--text-3); color: var(--text-3);
} }

View File

@@ -160,21 +160,21 @@
return html` return html`
<div class="user-menu-container" ref=${menuRef}></div> <div class="user-menu-container" ref=${menuRef}></div>
${needsConn ? html`<${ConnectionSetup} />` : html` ${needsConn ? html`<${ConnectionSetup} />` : html`
<div class="gb-shell"> <div class="ext-git-board-shell">
<header class="gb-header"> <header class="ext-git-board-header">
<div class="gb-header__left"> <div class="ext-git-board-header__left">
<h1 class="gb-title">Git Board</h1> <h1 class="ext-git-board-title">Git Board</h1>
<${RepoPicker} repos=${repos} owner=${owner} repo=${repo} <${RepoPicker} repos=${repos} owner=${owner} repo=${repo}
onSelect=${function (o, r) { setOwner(o); setRepo(r); }} /> onSelect=${function (o, r) { setOwner(o); setRepo(r); }} />
</div> </div>
<div class="gb-header__right"> <div class="ext-git-board-header__right">
<button class="sw-btn sw-btn--secondary sw-btn--sm" onClick=${loadBoard} disabled=${loading}> <button class="sw-btn sw-btn--secondary sw-btn--sm" onClick=${loadBoard} disabled=${loading}>
${loading ? '↻' : 'Refresh'} ${loading ? '↻' : 'Refresh'}
</button> </button>
</div> </div>
</header> </header>
${board ? html`<${Board} data=${board} onDrop=${onDrop} onCardClick=${openModal} />` : html` ${board ? html`<${Board} data=${board} onDrop=${onDrop} onCardClick=${openModal} />` : html`
<div class="gb-empty">${loading ? 'Loading…' : 'Select a repository'}</div> <div class="ext-git-board-empty">${loading ? 'Loading…' : 'Select a repository'}</div>
`} `}
</div> </div>
`} `}
@@ -186,11 +186,11 @@
function ConnectionSetup() { function ConnectionSetup() {
return html` return html`
<div class="gb-shell"> <div class="ext-git-board-shell">
<header class="gb-header"> <header class="ext-git-board-header">
<div class="gb-header__left"><h1 class="gb-title">Git Board</h1></div> <div class="ext-git-board-header__left"><h1 class="ext-git-board-title">Git Board</h1></div>
</header> </header>
<div class="gb-setup"> <div class="ext-git-board-setup">
<h2>Connect to Gitea</h2> <h2>Connect to Gitea</h2>
<p>Git Board requires a Gitea connection to fetch repositories, issues, and pull requests.</p> <p>Git Board requires a Gitea connection to fetch repositories, issues, and pull requests.</p>
<p>Ask your admin to add a <strong>Gitea</strong> connection in <p>Ask your admin to add a <strong>Gitea</strong> connection in
@@ -200,7 +200,7 @@
onClick=${function () { window.location.href = base + '/settings'; }}> onClick=${function () { window.location.href = base + '/settings'; }}>
Open Settings Open Settings
</button> </button>
<p class="gb-setup__hint">Connections are managed centrally — no API tokens in extension settings.</p> <p class="ext-git-board-setup__hint">Connections are managed centrally — no API tokens in extension settings.</p>
</div> </div>
</div> </div>
`; `;
@@ -211,7 +211,7 @@
function RepoPicker({ repos, owner, repo, onSelect }) { function RepoPicker({ repos, owner, repo, onSelect }) {
var current = owner + '/' + repo; var current = owner + '/' + repo;
return html` return html`
<select class="gb-repo-picker" value=${current} <select class="ext-git-board-repo-picker" value=${current}
onChange=${function (e) { onChange=${function (e) {
var parts = e.target.value.split('/'); var parts = e.target.value.split('/');
onSelect(parts[0], parts.slice(1).join('/')); onSelect(parts[0], parts.slice(1).join('/'));
@@ -233,7 +233,7 @@
var inProgress = issues.filter(function (i) { return i.state === 'open' && !!i.assignee; }); var inProgress = issues.filter(function (i) { return i.state === 'open' && !!i.assignee; });
return html` return html`
<div class="gb-board"> <div class="ext-git-board-board">
<${Column} id="open" title="Open" count=${openUnassigned.length} onDrop=${onDrop}> <${Column} id="open" title="Open" count=${openUnassigned.length} onDrop=${onDrop}>
${openUnassigned.map(function (i) { ${openUnassigned.map(function (i) {
return html`<${IssueCard} key=${i.number} issue=${i} onClick=${onCardClick} />`; return html`<${IssueCard} key=${i.number} issue=${i} onClick=${onCardClick} />`;
@@ -268,19 +268,19 @@
} : {}; } : {};
return html` return html`
<div class="gb-column ${over ? 'gb-column--dragover' : ''}" ...${handlers}> <div class="ext-git-board-column ${over ? 'ext-git-board-column--dragover' : ''}" ...${handlers}>
<div class="gb-column__header"> <div class="ext-git-board-column__header">
<span class="gb-column__title">${title}</span> <span class="ext-git-board-column__title">${title}</span>
<span class="gb-column__count">${count || 0}</span> <span class="ext-git-board-column__count">${count || 0}</span>
</div> </div>
<div class="gb-column__cards">${children}</div> <div class="ext-git-board-column__cards">${children}</div>
</div> </div>
`; `;
} }
function IssueCard({ issue, onClick }) { function IssueCard({ issue, onClick }) {
return html` return html`
<div class="gb-card" draggable="true" <div class="ext-git-board-card" draggable="true"
onDragStart=${function (e) { onDragStart=${function (e) {
e.dataTransfer.setData('text/plain', String(issue.number)); e.dataTransfer.setData('text/plain', String(issue.number));
e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.effectAllowed = 'move';
@@ -289,14 +289,14 @@
e.preventDefault(); e.preventDefault();
if (onClick) onClick(issue.number); if (onClick) onClick(issue.number);
}}> }}>
<div class="gb-card__header"> <div class="ext-git-board-card__header">
<span class="gb-card__number">#${issue.number}</span> <span class="ext-git-board-card__number">#${issue.number}</span>
${issue.assignee && html`<span class="gb-card__assignee">@${esc(issue.assignee)}</span>`} ${issue.assignee && html`<span class="ext-git-board-card__assignee">@${esc(issue.assignee)}</span>`}
</div> </div>
<div class="gb-card__title">${esc(issue.title)}</div> <div class="ext-git-board-card__title">${esc(issue.title)}</div>
<div class="gb-card__labels"> <div class="ext-git-board-card__labels">
${(issue.labels || []).map(function (l) { ${(issue.labels || []).map(function (l) {
return html`<span key=${l} class="badge">${esc(l)}</span>`; return html`<span key=${l} class="ext-git-board-badge">${esc(l)}</span>`;
})} })}
</div> </div>
</div> </div>
@@ -305,13 +305,13 @@
function PRCard({ pr }) { function PRCard({ pr }) {
return html` return html`
<a class="gb-card gb-card--pr" href=${pr.html_url} target="_blank" rel="noopener"> <a class="ext-git-board-card ext-git-board-card--pr" href=${pr.html_url} target="_blank" rel="noopener">
<div class="gb-card__header"> <div class="ext-git-board-card__header">
<span class="gb-card__number">#${pr.number}</span> <span class="ext-git-board-card__number">#${pr.number}</span>
<span class="gb-card__branch">${esc(pr.head)}${esc(pr.base)}</span> <span class="ext-git-board-card__branch">${esc(pr.head)}${esc(pr.base)}</span>
</div> </div>
<div class="gb-card__title">${esc(pr.title)}</div> <div class="ext-git-board-card__title">${esc(pr.title)}</div>
<div class="gb-card__meta"> <div class="ext-git-board-card__meta">
<span>@${esc(pr.user)}</span> <span>@${esc(pr.user)}</span>
<span>${timeAgo(pr.created_at)}</span> <span>${timeAgo(pr.created_at)}</span>
</div> </div>
@@ -387,68 +387,68 @@
}, [issue, owner, repo, number]); }, [issue, owner, repo, number]);
return html` return html`
<div class="gb-modal-overlay" onClick=${function (e) { <div class="ext-git-board-modal-overlay" onClick=${function (e) {
if (e.target.classList.contains('gb-modal-overlay')) onClose(changed); if (e.target.classList.contains('ext-git-board-modal-overlay')) onClose(changed);
}}> }}>
<div class="gb-modal"> <div class="ext-git-board-modal">
<div class="gb-modal__header"> <div class="ext-git-board-modal__header">
<div class="gb-modal__title-row"> <div class="ext-git-board-modal__title-row">
${loading ? 'Loading…' : html` ${loading ? 'Loading…' : html`
<span class="gb-card__number">#${number}</span> <span class="ext-git-board-card__number">#${number}</span>
<h2 class="gb-modal__title">${esc(issue && issue.title)}</h2> <h2 class="ext-git-board-modal__title">${esc(issue && issue.title)}</h2>
`} `}
</div> </div>
<button class="gb-modal__close" onClick=${function () { onClose(changed); }}>✕</button> <button class="ext-git-board-modal__close" onClick=${function () { onClose(changed); }}>✕</button>
</div> </div>
${!loading && issue && html` ${!loading && issue && html`
<div class="gb-modal__body" ref=${bodyRef}> <div class="ext-git-board-modal__body" ref=${bodyRef}>
<div class="gb-modal__meta"> <div class="ext-git-board-modal__meta">
<span class="badge ${issue.state === 'open' ? 'badge--green' : 'badge--muted'}">${issue.state}</span> <span class="ext-git-board-badge ${issue.state === 'open' ? 'ext-git-board-badge--green' : 'ext-git-board-badge--muted'}">${issue.state}</span>
${issue.assignee && html`<span class="gb-card__assignee">@${esc(issue.assignee)}</span>`} ${issue.assignee && html`<span class="ext-git-board-card__assignee">@${esc(issue.assignee)}</span>`}
${issue.created_at && html`<span class="gb-modal__date">${new Date(issue.created_at).toLocaleDateString()}</span>`} ${issue.created_at && html`<span class="ext-git-board-modal__date">${new Date(issue.created_at).toLocaleDateString()}</span>`}
<a href=${issue.html_url || '#'} target="_blank" rel="noopener" <a href=${issue.html_url || '#'} target="_blank" rel="noopener"
class="gb-modal__extlink">Open in Gitea ↗</a> class="ext-git-board-modal__extlink">Open in Gitea ↗</a>
</div> </div>
${issue.labels && issue.labels.length > 0 && html` ${issue.labels && issue.labels.length > 0 && html`
<div class="gb-card__labels" style="margin-bottom:12px;"> <div class="ext-git-board-card__labels" style="margin-bottom:12px;">
${issue.labels.map(function (l) { return html`<span key=${l} class="badge">${esc(l)}</span>`; })} ${issue.labels.map(function (l) { return html`<span key=${l} class="ext-git-board-badge">${esc(l)}</span>`; })}
</div> </div>
`} `}
<div class="gb-modal__description"> <div class="ext-git-board-modal__description">
${issue.body ? html`<pre class="gb-modal__body-text">${esc(issue.body)}</pre>` ${issue.body ? html`<pre class="ext-git-board-modal__body-text">${esc(issue.body)}</pre>`
: html`<p class="gb-modal__empty">No description.</p>`} : html`<p class="ext-git-board-modal__empty">No description.</p>`}
</div> </div>
<div class="gb-modal__comments"> <div class="ext-git-board-modal__comments">
<h3 class="gb-modal__section-title">Comments (${(issue.comments || []).length})</h3> <h3 class="ext-git-board-modal__section-title">Comments (${(issue.comments || []).length})</h3>
${(issue.comments || []).length === 0 && html` ${(issue.comments || []).length === 0 && html`
<p class="gb-modal__empty">No comments yet.</p> <p class="ext-git-board-modal__empty">No comments yet.</p>
`} `}
${(issue.comments || []).map(function (c, i) { ${(issue.comments || []).map(function (c, i) {
return html` return html`
<div class="gb-comment" key=${i}> <div class="ext-git-board-comment" key=${i}>
<div class="gb-comment__header"> <div class="ext-git-board-comment__header">
<strong>@${esc(c.user)}</strong> <strong>@${esc(c.user)}</strong>
<span class="gb-comment__date">${timeAgo(c.created_at)}</span> <span class="ext-git-board-comment__date">${timeAgo(c.created_at)}</span>
</div> </div>
<div class="gb-comment__body">${esc(c.body)}</div> <div class="ext-git-board-comment__body">${esc(c.body)}</div>
</div> </div>
`; `;
})} })}
</div> </div>
<div class="gb-modal__add-comment"> <div class="ext-git-board-modal__add-comment">
<textarea class="gb-modal__textarea" rows="3" <textarea class="ext-git-board-modal__textarea" rows="3"
placeholder="Add a comment…" placeholder="Add a comment…"
value=${comment} value=${comment}
onInput=${function (e) { setComment(e.target.value); }} onInput=${function (e) { setComment(e.target.value); }}
onKeyDown=${function (e) { onKeyDown=${function (e) {
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) postComment(); if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) postComment();
}} /> }} />
<div class="gb-modal__actions"> <div class="ext-git-board-modal__actions">
<button class="sw-btn sw-btn--primary sw-btn--sm" disabled=${posting || !comment.trim()} <button class="sw-btn sw-btn--primary sw-btn--sm" disabled=${posting || !comment.trim()}
onClick=${postComment}> onClick=${postComment}>
${posting ? 'Posting…' : 'Comment'} ${posting ? 'Posting…' : 'Comment'}

View File

@@ -2,16 +2,16 @@
Uses CSS custom properties from the platform theme system (variables.css). Uses CSS custom properties from the platform theme system (variables.css).
See EXTENSION-SURFACES.md for the full property reference. */ See EXTENSION-SURFACES.md for the full property reference. */
.hello-dashboard { max-width: 720px; margin: 0 auto; padding: 40px 24px; } .ext-hello-dashboard { max-width: 720px; margin: 0 auto; padding: 40px 24px; }
.hello-header { margin-bottom: 32px; } .ext-hello-dashboard-header { margin-bottom: 32px; }
.hello-header h1 { font-size: 28px; font-weight: 700; color: var(--text); margin: 0 0 8px 0; } .ext-hello-dashboard-header h1 { font-size: 28px; font-weight: 700; color: var(--text); margin: 0 0 8px 0; }
.hello-subtitle { font-size: 14px; color: var(--text-2); margin: 0; } .ext-hello-dashboard-subtitle { font-size: 14px; color: var(--text-2); margin: 0; }
.hello-subtitle code, .ext-hello-dashboard-subtitle code,
.hello-card-value code { background: var(--bg-raised); padding: 2px 6px; border-radius: 4px; font-size: 13px; } .ext-hello-dashboard-card-value code { background: var(--bg-raised); padding: 2px 6px; border-radius: 4px; font-size: 13px; }
.hello-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; } .ext-hello-dashboard-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; }
.hello-card { background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 16px; } .ext-hello-dashboard-card { background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 16px; }
.hello-card-title { font-size: 12px; font-weight: 600; color: var(--text-2); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; } .ext-hello-dashboard-card-title { font-size: 12px; font-weight: 600; color: var(--text-2); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; }
.hello-card-value { font-size: 20px; font-weight: 600; color: var(--text); margin-bottom: 4px; } .ext-hello-dashboard-card-value { font-size: 20px; font-weight: 600; color: var(--text); margin-bottom: 4px; }
.hello-card-detail { font-size: 12px; color: var(--text-3); } .ext-hello-dashboard-card-detail { font-size: 12px; color: var(--text-3); }
.hello-actions { display: flex; gap: 12px; margin-bottom: 24px; } .ext-hello-dashboard-actions { display: flex; gap: 12px; margin-bottom: 24px; }
.hello-manifest { background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; font-size: 12px; color: var(--text-2); overflow-x: auto; white-space: pre-wrap; font-family: var(--mono); line-height: 1.5; } .ext-hello-dashboard-manifest { background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; font-size: 12px; color: var(--text-2); overflow-x: auto; white-space: pre-wrap; font-family: var(--mono); line-height: 1.5; }

View File

@@ -20,12 +20,12 @@
var name = user.display_name || user.username || 'World'; var name = user.display_name || user.username || 'World';
mount.innerHTML = mount.innerHTML =
'<div class="hello-dashboard">' + '<div class="ext-hello-dashboard">' +
'<div class="hello-header">' + '<div class="ext-hello-dashboard-header">' +
'<h1>Hello, ' + esc(name) + '!</h1>' + '<h1>Hello, ' + esc(name) + '!</h1>' +
'<p class="hello-subtitle">Extension surface <code>' + esc(manifest.id || 'unknown') + '</code> loaded successfully.</p>' + '<p class="ext-hello-dashboard-subtitle">Extension surface <code>' + esc(manifest.id || 'unknown') + '</code> loaded successfully.</p>' +
'</div>' + '</div>' +
'<div class="hello-cards">' + '<div class="ext-hello-dashboard-cards">' +
card('Platform Access', (typeof UI !== 'undefined' ? '\u2713 Connected' : '\u2717 Unavailable'), card('Platform Access', (typeof UI !== 'undefined' ? '\u2713 Connected' : '\u2717 Unavailable'),
'API, Theme, UI primitives available', 'var(--accent)') + 'API, Theme, UI primitives available', 'var(--accent)') +
card('Theme', isDark ? '\uD83C\uDF19 Dark' : '\u2600\uFE0F Light', card('Theme', isDark ? '\uD83C\uDF19 Dark' : '\u2600\uFE0F Light',
@@ -33,11 +33,11 @@
card('Route', '<code>' + esc(manifest.route || '/s/hello-dashboard') + '</code>', card('Route', '<code>' + esc(manifest.route || '/s/hello-dashboard') + '</code>',
'Registered via surface manifest', '') + 'Registered via surface manifest', '') +
'</div>' + '</div>' +
'<div class="hello-actions">' + '<div class="ext-hello-dashboard-actions">' +
'<button class="sw-btn sw-btn--primary sw-btn--md" id="helloToast">Show Toast</button>' + '<button class="sw-btn sw-btn--primary sw-btn--md" id="helloToast">Show Toast</button>' +
'<button class="sw-btn sw-btn--secondary sw-btn--md" id="helloApi">Test API</button>' + '<button class="sw-btn sw-btn--secondary sw-btn--md" id="helloApi">Test API</button>' +
'</div>' + '</div>' +
'<pre class="hello-manifest">' + esc(JSON.stringify(manifest, null, 2)) + '</pre>' + '<pre class="ext-hello-dashboard-manifest">' + esc(JSON.stringify(manifest, null, 2)) + '</pre>' +
'</div>'; '</div>';
// Wire toast button // Wire toast button
@@ -69,10 +69,10 @@
function card(title, value, detail, color) { function card(title, value, detail, color) {
var style = color ? ' style="color:' + color + ';"' : ''; var style = color ? ' style="color:' + color + ';"' : '';
return '<div class="hello-card">' + return '<div class="ext-hello-dashboard-card">' +
'<div class="hello-card-title">' + esc(title) + '</div>' + '<div class="ext-hello-dashboard-card-title">' + esc(title) + '</div>' +
'<div class="hello-card-value"' + style + '>' + value + '</div>' + '<div class="ext-hello-dashboard-card-value"' + style + '>' + value + '</div>' +
'<div class="hello-card-detail">' + esc(detail) + '</div>' + '<div class="ext-hello-dashboard-card-detail">' + esc(detail) + '</div>' +
'</div>'; '</div>';
} }

View File

@@ -1,38 +1,38 @@
/* ICD Test Runner — Surface Styles */ /* ICD Test Runner — Surface Styles */
#extension-mount { .ext-icd-test-runner-root {
font-family: var(--font, 'DM Sans', sans-serif); font-family: var(--font, 'DM Sans', sans-serif);
} }
#extension-mount h1 { .ext-icd-test-runner-root h1 {
font-family: var(--font, 'DM Sans', sans-serif); font-family: var(--font, 'DM Sans', sans-serif);
letter-spacing: -0.3px; letter-spacing: -0.3px;
} }
#extension-mount table { .ext-icd-test-runner-root table {
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
#extension-mount table td, .ext-icd-test-runner-root table td,
#extension-mount table th { .ext-icd-test-runner-root table th {
border-color: var(--border); border-color: var(--border);
} }
#extension-mount table tbody tr:last-child { .ext-icd-test-runner-root table tbody tr:last-child {
border-bottom: none; border-bottom: none;
} }
#extension-mount table tbody tr:hover { .ext-icd-test-runner-root table tbody tr:hover {
background: var(--bg-hover) !important; background: var(--bg-hover) !important;
} }
/* Buttons — uses kernel sw-btn system, minor overrides for weight */ /* Buttons — uses kernel sw-btn system, minor overrides for weight */
#extension-mount .sw-btn { .ext-icd-test-runner-root .sw-btn {
font-weight: 600; font-weight: 600;
} }
/* Status dot animation for running state */ /* Status dot animation for running state */
@keyframes pulse-dot { @keyframes ext-icd-test-runner-pulse-dot {
0%, 100% { opacity: 1; } 0%, 100% { opacity: 1; }
50% { opacity: 0.4; } 50% { opacity: 0.4; }
} }

View File

@@ -24,6 +24,7 @@
var mount = document.getElementById('extension-mount'); var mount = document.getElementById('extension-mount');
if (!mount) return; if (!mount) return;
mount.classList.add('ext-icd-test-runner-root');
// ─── Boot Preact SDK (v0.37.14) ─────────────────────────── // ─── Boot Preact SDK (v0.37.14) ───────────────────────────
// Extension surfaces don't load Preact vendors or the SDK. // Extension surfaces don't load Preact vendors or the SDK.

View File

@@ -337,7 +337,7 @@
await T.test('sdk', 'pipe-render', 'Render filter modifies DOM', async function () { await T.test('sdk', 'pipe-render', 'Render filter modifies DOM', async function () {
sw.pipe.render(1, function (ctx) { sw.pipe.render(1, function (ctx) {
if (ctx.element.dataset.sdkRenderTest) { if (ctx.element.dataset.sdkRenderTest) {
ctx.element.classList.add('sdk-render-modified'); ctx.element.classList.add('ext-icd-test-runner-sdk-render-modified');
} }
return ctx; return ctx;
}, { source: '_test-render-dom' }); }, { source: '_test-render-dom' });
@@ -348,7 +348,7 @@
element: el, message: null, element: el, message: null,
channel: { id: 'fake', type: 'direct' } channel: { id: 'fake', type: 'direct' }
}); });
T.assert(el.classList.contains('sdk-render-modified'), 'element should have class'); T.assert(el.classList.contains('ext-icd-test-runner-sdk-render-modified'), 'element should have class');
}); });
await T.test('sdk', 'pipe-render', 'Priority ordering in render chain', async function () { await T.test('sdk', 'pipe-render', 'Priority ordering in render chain', async function () {

View File

@@ -1,13 +1,13 @@
/* ── Notes Surface Styles ─────────────────── */ /* ── Notes Surface Styles ─────────────────── */
.notes-app { .ext-notes-app {
display: flex; display: flex;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
} }
/* ── Sidebar ─────────────────────────────── */ /* ── Sidebar ─────────────────────────────── */
.notes-sidebar { .ext-notes-sidebar {
width: 280px; width: 280px;
min-width: 220px; min-width: 220px;
border-right: 1px solid var(--border); border-right: 1px solid var(--border);
@@ -16,7 +16,7 @@
background: var(--bg-raised); background: var(--bg-raised);
flex-shrink: 0; flex-shrink: 0;
} }
.notes-sidebar__header { .ext-notes-sidebar__header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
@@ -24,30 +24,30 @@
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
flex-shrink: 0; flex-shrink: 0;
} }
.notes-sidebar__title { .ext-notes-sidebar__title {
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
color: var(--text); color: var(--text);
flex: 1; flex: 1;
} }
.notes-sidebar__count { .ext-notes-sidebar__count {
font-size: 12px; font-size: 12px;
color: var(--text-3); color: var(--text-3);
} }
.notes-sidebar__actions { .ext-notes-sidebar__actions {
display: flex; display: flex;
gap: 4px; gap: 4px;
} }
/* ── Folder Tree ────────────────────────── */ /* ── Folder Tree ────────────────────────── */
.folder-tree { .ext-notes-folder-tree {
padding: 4px 0; padding: 4px 0;
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
flex-shrink: 0; flex-shrink: 0;
max-height: 40%; max-height: 40%;
overflow-y: auto; overflow-y: auto;
} }
.folder-tree__item { .ext-notes-folder-tree__item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
@@ -60,33 +60,33 @@
transition: var(--transition); transition: var(--transition);
user-select: none; user-select: none;
} }
.folder-tree__item:hover { .ext-notes-folder-tree__item:hover {
background: var(--bg-hover); background: var(--bg-hover);
color: var(--text); color: var(--text);
} }
.folder-tree__item--active { .ext-notes-folder-tree__item--active {
background: var(--bg-active); background: var(--bg-active);
color: var(--text); color: var(--text);
font-weight: 500; font-weight: 500;
} }
.folder-tree__toggle { .ext-notes-folder-tree__toggle {
width: 14px; width: 14px;
font-size: 9px; font-size: 9px;
text-align: center; text-align: center;
flex-shrink: 0; flex-shrink: 0;
color: var(--text-3); color: var(--text-3);
} }
.folder-tree__icon { .ext-notes-folder-tree__icon {
font-size: 13px; font-size: 13px;
flex-shrink: 0; flex-shrink: 0;
} }
.folder-tree__name { .ext-notes-folder-tree__name {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
flex: 1; flex: 1;
} }
.folder-tree__edit { .ext-notes-folder-tree__edit {
flex: 1; flex: 1;
font-size: 13px; font-size: 13px;
padding: 1px 4px; padding: 1px 4px;
@@ -97,23 +97,23 @@
font-family: var(--font); font-family: var(--font);
outline: none; outline: none;
} }
.folder-tree__add { .ext-notes-folder-tree__add {
padding: 6px 12px; padding: 6px 12px;
font-size: 12px; font-size: 12px;
color: var(--text-3); color: var(--text-3);
cursor: pointer; cursor: pointer;
transition: var(--transition); transition: var(--transition);
} }
.folder-tree__add:hover { .ext-notes-folder-tree__add:hover {
color: var(--accent); color: var(--accent);
} }
/* ── Search ──────────────────────────────── */ /* ── Search ──────────────────────────────── */
.notes-search { .ext-notes-search {
padding: 8px 14px; padding: 8px 14px;
flex-shrink: 0; flex-shrink: 0;
} }
.notes-search input { .ext-notes-search input {
width: 100%; width: 100%;
padding: 6px 10px; padding: 6px 10px;
font-size: 13px; font-size: 13px;
@@ -123,27 +123,27 @@
border-radius: var(--radius); border-radius: var(--radius);
font-family: var(--font); font-family: var(--font);
} }
.notes-search input:focus { .ext-notes-search input:focus {
outline: none; outline: none;
border-color: var(--accent); border-color: var(--accent);
} }
.notes-search input::placeholder { .ext-notes-search input::placeholder {
color: var(--text-3); color: var(--text-3);
} }
/* ── Note List ───────────────────────────── */ /* ── Note List ───────────────────────────── */
.notes-list { .ext-notes-list {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding: 4px 8px; padding: 4px 8px;
} }
.notes-list__empty { .ext-notes-list__empty {
padding: 40px 16px; padding: 40px 16px;
text-align: center; text-align: center;
color: var(--text-3); color: var(--text-3);
font-size: 13px; font-size: 13px;
} }
.notes-list__loading { .ext-notes-list__loading {
padding: 40px 0; padding: 40px 0;
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -151,20 +151,20 @@
} }
/* ── Note Card (sidebar item) ────────────── */ /* ── Note Card (sidebar item) ────────────── */
.note-card { .ext-notes-note-card {
padding: 10px 12px; padding: 10px 12px;
border-radius: var(--radius); border-radius: var(--radius);
cursor: pointer; cursor: pointer;
transition: var(--transition); transition: var(--transition);
margin-bottom: 2px; margin-bottom: 2px;
} }
.note-card:hover { .ext-notes-note-card:hover {
background: var(--bg-hover); background: var(--bg-hover);
} }
.note-card--active { .ext-notes-note-card--active {
background: var(--bg-active); background: var(--bg-active);
} }
.note-card__title { .ext-notes-note-card__title {
font-size: 13px; font-size: 13px;
font-weight: 500; font-weight: 500;
color: var(--text); color: var(--text);
@@ -175,11 +175,11 @@
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.note-card__pin { .ext-notes-note-card__pin {
font-size: 11px; font-size: 11px;
flex-shrink: 0; flex-shrink: 0;
} }
.note-card__snippet { .ext-notes-note-card__snippet {
font-size: 12px; font-size: 12px;
color: var(--text-3); color: var(--text-3);
margin-top: 3px; margin-top: 3px;
@@ -188,21 +188,21 @@
white-space: nowrap; white-space: nowrap;
line-height: 1.4; line-height: 1.4;
} }
.note-card__date { .ext-notes-note-card__date {
font-size: 11px; font-size: 11px;
color: var(--text-3); color: var(--text-3);
margin-top: 3px; margin-top: 3px;
} }
/* ── Editor Pane ─────────────────────────── */ /* ── Editor Pane ─────────────────────────── */
.notes-editor { .ext-notes-editor {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;
} }
.notes-editor__empty { .ext-notes-editor__empty {
flex: 1; flex: 1;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -212,7 +212,7 @@
} }
/* ── Editor Header ───────────────────────── */ /* ── Editor Header ───────────────────────── */
.notes-editor__header { .ext-notes-editor__header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
@@ -220,7 +220,7 @@
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
flex-shrink: 0; flex-shrink: 0;
} }
.notes-editor__title-input { .ext-notes-editor__title-input {
flex: 1; flex: 1;
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;
@@ -231,17 +231,17 @@
padding: 4px 0; padding: 4px 0;
font-family: var(--font); font-family: var(--font);
} }
.notes-editor__title-input::placeholder { .ext-notes-editor__title-input::placeholder {
color: var(--text-3); color: var(--text-3);
} }
.notes-editor__actions { .ext-notes-editor__actions {
display: flex; display: flex;
gap: 6px; gap: 6px;
align-items: center; align-items: center;
} }
/* ── Folder select in editor ────────────── */ /* ── Folder select in editor ────────────── */
.notes-editor__folder-select { .ext-notes-editor__folder-select {
font-size: 12px; font-size: 12px;
padding: 3px 6px; padding: 3px 6px;
border: 1px solid var(--border); border: 1px solid var(--border);
@@ -252,13 +252,13 @@
cursor: pointer; cursor: pointer;
max-width: 140px; max-width: 140px;
} }
.notes-editor__folder-select:focus { .ext-notes-editor__folder-select:focus {
outline: none; outline: none;
border-color: var(--accent); border-color: var(--accent);
} }
/* ── Editor Content (body + outline wrapper) */ /* ── Editor Content (body + outline wrapper) */
.notes-editor__content { .ext-notes-editor__content {
flex: 1; flex: 1;
display: flex; display: flex;
min-height: 0; min-height: 0;
@@ -266,20 +266,20 @@
} }
/* ── Editor Body ─────────────────────────── */ /* ── Editor Body ─────────────────────────── */
.notes-editor__body { .ext-notes-editor__body {
flex: 1; flex: 1;
display: flex; display: flex;
min-height: 0; min-height: 0;
overflow: hidden; overflow: hidden;
} }
.notes-editor__body--split { .ext-notes-editor__body--split {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
} }
.notes-editor__body--split .notes-preview { .ext-notes-editor__body--split .ext-notes-preview {
border-left: 1px solid var(--border); border-left: 1px solid var(--border);
} }
.notes-editor__textarea { .ext-notes-editor__textarea {
flex: 1; flex: 1;
resize: none; resize: none;
padding: 20px; padding: 20px;
@@ -292,39 +292,39 @@
font-family: var(--mono); font-family: var(--mono);
tab-size: 2; tab-size: 2;
} }
.notes-editor__textarea::placeholder { .ext-notes-editor__textarea::placeholder {
color: var(--text-3); color: var(--text-3);
} }
/* ── CodeMirror 6 container ─────────────── */ /* ── CodeMirror 6 container ─────────────── */
.notes-editor__cm { .ext-notes-editor__cm {
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
} }
.notes-editor__cm .cm-editor { .ext-notes-editor__cm .cm-editor {
height: 100%; height: 100%;
max-height: none; max-height: none;
} }
.notes-editor__cm .cm-scroller { .ext-notes-editor__cm .cm-scroller {
overflow: auto; overflow: auto;
padding: 12px 20px; padding: 12px 20px;
} }
/* ── Preview ─────────────────────────────── */ /* ── Preview ─────────────────────────────── */
.notes-preview { .ext-notes-preview {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding: 20px; padding: 20px;
border-left: 1px solid var(--border); border-left: 1px solid var(--border);
background: var(--bg-surface); background: var(--bg-surface);
} }
.notes-preview h1 { font-size: 24px; font-weight: 700; color: var(--text); margin: 0 0 12px; } .ext-notes-preview h1 { font-size: 24px; font-weight: 700; color: var(--text); margin: 0 0 12px; }
.notes-preview h2 { font-size: 20px; font-weight: 600; color: var(--text); margin: 20px 0 8px; } .ext-notes-preview h2 { font-size: 20px; font-weight: 600; color: var(--text); margin: 20px 0 8px; }
.notes-preview h3 { font-size: 16px; font-weight: 600; color: var(--text); margin: 16px 0 6px; } .ext-notes-preview h3 { font-size: 16px; font-weight: 600; color: var(--text); margin: 16px 0 6px; }
.notes-preview p { font-size: 14px; color: var(--text); line-height: 1.7; margin: 0 0 10px; } .ext-notes-preview p { font-size: 14px; color: var(--text); line-height: 1.7; margin: 0 0 10px; }
.notes-preview ul, .notes-preview ol { padding-left: 24px; margin: 0 0 10px; color: var(--text); font-size: 14px; line-height: 1.7; } .ext-notes-preview ul, .ext-notes-preview ol { padding-left: 24px; margin: 0 0 10px; color: var(--text); font-size: 14px; line-height: 1.7; }
.notes-preview li { margin-bottom: 2px; } .ext-notes-preview li { margin-bottom: 2px; }
.notes-preview code { .ext-notes-preview code {
font-family: var(--mono); font-family: var(--mono);
font-size: 13px; font-size: 13px;
background: var(--bg-raised); background: var(--bg-raised);
@@ -332,7 +332,7 @@
border-radius: 3px; border-radius: 3px;
color: var(--accent); color: var(--accent);
} }
.notes-preview pre { .ext-notes-preview pre {
background: var(--bg-raised); background: var(--bg-raised);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--radius); border-radius: var(--radius);
@@ -340,30 +340,30 @@
overflow-x: auto; overflow-x: auto;
margin: 0 0 12px; margin: 0 0 12px;
} }
.notes-preview pre code { .ext-notes-preview pre code {
background: none; background: none;
padding: 0; padding: 0;
color: var(--text); color: var(--text);
font-size: 13px; font-size: 13px;
} }
.notes-preview blockquote { .ext-notes-preview blockquote {
border-left: 3px solid var(--accent); border-left: 3px solid var(--accent);
margin: 0 0 12px; margin: 0 0 12px;
padding: 4px 16px; padding: 4px 16px;
color: var(--text-2); color: var(--text-2);
} }
.notes-preview a { color: var(--accent); text-decoration: none; } .ext-notes-preview a { color: var(--accent); text-decoration: none; }
.notes-preview a:hover { text-decoration: underline; } .ext-notes-preview a:hover { text-decoration: underline; }
.notes-preview hr { .ext-notes-preview hr {
border: none; border: none;
border-top: 1px solid var(--border); border-top: 1px solid var(--border);
margin: 16px 0; margin: 16px 0;
} }
.notes-preview strong { font-weight: 600; } .ext-notes-preview strong { font-weight: 600; }
.notes-preview em { font-style: italic; } .ext-notes-preview em { font-style: italic; }
/* ── Toggle button ───────────────────────── */ /* ── Toggle button ───────────────────────── */
.notes-toggle { .ext-notes-toggle {
padding: 4px 10px; padding: 4px 10px;
font-size: 12px; font-size: 12px;
border: 1px solid var(--border); border: 1px solid var(--border);
@@ -373,11 +373,11 @@
cursor: pointer; cursor: pointer;
transition: var(--transition); transition: var(--transition);
} }
.notes-toggle:hover { background: var(--bg-hover); color: var(--text); } .ext-notes-toggle:hover { background: var(--bg-hover); color: var(--text); }
.notes-toggle--active { background: var(--accent); color: #fff; border-color: var(--accent); } .ext-notes-toggle--active { background: var(--accent); color: #fff; border-color: var(--accent); }
/* ── Inline buttons ──────────────────────── */ /* ── Inline buttons ──────────────────────── */
.notes-btn { .ext-notes-btn {
border: none; border: none;
background: var(--bg-raised); background: var(--bg-raised);
color: var(--text-2); color: var(--text-2);
@@ -387,23 +387,23 @@
padding: 4px 10px; padding: 4px 10px;
font-size: 13px; font-size: 13px;
} }
.notes-btn:hover { background: var(--bg-hover); color: var(--text); } .ext-notes-btn:hover { background: var(--bg-hover); color: var(--text); }
.notes-btn--danger:hover { background: var(--danger-dim); color: var(--danger); } .ext-notes-btn--danger:hover { background: var(--danger-dim); color: var(--danger); }
.notes-btn--accent { background: var(--accent); color: #fff; } .ext-notes-btn--accent { background: var(--accent); color: #fff; }
.notes-btn--accent:hover { opacity: 0.9; color: #fff; } .ext-notes-btn--accent:hover { opacity: 0.9; color: #fff; }
/* ── Saved indicator ─────────────────────── */ /* ── Saved indicator ─────────────────────── */
.notes-saved { .ext-notes-saved {
font-size: 12px; font-size: 12px;
color: var(--text-3); color: var(--text-3);
padding: 2px 8px; padding: 2px 8px;
} }
.notes-saved--dirty { .ext-notes-saved--dirty {
color: var(--warning); color: var(--warning);
} }
/* ── Tag Pills (shared) ─────────────────── */ /* ── Tag Pills (shared) ─────────────────── */
.tag-pill { .ext-notes-tag-pill {
display: inline-block; display: inline-block;
padding: 1px 8px; padding: 1px 8px;
font-size: 11px; font-size: 11px;
@@ -417,23 +417,23 @@
line-height: 1.6; line-height: 1.6;
transition: var(--transition); transition: var(--transition);
} }
.tag-pill:hover { background: var(--bg-hover); color: var(--text); } .ext-notes-tag-pill:hover { background: var(--bg-hover); color: var(--text); }
.tag-pill--active { background: var(--accent); color: #fff; border-color: var(--accent); } .ext-notes-tag-pill--active { background: var(--accent); color: #fff; border-color: var(--accent); }
.tag-pill__remove { .ext-notes-tag-pill__remove {
margin-left: 4px; margin-left: 4px;
font-size: 10px; font-size: 10px;
opacity: 0.6; opacity: 0.6;
cursor: pointer; cursor: pointer;
} }
.tag-pill__remove:hover { opacity: 1; } .ext-notes-tag-pill__remove:hover { opacity: 1; }
/* ── Tag Filter (sidebar) ──────────────── */ /* ── Tag Filter (sidebar) ──────────────── */
.tag-filter { .ext-notes-tag-filter {
padding: 6px 14px 4px; padding: 6px 14px 4px;
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
flex-shrink: 0; flex-shrink: 0;
} }
.tag-filter__label { .ext-notes-tag-filter__label {
font-size: 11px; font-size: 11px;
color: var(--text-3); color: var(--text-3);
margin-bottom: 3px; margin-bottom: 3px;
@@ -441,7 +441,7 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
} }
.tag-filter__clear { .ext-notes-tag-filter__clear {
font-size: 10px; font-size: 10px;
color: var(--accent); color: var(--accent);
cursor: pointer; cursor: pointer;
@@ -450,18 +450,18 @@
padding: 0; padding: 0;
font-family: var(--font); font-family: var(--font);
} }
.tag-filter__clear:hover { text-decoration: underline; } .ext-notes-tag-filter__clear:hover { text-decoration: underline; }
.tag-filter__pills { .ext-notes-tag-filter__pills {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 3px; gap: 3px;
max-height: 60px; max-height: 60px;
overflow-y: auto; overflow-y: auto;
} }
.tag-filter__pills .tag-pill { font-size: 10px; padding: 0 6px; } .ext-notes-tag-filter__pills .ext-notes-tag-pill { font-size: 10px; padding: 0 6px; }
/* ── Tag Input (editor) ────────────────── */ /* ── Tag Input (editor) ────────────────── */
.tag-input { .ext-notes-tag-input {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 4px; gap: 4px;
@@ -470,7 +470,7 @@
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
position: relative; position: relative;
} }
.tag-input__field { .ext-notes-tag-input__field {
border: none; border: none;
outline: none; outline: none;
font-size: 12px; font-size: 12px;
@@ -481,11 +481,11 @@
padding: 2px 0; padding: 2px 0;
font-family: var(--font); font-family: var(--font);
} }
.tag-input__field::placeholder { color: var(--text-3); } .ext-notes-tag-input__field::placeholder { color: var(--text-3); }
.tag-input .tag-pill { font-size: 11px; } .ext-notes-tag-input .ext-notes-tag-pill { font-size: 11px; }
/* ── Tag Autocomplete ──────────────────── */ /* ── Tag Autocomplete ──────────────────── */
.tag-autocomplete { .ext-notes-tag-autocomplete {
position: absolute; position: absolute;
top: 100%; top: 100%;
left: 20px; left: 20px;
@@ -498,23 +498,23 @@
box-shadow: 0 4px 12px rgba(0,0,0,0.1); box-shadow: 0 4px 12px rgba(0,0,0,0.1);
min-width: 140px; min-width: 140px;
} }
.tag-autocomplete__item { .ext-notes-tag-autocomplete__item {
padding: 4px 10px; padding: 4px 10px;
font-size: 12px; font-size: 12px;
cursor: pointer; cursor: pointer;
color: var(--text); color: var(--text);
} }
.tag-autocomplete__item:hover { background: var(--bg-hover); } .ext-notes-tag-autocomplete__item:hover { background: var(--bg-hover); }
/* ── Note Card Tags ────────────────────── */ /* ── Note Card Tags ────────────────────── */
.note-card__tags { .ext-notes-note-card__tags {
display: flex; display: flex;
gap: 3px; gap: 3px;
margin-top: 3px; margin-top: 3px;
overflow: hidden; overflow: hidden;
} }
.note-card__tags .tag-pill { font-size: 10px; padding: 0 6px; cursor: default; } .ext-notes-note-card__tags .ext-notes-tag-pill { font-size: 10px; padding: 0 6px; cursor: default; }
.note-card__tags .tag-pill--overflow { .ext-notes-note-card__tags .ext-notes-tag-pill--overflow {
background: transparent; background: transparent;
border: none; border: none;
color: var(--text-3); color: var(--text-3);
@@ -523,7 +523,7 @@
} }
/* ── Folder Context Menu ───────────────── */ /* ── Folder Context Menu ───────────────── */
.folder-context-menu { .ext-notes-folder-context-menu {
position: fixed; position: fixed;
z-index: 100; z-index: 100;
background: var(--bg-surface); background: var(--bg-surface);
@@ -533,52 +533,52 @@
min-width: 150px; min-width: 150px;
padding: 4px 0; padding: 4px 0;
} }
.folder-context-menu__item { .ext-notes-folder-context-menu__item {
padding: 6px 14px; padding: 6px 14px;
font-size: 13px; font-size: 13px;
color: var(--text); color: var(--text);
cursor: pointer; cursor: pointer;
transition: var(--transition); transition: var(--transition);
} }
.folder-context-menu__item:hover { .ext-notes-folder-context-menu__item:hover {
background: var(--bg-hover); background: var(--bg-hover);
} }
.folder-context-menu__item--danger { .ext-notes-folder-context-menu__item--danger {
color: var(--danger); color: var(--danger);
} }
.folder-context-menu__item--danger:hover { .ext-notes-folder-context-menu__item--danger:hover {
background: var(--danger-dim); background: var(--danger-dim);
} }
/* ── Drag and Drop ─────────────────────── */ /* ── Drag and Drop ─────────────────────── */
.note-card--dragging { opacity: 0.4; } .ext-notes-note-card--dragging { opacity: 0.4; }
.folder-tree__item--drop-target { .ext-notes-folder-tree__item--drop-target {
background: color-mix(in srgb, var(--accent) 10%, transparent) !important; background: color-mix(in srgb, var(--accent) 10%, transparent) !important;
outline: 2px dashed var(--accent); outline: 2px dashed var(--accent);
outline-offset: -2px; outline-offset: -2px;
} }
/* ── Wikilinks ──────────────────────────── */ /* ── Wikilinks ──────────────────────────── */
.wikilink { .ext-notes-wikilink {
color: var(--accent); color: var(--accent);
text-decoration: none; text-decoration: none;
border-bottom: 1px dashed var(--accent); border-bottom: 1px dashed var(--accent);
cursor: pointer; cursor: pointer;
} }
.wikilink:hover { .ext-notes-wikilink:hover {
border-bottom-style: solid; border-bottom-style: solid;
} }
.wikilink.wikilink--unresolved { .ext-notes-wikilink.ext-notes-wikilink--unresolved {
color: var(--danger, #e53e3e); color: var(--danger, #e53e3e);
border-bottom-color: var(--danger, #e53e3e); border-bottom-color: var(--danger, #e53e3e);
opacity: 0.7; opacity: 0.7;
} }
.wikilink.wikilink--unresolved:hover { .ext-notes-wikilink.ext-notes-wikilink--unresolved:hover {
opacity: 1; opacity: 1;
} }
/* ── Backlinks Panel ────────────────────── */ /* ── Backlinks Panel ────────────────────── */
.backlinks-panel { .ext-notes-backlinks-panel {
border-top: 1px solid var(--border); border-top: 1px solid var(--border);
padding: 10px 16px; padding: 10px 16px;
background: var(--bg-raised); background: var(--bg-raised);
@@ -586,7 +586,7 @@
max-height: 200px; max-height: 200px;
overflow-y: auto; overflow-y: auto;
} }
.backlinks-panel__header { .ext-notes-backlinks-panel__header {
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 600;
color: var(--text-2); color: var(--text-2);
@@ -599,11 +599,11 @@
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
} }
.backlinks-panel__toggle { .ext-notes-backlinks-panel__toggle {
font-size: 9px; font-size: 9px;
color: var(--text-2); color: var(--text-2);
} }
.backlinks-panel__count { .ext-notes-backlinks-panel__count {
background: var(--bg-hover); background: var(--bg-hover);
color: var(--text-2); color: var(--text-2);
font-size: 10px; font-size: 10px;
@@ -611,7 +611,7 @@
border-radius: 8px; border-radius: 8px;
line-height: 16px; line-height: 16px;
} }
.backlinks-panel__item { .ext-notes-backlinks-panel__item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
@@ -620,25 +620,25 @@
cursor: pointer; cursor: pointer;
font-size: 13px; font-size: 13px;
} }
.backlinks-panel__item:hover { .ext-notes-backlinks-panel__item:hover {
background: var(--bg-hover); background: var(--bg-hover);
} }
.backlinks-panel__title { .ext-notes-backlinks-panel__title {
color: var(--accent); color: var(--accent);
font-weight: 500; font-weight: 500;
} }
.backlinks-panel__context { .ext-notes-backlinks-panel__context {
color: var(--text-2); color: var(--text-2);
font-size: 11px; font-size: 11px;
} }
/* ── Sidebar Tabs ────────────────────────── */ /* ── Sidebar Tabs ────────────────────────── */
.sidebar-tabs { .ext-notes-sidebar-tabs {
display: flex; display: flex;
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
flex-shrink: 0; flex-shrink: 0;
} }
.sidebar-tabs__tab { .ext-notes-sidebar-tabs__tab {
flex: 1; flex: 1;
padding: 10px 14px; padding: 10px 14px;
font-size: 13px; font-size: 13px;
@@ -652,37 +652,37 @@
text-align: center; text-align: center;
font-family: var(--font); font-family: var(--font);
} }
.sidebar-tabs__tab:hover { .ext-notes-sidebar-tabs__tab:hover {
color: var(--text); color: var(--text);
background: var(--bg-hover); background: var(--bg-hover);
} }
.sidebar-tabs__tab--active { .ext-notes-sidebar-tabs__tab--active {
color: var(--text); color: var(--text);
border-bottom-color: var(--accent); border-bottom-color: var(--accent);
} }
.sidebar-tabs__tab--disabled { .ext-notes-sidebar-tabs__tab--disabled {
color: var(--text-3); color: var(--text-3);
cursor: default; cursor: default;
opacity: 0.5; opacity: 0.5;
} }
.sidebar-tabs__tab--disabled:hover { .ext-notes-sidebar-tabs__tab--disabled:hover {
background: none; background: none;
color: var(--text-3); color: var(--text-3);
} }
/* ── Sidebar Outline ─────────────────────── */ /* ── Sidebar Outline ─────────────────────── */
.sidebar-outline { .ext-notes-sidebar-outline {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding: 8px 0; padding: 8px 0;
} }
.sidebar-outline__empty { .ext-notes-sidebar-outline__empty {
padding: 40px 16px; padding: 40px 16px;
text-align: center; text-align: center;
color: var(--text-3); color: var(--text-3);
font-size: 13px; font-size: 13px;
} }
.sidebar-outline__item { .ext-notes-sidebar-outline__item {
padding: 5px 12px; padding: 5px 12px;
font-size: 13px; font-size: 13px;
color: var(--text-2); color: var(--text-2);
@@ -694,34 +694,34 @@
border-radius: var(--radius); border-radius: var(--radius);
margin: 1px 6px; margin: 1px 6px;
} }
.sidebar-outline__item:hover { .ext-notes-sidebar-outline__item:hover {
background: var(--bg-hover); background: var(--bg-hover);
color: var(--text); color: var(--text);
} }
.sidebar-outline__item--active { .ext-notes-sidebar-outline__item--active {
background: var(--bg-active); background: var(--bg-active);
color: var(--text); color: var(--text);
font-weight: 500; font-weight: 500;
} }
.sidebar-outline__item--h1 { font-weight: 600; color: var(--text); } .ext-notes-sidebar-outline__item--h1 { font-weight: 600; color: var(--text); }
.sidebar-outline__item--h2 { font-weight: 500; } .ext-notes-sidebar-outline__item--h2 { font-weight: 500; }
/* ── Graph Pane ─────────────────────────── */ /* ── Graph Pane ─────────────────────────── */
.graph-pane { .ext-notes-graph-pane {
flex: 1; flex: 1;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
background: var(--bg-surface, #fff); background: var(--bg-surface, #fff);
min-height: 0; min-height: 0;
} }
.graph-pane canvas { .ext-notes-graph-pane canvas {
display: block; display: block;
width: 100%; width: 100%;
height: 100%; height: 100%;
cursor: grab; cursor: grab;
} }
.graph-pane canvas:active { cursor: grabbing; } .ext-notes-graph-pane canvas:active { cursor: grabbing; }
.graph-toolbar { .ext-notes-graph-toolbar {
position: absolute; position: absolute;
top: 12px; top: 12px;
right: 12px; right: 12px;
@@ -732,15 +732,15 @@
font-size: 12px; font-size: 12px;
color: var(--text-2, #666); color: var(--text-2, #666);
} }
.graph-toolbar label { .ext-notes-graph-toolbar label {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
} }
.graph-toolbar input[type="checkbox"] { margin: 0; } .ext-notes-graph-toolbar input[type="checkbox"] { margin: 0; }
.graph-tooltip { .ext-notes-graph-tooltip {
position: absolute; position: absolute;
pointer-events: none; pointer-events: none;
background: var(--bg-raised, #fff); background: var(--bg-raised, #fff);
@@ -752,30 +752,30 @@
max-width: 220px; max-width: 220px;
font-size: 12px; font-size: 12px;
} }
.graph-tooltip__title { .ext-notes-graph-tooltip__title {
font-weight: 600; font-weight: 600;
font-size: 13px; font-size: 13px;
margin-bottom: 2px; margin-bottom: 2px;
} }
.graph-tooltip__tags { .ext-notes-graph-tooltip__tags {
display: flex; display: flex;
gap: 4px; gap: 4px;
flex-wrap: wrap; flex-wrap: wrap;
margin-top: 4px; margin-top: 4px;
} }
.graph-tooltip__tags .tag-pill { .ext-notes-graph-tooltip__tags .ext-notes-tag-pill {
font-size: 10px; font-size: 10px;
padding: 1px 6px; padding: 1px 6px;
border-radius: 8px; border-radius: 8px;
background: var(--bg-hover, #eee); background: var(--bg-hover, #eee);
color: var(--text-2, #666); color: var(--text-2, #666);
} }
.graph-tooltip__edges { .ext-notes-graph-tooltip__edges {
font-size: 11px; font-size: 11px;
color: var(--text-3, #999); color: var(--text-3, #999);
margin-top: 4px; margin-top: 4px;
} }
.graph-empty { .ext-notes-graph-empty {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -786,10 +786,10 @@
/* ── Responsive ──────────────────────────── */ /* ── Responsive ──────────────────────────── */
@media (max-width: 700px) { @media (max-width: 700px) {
.notes-sidebar { width: 100%; border-right: none; border-bottom: 1px solid var(--border); max-height: 40vh; } .ext-notes-sidebar { width: 100%; border-right: none; border-bottom: 1px solid var(--border); max-height: 40vh; }
.notes-app { flex-direction: column; } .ext-notes-app { flex-direction: column; }
.folder-tree { max-height: 30vh; } .ext-notes-folder-tree { max-height: 30vh; }
.notes-editor__body--split { grid-template-columns: 1fr; } .ext-notes-editor__body--split { grid-template-columns: 1fr; }
.notes-editor__body--split .notes-preview { display: none; } .ext-notes-editor__body--split .ext-notes-preview { display: none; }
.graph-pane { min-height: 50vh; } .ext-notes-graph-pane { min-height: 50vh; }
} }

View File

@@ -132,16 +132,16 @@
} }
return html` return html`
<div class="tag-input"> <div class="ext-notes-tag-input">
${(tags || []).map(function(tag) { ${(tags || []).map(function(tag) {
return html` return html`
<span key=${tag} class="tag-pill"> <span key=${tag} class="ext-notes-tag-pill">
${tag} ${tag}
<span class="tag-pill__remove" onClick=${function() { removeTag(tag); }}>×</span> <span class="ext-notes-tag-pill__remove" onClick=${function() { removeTag(tag); }}>×</span>
</span> </span>
`; `;
})} })}
<input ref=${inputRef} class="tag-input__field" type="text" <input ref=${inputRef} class="ext-notes-tag-input__field" type="text"
placeholder=${(tags && tags.length) ? 'Add tag…' : 'Add tags (comma to separate)…'} placeholder=${(tags && tags.length) ? 'Add tag…' : 'Add tags (comma to separate)…'}
value=${input} value=${input}
onInput=${handleInput} onInput=${handleInput}
@@ -149,9 +149,9 @@
onFocus=${function() { if (input.trim()) setShowAC(true); }} onFocus=${function() { if (input.trim()) setShowAC(true); }}
onBlur=${function() { setTimeout(function() { setShowAC(false); }, 150); }} /> onBlur=${function() { setTimeout(function() { setShowAC(false); }, 150); }} />
${showAC && suggestions.length > 0 && html` ${showAC && suggestions.length > 0 && html`
<div class="tag-autocomplete"> <div class="ext-notes-tag-autocomplete">
${suggestions.map(function(s) { ${suggestions.map(function(s) {
return html`<div key=${s} class="tag-autocomplete__item" return html`<div key=${s} class="ext-notes-tag-autocomplete__item"
onClick=${function() { addTag(s); }}>${s}</div>`; onClick=${function() { addTag(s); }}>${s}</div>`;
})} })}
</div> </div>
@@ -169,17 +169,17 @@
if (!tags || tags.length === 0) return null; if (!tags || tags.length === 0) return null;
return html` return html`
<div class="tag-filter"> <div class="ext-notes-tag-filter">
<div class="tag-filter__label"> <div class="ext-notes-tag-filter__label">
<span>Tags</span> <span>Tags</span>
${activeTag && html` ${activeTag && html`
<button class="tag-filter__clear" onClick=${function() { onSelect(null); }}>Clear</button> <button class="ext-notes-tag-filter__clear" onClick=${function() { onSelect(null); }}>Clear</button>
`} `}
</div> </div>
<div class="tag-filter__pills"> <div class="ext-notes-tag-filter__pills">
${tags.map(function(tag) { ${tags.map(function(tag) {
return html` return html`
<span key=${tag} class="tag-pill ${activeTag === tag ? 'tag-pill--active' : ''}" <span key=${tag} class="ext-notes-tag-pill ${activeTag === tag ? 'ext-notes-tag-pill--active' : ''}"
onClick=${function() { onSelect(activeTag === tag ? null : tag); }}> onClick=${function() { onSelect(activeTag === tag ? null : tag); }}>
${tag} ${tag}
</span> </span>
@@ -216,23 +216,23 @@
} }
return html` return html`
<div class="note-card ${active ? 'note-card--active' : ''} ${dragging ? 'note-card--dragging' : ''}" <div class="ext-notes-note-card ${active ? 'ext-notes-note-card--active' : ''} ${dragging ? 'ext-notes-note-card--dragging' : ''}"
onClick=${onClick} onClick=${onClick}
draggable="true" draggable="true"
onDragStart=${handleDragStart} onDragStart=${handleDragStart}
onDragEnd=${handleDragEnd}> onDragEnd=${handleDragEnd}>
<div class="note-card__title"> <div class="ext-notes-note-card__title">
${note.pinned ? html`<span class="note-card__pin">📌</span>` : null} ${note.pinned ? html`<span class="ext-notes-note-card__pin">📌</span>` : null}
${note.title || 'Untitled'} ${note.title || 'Untitled'}
</div> </div>
${note.snippet && html`<div class="note-card__snippet">${note.snippet}</div>`} ${note.snippet && html`<div class="ext-notes-note-card__snippet">${note.snippet}</div>`}
${showTags.length > 0 && html` ${showTags.length > 0 && html`
<div class="note-card__tags"> <div class="ext-notes-note-card__tags">
${showTags.map(function(t) { return html`<span key=${t} class="tag-pill">${t}</span>`; })} ${showTags.map(function(t) { return html`<span key=${t} class="ext-notes-tag-pill">${t}</span>`; })}
${overflow > 0 && html`<span class="tag-pill tag-pill--overflow">+${overflow}</span>`} ${overflow > 0 && html`<span class="ext-notes-tag-pill ext-notes-tag-pill--overflow">+${overflow}</span>`}
</div> </div>
`} `}
${dateStr && html`<div class="note-card__date">${dateStr}</div>`} ${dateStr && html`<div class="ext-notes-note-card__date">${dateStr}</div>`}
</div> </div>
`; `;
} }
@@ -254,10 +254,10 @@
}, [onClose]); }, [onClose]);
return html` return html`
<div ref=${menuRef} class="folder-context-menu" style="top: ${y}px; left: ${x}px"> <div ref=${menuRef} class="ext-notes-folder-context-menu" style="top: ${y}px; left: ${x}px">
<div class="folder-context-menu__item" onClick=${onAddSub}>Add subfolder</div> <div class="ext-notes-folder-context-menu__item" onClick=${onAddSub}>Add subfolder</div>
<div class="folder-context-menu__item" onClick=${onRename}>Rename</div> <div class="ext-notes-folder-context-menu__item" onClick=${onRename}>Rename</div>
<div class="folder-context-menu__item folder-context-menu__item--danger" onClick=${onDelete}>Delete</div> <div class="ext-notes-folder-context-menu__item ext-notes-folder-context-menu__item--danger" onClick=${onDelete}>Delete</div>
</div> </div>
`; `;
} }
@@ -310,25 +310,25 @@
return html` return html`
<div> <div>
<div class="folder-tree__item ${isActive ? 'folder-tree__item--active' : ''} ${dropOver ? 'folder-tree__item--drop-target' : ''}" <div class="ext-notes-folder-tree__item ${isActive ? 'ext-notes-folder-tree__item--active' : ''} ${dropOver ? 'ext-notes-folder-tree__item--drop-target' : ''}"
style="padding-left: ${indent}px" style="padding-left: ${indent}px"
onClick=${function() { onSelect(node.folder.id); }} onClick=${function() { onSelect(node.folder.id); }}
onContextMenu=${handleContext} onContextMenu=${handleContext}
onDragOver=${handleDragOver} onDragOver=${handleDragOver}
onDragLeave=${handleDragLeave} onDragLeave=${handleDragLeave}
onDrop=${handleDrop}> onDrop=${handleDrop}>
<span class="folder-tree__toggle" onClick=${handleToggle}> <span class="ext-notes-folder-tree__toggle" onClick=${handleToggle}>
${hasChildren ? (expanded ? '▼' : '▶') : '·'} ${hasChildren ? (expanded ? '▼' : '▶') : '·'}
</span> </span>
<span class="folder-tree__icon">${isActive ? '📂' : '📁'}</span> <span class="ext-notes-folder-tree__icon">${isActive ? '📂' : '📁'}</span>
${editing ${editing
? html`<input class="folder-tree__edit" value=${editName} ? html`<input class="ext-notes-folder-tree__edit" value=${editName}
onInput=${function(e) { setEditName(e.target.value); }} onInput=${function(e) { setEditName(e.target.value); }}
onBlur=${handleRename} onBlur=${handleRename}
onKeyDown=${function(e) { if (e.key === 'Enter') handleRename(); if (e.key === 'Escape') setEditing(false); }} onKeyDown=${function(e) { if (e.key === 'Enter') handleRename(); if (e.key === 'Escape') setEditing(false); }}
onClick=${function(e) { e.stopPropagation(); }} onClick=${function(e) { e.stopPropagation(); }}
ref=${function(el) { if (el) el.focus(); }} />` ref=${function(el) { if (el) el.focus(); }} />`
: html`<span class="folder-tree__name">${node.folder.name}</span>` : html`<span class="ext-notes-folder-tree__name">${node.folder.name}</span>`
} }
</div> </div>
${menu && html`<${FolderContextMenu} x=${menu.x} y=${menu.y} ${menu && html`<${FolderContextMenu} x=${menu.x} y=${menu.y}
@@ -399,18 +399,18 @@
var unfiledDrop = makeDropHandlers(setDropUnfiled); var unfiledDrop = makeDropHandlers(setDropUnfiled);
return html` return html`
<div class="folder-tree"> <div class="ext-notes-folder-tree">
<div class="folder-tree__item ${!activeFolderId && !showUnfiled ? 'folder-tree__item--active' : ''} ${dropAll ? 'folder-tree__item--drop-target' : ''}" <div class="ext-notes-folder-tree__item ${!activeFolderId && !showUnfiled ? 'ext-notes-folder-tree__item--active' : ''} ${dropAll ? 'ext-notes-folder-tree__item--drop-target' : ''}"
onClick=${onSelectAll} onClick=${onSelectAll}
onDragOver=${allDrop.onDragOver} onDragLeave=${allDrop.onDragLeave} onDrop=${allDrop.onDrop}> onDragOver=${allDrop.onDragOver} onDragLeave=${allDrop.onDragLeave} onDrop=${allDrop.onDrop}>
<span class="folder-tree__icon">📋</span> <span class="ext-notes-folder-tree__icon">📋</span>
<span class="folder-tree__name">All Notes</span> <span class="ext-notes-folder-tree__name">All Notes</span>
</div> </div>
<div class="folder-tree__item ${showUnfiled ? 'folder-tree__item--active' : ''} ${dropUnfiled ? 'folder-tree__item--drop-target' : ''}" <div class="ext-notes-folder-tree__item ${showUnfiled ? 'ext-notes-folder-tree__item--active' : ''} ${dropUnfiled ? 'ext-notes-folder-tree__item--drop-target' : ''}"
onClick=${onSelectUnfiled} onClick=${onSelectUnfiled}
onDragOver=${unfiledDrop.onDragOver} onDragLeave=${unfiledDrop.onDragLeave} onDrop=${unfiledDrop.onDrop}> onDragOver=${unfiledDrop.onDragOver} onDragLeave=${unfiledDrop.onDragLeave} onDrop=${unfiledDrop.onDrop}>
<span class="folder-tree__icon">📄</span> <span class="ext-notes-folder-tree__icon">📄</span>
<span class="folder-tree__name">Unfiled</span> <span class="ext-notes-folder-tree__name">Unfiled</span>
</div> </div>
${tree.map(function(node) { ${tree.map(function(node) {
return html`<${FolderNode} key=${node.folder.id} node=${node} depth=${0} return html`<${FolderNode} key=${node.folder.id} node=${node} depth=${0}
@@ -419,7 +419,7 @@
onDelete=${onDeleteFolder} onCreateSub=${onCreateFolder} onDelete=${onDeleteFolder} onCreateSub=${onCreateFolder}
onDropNote=${onDropNote} />`; onDropNote=${onDropNote} />`;
})} })}
<div class="folder-tree__add" onClick=${function() { onCreateFolder(''); }}> <div class="ext-notes-folder-tree__add" onClick=${function() { onCreateFolder(''); }}>
+ New Folder + New Folder
</div> </div>
</div> </div>
@@ -446,18 +446,18 @@
if (!backlinks.length) return null; if (!backlinks.length) return null;
return html` return html`
<div class="backlinks-panel"> <div class="ext-notes-backlinks-panel">
<div class="backlinks-panel__header" onClick=${function() { setCollapsed(!collapsed); }}> <div class="ext-notes-backlinks-panel__header" onClick=${function() { setCollapsed(!collapsed); }}>
<span class="backlinks-panel__toggle">${collapsed ? '▶' : '▼'}</span> <span class="ext-notes-backlinks-panel__toggle">${collapsed ? '▶' : '▼'}</span>
<span>Backlinks</span> <span>Backlinks</span>
<span class="backlinks-panel__count">${backlinks.length}</span> <span class="ext-notes-backlinks-panel__count">${backlinks.length}</span>
</div> </div>
${!collapsed && backlinks.map(function(bl) { ${!collapsed && backlinks.map(function(bl) {
return html` return html`
<div key=${bl.source_id} class="backlinks-panel__item" <div key=${bl.source_id} class="ext-notes-backlinks-panel__item"
onClick=${function() { if (onNavigate) onNavigate(bl.source_id); }}> onClick=${function() { if (onNavigate) onNavigate(bl.source_id); }}>
<span class="backlinks-panel__title">${bl.source_title}</span> <span class="ext-notes-backlinks-panel__title">${bl.source_title}</span>
<span class="backlinks-panel__context">via [[${bl.link_text}]]</span> <span class="ext-notes-backlinks-panel__context">via [[${bl.link_text}]]</span>
</div> </div>
`; `;
})} })}
@@ -547,10 +547,10 @@
function SidebarTabs({ activeTab, onTabChange, noteSelected }) { function SidebarTabs({ activeTab, onTabChange, noteSelected }) {
return html` return html`
<div class="sidebar-tabs"> <div class="ext-ext-notes-sidebar-tabs">
<button class="sidebar-tabs__tab ${activeTab === 'notes' ? 'sidebar-tabs__tab--active' : ''}" <button class="ext-ext-notes-sidebar-tabs__tab ${activeTab === 'notes' ? 'ext-ext-notes-sidebar-tabs__tab--active' : ''}"
onClick=${function() { onTabChange('notes'); }}>Notes</button> onClick=${function() { onTabChange('notes'); }}>Notes</button>
<button class="sidebar-tabs__tab ${activeTab === 'outline' ? 'sidebar-tabs__tab--active' : ''} ${!noteSelected ? 'sidebar-tabs__tab--disabled' : ''}" <button class="ext-ext-notes-sidebar-tabs__tab ${activeTab === 'outline' ? 'ext-ext-notes-sidebar-tabs__tab--active' : ''} ${!noteSelected ? 'ext-ext-notes-sidebar-tabs__tab--disabled' : ''}"
onClick=${function() { if (noteSelected) onTabChange('outline'); }}>Outline</button> onClick=${function() { if (noteSelected) onTabChange('outline'); }}>Outline</button>
</div> </div>
`; `;
@@ -563,14 +563,14 @@
function SidebarOutline({ headings, activeHeadingIdx, onScrollToHeading }) { function SidebarOutline({ headings, activeHeadingIdx, onScrollToHeading }) {
if (!headings || headings.length === 0) { if (!headings || headings.length === 0) {
return html`<div class="sidebar-outline__empty">No headings found</div>`; return html`<div class="ext-ext-notes-sidebar-outline__empty">No headings found</div>`;
} }
return html` return html`
<div class="sidebar-outline"> <div class="ext-ext-notes-sidebar-outline">
${headings.map(function(h, i) { ${headings.map(function(h, i) {
return html` return html`
<div key=${i} class="sidebar-outline__item sidebar-outline__item--h${h.level} ${i === activeHeadingIdx ? 'sidebar-outline__item--active' : ''}" <div key=${i} class="ext-ext-notes-sidebar-outline__item ext-ext-notes-sidebar-outline__item--h${h.level} ${i === activeHeadingIdx ? 'ext-ext-notes-sidebar-outline__item--active' : ''}"
style="padding-left: ${10 + (h.level - 1) * 14}px" style="padding-left: ${10 + (h.level - 1) * 14}px"
onClick=${function() { onScrollToHeading(h); }}> onClick=${function() { onScrollToHeading(h); }}>
${h.text} ${h.text}
@@ -772,8 +772,8 @@
for (var i = 0; i < outgoingLinks.length; i++) { for (var i = 0; i < outgoingLinks.length; i++) {
var link = outgoingLinks[i]; var link = outgoingLinks[i];
if (!link.target_id) { if (!link.target_id) {
var search = 'class="wikilink" data-wikilink="' + _escAttr(link.link_text) + '"'; var search = 'class="ext-notes-wikilink" data-wikilink="' + _escAttr(link.link_text) + '"';
var replace = 'class="wikilink wikilink--unresolved" data-wikilink="' + _escAttr(link.link_text) + '"'; var replace = 'class="ext-notes-wikilink ext-notes-wikilink--unresolved" data-wikilink="' + _escAttr(link.link_text) + '"';
h = h.split(search).join(replace); h = h.split(search).join(replace);
} }
} }
@@ -787,7 +787,7 @@
if (!el || viewMode === 'edit') return; if (!el || viewMode === 'edit') return;
function handleClick(e) { function handleClick(e) {
var target = e.target; var target = e.target;
if (target.classList && target.classList.contains('wikilink')) { if (target.classList && target.classList.contains('ext-notes-wikilink')) {
e.preventDefault(); e.preventDefault();
var linkText = target.getAttribute('data-wikilink'); var linkText = target.getAttribute('data-wikilink');
if (linkText && onNavigateToTitle) onNavigateToTitle(linkText); if (linkText && onNavigateToTitle) onNavigateToTitle(linkText);
@@ -939,44 +939,44 @@
}, [folders]); }, [folders]);
if (!note) { if (!note) {
return html`<div class="notes-editor__empty">Select a note or create a new one</div>`; return html`<div class="ext-notes-editor__empty">Select a note or create a new one</div>`;
} }
// ── Render editor body ───────────────────── // ── Render editor body ─────────────────────
function renderEditorBody() { function renderEditorBody() {
if (viewMode === 'rendered') { if (viewMode === 'rendered') {
return html`<div class="notes-preview" ref=${previewRef} dangerouslySetInnerHTML=${{ __html: previewHtml }} />`; return html`<div class="ext-notes-preview" ref=${previewRef} dangerouslySetInnerHTML=${{ __html: previewHtml }} />`;
} }
if (viewMode === 'split') { if (viewMode === 'split') {
var editor = hasCM var editor = hasCM
? html`<div class="notes-editor__cm" ref=${cmContainerRef} />` ? html`<div class="ext-notes-editor__cm" ref=${cmContainerRef} />`
: html`<textarea class="notes-editor__textarea" ref=${textareaRef} : html`<textarea class="ext-notes-editor__textarea" ref=${textareaRef}
value=${body} onInput=${handleBodyChange} value=${body} onInput=${handleBodyChange}
onKeyDown=${handleKeyDown} onKeyDown=${handleKeyDown}
placeholder="Start writing in Markdown…" />`; placeholder="Start writing in Markdown…" />`;
return [ return [
editor, editor,
html`<div class="notes-preview" ref=${previewRef} dangerouslySetInnerHTML=${{ __html: previewHtml }} />` html`<div class="ext-notes-preview" ref=${previewRef} dangerouslySetInnerHTML=${{ __html: previewHtml }} />`
]; ];
} }
// edit mode // edit mode
if (hasCM) { if (hasCM) {
return html`<div class="notes-editor__cm" ref=${cmContainerRef} />`; return html`<div class="ext-notes-editor__cm" ref=${cmContainerRef} />`;
} }
return html`<textarea class="notes-editor__textarea" ref=${textareaRef} return html`<textarea class="ext-notes-editor__textarea" ref=${textareaRef}
value=${body} onInput=${handleBodyChange} value=${body} onInput=${handleBodyChange}
onKeyDown=${handleKeyDown} onKeyDown=${handleKeyDown}
placeholder="Start writing in Markdown…" />`; placeholder="Start writing in Markdown…" />`;
} }
return html` return html`
<div class="notes-editor"> <div class="ext-notes-editor">
<div class="notes-editor__header"> <div class="ext-notes-editor__header">
<input class="notes-editor__title-input" type="text" <input class="ext-notes-editor__title-input" type="text"
value=${title} onInput=${handleTitleChange} value=${title} onInput=${handleTitleChange}
placeholder="Note title" /> placeholder="Note title" />
<div class="notes-editor__actions"> <div class="ext-notes-editor__actions">
<select class="notes-editor__folder-select" <select class="ext-notes-editor__folder-select"
value=${note.folder_id || ''} value=${note.folder_id || ''}
onChange=${async function(e) { onChange=${async function(e) {
await api.post('/notes/move', { note_id: note.id, folder_id: e.target.value }); await api.post('/notes/move', { note_id: note.id, folder_id: e.target.value });
@@ -987,28 +987,28 @@
return html`<option key=${f.id} value=${f.id}>${f.label}</option>`; return html`<option key=${f.id} value=${f.id}>${f.label}</option>`;
})} })}
</select> </select>
<span class=${dirty ? 'notes-saved notes-saved--dirty' : 'notes-saved'}> <span class=${dirty ? 'ext-notes-saved ext-notes-saved--dirty' : 'ext-notes-saved'}>
${saving ? 'Saving…' : (dirty ? 'Unsaved' : 'Saved')} ${saving ? 'Saving…' : (dirty ? 'Unsaved' : 'Saved')}
</span> </span>
<button class="notes-toggle" onClick=${function() { <button class="ext-notes-toggle" onClick=${function() {
var next = viewMode === 'rendered' ? 'edit' : viewMode === 'edit' ? 'split' : 'rendered'; var next = viewMode === 'rendered' ? 'edit' : viewMode === 'edit' ? 'split' : 'rendered';
_modeStore.set('editor_mode', next); _modeStore.set('editor_mode', next);
setViewMode(next); setViewMode(next);
}}> }}>
${viewMode === 'rendered' ? 'Edit' : viewMode === 'edit' ? 'Split' : 'Read'} ${viewMode === 'rendered' ? 'Edit' : viewMode === 'edit' ? 'Split' : 'Read'}
</button> </button>
<button class="notes-btn" onClick=${handleExport} title="Export as .md">Export</button> <button class="ext-notes-btn" onClick=${handleExport} title="Export as .md">Export</button>
<button class="notes-btn" onClick=${handlePin} <button class="ext-notes-btn" onClick=${handlePin}
title=${note.pinned ? 'Unpin' : 'Pin'}> title=${note.pinned ? 'Unpin' : 'Pin'}>
${note.pinned ? '📌' : '📍'} ${note.pinned ? '📌' : '📍'}
</button> </button>
<button class="notes-btn notes-btn--danger" onClick=${handleArchive} <button class="ext-notes-btn ext-notes-btn--danger" onClick=${handleArchive}
title="Archive">🗑</button> title="Archive">🗑</button>
</div> </div>
</div> </div>
<${TagInput} tags=${noteTags} allTags=${allTags} onChange=${handleTagsChange} /> <${TagInput} tags=${noteTags} allTags=${allTags} onChange=${handleTagsChange} />
<div class="notes-editor__content"> <div class="ext-notes-editor__content">
<div class="notes-editor__body ${viewMode === 'split' ? 'notes-editor__body--split' : ''}"> <div class="ext-notes-editor__body ${viewMode === 'split' ? 'ext-notes-editor__body--split' : ''}">
${renderEditorBody()} ${renderEditorBody()}
</div> </div>
</div> </div>
@@ -1405,17 +1405,17 @@
}, []); }, []);
if (loading) { if (loading) {
return html`<div class="graph-pane"><div class="graph-empty"><${Spinner} size="md" /></div></div>`; return html`<div class="ext-notes-graph-pane"><div class="ext-notes-graph-empty"><${Spinner} size="md" /></div></div>`;
} }
if (!graphData || graphData.nodes.length === 0) { if (!graphData || graphData.nodes.length === 0) {
return html`<div class="graph-pane"><div class="graph-empty">No notes to graph. Create notes with [[wikilinks]] to see connections.</div></div>`; return html`<div class="ext-notes-graph-pane"><div class="ext-notes-graph-empty">No notes to graph. Create notes with [[wikilinks]] to see connections.</div></div>`;
} }
var adj = graphData.adj; var adj = graphData.adj;
return html` return html`
<div class="graph-pane"> <div class="ext-notes-graph-pane">
<div class="graph-toolbar"> <div class="ext-notes-graph-toolbar">
<label> <label>
<input type="checkbox" checked=${hideOrphans} <input type="checkbox" checked=${hideOrphans}
onChange=${function(e) { setHideOrphans(e.target.checked); }} /> onChange=${function(e) { setHideOrphans(e.target.checked); }} />
@@ -1430,14 +1430,14 @@
onMouseUp=${handleMouseUp} onMouseUp=${handleMouseUp}
onMouseLeave=${handleMouseUp} /> onMouseLeave=${handleMouseUp} />
${hoveredNode && html` ${hoveredNode && html`
<div class="graph-tooltip" style=${{ left: (mousePos.x + 14) + 'px', top: (mousePos.y - 10) + 'px' }}> <div class="ext-notes-graph-tooltip" style=${{ left: (mousePos.x + 14) + 'px', top: (mousePos.y - 10) + 'px' }}>
<div class="graph-tooltip__title">${hoveredNode.title}</div> <div class="ext-notes-graph-tooltip__title">${hoveredNode.title}</div>
${hoveredNode.tags && hoveredNode.tags.length > 0 && html` ${hoveredNode.tags && hoveredNode.tags.length > 0 && html`
<div class="graph-tooltip__tags"> <div class="ext-notes-graph-tooltip__tags">
${hoveredNode.tags.map(function(t) { return html`<span class="tag-pill" key=${t}>${t}</span>`; })} ${hoveredNode.tags.map(function(t) { return html`<span class="ext-notes-tag-pill" key=${t}>${t}</span>`; })}
</div> </div>
`} `}
<div class="graph-tooltip__edges">${(adj[hoveredNode.id] ? adj[hoveredNode.id].size : 0)} connections</div> <div class="ext-notes-graph-tooltip__edges">${(adj[hoveredNode.id] ? adj[hoveredNode.id].size : 0)} connections</div>
</div> </div>
`} `}
</div> </div>
@@ -1736,8 +1736,8 @@
<${Button} variant=${showGraph ? 'primary' : 'secondary'} size="sm" <${Button} variant=${showGraph ? 'primary' : 'secondary'} size="sm"
onClick=${function() { setShowGraph(!showGraph); }}>Graph<//> onClick=${function() { setShowGraph(!showGraph); }}>Graph<//>
<//> <//>
<div class="notes-app"> <div class="ext-notes-app">
<div class="notes-sidebar"> <div class="ext-notes-sidebar">
<${SidebarTabs} activeTab=${sidebarTab} onTabChange=${setSidebarTab} noteSelected=${!!activeNote} /> <${SidebarTabs} activeTab=${sidebarTab} onTabChange=${setSidebarTab} noteSelected=${!!activeNote} />
${sidebarTab === 'notes' && html` ${sidebarTab === 'notes' && html`
<${FolderTree} folders=${folders} <${FolderTree} folders=${folders}
@@ -1749,14 +1749,14 @@
onDeleteFolder=${handleDeleteFolder} onDeleteFolder=${handleDeleteFolder}
onDropNote=${handleDropNote} /> onDropNote=${handleDropNote} />
<${TagFilter} tags=${allTags} activeTag=${activeTag} onSelect=${handleTagFilter} /> <${TagFilter} tags=${allTags} activeTag=${activeTag} onSelect=${handleTagFilter} />
<div class="notes-search"> <div class="ext-notes-search">
<input type="text" placeholder="Search notes…" <input type="text" placeholder="Search notes…"
value=${searchTerm} onInput=${handleSearch} /> value=${searchTerm} onInput=${handleSearch} />
</div> </div>
<div class="notes-list"> <div class="ext-notes-list">
${loading && html`<div class="notes-list__loading"><${Spinner} size="md" /></div>`} ${loading && html`<div class="ext-notes-list__loading"><${Spinner} size="md" /></div>`}
${!loading && notes.length === 0 && html` ${!loading && notes.length === 0 && html`
<div class="notes-list__empty"> <div class="ext-notes-list__empty">
${searchTerm ? 'No matching notes' : (activeTag ? 'No notes with tag "' + activeTag + '"' : 'No notes yet. Click + New Note to start.')} ${searchTerm ? 'No matching notes' : (activeTag ? 'No notes with tag "' + activeTag + '"' : 'No notes yet. Click + New Note to start.')}
</div> </div>
`} `}

View File

@@ -1,6 +1,6 @@
/* ── Schedules Surface ───────────────────── */ /* ── Schedules Surface ───────────────────── */
.sched-app { .ext-schedules-app {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
@@ -8,7 +8,7 @@
color: var(--text); color: var(--text);
} }
.sched-header { .ext-schedules-header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
@@ -16,13 +16,13 @@
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
} }
.sched-header h1 { .ext-schedules-header h1 {
font-size: 1rem; font-size: 1rem;
font-weight: 600; font-weight: 600;
margin: 0; margin: 0;
} }
.sched-body { .ext-schedules-body {
flex: 1; flex: 1;
overflow: auto; overflow: auto;
padding: 16px; padding: 16px;
@@ -31,28 +31,28 @@
gap: 16px; gap: 16px;
} }
.sched-count { .ext-schedules-count {
font-size: 0.8rem; font-size: 0.8rem;
color: var(--text-3); color: var(--text-3);
} }
/* ── Table ────────────────────────────────── */ /* ── Table ────────────────────────────────── */
.sched-list__loading, .ext-schedules-list__loading,
.sched-list__empty { .ext-schedules-list__empty {
padding: 40px 16px; padding: 40px 16px;
text-align: center; text-align: center;
color: var(--text-3); color: var(--text-3);
font-size: 0.85rem; font-size: 0.85rem;
} }
.sched-table { .ext-schedules-table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
font-size: 0.85rem; font-size: 0.85rem;
} }
.sched-table th { .ext-schedules-table th {
text-align: left; text-align: left;
padding: 8px 12px; padding: 8px 12px;
font-weight: 600; font-weight: 600;
@@ -63,19 +63,19 @@
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
} }
.sched-table td { .ext-schedules-table td {
padding: 10px 12px; padding: 10px 12px;
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
vertical-align: top; vertical-align: top;
} }
.sched-row:hover { .ext-schedules-row:hover {
background: var(--bg-hover); background: var(--bg-hover);
} }
.sched-row__name { min-width: 160px; } .ext-schedules-row__name { min-width: 160px; }
.sched-link { .ext-schedules-link {
background: none; background: none;
border: none; border: none;
color: var(--accent); color: var(--accent);
@@ -85,17 +85,17 @@
padding: 0; padding: 0;
text-align: left; text-align: left;
} }
.sched-link:hover { text-decoration: underline; } .ext-schedules-link:hover { text-decoration: underline; }
.sched-row__desc { .ext-schedules-row__desc {
font-size: 0.75rem; font-size: 0.75rem;
color: var(--text-3); color: var(--text-3);
margin-top: 2px; margin-top: 2px;
} }
.sched-row__cron { white-space: nowrap; } .ext-schedules-row__cron { white-space: nowrap; }
.sched-cron-badge { .ext-schedules-cron-badge {
font-family: monospace; font-family: monospace;
font-size: 0.8rem; font-size: 0.8rem;
background: var(--bg-raised); background: var(--bg-raised);
@@ -104,19 +104,19 @@
color: var(--text-2); color: var(--text-2);
} }
.sched-cron-human { .ext-schedules-cron-human {
font-size: 0.75rem; font-size: 0.75rem;
color: var(--text-3); color: var(--text-3);
margin-top: 2px; margin-top: 2px;
} }
.sched-row__next { .ext-schedules-row__next {
font-size: 0.8rem; font-size: 0.8rem;
color: var(--text-2); color: var(--text-2);
white-space: nowrap; white-space: nowrap;
} }
.sched-row__actions { .ext-schedules-row__actions {
display: flex; display: flex;
gap: 4px; gap: 4px;
white-space: nowrap; white-space: nowrap;
@@ -124,7 +124,7 @@
/* ── Toggle button ────────────────────────── */ /* ── Toggle button ────────────────────────── */
.sched-toggle-btn { .ext-schedules-toggle-btn {
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 600; font-weight: 600;
padding: 2px 10px; padding: 2px 10px;
@@ -134,22 +134,22 @@
transition: background 0.15s, color 0.15s; transition: background 0.15s, color 0.15s;
} }
.sched-toggle-btn--on { .ext-schedules-toggle-btn--on {
background: var(--success); background: var(--success);
color: #fff; color: #fff;
border-color: var(--success); border-color: var(--success);
} }
.sched-toggle-btn--off { .ext-schedules-toggle-btn--off {
background: var(--bg-raised); background: var(--bg-raised);
color: var(--text-3); color: var(--text-3);
} }
.sched-toggle-btn:hover { opacity: 0.85; } .ext-schedules-toggle-btn:hover { opacity: 0.85; }
/* ── Action buttons ───────────────────────── */ /* ── Action buttons ───────────────────────── */
.sched-btn { .ext-schedules-btn {
background: none; background: none;
border: 1px solid var(--border); border: 1px solid var(--border);
color: var(--text-2); color: var(--text-2);
@@ -157,14 +157,14 @@
cursor: pointer; cursor: pointer;
transition: background 0.15s; transition: background 0.15s;
} }
.sched-btn:hover { background: var(--bg-hover); } .ext-schedules-btn:hover { background: var(--bg-hover); }
.sched-btn--sm { .ext-schedules-btn--sm {
padding: 3px 8px; padding: 3px 8px;
font-size: 0.8rem; font-size: 0.8rem;
} }
.sched-btn--danger:hover { .ext-schedules-btn--danger:hover {
background: var(--danger-dim); background: var(--danger-dim);
color: var(--danger); color: var(--danger);
border-color: var(--danger); border-color: var(--danger);
@@ -172,28 +172,28 @@
/* ── Form ─────────────────────────────────── */ /* ── Form ─────────────────────────────────── */
.sched-form { display: flex; flex-direction: column; gap: 12px; } .ext-schedules-form { display: flex; flex-direction: column; gap: 12px; }
.sched-form__row { display: flex; align-items: center; gap: 12px; } .ext-schedules-form__row { display: flex; align-items: center; gap: 12px; }
.sched-form__actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 8px; } .ext-schedules-form__actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 8px; }
.sched-cron-input { .ext-schedules-cron-input {
font-family: monospace; font-family: monospace;
} }
.sched-cron-preview { .ext-schedules-cron-preview {
font-size: 0.8rem; font-size: 0.8rem;
color: var(--accent); color: var(--accent);
margin-top: 4px; margin-top: 4px;
font-style: italic; font-style: italic;
} }
.sched-script { .ext-schedules-script {
font-family: monospace; font-family: monospace;
font-size: 0.85rem; font-size: 0.85rem;
resize: vertical; resize: vertical;
} }
.sched-toggle { .ext-schedules-toggle {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
@@ -203,14 +203,14 @@
/* ── Logs panel ───────────────────────────── */ /* ── Logs panel ───────────────────────────── */
.sched-logs { .ext-schedules-logs {
background: var(--bg-surface); background: var(--bg-surface);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
} }
.sched-logs__header { .ext-schedules-logs__header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
@@ -220,15 +220,15 @@
font-weight: 600; font-weight: 600;
} }
.sched-logs__loading, .ext-schedules-logs__loading,
.sched-logs__empty { .ext-schedules-logs__empty {
padding: 20px 12px; padding: 20px 12px;
text-align: center; text-align: center;
color: var(--text-3); color: var(--text-3);
font-size: 0.8rem; font-size: 0.8rem;
} }
.sched-log-entry { .ext-schedules-log-entry {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
@@ -236,28 +236,28 @@
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
font-size: 0.8rem; font-size: 0.8rem;
} }
.sched-log-entry:last-child { border-bottom: none; } .ext-schedules-log-entry:last-child { border-bottom: none; }
.sched-log-entry__status { .ext-schedules-log-entry__status {
font-weight: 700; font-weight: 700;
width: 18px; width: 18px;
text-align: center; text-align: center;
} }
.sched-log-entry__status--ok { color: var(--success); } .ext-schedules-log-entry__status--ok { color: var(--success); }
.sched-log-entry__status--fail { color: var(--danger); } .ext-schedules-log-entry__status--fail { color: var(--danger); }
.sched-log-entry__time { .ext-schedules-log-entry__time {
color: var(--text-2); color: var(--text-2);
white-space: nowrap; white-space: nowrap;
} }
.sched-log-entry__duration { .ext-schedules-log-entry__duration {
color: var(--text-3); color: var(--text-3);
font-family: monospace; font-family: monospace;
font-size: 0.75rem; font-size: 0.75rem;
} }
.sched-log-entry__error { .ext-schedules-log-entry__error {
color: var(--danger); color: var(--danger);
font-size: 0.75rem; font-size: 0.75rem;
overflow: hidden; overflow: hidden;
@@ -269,9 +269,9 @@
/* ── Responsive ───────────────────────────── */ /* ── Responsive ───────────────────────────── */
@media (max-width: 768px) { @media (max-width: 768px) {
.sched-table th:nth-child(3), .ext-schedules-table th:nth-child(3),
.sched-table td:nth-child(3) { .ext-schedules-table td:nth-child(3) {
display: none; display: none;
} }
.sched-body { padding: 8px; } .ext-schedules-body { padding: 8px; }
} }

View File

@@ -148,7 +148,7 @@
return html` return html`
<${Dialog} open onClose=${onClose} title=${schedule ? 'Edit Schedule' : 'New Schedule'}> <${Dialog} open onClose=${onClose} title=${schedule ? 'Edit Schedule' : 'New Schedule'}>
<form onSubmit=${handleSubmit} class="sched-form"> <form onSubmit=${handleSubmit} class="ext-schedules-form">
${error && html`<${Banner} variant="danger" text=${error} />`} ${error && html`<${Banner} variant="danger" text=${error} />`}
<${FormField} label="Name" required> <${FormField} label="Name" required>
<input type="text" value=${name} onInput=${e => setName(e.target.value)} <input type="text" value=${name} onInput=${e => setName(e.target.value)}
@@ -160,22 +160,22 @@
<//> <//>
<${FormField} label="Cron Expression" required> <${FormField} label="Cron Expression" required>
<input type="text" value=${cron} onInput=${e => setCron(e.target.value)} <input type="text" value=${cron} onInput=${e => setCron(e.target.value)}
placeholder="e.g. 0 9 * * 1-5" class="sched-cron-input" /> placeholder="e.g. 0 9 * * 1-5" class="ext-schedules-cron-input" />
${preview && preview !== cron && html` ${preview && preview !== cron && html`
<div class="sched-cron-preview">${preview}</div> <div class="ext-schedules-cron-preview">${preview}</div>
`} `}
<//> <//>
<${FormField} label="Script"> <${FormField} label="Script">
<textarea rows="6" value=${script} onInput=${e => setScript(e.target.value)} <textarea rows="6" value=${script} onInput=${e => setScript(e.target.value)}
placeholder="Starlark script (optional)" class="sched-script" /> placeholder="Starlark script (optional)" class="ext-schedules-script" />
<//> <//>
<div class="sched-form__row"> <div class="ext-schedules-form__row">
<label class="sched-toggle"> <label class="ext-schedules-toggle">
<input type="checkbox" checked=${enabled} onChange=${e => setEnabled(e.target.checked)} /> <input type="checkbox" checked=${enabled} onChange=${e => setEnabled(e.target.checked)} />
<span>Enabled</span> <span>Enabled</span>
</label> </label>
</div> </div>
<div class="sched-form__actions"> <div class="ext-schedules-form__actions">
<${Button} variant="ghost" type="button" onClick=${onClose}>Cancel<//> <${Button} variant="ghost" type="button" onClick=${onClose}>Cancel<//>
<${Button} variant="primary" type="submit" disabled=${saving}> <${Button} variant="primary" type="submit" disabled=${saving}>
${saving ? 'Saving…' : (schedule ? 'Update' : 'Create')} ${saving ? 'Saving…' : (schedule ? 'Update' : 'Create')}
@@ -208,25 +208,25 @@
}, [scheduleId]); }, [scheduleId]);
return html` return html`
<div class="sched-logs"> <div class="ext-schedules-logs">
<div class="sched-logs__header"> <div class="ext-schedules-logs__header">
<span>Execution History</span> <span>Execution History</span>
<button class="sched-btn sched-btn--sm" onClick=${onClose}>Close</button> <button class="ext-schedules-btn ext-schedules-btn--sm" onClick=${onClose}>Close</button>
</div> </div>
${loading && html`<div class="sched-logs__loading"><${Spinner} size="sm" /></div>`} ${loading && html`<div class="ext-schedules-logs__loading"><${Spinner} size="sm" /></div>`}
${!loading && logs.length === 0 && html` ${!loading && logs.length === 0 && html`
<div class="sched-logs__empty">No executions yet</div> <div class="ext-schedules-logs__empty">No executions yet</div>
`} `}
${!loading && logs.map(function (log) { ${!loading && logs.map(function (log) {
var success = log.status === 'success' || log.status === 'ok'; var success = log.status === 'success' || log.status === 'ok';
return html` return html`
<div class="sched-log-entry" key=${log.id || log.started_at}> <div class="ext-schedules-log-entry" key=${log.id || log.started_at}>
<span class="sched-log-entry__status sched-log-entry__status--${success ? 'ok' : 'fail'}"> <span class="ext-schedules-log-entry__status ext-schedules-log-entry__status--${success ? 'ok' : 'fail'}">
${success ? '✓' : '✗'} ${success ? '✓' : '✗'}
</span> </span>
<span class="sched-log-entry__time">${_formatTime(log.started_at)}</span> <span class="ext-schedules-log-entry__time">${_formatTime(log.started_at)}</span>
<span class="sched-log-entry__duration">${_formatDuration(log.duration_ms)}</span> <span class="ext-schedules-log-entry__duration">${_formatDuration(log.duration_ms)}</span>
${log.error && html`<span class="sched-log-entry__error" title=${log.error}>${log.error}</span>`} ${log.error && html`<span class="ext-schedules-log-entry__error" title=${log.error}>${log.error}</span>`}
</div> </div>
`; `;
})} })}
@@ -286,13 +286,13 @@
} }
return html` return html`
<div class="sched-list"> <div class="ext-schedules-list">
${loading && html`<div class="sched-list__loading"><${Spinner} size="md" /></div>`} ${loading && html`<div class="ext-schedules-list__loading"><${Spinner} size="md" /></div>`}
${!loading && items.length === 0 && html` ${!loading && items.length === 0 && html`
<div class="sched-list__empty">No schedules yet. Create one to get started.</div> <div class="ext-schedules-list__empty">No schedules yet. Create one to get started.</div>
`} `}
${!loading && items.length > 0 && html` ${!loading && items.length > 0 && html`
<table class="sched-table"> <table class="ext-schedules-table">
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Name</th>
@@ -305,27 +305,27 @@
<tbody> <tbody>
${items.map(function (s) { ${items.map(function (s) {
return html` return html`
<tr key=${s.id} class="sched-row"> <tr key=${s.id} class="ext-schedules-row">
<td class="sched-row__name"> <td class="ext-schedules-row__name">
<button class="sched-link" onClick=${() => onEdit(s)}>${s.name}</button> <button class="ext-schedules-link" onClick=${() => onEdit(s)}>${s.name}</button>
${s.description && html`<div class="sched-row__desc">${s.description}</div>`} ${s.description && html`<div class="ext-schedules-row__desc">${s.description}</div>`}
</td> </td>
<td class="sched-row__cron"> <td class="ext-schedules-row__cron">
<span class="sched-cron-badge">${s.cron_expr}</span> <span class="ext-schedules-cron-badge">${s.cron_expr}</span>
<div class="sched-cron-human">${describeCron(s.cron_expr)}</div> <div class="ext-schedules-cron-human">${describeCron(s.cron_expr)}</div>
</td> </td>
<td class="sched-row__next">${s.next_fire_at ? _formatTime(s.next_fire_at) : '—'}</td> <td class="ext-schedules-row__next">${s.next_fire_at ? _formatTime(s.next_fire_at) : '—'}</td>
<td> <td>
<button class="sched-toggle-btn sched-toggle-btn--${s.enabled ? 'on' : 'off'}" <button class="ext-schedules-toggle-btn ext-schedules-toggle-btn--${s.enabled ? 'on' : 'off'}"
onClick=${() => handleToggle(s)} onClick=${() => handleToggle(s)}
title=${s.enabled ? 'Disable' : 'Enable'}> title=${s.enabled ? 'Disable' : 'Enable'}>
${s.enabled ? 'On' : 'Off'} ${s.enabled ? 'On' : 'Off'}
</button> </button>
</td> </td>
<td class="sched-row__actions"> <td class="ext-schedules-row__actions">
<button class="sched-btn sched-btn--sm" onClick=${() => handleRun(s)} title="Run now">▶</button> <button class="ext-schedules-btn ext-schedules-btn--sm" onClick=${() => handleRun(s)} title="Run now">▶</button>
<button class="sched-btn sched-btn--sm" onClick=${() => onShowLogs(s.id)} title="View logs">📋</button> <button class="ext-schedules-btn ext-schedules-btn--sm" onClick=${() => onShowLogs(s.id)} title="View logs">📋</button>
<button class="sched-btn sched-btn--sm sched-btn--danger" onClick=${() => handleDelete(s)} <button class="ext-schedules-btn ext-schedules-btn--sm ext-schedules-btn--danger" onClick=${() => handleDelete(s)}
title="Delete">×</button> title="Delete">×</button>
</td> </td>
</tr> </tr>
@@ -371,14 +371,14 @@
} }
return html` return html`
<div class="sched-app"> <div class="ext-schedules-app">
${Topbar ? html` ${Topbar ? html`
<${Topbar} title="Schedules"> <${Topbar} title="Schedules">
<span class="sched-count">${items.length} schedule${items.length !== 1 ? 's' : ''}</span> <span class="ext-schedules-count">${items.length} schedule${items.length !== 1 ? 's' : ''}</span>
<${Button} variant="primary" size="sm" onClick=${() => setEditing({})}>+ New Schedule<//> <${Button} variant="primary" size="sm" onClick=${() => setEditing({})}>+ New Schedule<//>
<//> <//>
` : html` ` : html`
<div class="sched-header"> <div class="ext-schedules-header">
<h1>Schedules</h1> <h1>Schedules</h1>
<${Button} variant="primary" size="sm" onClick=${() => setEditing({})}>+ New Schedule<//> <${Button} variant="primary" size="sm" onClick=${() => setEditing({})}>+ New Schedule<//>
</div> </div>
@@ -386,7 +386,7 @@
${error && html`<${Banner} variant="danger" text=${error} />`} ${error && html`<${Banner} variant="danger" text=${error} />`}
<div class="sched-body"> <div class="ext-schedules-body">
<${ScheduleList} <${ScheduleList}
items=${items} items=${items}
loading=${loading} loading=${loading}

View File

@@ -1,23 +1,23 @@
/* SDK Test Runner — Styles */ /* SDK Test Runner — Styles */
.sdkr-header { .ext-sdk-test-runner-header {
display: flex; display: flex;
align-items: baseline; align-items: baseline;
gap: 12px; gap: 12px;
margin-bottom: 4px; margin-bottom: 4px;
} }
.sdkr-header h1 { .ext-sdk-test-runner-header h1 {
font-size: 20px; font-size: 20px;
font-weight: 600; font-weight: 600;
margin: 0; margin: 0;
color: var(--text, #e0e0e0); color: var(--text, #e0e0e0);
} }
.sdkr-version { .ext-sdk-test-runner-version {
font-size: 12px; font-size: 12px;
color: var(--text-3, #888); color: var(--text-3, #888);
font-family: 'JetBrains Mono', monospace; font-family: 'JetBrains Mono', monospace;
} }
.sdkr-desc { .ext-sdk-test-runner-desc {
font-size: 13px; font-size: 13px;
color: var(--text-2, #aaa); color: var(--text-2, #aaa);
margin: 0 0 16px 0; margin: 0 0 16px 0;
@@ -25,27 +25,27 @@
} }
/* Fixtures panel */ /* Fixtures panel */
.sdkr-fixtures { .ext-sdk-test-runner-fixtures {
margin-bottom: 16px; margin-bottom: 16px;
padding: 10px 14px; padding: 10px 14px;
border: 1px solid var(--border, #333); border: 1px solid var(--border, #333);
border-radius: 6px; border-radius: 6px;
background: var(--bg-raised, #1a1a1a); background: var(--bg-raised, #1a1a1a);
} }
.sdkr-fixture-row { .ext-sdk-test-runner-fixture-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
flex-wrap: wrap; flex-wrap: wrap;
} }
.sdkr-fixture-label { .ext-sdk-test-runner-fixture-label {
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
color: var(--text-2, #aaa); color: var(--text-2, #aaa);
text-transform: uppercase; text-transform: uppercase;
white-space: nowrap; white-space: nowrap;
} }
.sdkr-select { .ext-sdk-test-runner-select {
padding: 4px 8px; padding: 4px 8px;
font-size: 13px; font-size: 13px;
background: var(--bg-raised, #222); background: var(--bg-raised, #222);
@@ -54,7 +54,7 @@
border-radius: 4px; border-radius: 4px;
min-width: 130px; min-width: 130px;
} }
.sdkr-input { .ext-sdk-test-runner-input {
padding: 4px 8px; padding: 4px 8px;
font-size: 13px; font-size: 13px;
background: var(--bg-raised, #222); background: var(--bg-raised, #222);
@@ -66,16 +66,16 @@
max-width: 400px; max-width: 400px;
font-family: 'JetBrains Mono', monospace; font-family: 'JetBrains Mono', monospace;
} }
.sdkr-input::placeholder { .ext-sdk-test-runner-input::placeholder {
color: var(--text-3, #666); color: var(--text-3, #666);
font-family: inherit; font-family: inherit;
} }
.sdkr-fixture-status { .ext-sdk-test-runner-fixture-status {
font-size: 12px; font-size: 12px;
color: var(--text-3, #888); color: var(--text-3, #888);
font-family: 'JetBrains Mono', monospace; font-family: 'JetBrains Mono', monospace;
} }
.sdkr-fixture-note { .ext-sdk-test-runner-fixture-note {
font-size: 11px; font-size: 11px;
color: var(--text-3, #666); color: var(--text-3, #666);
margin-top: 6px; margin-top: 6px;
@@ -83,26 +83,26 @@
} }
/* Controls */ /* Controls */
.sdkr-controls { .ext-sdk-test-runner-controls {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
margin-bottom: 16px; margin-bottom: 16px;
} }
.sdkr-filters { .ext-sdk-test-runner-filters {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
} }
.sdkr-filter-label { .ext-sdk-test-runner-filter-label {
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
color: var(--text-2, #aaa); color: var(--text-2, #aaa);
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.05em;
} }
.sdkr-check { .ext-sdk-test-runner-check {
font-size: 13px; font-size: 13px;
color: var(--text, #e0e0e0); color: var(--text, #e0e0e0);
cursor: pointer; cursor: pointer;
@@ -110,12 +110,12 @@
align-items: center; align-items: center;
gap: 3px; gap: 3px;
} }
.sdkr-check input { cursor: pointer; } .ext-sdk-test-runner-check input { cursor: pointer; }
.sdkr-btn-row { .ext-sdk-test-runner-btn-row {
display: flex; display: flex;
gap: 8px; gap: 8px;
} }
.sdkr-btn { .ext-sdk-test-runner-btn {
padding: 6px 14px; padding: 6px 14px;
border-radius: 6px; border-radius: 6px;
border: 1px solid var(--border, #444); border: 1px solid var(--border, #444);
@@ -125,22 +125,22 @@
cursor: pointer; cursor: pointer;
transition: background 0.15s; transition: background 0.15s;
} }
.sdkr-btn:hover { background: var(--bg-hover, #383838); } .ext-sdk-test-runner-btn:hover { background: var(--bg-hover, #383838); }
.sdkr-btn:disabled { opacity: 0.5; cursor: not-allowed; } .ext-sdk-test-runner-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.sdkr-btn-primary { .ext-sdk-test-runner-btn-primary {
background: var(--accent, #3b82f6); background: var(--accent, #3b82f6);
border-color: var(--accent, #3b82f6); border-color: var(--accent, #3b82f6);
color: #fff; color: #fff;
} }
.sdkr-btn-primary:hover { filter: brightness(1.15); } .ext-sdk-test-runner-btn-primary:hover { filter: brightness(1.15); }
.sdkr-btn-danger { .ext-sdk-test-runner-btn-danger {
background: var(--danger, #ef4444); background: var(--danger, #ef4444);
border-color: var(--danger, #ef4444); border-color: var(--danger, #ef4444);
color: #fff; color: #fff;
} }
/* Summary */ /* Summary */
.sdkr-summary { .ext-sdk-test-runner-summary {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px; gap: 8px;
@@ -150,7 +150,7 @@
border-radius: 8px; border-radius: 8px;
border: 1px solid var(--border, #333); border: 1px solid var(--border, #333);
} }
.sdkr-badge { .ext-sdk-test-runner-badge {
display: inline-block; display: inline-block;
padding: 3px 10px; padding: 3px 10px;
border-radius: 4px; border-radius: 4px;
@@ -160,17 +160,17 @@
} }
/* Table */ /* Table */
.sdkr-table-wrap { .ext-sdk-test-runner-table-wrap {
overflow-x: auto; overflow-x: auto;
border: 1px solid var(--border, #333); border: 1px solid var(--border, #333);
border-radius: 8px; border-radius: 8px;
} }
.sdkr-table { .ext-sdk-test-runner-table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
font-size: 13px; font-size: 13px;
} }
.sdkr-table th { .ext-sdk-test-runner-table th {
text-align: left; text-align: left;
padding: 8px 10px; padding: 8px 10px;
background: var(--bg-raised, #1e1e1e); background: var(--bg-raised, #1e1e1e);
@@ -184,31 +184,31 @@
top: 0; top: 0;
z-index: 1; z-index: 1;
} }
.sdkr-table td { .ext-sdk-test-runner-table td {
padding: 6px 10px; padding: 6px 10px;
border-bottom: 1px solid var(--border-subtle, #2a2a2a); border-bottom: 1px solid var(--border-subtle, #2a2a2a);
color: var(--text, #e0e0e0); color: var(--text, #e0e0e0);
vertical-align: top; vertical-align: top;
} }
.sdkr-table tbody tr:hover { .ext-sdk-test-runner-table tbody tr:hover {
background: var(--bg-hover, rgba(255,255,255,0.03)); background: var(--bg-hover, rgba(255,255,255,0.03));
} }
.sdkr-test-name { .ext-sdk-test-runner-test-name {
font-family: 'JetBrains Mono', monospace; font-family: 'JetBrains Mono', monospace;
font-size: 12px; font-size: 12px;
} }
.sdkr-verdict { .ext-sdk-test-runner-verdict {
font-weight: 700; font-weight: 700;
font-family: 'JetBrains Mono', monospace; font-family: 'JetBrains Mono', monospace;
font-size: 11px; font-size: 11px;
} }
.sdkr-ms { .ext-sdk-test-runner-ms {
font-family: 'JetBrains Mono', monospace; font-family: 'JetBrains Mono', monospace;
font-size: 11px; font-size: 11px;
color: var(--text-3, #888); color: var(--text-3, #888);
text-align: right; text-align: right;
} }
.sdkr-detail { .ext-sdk-test-runner-detail {
font-size: 11px; font-size: 11px;
color: var(--text-3, #888); color: var(--text-3, #888);
max-width: 400px; max-width: 400px;
@@ -216,7 +216,7 @@
} }
/* Row verdict highlights */ /* Row verdict highlights */
.sdkr-row-sdk_bug td { background: rgba(245, 158, 11, 0.06); } .ext-sdk-test-runner-row-sdk_bug td { background: rgba(245, 158, 11, 0.06); }
.sdkr-row-icd_bug td { background: rgba(239, 68, 68, 0.06); } .ext-sdk-test-runner-row-icd_bug td { background: rgba(239, 68, 68, 0.06); }
.sdkr-row-shape_bug td { background: rgba(168, 85, 247, 0.06); } .ext-sdk-test-runner-row-shape_bug td { background: rgba(168, 85, 247, 0.06); }
.sdkr-row-error td { background: rgba(239, 68, 68, 0.08); } .ext-sdk-test-runner-row-error td { background: rgba(239, 68, 68, 0.08); }

View File

@@ -58,25 +58,25 @@
mount.innerHTML = ''; mount.innerHTML = '';
// Header // Header
var header = el('div', { className: 'sdkr-header' }, [ var header = el('div', { className: 'ext-sdk-test-runner-header' }, [
el('h1', {}, 'SDK Test Runner'), el('h1', {}, 'SDK Test Runner'),
el('span', { className: 'sdkr-version' }, 'v' + (T.manifest.version || '?')) el('span', { className: 'ext-sdk-test-runner-version' }, 'v' + (T.manifest.version || '?'))
]); ]);
mount.appendChild(header); mount.appendChild(header);
// Description // Description
mount.appendChild(el('p', { className: 'sdkr-desc' }, mount.appendChild(el('p', { className: 'ext-sdk-test-runner-desc' },
'Dual-path validation: SDK call → raw ICD fetch. ' + 'Dual-path validation: SDK call → raw ICD fetch. ' +
'SDK fail + ICD pass = SDK bug. Both fail = ICD bug.')); 'SDK fail + ICD pass = SDK bug. Both fail = ICD bug.'));
// ── Fixtures Panel ── // ── Fixtures Panel ──
var fixturePanel = el('div', { className: 'sdkr-fixtures' }); var fixturePanel = el('div', { className: 'ext-sdk-test-runner-fixtures' });
var fixRow = el('div', { className: 'sdkr-fixture-row' }); var fixRow = el('div', { className: 'ext-sdk-test-runner-fixture-row' });
fixRow.appendChild(el('span', { className: 'sdkr-fixture-label' }, 'FIXTURES:')); fixRow.appendChild(el('span', { className: 'ext-sdk-test-runner-fixture-label' }, 'FIXTURES:'));
// Provider dropdown // Provider dropdown
var provSelect = el('select', { className: 'sdkr-select' }); var provSelect = el('select', { className: 'ext-sdk-test-runner-select' });
provSelect.appendChild(el('option', { value: '' }, '— provider —')); provSelect.appendChild(el('option', { value: '' }, '— provider —'));
['openrouter', 'openai', 'anthropic', 'venice'].forEach(function (p) { ['openrouter', 'openai', 'anthropic', 'venice'].forEach(function (p) {
provSelect.appendChild(el('option', { value: p }, p)); provSelect.appendChild(el('option', { value: p }, p));
@@ -85,18 +85,18 @@
// API key input // API key input
var keyInput = el('input', { var keyInput = el('input', {
type: 'password', className: 'sdkr-input', type: 'password', className: 'ext-sdk-test-runner-input',
placeholder: 'API key (optional — enables model tests)' placeholder: 'API key (optional — enables model tests)'
}); });
fixRow.appendChild(keyInput); fixRow.appendChild(keyInput);
// Fixture status // Fixture status
var fixStatus = el('span', { className: 'sdkr-fixture-status' }, ''); var fixStatus = el('span', { className: 'ext-sdk-test-runner-fixture-status' }, '');
fixRow.appendChild(fixStatus); fixRow.appendChild(fixStatus);
fixturePanel.appendChild(fixRow); fixturePanel.appendChild(fixRow);
var fixNote = el('div', { className: 'sdkr-fixture-note' }, var fixNote = el('div', { className: 'ext-sdk-test-runner-fixture-note' },
'Provide a provider + API key to unlock embedding/model tests. ' + 'Provide a provider + API key to unlock embedding/model tests. ' +
'Without a key, those tests are skipped. Key is never stored.'); 'Without a key, those tests are skipped. Key is never stored.');
fixturePanel.appendChild(fixNote); fixturePanel.appendChild(fixNote);
@@ -107,16 +107,16 @@
T._fixtureInput = { provSelect: provSelect, keyInput: keyInput, statusEl: fixStatus }; T._fixtureInput = { provSelect: provSelect, keyInput: keyInput, statusEl: fixStatus };
// Controls row // Controls row
var controlRow = el('div', { className: 'sdkr-controls' }); var controlRow = el('div', { className: 'ext-sdk-test-runner-controls' });
// Domain filter checkboxes // Domain filter checkboxes
var filterBox = el('div', { className: 'sdkr-filters' }); var filterBox = el('div', { className: 'ext-sdk-test-runner-filters' });
filterBox.appendChild(el('span', { className: 'sdkr-filter-label' }, 'Domains: ')); filterBox.appendChild(el('span', { className: 'ext-sdk-test-runner-filter-label' }, 'Domains: '));
var domainNames = Object.keys(T.domains).sort(); var domainNames = Object.keys(T.domains).sort();
var domainChecks = {}; var domainChecks = {};
domainNames.forEach(function (name) { domainNames.forEach(function (name) {
var label = el('label', { className: 'sdkr-check' }); var label = el('label', { className: 'ext-sdk-test-runner-check' });
var cb = el('input', { type: 'checkbox', checked: 'checked' }); var cb = el('input', { type: 'checkbox', checked: 'checked' });
domainChecks[name] = cb; domainChecks[name] = cb;
label.appendChild(cb); label.appendChild(cb);
@@ -126,11 +126,11 @@
controlRow.appendChild(filterBox); controlRow.appendChild(filterBox);
// Buttons // Buttons
var btnRow = el('div', { className: 'sdkr-btn-row' }); var btnRow = el('div', { className: 'ext-sdk-test-runner-btn-row' });
var runBtn = el('button', { className: 'sdkr-btn sdkr-btn-primary', onClick: onRun }, 'Run All'); var runBtn = el('button', { className: 'ext-sdk-test-runner-btn ext-sdk-test-runner-btn-primary', onClick: onRun }, 'Run All');
var abortBtn = el('button', { className: 'sdkr-btn sdkr-btn-danger', onClick: function () { T.abort(); } }, 'Abort'); var abortBtn = el('button', { className: 'ext-sdk-test-runner-btn ext-sdk-test-runner-btn-danger', onClick: function () { T.abort(); } }, 'Abort');
abortBtn.style.display = 'none'; abortBtn.style.display = 'none';
var exportBtn = el('button', { className: 'sdkr-btn', onClick: onExport }, 'Export JSON'); var exportBtn = el('button', { className: 'ext-sdk-test-runner-btn', onClick: onExport }, 'Export JSON');
btnRow.appendChild(runBtn); btnRow.appendChild(runBtn);
btnRow.appendChild(abortBtn); btnRow.appendChild(abortBtn);
btnRow.appendChild(exportBtn); btnRow.appendChild(exportBtn);
@@ -138,12 +138,12 @@
mount.appendChild(controlRow); mount.appendChild(controlRow);
// Summary bar // Summary bar
var summaryBar = el('div', { className: 'sdkr-summary' }); var summaryBar = el('div', { className: 'ext-sdk-test-runner-summary' });
mount.appendChild(summaryBar); mount.appendChild(summaryBar);
// Results table // Results table
var tableWrap = el('div', { className: 'sdkr-table-wrap' }); var tableWrap = el('div', { className: 'ext-sdk-test-runner-table-wrap' });
var table = el('table', { className: 'sdkr-table' }); var table = el('table', { className: 'ext-sdk-test-runner-table' });
var thead = el('thead', {}, [ var thead = el('thead', {}, [
el('tr', {}, [ el('tr', {}, [
el('th', {}, '#'), el('th', {}, '#'),
@@ -176,7 +176,7 @@
]; ];
items.forEach(function (item) { items.forEach(function (item) {
var badge = el('span', { var badge = el('span', {
className: 'sdkr-badge', className: 'ext-sdk-test-runner-badge',
style: { style: {
background: item[1] > 0 ? COLORS[item[0]] : 'var(--bg2, #333)', background: item[1] > 0 ? COLORS[item[0]] : 'var(--bg2, #333)',
color: item[1] > 0 ? '#fff' : 'var(--text3, #888)' color: item[1] > 0 ? '#fff' : 'var(--text3, #888)'
@@ -184,7 +184,7 @@
}, LABELS[item[0]] + ': ' + item[1]); }, LABELS[item[0]] + ': ' + item[1]);
summaryBar.appendChild(badge); summaryBar.appendChild(badge);
}); });
summaryBar.appendChild(el('span', { className: 'sdkr-badge', summaryBar.appendChild(el('span', { className: 'ext-sdk-test-runner-badge',
style: { background: 'var(--bg2, #333)' } style: { background: 'var(--bg2, #333)' }
}, 'Total: ' + total)); }, 'Total: ' + total));
} }
@@ -197,19 +197,19 @@
if (entry.rawErr) detail += (detail ? ' | ' : '') + 'ICD: ' + esc(entry.rawErr); if (entry.rawErr) detail += (detail ? ' | ' : '') + 'ICD: ' + esc(entry.rawErr);
if (entry.note) detail += (detail ? ' | ' : '') + esc(entry.note); if (entry.note) detail += (detail ? ' | ' : '') + esc(entry.note);
var row = el('tr', { className: 'sdkr-row sdkr-row-' + v.toLowerCase() }, [ var row = el('tr', { className: 'ext-sdk-test-runner-row ext-sdk-test-runner-row-' + v.toLowerCase() }, [
el('td', {}, String(idx)), el('td', {}, String(idx)),
el('td', {}, esc(entry.domain)), el('td', {}, esc(entry.domain)),
el('td', {}, esc(entry.group)), el('td', {}, esc(entry.group)),
el('td', { className: 'sdkr-test-name' }, esc(entry.name)), el('td', { className: 'ext-sdk-test-runner-test-name' }, esc(entry.name)),
el('td', {}, [ el('td', {}, [
el('span', { el('span', {
className: 'sdkr-verdict', className: 'ext-sdk-test-runner-verdict',
style: { color: COLORS[v] || '#fff' } style: { color: COLORS[v] || '#fff' }
}, LABELS[v] || v) }, LABELS[v] || v)
]), ]),
el('td', { className: 'sdkr-ms' }, String(entry.durationMs)), el('td', { className: 'ext-sdk-test-runner-ms' }, String(entry.durationMs)),
el('td', { className: 'sdkr-detail' }, detail) el('td', { className: 'ext-sdk-test-runner-detail' }, detail)
]); ]);
tbody.appendChild(row); tbody.appendChild(row);
updateSummary(); updateSummary();

View File

@@ -1,6 +1,6 @@
/* ── Tasks Surface Styles ────────────────── */ /* ── Tasks Surface Styles ────────────────── */
.tasks-app { .ext-tasks-app {
max-width: 1100px; max-width: 1100px;
margin: 0 auto; margin: 0 auto;
padding: 24px 20px; padding: 24px 20px;
@@ -11,36 +11,36 @@
} }
/* ── Header ──────────────────────────────── */ /* ── Header ──────────────────────────────── */
.tasks-header { .ext-tasks-header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
margin-bottom: 20px; margin-bottom: 20px;
flex-shrink: 0; flex-shrink: 0;
} }
.tasks-title { .ext-tasks-title {
font-size: 20px; font-size: 20px;
font-weight: 600; font-weight: 600;
color: var(--text); color: var(--text);
} }
.tasks-stats { .ext-tasks-stats {
font-size: 13px; font-size: 13px;
color: var(--text-3); color: var(--text-3);
} }
.tasks-header__right { .ext-tasks-header__right {
margin-left: auto; margin-left: auto;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
} }
.tasks-view-tabs { .ext-tasks-view-tabs {
display: flex; display: flex;
gap: 2px; gap: 2px;
background: var(--bg-raised); background: var(--bg-raised);
border-radius: var(--radius); border-radius: var(--radius);
padding: 2px; padding: 2px;
} }
.tasks-view-tab { .ext-tasks-view-tab {
padding: 5px 14px; padding: 5px 14px;
font-size: 13px; font-size: 13px;
border: none; border: none;
@@ -50,25 +50,25 @@
cursor: pointer; cursor: pointer;
transition: var(--transition); transition: var(--transition);
} }
.tasks-view-tab:hover { color: var(--text); background: var(--bg-hover); } .ext-tasks-view-tab:hover { color: var(--text); background: var(--bg-hover); }
.tasks-view-tab--active { color: var(--text); background: var(--bg-surface); } .ext-tasks-view-tab--active { color: var(--text); background: var(--bg-surface); }
/* ── List View ───────────────────────────── */ /* ── List View ───────────────────────────── */
.task-list { .ext-tasks-list {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 6px; gap: 6px;
} }
.task-list__filters { .ext-tasks-list__filters {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
margin-bottom: 8px; margin-bottom: 8px;
flex-shrink: 0; flex-shrink: 0;
} }
.task-filter { .ext-tasks-filter {
padding: 5px 10px; padding: 5px 10px;
font-size: 13px; font-size: 13px;
background: var(--bg-raised); background: var(--bg-raised);
@@ -76,11 +76,11 @@
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--radius); border-radius: var(--radius);
} }
.task-list__count { .ext-tasks-list__count {
font-size: 12px; font-size: 12px;
color: var(--text-3); color: var(--text-3);
} }
.task-list__loading, .task-list__empty { .ext-tasks-list__loading, .ext-tasks-list__empty {
display: flex; display: flex;
justify-content: center; justify-content: center;
padding: 40px 0; padding: 40px 0;
@@ -89,43 +89,43 @@
} }
/* ── Task Card ───────────────────────────── */ /* ── Task Card ───────────────────────────── */
.task-card { .ext-tasks-card {
background: var(--bg-surface); background: var(--bg-surface);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--radius); border-radius: var(--radius);
padding: 12px 14px; padding: 12px 14px;
transition: var(--transition); transition: var(--transition);
} }
.task-card:hover { .ext-tasks-card:hover {
border-color: var(--border-light); border-color: var(--border-light);
background: var(--bg-raised); background: var(--bg-raised);
} }
.task-card__header { .ext-tasks-card__header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
} }
.task-card__priority { font-size: 10px; } .ext-tasks-card__priority { font-size: 10px; }
.task-card__title { .ext-tasks-card__title {
flex: 1; flex: 1;
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
color: var(--text); color: var(--text);
cursor: pointer; cursor: pointer;
} }
.task-card__title:hover { color: var(--accent); } .ext-tasks-card__title:hover { color: var(--accent); }
.task-card__status { .ext-tasks-card__status {
font-size: 11px; font-size: 11px;
padding: 2px 8px; padding: 2px 8px;
border-radius: 10px; border-radius: 10px;
font-weight: 500; font-weight: 500;
} }
.task-card__status--todo { background: var(--accent-dim); color: var(--accent); } .ext-tasks-card__status--todo { background: var(--accent-dim); color: var(--accent); }
.task-card__status--in_progress { background: var(--warning-dim); color: var(--warning); } .ext-tasks-card__status--in_progress { background: var(--warning-dim); color: var(--warning); }
.task-card__status--done { background: var(--success-dim); color: var(--success); } .ext-tasks-card__status--done { background: var(--success-dim); color: var(--success); }
.task-card__status--cancelled { background: var(--bg-raised); color: var(--text-3); } .ext-tasks-card__status--cancelled { background: var(--bg-raised); color: var(--text-3); }
.task-card__desc { .ext-tasks-card__desc {
font-size: 13px; font-size: 13px;
color: var(--text-2); color: var(--text-2);
margin-top: 6px; margin-top: 6px;
@@ -134,7 +134,7 @@
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.task-card__footer { .ext-tasks-card__footer {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
@@ -142,17 +142,17 @@
font-size: 12px; font-size: 12px;
color: var(--text-3); color: var(--text-3);
} }
.task-card__actions { .ext-tasks-card__actions {
margin-left: auto; margin-left: auto;
display: flex; display: flex;
gap: 4px; gap: 4px;
opacity: 0; opacity: 0;
transition: var(--transition); transition: var(--transition);
} }
.task-card:hover .task-card__actions { opacity: 1; } .ext-tasks-card:hover .ext-tasks-card__actions { opacity: 1; }
/* ── Inline buttons ──────────────────────── */ /* ── Inline buttons ──────────────────────── */
.task-btn { .ext-tasks-btn {
border: none; border: none;
background: var(--bg-raised); background: var(--bg-raised);
color: var(--text-2); color: var(--text-2);
@@ -160,12 +160,12 @@
border-radius: var(--radius); border-radius: var(--radius);
transition: var(--transition); transition: var(--transition);
} }
.task-btn--sm { padding: 2px 8px; font-size: 13px; } .ext-tasks-btn--sm { padding: 2px 8px; font-size: 13px; }
.task-btn:hover { background: var(--bg-hover); color: var(--text); } .ext-tasks-btn:hover { background: var(--bg-hover); color: var(--text); }
.task-btn--danger:hover { background: var(--danger-dim); color: var(--danger); } .ext-tasks-btn--danger:hover { background: var(--danger-dim); color: var(--danger); }
/* ── Board View ──────────────────────────── */ /* ── Board View ──────────────────────────── */
.task-board { .ext-tasks-board {
flex: 1; flex: 1;
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
@@ -173,14 +173,14 @@
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
} }
.task-board__col { .ext-tasks-board__col {
background: var(--bg-raised); background: var(--bg-raised);
border-radius: var(--radius); border-radius: var(--radius);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 0; min-height: 0;
} }
.task-board__col-header { .ext-tasks-board__col-header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
@@ -191,14 +191,14 @@
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
flex-shrink: 0; flex-shrink: 0;
} }
.task-board__col-count { .ext-tasks-board__col-count {
font-size: 11px; font-size: 11px;
color: var(--text-3); color: var(--text-3);
background: var(--bg-hover); background: var(--bg-hover);
padding: 1px 7px; padding: 1px 7px;
border-radius: 10px; border-radius: 10px;
} }
.task-board__col-body { .ext-tasks-board__col-body {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding: 8px; padding: 8px;
@@ -206,7 +206,7 @@
flex-direction: column; flex-direction: column;
gap: 6px; gap: 6px;
} }
.task-board__card { .ext-tasks-board__card {
background: var(--bg-surface); background: var(--bg-surface);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--radius); border-radius: var(--radius);
@@ -214,10 +214,10 @@
cursor: pointer; cursor: pointer;
transition: var(--transition); transition: var(--transition);
} }
.task-board__card:hover { .ext-tasks-board__card:hover {
border-color: var(--accent); border-color: var(--accent);
} }
.task-board__card-title { .ext-tasks-board__card-title {
font-size: 13px; font-size: 13px;
font-weight: 500; font-weight: 500;
color: var(--text); color: var(--text);
@@ -225,19 +225,19 @@
align-items: center; align-items: center;
gap: 6px; gap: 6px;
} }
.task-board__card-due { .ext-tasks-board__card-due {
font-size: 11px; font-size: 11px;
color: var(--text-3); color: var(--text-3);
margin-top: 4px; margin-top: 4px;
} }
/* ── Form ────────────────────────────────── */ /* ── Form ────────────────────────────────── */
.task-form { display: flex; flex-direction: column; gap: 12px; } .ext-tasks-form { display: flex; flex-direction: column; gap: 12px; }
.task-form__row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } .ext-tasks-form__row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.task-form__actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 4px; } .ext-tasks-form__actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 4px; }
.task-form textarea, .ext-tasks-form textarea,
.task-form input, .ext-tasks-form input,
.task-form select { .ext-tasks-form select {
width: 100%; width: 100%;
padding: 7px 10px; padding: 7px 10px;
font-size: 13px; font-size: 13px;
@@ -247,15 +247,15 @@
border-radius: var(--radius); border-radius: var(--radius);
font-family: var(--font); font-family: var(--font);
} }
.task-form textarea:focus, .ext-tasks-form textarea:focus,
.task-form input:focus, .ext-tasks-form input:focus,
.task-form select:focus { .ext-tasks-form select:focus {
outline: none; outline: none;
border-color: var(--accent); border-color: var(--accent);
} }
/* ── Statusbar widget ────────────────────── */ /* ── Statusbar widget ────────────────────── */
.task-status-widget { .ext-tasks-status-widget {
font-size: 12px; font-size: 12px;
color: var(--text-3); color: var(--text-3);
padding: 4px 10px; padding: 4px 10px;

View File

@@ -88,7 +88,7 @@
return html` return html`
<${Dialog} open onClose=${onClose} title=${task ? 'Edit Task' : 'New Task'}> <${Dialog} open onClose=${onClose} title=${task ? 'Edit Task' : 'New Task'}>
<form onSubmit=${handleSubmit} class="task-form"> <form onSubmit=${handleSubmit} class="ext-tasks-form">
${error && html`<${Banner} variant="danger" text=${error} />`} ${error && html`<${Banner} variant="danger" text=${error} />`}
<${FormField} label="Title" required> <${FormField} label="Title" required>
<input type="text" value=${title} onInput=${e => setTitle(e.target.value)} <input type="text" value=${title} onInput=${e => setTitle(e.target.value)}
@@ -98,7 +98,7 @@
<textarea rows="3" value=${desc} onInput=${e => setDesc(e.target.value)} <textarea rows="3" value=${desc} onInput=${e => setDesc(e.target.value)}
placeholder="Details (optional)" /> placeholder="Details (optional)" />
<//> <//>
<div class="task-form__row"> <div class="ext-tasks-form__row">
<${FormField} label="Status"> <${FormField} label="Status">
<select value=${status} onChange=${e => setStatus(e.target.value)}> <select value=${status} onChange=${e => setStatus(e.target.value)}>
${STATUSES.map(s => html`<option value=${s}>${STATUS_LABELS[s] || s}</option>`)} ${STATUSES.map(s => html`<option value=${s}>${STATUS_LABELS[s] || s}</option>`)}
@@ -110,7 +110,7 @@
</select> </select>
<//> <//>
</div> </div>
<div class="task-form__row"> <div class="ext-tasks-form__row">
<${FormField} label="Due Date"> <${FormField} label="Due Date">
<input type="date" value=${dueDate} onInput=${e => setDueDate(e.target.value)} /> <input type="date" value=${dueDate} onInput=${e => setDueDate(e.target.value)} />
<//> <//>
@@ -119,7 +119,7 @@
placeholder="comma-separated" /> placeholder="comma-separated" />
<//> <//>
</div> </div>
<div class="task-form__actions"> <div class="ext-tasks-form__actions">
<${Button} variant="ghost" type="button" onClick=${onClose}>Cancel<//> <${Button} variant="ghost" type="button" onClick=${onClose}>Cancel<//>
<${Button} variant="primary" type="submit" disabled=${saving}> <${Button} variant="primary" type="submit" disabled=${saving}>
${saving ? 'Saving…' : (task ? 'Update' : 'Create')} ${saving ? 'Saving…' : (task ? 'Update' : 'Create')}
@@ -155,45 +155,45 @@
} }
return html` return html`
<div class="task-list"> <div class="ext-tasks-list">
<div class="task-list__filters"> <div class="ext-tasks-list__filters">
<select value=${filter} onChange=${e => setFilter(e.target.value)} class="task-filter"> <select value=${filter} onChange=${e => setFilter(e.target.value)} class="ext-tasks-filter">
<option value="all">All</option> <option value="all">All</option>
${STATUSES.map(s => html`<option value=${s}>${STATUS_LABELS[s] || s}</option>`)} ${STATUSES.map(s => html`<option value=${s}>${STATUS_LABELS[s] || s}</option>`)}
</select> </select>
<span class="task-list__count">${filtered.length} task${filtered.length !== 1 ? 's' : ''}</span> <span class="ext-tasks-list__count">${filtered.length} task${filtered.length !== 1 ? 's' : ''}</span>
</div> </div>
${loading && html`<div class="task-list__loading"><${Spinner} size="md" /></div>`} ${loading && html`<div class="ext-tasks-list__loading"><${Spinner} size="md" /></div>`}
${!loading && filtered.length === 0 && html` ${!loading && filtered.length === 0 && html`
<div class="task-list__empty">No tasks${filter !== 'all' ? ' matching filter' : ''}. Create one to get started.</div> <div class="ext-tasks-list__empty">No tasks${filter !== 'all' ? ' matching filter' : ''}. Create one to get started.</div>
`} `}
${!loading && filtered.map(function(t) { ${!loading && filtered.map(function(t) {
return html` return html`
<div class="task-card" key=${t.id}> <div class="ext-tasks-card" key=${t.id}>
<div class="task-card__header"> <div class="ext-tasks-card__header">
<span class="task-card__priority" style="color:${PRIORITY_COLORS[t.priority] || 'var(--text-3)'}"> <span class="ext-tasks-card__priority" style="color:${PRIORITY_COLORS[t.priority] || 'var(--text-3)'}">
</span> </span>
<span class="task-card__title" onClick=${() => onEdit(t)}>${t.title}</span> <span class="ext-tasks-card__title" onClick=${() => onEdit(t)}>${t.title}</span>
<span class="task-card__status task-card__status--${t.status}"> <span class="ext-tasks-card__status ext-tasks-card__status--${t.status}">
${STATUS_LABELS[t.status] || t.status} ${STATUS_LABELS[t.status] || t.status}
</span> </span>
</div> </div>
${t.description && html` ${t.description && html`
<div class="task-card__desc">${t.description}</div> <div class="ext-tasks-card__desc">${t.description}</div>
`} `}
<div class="task-card__footer"> <div class="ext-tasks-card__footer">
${t.due_date && html`<span class="task-card__due">Due: ${t.due_date}</span>`} ${t.due_date && html`<span class="ext-tasks-card__due">Due: ${t.due_date}</span>`}
${t.tags && html`<span class="task-card__tags">${t.tags}</span>`} ${t.tags && html`<span class="ext-tasks-card__tags">${t.tags}</span>`}
<div class="task-card__actions"> <div class="ext-tasks-card__actions">
${t.status !== 'done' && html` ${t.status !== 'done' && html`
<button class="task-btn task-btn--sm" onClick=${() => handleQuickStatus(t.id, 'done')} <button class="ext-tasks-btn ext-tasks-btn--sm" onClick=${() => handleQuickStatus(t.id, 'done')}
title="Mark done">✓</button> title="Mark done">✓</button>
`} `}
<button class="task-btn task-btn--sm task-btn--danger" onClick=${() => handleDelete(t.id)} <button class="ext-tasks-btn ext-tasks-btn--sm ext-tasks-btn--danger" onClick=${() => handleDelete(t.id)}
title="Delete">×</button> title="Delete">×</button>
</div> </div>
</div> </div>
@@ -216,25 +216,25 @@
} }
return html` return html`
<div class="task-board"> <div class="ext-tasks-board">
${loading && html`<div class="task-list__loading"><${Spinner} size="md" /></div>`} ${loading && html`<div class="ext-tasks-list__loading"><${Spinner} size="md" /></div>`}
${!loading && STATUSES.map(function(status) { ${!loading && STATUSES.map(function(status) {
var col = items.filter(function(t) { return t.status === status; }); var col = items.filter(function(t) { return t.status === status; });
return html` return html`
<div class="task-board__col" key=${status}> <div class="ext-tasks-board__col" key=${status}>
<div class="task-board__col-header"> <div class="ext-tasks-board__col-header">
<span class="task-board__col-title">${STATUS_LABELS[status] || status}</span> <span class="ext-tasks-board__col-title">${STATUS_LABELS[status] || status}</span>
<span class="task-board__col-count">${col.length}</span> <span class="ext-tasks-board__col-count">${col.length}</span>
</div> </div>
<div class="task-board__col-body"> <div class="ext-tasks-board__col-body">
${col.map(function(t) { ${col.map(function(t) {
return html` return html`
<div class="task-board__card" key=${t.id} onClick=${() => onEdit(t)}> <div class="ext-tasks-board__card" key=${t.id} onClick=${() => onEdit(t)}>
<div class="task-board__card-title"> <div class="ext-tasks-board__card-title">
<span class="task-card__priority" style="color:${PRIORITY_COLORS[t.priority] || 'var(--text-3)'}">●</span> <span class="ext-tasks-card__priority" style="color:${PRIORITY_COLORS[t.priority] || 'var(--text-3)'}">●</span>
${t.title} ${t.title}
</div> </div>
${t.due_date && html`<div class="task-board__card-due">Due: ${t.due_date}</div>`} ${t.due_date && html`<div class="ext-tasks-board__card-due">Due: ${t.due_date}</div>`}
</div> </div>
`; `;
})} })}
@@ -307,22 +307,22 @@
var Topbar = sw.shell?.Topbar; var Topbar = sw.shell?.Topbar;
return html` return html`
<div class="tasks-app"> <div class="ext-tasks-app">
${Topbar ? html` ${Topbar ? html`
<${Topbar} title="Tasks"> <${Topbar} title="Tasks">
${stats && html`<span class="tasks-stats">${stats.total || 0} total</span>`} ${stats && html`<span class="ext-tasks-stats">${stats.total || 0} total</span>`}
<div class="tasks-view-tabs"> <div class="ext-tasks-view-tabs">
${tabs.map(function(tab) { ${tabs.map(function(tab) {
return html`<button key=${tab.id} return html`<button key=${tab.id}
class="tasks-view-tab ${view === tab.id ? 'tasks-view-tab--active' : ''}" class="ext-tasks-view-tab ${view === tab.id ? 'ext-tasks-view-tab--active' : ''}"
onClick=${() => setView(tab.id)}>${tab.label}</button>`; onClick=${() => setView(tab.id)}>${tab.label}</button>`;
})} })}
</div> </div>
<${Button} variant="primary" size="sm" onClick=${() => setEditTask({})}>+ New Task<//> <${Button} variant="primary" size="sm" onClick=${() => setEditTask({})}>+ New Task<//>
<//> <//>
` : html` ` : html`
<div class="tasks-header"> <div class="ext-tasks-header">
<h1 class="tasks-title">Tasks</h1> <h1 class="ext-tasks-title">Tasks</h1>
<${Button} variant="primary" size="sm" onClick=${() => setEditTask({})}>+ New Task<//> <${Button} variant="primary" size="sm" onClick=${() => setEditTask({})}>+ New Task<//>
</div> </div>
`} `}
@@ -381,7 +381,7 @@
}, []); }, []);
if (count === null) return null; if (count === null) return null;
return html`<span class="task-status-widget" title="Open tasks">${count} open task${count !== 1 ? 's' : ''}</span>`; return html`<span class="ext-tasks-status-widget" title="Open tasks">${count} open task${count !== 1 ? 's' : ''}</span>`;
} }
sw.slots.register('statusbar', { sw.slots.register('statusbar', {

View File

@@ -2,7 +2,7 @@
* Uses platform CSS variables exclusively for theming. * Uses platform CSS variables exclusively for theming.
*/ */
.tal-shell { .ext-team-activity-log-shell {
max-width: 720px; max-width: 720px;
margin: 0 auto; margin: 0 auto;
padding: 24px 16px; padding: 24px 16px;
@@ -14,34 +14,34 @@
/* ── Header ────────────────────────────────── */ /* ── Header ────────────────────────────────── */
.tal-header { .ext-team-activity-log-header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 12px; gap: 12px;
} }
.tal-header__left { .ext-team-activity-log-header__left {
display: flex; display: flex;
align-items: baseline; align-items: baseline;
gap: 10px; gap: 10px;
} }
.tal-title { .ext-team-activity-log-title {
font-size: 20px; font-size: 20px;
font-weight: 600; font-weight: 600;
color: var(--text); color: var(--text);
margin: 0; margin: 0;
} }
.tal-subtitle { .ext-team-activity-log-subtitle {
font-size: 13px; font-size: 13px;
color: var(--text-3); color: var(--text-3);
} }
/* ── Entry Form ────────────────────────────── */ /* ── Entry Form ────────────────────────────── */
.tal-form { .ext-team-activity-log-form {
display: flex; display: flex;
gap: 8px; gap: 8px;
background: var(--bg-surface); background: var(--bg-surface);
@@ -50,7 +50,7 @@
padding: 10px 12px; padding: 10px 12px;
} }
.tal-form__select { .ext-team-activity-log-form__select {
background: var(--bg-raised); background: var(--bg-raised);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--radius); border-radius: var(--radius);
@@ -61,7 +61,7 @@
cursor: pointer; cursor: pointer;
} }
.tal-form__input { .ext-team-activity-log-form__input {
flex: 1; flex: 1;
background: var(--bg-raised); background: var(--bg-raised);
border: 1px solid var(--border); border: 1px solid var(--border);
@@ -74,15 +74,15 @@
transition: border-color var(--transition); transition: border-color var(--transition);
} }
.tal-form__input:focus { .ext-team-activity-log-form__input:focus {
border-color: var(--accent); border-color: var(--accent);
} }
.tal-form__input::placeholder { .ext-team-activity-log-form__input::placeholder {
color: var(--text-3); color: var(--text-3);
} }
.tal-form__btn { .ext-team-activity-log-form__btn {
background: var(--accent); background: var(--accent);
color: #fff; color: #fff;
border: none; border: none;
@@ -95,25 +95,25 @@
transition: background var(--transition); transition: background var(--transition);
} }
.tal-form__btn:hover:not(:disabled) { .ext-team-activity-log-form__btn:hover:not(:disabled) {
background: var(--accent-hover); background: var(--accent-hover);
} }
.tal-form__btn:disabled { .ext-team-activity-log-form__btn:disabled {
opacity: 0.5; opacity: 0.5;
cursor: default; cursor: default;
} }
/* ── Filters ───────────────────────────────── */ /* ── Filters ───────────────────────────────── */
.tal-filters { .ext-team-activity-log-filters {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
flex-wrap: wrap; flex-wrap: wrap;
} }
.tal-filter { .ext-team-activity-log-filter {
background: var(--bg-raised); background: var(--bg-raised);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--radius); border-radius: var(--radius);
@@ -125,18 +125,18 @@
transition: all var(--transition); transition: all var(--transition);
} }
.tal-filter:hover { .ext-team-activity-log-filter:hover {
background: var(--bg-hover); background: var(--bg-hover);
color: var(--text); color: var(--text);
} }
.tal-filter.active { .ext-team-activity-log-filter.ext-team-activity-log-active {
background: var(--accent-dim); background: var(--accent-dim);
border-color: var(--accent); border-color: var(--accent);
color: var(--accent); color: var(--accent);
} }
.tal-count { .ext-team-activity-log-count {
margin-left: auto; margin-left: auto;
font-size: 12px; font-size: 12px;
color: var(--text-3); color: var(--text-3);
@@ -144,13 +144,13 @@
/* ── Entry List ────────────────────────────── */ /* ── Entry List ────────────────────────────── */
.tal-entries { .ext-team-activity-log-entries {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 6px; gap: 6px;
} }
.tal-entry { .ext-team-activity-log-entry {
background: var(--bg-surface); background: var(--bg-surface);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--radius); border-radius: var(--radius);
@@ -158,36 +158,36 @@
transition: border-color var(--transition); transition: border-color var(--transition);
} }
.tal-entry:hover { .ext-team-activity-log-entry:hover {
border-color: var(--border-light); border-color: var(--border-light);
} }
.tal-entry__header { .ext-team-activity-log-entry__header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
margin-bottom: 4px; margin-bottom: 4px;
} }
.tal-entry__category { .ext-team-activity-log-entry__category {
font-size: 11px; font-size: 11px;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.02em; letter-spacing: 0.02em;
} }
.tal-entry__user { .ext-team-activity-log-entry__user {
font-size: 13px; font-size: 13px;
font-weight: 500; font-weight: 500;
color: var(--text); color: var(--text);
} }
.tal-entry__time { .ext-team-activity-log-entry__time {
font-size: 12px; font-size: 12px;
color: var(--text-3); color: var(--text-3);
margin-left: auto; margin-left: auto;
} }
.tal-entry__delete { .ext-team-activity-log-entry__delete {
background: none; background: none;
border: none; border: none;
color: var(--text-3); color: var(--text-3);
@@ -199,15 +199,15 @@
transition: all var(--transition); transition: all var(--transition);
} }
.tal-entry:hover .tal-entry__delete { .ext-team-activity-log-entry:hover .ext-team-activity-log-entry__delete {
opacity: 1; opacity: 1;
} }
.tal-entry__delete:hover { .ext-team-activity-log-entry__delete:hover {
color: var(--danger); color: var(--danger);
} }
.tal-entry__message { .ext-team-activity-log-entry__message {
font-size: 14px; font-size: 14px;
color: var(--text); color: var(--text);
line-height: 1.5; line-height: 1.5;
@@ -215,8 +215,8 @@
/* ── Empty / Loading ───────────────────────── */ /* ── Empty / Loading ───────────────────────── */
.tal-empty, .ext-team-activity-log-empty,
.tal-loading { .ext-team-activity-log-loading {
text-align: center; text-align: center;
padding: 40px 16px; padding: 40px 16px;
color: var(--text-3); color: var(--text-3);

View File

@@ -104,34 +104,34 @@
} }
return html` return html`
<div class="tal-shell"> <div class="ext-team-activity-log-shell">
<header class="tal-header"> <header class="ext-team-activity-log-header">
<div class="tal-header__left"> <div class="ext-team-activity-log-header__left">
<h1 class="tal-title">Activity Log</h1> <h1 class="ext-team-activity-log-title">Activity Log</h1>
<span class="tal-subtitle">Team activity tracker</span> <span class="ext-team-activity-log-subtitle">Team activity tracker</span>
</div> </div>
<div class="tal-header__right"> <div class="ext-team-activity-log-header__right">
<div ref=${menuRef}></div> <div ref=${menuRef}></div>
</div> </div>
</header> </header>
<${EntryForm} onSubmit=${addEntry} /> <${EntryForm} onSubmit=${addEntry} />
<div class="tal-filters"> <div class="ext-team-activity-log-filters">
<button class="tal-filter ${filter === '' ? 'active' : ''}" <button class="ext-team-activity-log-filter ${filter === '' ? 'ext-team-activity-log-active' : ''}"
onClick=${() => setFilter('')}>All</button> onClick=${() => setFilter('')}>All</button>
${CATEGORIES.map(c => html` ${CATEGORIES.map(c => html`
<button key=${c} <button key=${c}
class="tal-filter ${filter === c ? 'active' : ''}" class="ext-team-activity-log-filter ${filter === c ? 'ext-team-activity-log-active' : ''}"
onClick=${() => setFilter(c)}> onClick=${() => setFilter(c)}>
${c} ${c}
</button> </button>
`)} `)}
<span class="tal-count">${entries.length} entries</span> <span class="ext-team-activity-log-count">${entries.length} entries</span>
</div> </div>
${loading ${loading
? html`<div class="tal-loading">Loading…</div>` ? html`<div class="ext-team-activity-log-loading">Loading…</div>`
: html`<${EntryList} entries=${entries} onDelete=${deleteEntry} />` : html`<${EntryList} entries=${entries} onDelete=${deleteEntry} />`
} }
</div> </div>
@@ -153,17 +153,17 @@
} }
return html` return html`
<form class="tal-form" onSubmit=${submit}> <form class="ext-team-activity-log-form" onSubmit=${submit}>
<select class="tal-form__select" value=${category} <select class="ext-team-activity-log-form__select" value=${category}
onChange=${e => setCategory(e.target.value)}> onChange=${e => setCategory(e.target.value)}>
${CATEGORIES.map(c => html`<option key=${c} value=${c}>${c}</option>`)} ${CATEGORIES.map(c => html`<option key=${c} value=${c}>${c}</option>`)}
</select> </select>
<input class="tal-form__input" type="text" <input class="ext-team-activity-log-form__input" type="text"
placeholder="What happened?" placeholder="What happened?"
value=${message} value=${message}
onInput=${e => setMessage(e.target.value)} onInput=${e => setMessage(e.target.value)}
disabled=${submitting} /> disabled=${submitting} />
<button class="tal-form__btn" type="submit" disabled=${submitting || !message.trim()}> <button class="ext-team-activity-log-form__btn" type="submit" disabled=${submitting || !message.trim()}>
${submitting ? '…' : 'Log'} ${submitting ? '…' : 'Log'}
</button> </button>
</form> </form>
@@ -172,21 +172,21 @@
function EntryList({ entries, onDelete }) { function EntryList({ entries, onDelete }) {
if (!entries.length) { if (!entries.length) {
return html`<div class="tal-empty">No entries yet. Log your first activity above.</div>`; return html`<div class="ext-team-activity-log-empty">No entries yet. Log your first activity above.</div>`;
} }
return html` return html`
<div class="tal-entries"> <div class="ext-team-activity-log-entries">
${entries.map(e => html` ${entries.map(e => html`
<div key=${e.id} class="tal-entry"> <div key=${e.id} class="ext-team-activity-log-entry">
<div class="tal-entry__header"> <div class="ext-team-activity-log-entry__header">
<span class="tal-entry__category badge">${e.category}</span> <span class="ext-team-activity-log-entry__category badge">${e.category}</span>
<span class="tal-entry__user">${esc(e.username || 'unknown')}</span> <span class="ext-team-activity-log-entry__user">${esc(e.username || 'unknown')}</span>
<span class="tal-entry__time">${timeAgo(e.created_at)}</span> <span class="ext-team-activity-log-entry__time">${timeAgo(e.created_at)}</span>
<button class="tal-entry__delete" onClick=${() => onDelete(e.id)} <button class="ext-team-activity-log-entry__delete" onClick=${() => onDelete(e.id)}
title="Delete">×</button> title="Delete">×</button>
</div> </div>
<div class="tal-entry__message">${esc(e.message)}</div> <div class="ext-team-activity-log-entry__message">${esc(e.message)}</div>
</div> </div>
`)} `)}
</div> </div>

View File

@@ -4,14 +4,14 @@
defined in variables.css for automatic dark/light support. defined in variables.css for automatic dark/light support.
========================================== */ ========================================== */
.surface-workflow-demo { .ext-workflow-demo {
max-width: 1100px; max-width: 1100px;
margin: 0 auto; margin: 0 auto;
padding: 0 1.5rem 2rem; padding: 0 1.5rem 2rem;
} }
/* Topbar */ /* Topbar */
.demo-topbar { .ext-workflow-demo-topbar {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
@@ -19,7 +19,7 @@
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.demo-topbar h1 { .ext-workflow-demo-topbar h1 {
font-size: 1.5rem; font-size: 1.5rem;
font-weight: 700; font-weight: 700;
margin: 0; margin: 0;
@@ -27,26 +27,26 @@
} }
/* Subtitle */ /* Subtitle */
.demo-subtitle { .ext-workflow-demo-subtitle {
color: var(--text-2); color: var(--text-2);
margin: 0 0 1.5rem; margin: 0 0 1.5rem;
font-size: 0.9rem; font-size: 0.9rem;
} }
/* Grid */ /* Grid */
.demo-grid { .ext-workflow-demo-grid {
display: grid; display: grid;
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: 1.5rem; gap: 1.5rem;
} }
@media (min-width: 768px) { @media (min-width: 768px) {
.demo-grid { .ext-workflow-demo-grid {
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
} }
} }
/* Card */ /* Card */
.demo-card { .ext-workflow-demo-card {
background: var(--bg-surface); background: var(--bg-surface);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--radius); border-radius: var(--radius);
@@ -58,22 +58,22 @@
} }
/* Title row */ /* Title row */
.card-title-row { .ext-workflow-demo-card-title-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
} }
.card-title-row h2 { .ext-workflow-demo-card-title-row h2 {
font-size: 1.1rem; font-size: 1.1rem;
font-weight: 600; font-weight: 600;
margin: 0; margin: 0;
flex: 1; flex: 1;
color: var(--text); color: var(--text);
} }
.card-icon { .ext-workflow-demo-card-icon {
font-size: 1.3rem; font-size: 1.3rem;
} }
.card-tier { .ext-workflow-demo-card-tier {
font-size: 0.7rem; font-size: 0.7rem;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.05em;
@@ -81,11 +81,11 @@
border-radius: 4px; border-radius: 4px;
font-weight: 600; font-weight: 600;
} }
.badge-browser { background: var(--success-dim); color: var(--success-light); } .ext-workflow-demo-badge-browser { background: var(--success-dim); color: var(--success-light); }
.badge-starlark { background: var(--accent-dim); color: var(--accent-light); } .ext-workflow-demo-badge-starlark { background: var(--accent-dim); color: var(--accent-light); }
/* Description */ /* Description */
.card-desc { .ext-workflow-demo-card-desc {
color: var(--text-2); color: var(--text-2);
font-size: 0.85rem; font-size: 0.85rem;
margin: 0; margin: 0;
@@ -93,12 +93,12 @@
} }
/* Badges */ /* Badges */
.card-badges { .ext-workflow-demo-card-badges {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.35rem; gap: 0.35rem;
} }
.demo-card .badge { .ext-workflow-demo-card .ext-workflow-demo-badge {
font-size: 0.7rem; font-size: 0.7rem;
padding: 2px 8px; padding: 2px 8px;
border-radius: 10px; border-radius: 10px;
@@ -106,12 +106,12 @@
color: var(--text-2); color: var(--text-2);
white-space: nowrap; white-space: nowrap;
} }
.demo-card .badge-installed { background: var(--success-dim); color: var(--success-light); } .ext-workflow-demo-card .ext-workflow-demo-badge-installed { background: var(--success-dim); color: var(--success-light); }
.demo-card .badge-not-installed { background: var(--warning-dim); color: var(--warning-light); } .ext-workflow-demo-card .ext-workflow-demo-badge-not-installed { background: var(--warning-dim); color: var(--warning-light); }
.demo-card .badge-needs-publish { background: var(--danger-dim); color: var(--danger-light); } .ext-workflow-demo-card .ext-workflow-demo-badge-needs-publish { background: var(--danger-dim); color: var(--danger-light); }
/* Stage flow diagram */ /* Stage flow diagram */
.stage-flow { .ext-workflow-demo-stage-flow {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
gap: 0.25rem; gap: 0.25rem;
@@ -122,7 +122,7 @@
overflow-x: auto; overflow-x: auto;
} }
.stage-node { .ext-workflow-demo-stage-node {
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 6px; border-radius: 6px;
padding: 0.4rem 0.6rem; padding: 0.4rem 0.6rem;
@@ -132,31 +132,31 @@
font-size: 0.75rem; font-size: 0.75rem;
color: var(--text); color: var(--text);
} }
.stage-node.stage-public { .ext-workflow-demo-stage-node.ext-workflow-demo-stage-public {
border-color: var(--success); border-color: var(--success);
background: var(--success-dim); background: var(--success-dim);
} }
.stage-node.stage-system { .ext-workflow-demo-stage-node.ext-workflow-demo-stage-system {
border-color: var(--accent); border-color: var(--accent);
background: var(--accent-dim); background: var(--accent-dim);
} }
.stage-label { .ext-workflow-demo-stage-label {
font-weight: 600; font-weight: 600;
font-size: 0.8rem; font-size: 0.8rem;
} }
.stage-meta { .ext-workflow-demo-stage-meta {
color: var(--text-3); color: var(--text-3);
font-size: 0.65rem; font-size: 0.65rem;
} }
.stage-note { .ext-workflow-demo-stage-note {
color: var(--warning-light); color: var(--warning-light);
font-size: 0.6rem; font-size: 0.6rem;
font-style: italic; font-style: italic;
margin-top: 2px; margin-top: 2px;
} }
.stage-arrow { .ext-workflow-demo-stage-arrow {
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 1.1rem; font-size: 1.1rem;
@@ -166,25 +166,25 @@
} }
/* Collapsible sections */ /* Collapsible sections */
.card-collapsible { .ext-workflow-demo-card-collapsible {
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 6px; border-radius: 6px;
font-size: 0.8rem; font-size: 0.8rem;
} }
.card-collapsible summary { .ext-workflow-demo-card-collapsible summary {
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
cursor: pointer; cursor: pointer;
font-weight: 600; font-weight: 600;
user-select: none; user-select: none;
color: var(--text); color: var(--text);
} }
.card-collapsible summary:hover { .ext-workflow-demo-card-collapsible summary:hover {
background: var(--bg-hover); background: var(--bg-hover);
} }
.collapsible-content { .ext-workflow-demo-collapsible-content {
padding: 0 0.75rem 0.75rem; padding: 0 0.75rem 0.75rem;
} }
.collapsible-content pre { .ext-workflow-demo-collapsible-content pre {
background: var(--bg-raised); background: var(--bg-raised);
color: var(--text); color: var(--text);
padding: 0.5rem; padding: 0.5rem;
@@ -197,7 +197,7 @@
} }
/* Actions */ /* Actions */
.card-actions { .ext-workflow-demo-card-actions {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
@@ -205,7 +205,7 @@
padding-top: 0.5rem; padding-top: 0.5rem;
border-top: 1px solid var(--border); border-top: 1px solid var(--border);
} }
.demo-card .btn { .ext-workflow-demo-card .ext-workflow-demo-btn {
padding: 0.4rem 1rem; padding: 0.4rem 1rem;
border: none; border: none;
border-radius: 6px; border-radius: 6px;
@@ -216,13 +216,13 @@
/* demo-card buttons — uses kernel .sw-btn--primary */ /* demo-card buttons — uses kernel .sw-btn--primary */
/* Public link row */ /* Public link row */
.public-link-row { .ext-workflow-demo-public-link-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
margin-top: 0.25rem; margin-top: 0.25rem;
} }
.public-link-row input { .ext-workflow-demo-public-link-row input {
flex: 1; flex: 1;
font-size: 0.7rem; font-size: 0.7rem;
padding: 4px 8px; padding: 4px 8px;
@@ -232,7 +232,7 @@
color: var(--text); color: var(--text);
font-family: var(--mono); font-family: var(--mono);
} }
.public-link-row .btn-copy { .ext-workflow-demo-public-link-row .ext-workflow-demo-btn-copy {
padding: 4px 10px; padding: 4px 10px;
font-size: 0.7rem; font-size: 0.7rem;
background: var(--bg-raised); background: var(--bg-raised);
@@ -241,6 +241,6 @@
color: var(--text-2); color: var(--text-2);
cursor: pointer; cursor: pointer;
} }
.public-link-row .btn-copy:hover { .ext-workflow-demo-public-link-row .ext-workflow-demo-btn-copy:hover {
background: var(--bg-hover); background: var(--bg-hover);
} }

View File

@@ -103,25 +103,25 @@
if (!mount) return; if (!mount) return;
const surface = document.createElement('div'); const surface = document.createElement('div');
surface.className = 'surface-workflow-demo'; surface.className = 'ext-workflow-demo';
mount.appendChild(surface); mount.appendChild(surface);
// Topbar with user menu // Topbar with user menu
const topbar = document.createElement('div'); const topbar = document.createElement('div');
topbar.className = 'demo-topbar'; topbar.className = 'ext-workflow-demo-topbar';
topbar.innerHTML = '<h1>Workflow Demo</h1>'; topbar.innerHTML = '<h1>Workflow Demo</h1>';
surface.appendChild(topbar); surface.appendChild(topbar);
sw.userMenu(topbar, { flyout: 'down' }); sw.userMenu(topbar, { flyout: 'down' });
// Subtitle // Subtitle
const subtitle = document.createElement('p'); const subtitle = document.createElement('p');
subtitle.className = 'demo-subtitle'; subtitle.className = 'ext-workflow-demo-subtitle';
subtitle.textContent = 'Four example workflows that prove the engine works end-to-end. Install any package via Admin > Packages.'; subtitle.textContent = 'Four example workflows that prove the engine works end-to-end. Install any package via Admin > Packages.';
surface.appendChild(subtitle); surface.appendChild(subtitle);
// Workflow cards // Workflow cards
const grid = document.createElement('div'); const grid = document.createElement('div');
grid.className = 'demo-grid'; grid.className = 'ext-workflow-demo-grid';
surface.appendChild(grid); surface.appendChild(grid);
// Fetch installed workflows to check status // Fetch installed workflows to check status
@@ -145,28 +145,28 @@
function _buildCard(wf, installedWf) { function _buildCard(wf, installedWf) {
const card = document.createElement('div'); const card = document.createElement('div');
card.className = 'demo-card'; card.className = 'ext-workflow-demo-card';
// Title row // Title row
const titleRow = document.createElement('div'); const titleRow = document.createElement('div');
titleRow.className = 'card-title-row'; titleRow.className = 'ext-workflow-demo-card-title-row';
titleRow.innerHTML = '<span class="card-icon">' + wf.icon + '</span>' titleRow.innerHTML = '<span class="ext-workflow-demo-card-icon">' + wf.icon + '</span>'
+ '<h2>' + _esc(wf.title) + '</h2>' + '<h2>' + _esc(wf.title) + '</h2>'
+ '<span class="card-tier badge-' + wf.tier + '">' + wf.tier + '</span>'; + '<span class="ext-workflow-demo-card-tier ext-workflow-demo-badge-' + wf.tier + '">' + wf.tier + '</span>';
card.appendChild(titleRow); card.appendChild(titleRow);
// Description // Description
const desc = document.createElement('p'); const desc = document.createElement('p');
desc.className = 'card-desc'; desc.className = 'ext-workflow-demo-card-desc';
desc.textContent = wf.description; desc.textContent = wf.description;
card.appendChild(desc); card.appendChild(desc);
// Badges // Badges
const badges = document.createElement('div'); const badges = document.createElement('div');
badges.className = 'card-badges'; badges.className = 'ext-workflow-demo-card-badges';
for (const b of wf.badges) { for (const b of wf.badges) {
const span = document.createElement('span'); const span = document.createElement('span');
span.className = 'badge'; span.className = 'ext-workflow-demo-badge';
span.textContent = b; span.textContent = b;
badges.appendChild(span); badges.appendChild(span);
} }
@@ -188,7 +188,7 @@
// Status + action // Status + action
const actions = document.createElement('div'); const actions = document.createElement('div');
actions.className = 'card-actions'; actions.className = 'ext-workflow-demo-card-actions';
if (installedWf) { if (installedWf) {
const isActive = installedWf.is_active; const isActive = installedWf.is_active;
@@ -199,18 +199,18 @@
// Installed badge // Installed badge
const status = document.createElement('span'); const status = document.createElement('span');
status.className = 'badge badge-installed'; status.className = 'ext-workflow-demo-badge ext-workflow-demo-badge-installed';
status.textContent = 'Installed'; status.textContent = 'Installed';
actions.appendChild(status); actions.appendChild(status);
if (!isActive) { if (!isActive) {
const hint = document.createElement('span'); const hint = document.createElement('span');
hint.className = 'badge badge-needs-publish'; hint.className = 'ext-workflow-demo-badge ext-workflow-demo-badge-needs-publish';
hint.textContent = 'Not Active'; hint.textContent = 'Not Active';
actions.appendChild(hint); actions.appendChild(hint);
} else if (!isPublished) { } else if (!isPublished) {
const hint = document.createElement('span'); const hint = document.createElement('span');
hint.className = 'badge badge-needs-publish'; hint.className = 'ext-workflow-demo-badge ext-workflow-demo-badge-needs-publish';
hint.textContent = 'Not Published'; hint.textContent = 'Not Published';
actions.appendChild(hint); actions.appendChild(hint);
} }
@@ -235,14 +235,14 @@
// Public link for public_link workflows // Public link for public_link workflows
if (installedWf.entry_mode === 'public_link' && isReady) { if (installedWf.entry_mode === 'public_link' && isReady) {
const linkRow = document.createElement('div'); const linkRow = document.createElement('div');
linkRow.className = 'public-link-row'; linkRow.className = 'ext-workflow-demo-public-link-row';
const linkInput = document.createElement('input'); const linkInput = document.createElement('input');
linkInput.type = 'text'; linkInput.type = 'text';
linkInput.readOnly = true; linkInput.readOnly = true;
linkInput.value = window.location.origin + base + '/api/v1/public/workflows/' + installedWf.id + '/start'; linkInput.value = window.location.origin + base + '/api/v1/public/workflows/' + installedWf.id + '/start';
linkRow.appendChild(linkInput); linkRow.appendChild(linkInput);
const copyBtn = document.createElement('button'); const copyBtn = document.createElement('button');
copyBtn.className = 'btn-copy'; copyBtn.className = 'ext-workflow-demo-btn-copy';
copyBtn.textContent = 'Copy'; copyBtn.textContent = 'Copy';
copyBtn.onclick = function () { copyBtn.onclick = function () {
navigator.clipboard.writeText(linkInput.value).then(function () { navigator.clipboard.writeText(linkInput.value).then(function () {
@@ -255,7 +255,7 @@
} }
} else { } else {
const status = document.createElement('span'); const status = document.createElement('span');
status.className = 'badge badge-not-installed'; status.className = 'ext-workflow-demo-badge ext-workflow-demo-badge-not-installed';
status.textContent = 'Not Installed'; status.textContent = 'Not Installed';
actions.appendChild(status); actions.appendChild(status);
} }
@@ -268,7 +268,7 @@
function _buildStageDiagram(wf) { function _buildStageDiagram(wf) {
const container = document.createElement('div'); const container = document.createElement('div');
container.className = 'stage-flow'; container.className = 'ext-workflow-demo-stage-flow';
const stages = wf.stages; const stages = wf.stages;
let hasBranch = false; let hasBranch = false;
@@ -277,21 +277,21 @@
const s = stages[i]; const s = stages[i];
const node = document.createElement('div'); const node = document.createElement('div');
node.className = 'stage-node stage-' + s.audience; node.className = 'ext-workflow-demo-stage-node ext-workflow-demo-stage-' + s.audience;
const label = document.createElement('div'); const label = document.createElement('div');
label.className = 'stage-label'; label.className = 'ext-workflow-demo-stage-label';
label.textContent = s.name; label.textContent = s.name;
node.appendChild(label); node.appendChild(label);
const meta = document.createElement('div'); const meta = document.createElement('div');
meta.className = 'stage-meta'; meta.className = 'ext-workflow-demo-stage-meta';
meta.textContent = s.mode + (s.audience !== 'team' ? ' (' + s.audience + ')' : ''); meta.textContent = s.mode + (s.audience !== 'team' ? ' (' + s.audience + ')' : '');
node.appendChild(meta); node.appendChild(meta);
if (s.note) { if (s.note) {
const note = document.createElement('div'); const note = document.createElement('div');
note.className = 'stage-note'; note.className = 'ext-workflow-demo-stage-note';
note.textContent = s.note; note.textContent = s.note;
node.appendChild(note); node.appendChild(note);
} }
@@ -301,13 +301,13 @@
// Arrow (except after last stage) // Arrow (except after last stage)
if (i < stages.length - 1 && !stages[i + 1].branch) { if (i < stages.length - 1 && !stages[i + 1].branch) {
const arrow = document.createElement('div'); const arrow = document.createElement('div');
arrow.className = 'stage-arrow'; arrow.className = 'ext-workflow-demo-stage-arrow';
arrow.textContent = '\u2192'; arrow.textContent = '\u2192';
container.appendChild(arrow); container.appendChild(arrow);
} else if (stages[i + 1] && stages[i + 1].branch) { } else if (stages[i + 1] && stages[i + 1].branch) {
if (!hasBranch) { if (!hasBranch) {
const split = document.createElement('div'); const split = document.createElement('div');
split.className = 'stage-arrow stage-branch'; split.className = 'ext-workflow-demo-stage-arrow ext-workflow-demo-stage-branch';
split.textContent = '\u2193\u2197'; split.textContent = '\u2193\u2197';
container.appendChild(split); container.appendChild(split);
hasBranch = true; hasBranch = true;
@@ -322,14 +322,14 @@
function _buildCollapsible(title, innerHtml) { function _buildCollapsible(title, innerHtml) {
const details = document.createElement('details'); const details = document.createElement('details');
details.className = 'card-collapsible'; details.className = 'ext-workflow-demo-card-collapsible';
const summary = document.createElement('summary'); const summary = document.createElement('summary');
summary.textContent = title; summary.textContent = title;
details.appendChild(summary); details.appendChild(summary);
const content = document.createElement('div'); const content = document.createElement('div');
content.className = 'collapsible-content'; content.className = 'ext-workflow-demo-collapsible-content';
content.innerHTML = innerHtml; content.innerHTML = innerHtml;
details.appendChild(content); details.appendChild(content);

129
scripts/lint-package-css.sh Executable file
View File

@@ -0,0 +1,129 @@
#!/usr/bin/env bash
# lint-package-css.sh — Enforce extension CSS prefix convention
#
# Rule: The FIRST class selector in every rule must start with .ext-{slug}.
# Subsequent classes in compound/descendant selectors are allowed (they're
# scoped under the extension's namespace). Kernel .sw-* classes are always
# allowed. CodeMirror .cm-* classes are allowed as descendants.
#
# Exemptions: :root, @keyframes blocks, @font-face blocks, @media/@supports
# wrappers, CSS comments.
#
# Usage: bash scripts/lint-package-css.sh [packages-dir]
# Exit 0 = all clean, Exit 1 = violations found.
set -euo pipefail
PKG_DIR="${1:-packages}"
VIOLATIONS=0
for css in "$PKG_DIR"/*/css/main.css; do
[ -f "$css" ] || continue
# Derive slug from directory: {pkg_dir}/{slug}/css/main.css
slug="$(basename "$(dirname "$(dirname "$css")")")"
prefix="ext-${slug}"
in_keyframes=0
in_fontface=0
in_comment=0
brace_depth=0
lineno=0
while IFS= read -r line; do
lineno=$((lineno + 1))
# Track block comments
if (( in_comment )); then
[[ "$line" == *"*/"* ]] && in_comment=0
continue
fi
if [[ "$line" == *"/*"* ]] && [[ "$line" != *"*/"* ]]; then
in_comment=1
continue
fi
# Skip single-line comments
trimmed="${line#"${line%%[![:space:]]*}"}"
[[ "$trimmed" == "" ]] && continue
[[ "$trimmed" == /\** ]] && continue
# Track @keyframes blocks
if [[ "$trimmed" =~ ^@keyframes ]]; then
in_keyframes=1
continue
fi
if (( in_keyframes )); then
[[ "$trimmed" == "}" ]] && in_keyframes=0
continue
fi
# Skip @font-face
if [[ "$trimmed" =~ ^@font-face ]]; then
in_fontface=1
continue
fi
if (( in_fontface )); then
[[ "$trimmed" == *"}"* ]] && in_fontface=0
continue
fi
# Skip @-rule wrappers
[[ "$trimmed" =~ ^@ ]] && continue
# Skip :root selectors
[[ "$trimmed" =~ ^:root ]] && continue
# Skip closing braces
[[ "$trimmed" == "}" ]] && continue
# Skip property declarations (contain : but not {)
if [[ "$trimmed" == *":"* ]] && [[ "$trimmed" != *"{"* ]]; then
continue
fi
# Selector line — check if it contains a class
if [[ "$trimmed" == *"."* ]]; then
# Split comma-separated selectors and check each one
# We process the whole line as potentially multiple selectors
IFS=',' read -ra selectors <<< "$trimmed"
for sel in "${selectors[@]}"; do
# Trim whitespace
sel="${sel#"${sel%%[![:space:]]*}"}"
sel="${sel%"${sel##*[![:space:]]}"}"
# Remove trailing { if present
sel="${sel%\{}"
sel="${sel%"${sel##*[![:space:]]}"}"
# Skip if empty
[[ -z "$sel" ]] && continue
# Find the FIRST class selector in this selector
if [[ "$sel" =~ \.([a-zA-Z_][a-zA-Z0-9_-]*) ]]; then
first_cls="${BASH_REMATCH[1]}"
# Allow kernel .sw-* as first class
[[ "$first_cls" == sw-* ]] && continue
# Allow [data-ext=...] scoped selectors
[[ "$sel" =~ ^\[data-ext ]] && continue
# Must start with ext-{slug}
if [[ "$first_cls" != ${prefix}* ]]; then
echo "VIOLATION: $css:$lineno — .${first_cls} (expected .${prefix}-*)"
VIOLATIONS=$((VIOLATIONS + 1))
fi
fi
done
fi
done < "$css"
done
if (( VIOLATIONS > 0 )); then
echo ""
echo "Found $VIOLATIONS violation(s). First class selector in each rule must start with .ext-{slug}-"
exit 1
else
echo "All package CSS files pass prefix enforcement."
exit 0
fi

View File

@@ -13,6 +13,6 @@
{{define "surface-extension"}} {{define "surface-extension"}}
<div id="extension-surface" class="extension-surface" <div id="extension-surface" class="extension-surface"
data-surface-id="{{.Surface}}"> data-surface-id="{{.Surface}}">
<div id="extension-mount" class="extension-mount"></div> <div id="extension-mount" class="extension-mount" data-ext="{{.Surface}}"></div>
</div> </div>
{{end}} {{end}}

View File

@@ -103,7 +103,7 @@ export function createMarkdown(renderers) {
renderer(token) { renderer(token) {
const target = _escapeAttr(token.target); const target = _escapeAttr(token.target);
const display = _escapeHtml(token.display); const display = _escapeHtml(token.display);
return `<a class="wikilink" data-wikilink="${target}" href="javascript:void(0)">${display}</a>`; return `<a class="ext-notes-wikilink" data-wikilink="${target}" href="javascript:void(0)">${display}</a>`;
}, },
}], }],