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.
## 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
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. |
| 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 |
|------|-------------|
| 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. |
| 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. |
| 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`. |
| Migrate in-tree packages | Add `data-ext` attribute to extension mount container. Prefix all in-tree package CSS. Verify no visual regressions. |
| Scoping strategy | Prefix enforcement (option B). `@scope` support still patchy; prefix works everywhere, trivially lintable. |
| 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 | `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 | `data-ext="{{.Surface}}"` on extension mount. All 12 packages prefixed to `.ext-{slug}-*`. CSS + JS updated in lockstep. |
### v0.6.13 — Responsive & Spacing
@@ -120,7 +121,7 @@ v0.6.10 Viewport Foundation ✅ 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

View File

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

View File

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

View File

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

View File

@@ -28,7 +28,7 @@
if (!mount) return;
const surface = document.createElement('div');
surface.className = 'surface-dashboard';
surface.className = 'ext-dashboard';
surface.id = 'dashboardSurface';
mount.appendChild(surface);
@@ -41,12 +41,12 @@
// ── Body ──
const body = document.createElement('div');
body.className = 'dashboard-body';
body.className = 'ext-dashboard-body';
surface.appendChild(body);
// ── Sidebar (tabs: activity + notes) ──
const sidebar = document.createElement('div');
sidebar.className = 'dashboard-sidebar';
sidebar.className = 'ext-dashboard-sidebar';
body.appendChild(sidebar);
// sw.tabs — two tabs
@@ -83,14 +83,14 @@
// ── Main content area ──
const main = document.createElement('div');
main.className = 'dashboard-main';
main.className = 'ext-dashboard-main';
body.appendChild(main);
_buildMainContent(main);
// ── Theme reactivity ──
sw.theme.on('change', function (resolved) {
const cards = main.querySelectorAll('.dashboard-card');
const cards = main.querySelectorAll('.ext-dashboard-card');
cards.forEach(function (c) {
c.style.borderColor = ''; // reset to CSS default for new theme
});
@@ -98,7 +98,7 @@
// ── Cross-component events ──
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');
@@ -108,15 +108,15 @@
function _buildTopbar() {
const el = document.createElement('div');
el.className = 'dashboard-topbar';
el.className = 'ext-dashboard-topbar';
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>' +
'Back' +
'</a>' +
'<div class="dashboard-topbar-sep"></div>' +
'<span class="dashboard-topbar-title">Dashboard</span>' +
'<div class="dashboard-topbar-spacer"></div>';
'<div class="ext-dashboard-topbar-sep"></div>' +
'<span class="ext-dashboard-topbar-title">Dashboard</span>' +
'<div class="ext-dashboard-topbar-spacer"></div>';
// sw.toolbar — action buttons
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>',
title: 'Refresh',
onClick: function () {
const cards = document.querySelector('.dashboard-cards');
const cards = document.querySelector('.ext-dashboard-cards');
if (cards) _loadChannels(cards, _currentFilter);
sw.toast('Refreshed', 'success');
},
@@ -155,22 +155,22 @@
// sw.user — greeting
const user = sw.user;
const greeting = document.createElement('div');
greeting.className = 'dashboard-greeting';
greeting.className = 'ext-dashboard-greeting';
greeting.textContent = 'Welcome back' + (user ? ', ' + (user.display_name || user.username) : '');
main.appendChild(greeting);
const subtitle = document.createElement('div');
subtitle.className = 'dashboard-subtitle';
subtitle.className = 'ext-dashboard-subtitle';
subtitle.textContent = 'Your recent conversations' + (sw.isAdmin ? ' \u00b7 Admin' : '');
main.appendChild(subtitle);
// ── Filter bar ──
const filterBar = document.createElement('div');
filterBar.className = 'dashboard-filter-bar';
filterBar.className = 'ext-dashboard-filter-bar';
main.appendChild(filterBar);
const filterLabel = document.createElement('span');
filterLabel.className = 'dashboard-filter-label';
filterLabel.className = 'ext-dashboard-filter-label';
filterLabel.textContent = 'Filter';
filterBar.appendChild(filterLabel);
@@ -192,7 +192,7 @@
// ── Cards ──
const cards = document.createElement('div');
cards.className = 'dashboard-cards';
cards.className = 'ext-dashboard-cards';
main.appendChild(cards);
_loadChannels(cards, '');
@@ -200,16 +200,16 @@
// ── Admin section (sw.isAdmin) ──
if (sw.isAdmin) {
const adminSection = document.createElement('div');
adminSection.className = 'dashboard-admin-section';
adminSection.className = 'ext-dashboard-admin-section';
main.appendChild(adminSection);
const sectionTitle = document.createElement('div');
sectionTitle.className = 'dashboard-section-title';
sectionTitle.className = 'ext-dashboard-section-title';
sectionTitle.textContent = 'Administration';
adminSection.appendChild(sectionTitle);
const adminCards = document.createElement('div');
adminCards.className = 'dashboard-cards';
adminCards.className = 'ext-dashboard-cards';
adminSection.appendChild(adminCards);
_loadAdminCards(adminCards);
@@ -220,7 +220,7 @@
async function _loadChannels(container, typeFilter) {
if (!container) return;
container.innerHTML = '<div class="dashboard-empty">Loading\u2026</div>';
container.innerHTML = '<div class="ext-dashboard-empty">Loading\u2026</div>';
try {
// sw.api — real API call
@@ -231,7 +231,7 @@
container.innerHTML = '';
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;
}
@@ -239,31 +239,31 @@
container.appendChild(_buildChannelCard(ch));
});
} 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) {
const card = document.createElement('div');
card.className = 'dashboard-card';
card.className = 'ext-dashboard-card';
const header = document.createElement('div');
header.className = 'dashboard-card-header';
header.className = 'ext-dashboard-card-header';
card.appendChild(header);
const title = document.createElement('div');
title.className = 'dashboard-card-title';
title.className = 'ext-dashboard-card-title';
title.textContent = ch.title || ch.name || 'Untitled';
header.appendChild(title);
const meta = document.createElement('div');
meta.className = 'dashboard-card-meta';
meta.className = 'ext-dashboard-card-meta';
meta.textContent = ch.type || '';
card.appendChild(meta);
if (ch.description) {
const desc = document.createElement('div');
desc.className = 'dashboard-card-desc';
desc.className = 'ext-dashboard-card-desc';
desc.textContent = ch.description.slice(0, 120);
card.appendChild(desc);
}
@@ -271,14 +271,14 @@
const updated = ch.updated_at || ch.created_at;
if (updated) {
const date = document.createElement('div');
date.className = 'dashboard-card-meta';
date.className = 'ext-dashboard-card-meta';
date.textContent = new Date(updated).toLocaleDateString();
card.appendChild(date);
}
// ── Card action menu ──
const actions = document.createElement('div');
actions.className = 'dashboard-card-actions';
actions.className = 'ext-dashboard-card-actions';
card.appendChild(actions);
const menuBtn = document.createElement('button');
@@ -372,7 +372,7 @@
// sw.toast — success feedback
sw.toast('Channel deleted', 'success');
// Refresh cards
const cards = document.querySelector('.dashboard-cards');
const cards = document.querySelector('.ext-dashboard-cards');
if (cards) _loadChannels(cards, _currentFilter);
} catch (e) {
// sw.toast — error feedback
@@ -390,21 +390,21 @@
container.innerHTML = '';
packages.forEach(function (pkg) {
const card = document.createElement('div');
card.className = 'dashboard-card';
card.className = 'ext-dashboard-card';
card.innerHTML =
'<div class="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-header">' +
'<div class="ext-dashboard-card-title">' + (typeof esc === 'function' ? esc(pkg.title || pkg.id) : (pkg.title || pkg.id)) + '</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="dashboard-card-desc">' + (typeof esc === 'function' ? esc(pkg.description || '') : (pkg.description || '')) + '</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="ext-dashboard-card-desc">' + (typeof esc === 'function' ? esc(pkg.description || '') : (pkg.description || '')) + '</div>';
container.appendChild(card);
});
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 (_) {
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-editor {
.ext-editor {
display: flex;
flex-direction: column;
height: 100%;
@@ -18,7 +18,7 @@
/* ── Topbar ────────────────────────────────── */
.editor-topbar {
.ext-editor-topbar {
display: flex;
align-items: center;
gap: 8px;
@@ -32,7 +32,7 @@
z-index: 20;
}
.editor-topbar-back {
.ext-editor-topbar-back {
display: flex;
align-items: center;
gap: 4px;
@@ -45,18 +45,18 @@
transition: color 0.15s, background 0.15s;
}
.editor-topbar-back:hover {
.ext-editor-topbar-back:hover {
color: var(--text, #eee);
background: var(--bg-hover);
}
.editor-topbar-sep {
.ext-editor-topbar-sep {
width: 1px;
height: 18px;
background: var(--border, #2a2a2e);
}
.editor-topbar-name {
.ext-editor-topbar-name {
font-size: 13px;
font-weight: 600;
color: var(--text, #eee);
@@ -64,11 +64,11 @@
/* ── Workspace Selector ────────────────────── */
.editor-ws-selector {
.ext-editor-ws-selector {
position: relative;
}
.editor-ws-selector-btn {
.ext-editor-ws-selector-btn {
display: flex;
align-items: center;
gap: 6px;
@@ -84,12 +84,12 @@
transition: border-color 0.15s, background 0.15s;
}
.editor-ws-selector-btn:hover {
.ext-editor-ws-selector-btn:hover {
border-color: var(--border, #2a2a2e);
background: var(--bg-hover);
}
.editor-ws-dropdown {
.ext-editor-ws-dropdown {
display: none;
position: absolute;
top: 100%;
@@ -106,14 +106,14 @@
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;
overflow-y: auto;
}
.editor-ws-dropdown-item {
.ext-editor-ws-dropdown-item {
display: block;
width: 100%;
text-align: left;
@@ -127,29 +127,29 @@
transition: background 0.1s;
}
.editor-ws-dropdown-item:hover {
.ext-editor-ws-dropdown-item:hover {
background: var(--bg-hover);
}
.editor-ws-dropdown-item.active {
.ext-editor-ws-dropdown-item.active {
color: var(--accent, #b38a4e);
font-weight: 600;
}
.editor-ws-dropdown-divider {
.ext-editor-ws-dropdown-divider {
height: 1px;
background: var(--border, #2a2a2e);
margin: 4px 0;
}
.editor-ws-new {
.ext-editor-ws-new {
display: flex;
align-items: center;
gap: 6px;
color: var(--accent, #b38a4e);
}
.editor-topbar-branch {
.ext-editor-topbar-branch {
display: flex;
align-items: center;
gap: 4px;
@@ -158,7 +158,7 @@
border-radius: 4px;
}
.editor-topbar-branch-text {
.ext-editor-topbar-branch-text {
font-size: 11px;
font-weight: 600;
color: var(--purple, #a078ff);
@@ -167,7 +167,7 @@
/* ── Body ──────────────────────────────────── */
.editor-body {
.ext-editor-body {
flex: 1;
min-height: 0;
overflow: hidden;
@@ -175,14 +175,14 @@
/* ── Bootstrap (no workspace) ──────────────── */
.editor-bootstrap {
.ext-editor-bootstrap {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.editor-bootstrap-card {
.ext-editor-bootstrap-card {
text-align: center;
padding: 40px;
background: var(--bg-secondary, #151517);
@@ -191,7 +191,7 @@
max-width: 360px;
}
.editor-bootstrap-input {
.ext-editor-bootstrap-input {
width: 100%;
padding: 8px 12px;
background: var(--bg, #0e0e10);
@@ -205,11 +205,11 @@
box-sizing: border-box;
}
.editor-bootstrap-input:focus {
.ext-editor-bootstrap-input:focus {
border-color: var(--accent, #b38a4e);
}
.editor-bootstrap-btn {
.ext-editor-bootstrap-btn {
width: 100%;
padding: 10px 16px;
background: var(--accent, #b38a4e);
@@ -223,11 +223,11 @@
transition: opacity 0.15s;
}
.editor-bootstrap-btn:hover { opacity: 0.9; }
.editor-bootstrap-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.ext-editor-bootstrap-btn:hover { opacity: 0.9; }
.ext-editor-bootstrap-btn:disabled { opacity: 0.5; cursor: not-allowed; }
/* Workspace list in bootstrap */
.editor-bootstrap-ws-item {
.ext-editor-bootstrap-ws-item {
display: flex;
align-items: center;
justify-content: space-between;
@@ -245,30 +245,30 @@
text-align: left;
}
.editor-bootstrap-ws-item:hover {
.ext-editor-bootstrap-ws-item:hover {
border-color: var(--accent, #b38a4e);
background: var(--bg-hover);
}
.editor-bootstrap-ws-name {
.ext-editor-bootstrap-ws-name {
font-weight: 600;
}
.editor-bootstrap-ws-date {
.ext-editor-bootstrap-ws-date {
font-size: 11px;
color: var(--text-3, #777);
}
/* ── FileTree overrides (in editor context) ── */
.surface-editor .file-tree {
.ext-editor .file-tree {
height: 100%;
display: flex;
flex-direction: column;
border-right: 1px solid var(--border, #2a2a2e);
}
.surface-editor .file-tree-header {
.ext-editor .file-tree-header {
display: flex;
align-items: center;
gap: 6px;
@@ -276,7 +276,7 @@
border-bottom: 1px solid var(--border, #2a2a2e);
}
.surface-editor .file-tree-title {
.ext-editor .file-tree-title {
font-size: 11px;
font-weight: 600;
color: var(--text-2, #999);
@@ -285,13 +285,13 @@
flex: 1;
}
.surface-editor .file-tree-items {
.ext-editor .file-tree-items {
flex: 1;
overflow-y: auto;
padding: 4px 0;
}
.surface-editor .file-tree-row {
.ext-editor .file-tree-row {
display: flex;
align-items: center;
gap: 4px;
@@ -302,29 +302,29 @@
transition: background 0.1s;
}
.surface-editor .file-tree-row:hover {
.ext-editor .file-tree-row: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));
color: var(--text, #eee);
}
.surface-editor .file-tree-arrow {
.ext-editor .file-tree-arrow {
width: 12px;
font-size: 10px;
color: var(--text-3, #555);
text-align: center;
}
.surface-editor .file-tree-icon {
.ext-editor .file-tree-icon {
font-size: 13px;
width: 18px;
text-align: center;
}
.surface-editor .file-tree-name {
.ext-editor .file-tree-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
@@ -332,13 +332,13 @@
}
/* Git status indicators */
.surface-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); }
.surface-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-modified .file-tree-name { color: var(--warning, #e5a842); }
.ext-editor .file-tree-row.git-added .file-tree-name { color: var(--success, #4caf50); }
.ext-editor .file-tree-row.git-untracked .file-tree-name { color: var(--text-3, #555); font-style: italic; }
.ext-editor .file-tree-row.git-deleted .file-tree-name { color: var(--danger, #f44336); text-decoration: line-through; }
/* Context menu */
.file-tree-ctx-menu {
.ext-editor-file-tree-ctx-menu {
position: fixed;
background: var(--bg-secondary, #1a1a1e);
border: 1px solid var(--border, #2a2a2e);
@@ -349,26 +349,26 @@
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
}
.file-tree-ctx-item {
.ext-editor-file-tree-ctx-item {
padding: 6px 12px;
font-size: 12px;
color: var(--text, #eee);
cursor: pointer;
}
.file-tree-ctx-item:hover {
.ext-editor-file-tree-ctx-item:hover {
background: var(--bg-hover);
}
/* ── CodeEditor overrides ──────────────────── */
.surface-editor .code-editor {
.ext-editor .code-editor {
height: 100%;
display: flex;
flex-direction: column;
}
.surface-editor .code-editor-tabs {
.ext-editor .code-editor-tabs {
display: flex;
align-items: center;
gap: 0;
@@ -379,9 +379,9 @@
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;
align-items: center;
gap: 4px;
@@ -395,14 +395,14 @@
white-space: nowrap;
}
.surface-editor .code-editor-tab:hover { background: var(--bg-hover); }
.surface-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:hover { background: var(--bg-hover); }
.ext-editor .code-editor-tab.active { color: var(--text, #eee); background: var(--bg, #0e0e10); }
.ext-editor .code-editor-tab.modified .code-editor-tab-modified { color: var(--warning, #e5a842); }
.surface-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-icon { font-size: 12px; }
.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;
border: none;
color: var(--text-3, #555);
@@ -414,32 +414,32 @@
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));
color: var(--danger, #f44336);
}
.surface-editor .code-editor-content {
.ext-editor .code-editor-content {
flex: 1;
min-height: 0;
overflow: hidden;
position: relative;
}
.surface-editor .code-editor-welcome {
.ext-editor .code-editor-welcome {
height: 100%;
}
.surface-editor .code-editor-cm-wrap {
.ext-editor .code-editor-cm-wrap {
height: 100%;
overflow: auto;
}
.surface-editor .code-editor-cm-wrap .cm-editor {
.ext-editor .code-editor-cm-wrap .cm-editor {
height: 100%;
}
.surface-editor .code-editor-statusbar {
.ext-editor .code-editor-statusbar {
display: flex;
align-items: center;
gap: 16px;
@@ -453,7 +453,7 @@
font-family: var(--mono, 'SF Mono', monospace);
}
.surface-editor .code-editor-textarea-fallback {
.ext-editor .code-editor-textarea-fallback {
width: 100%;
height: 100%;
background: var(--bg, #0e0e10);
@@ -469,18 +469,18 @@
/* ── Tabbed assist pane overrides ──────────── */
.surface-editor .pane-tabbed {
.ext-editor .pane-tabbed {
border-left: 1px solid var(--border, #2a2a2e);
}
/* ChatPane in editor tabbed pane */
.surface-editor .chat-pane {
.ext-editor .chat-pane {
position: absolute;
inset: 0;
}
/* Notes in editor pane */
.surface-editor .note-editor {
.ext-editor .ext-notes-editor {
position: absolute;
inset: 0;
display: flex;
@@ -488,20 +488,20 @@
overflow: hidden;
}
.surface-editor .note-editor-list-view {
.ext-editor .ext-notes-editor-list-view {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.surface-editor .notes-list {
.ext-editor .ext-notes-list {
flex: 1;
overflow-y: auto;
}
/* Compact notes toolbar for narrow pane */
.surface-editor .notes-toolbar {
.ext-editor .ext-notes-toolbar {
display: flex;
flex-wrap: wrap;
gap: 4px;
@@ -509,22 +509,22 @@
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;
padding: 3px 6px;
}
.surface-editor .notes-search-row {
.ext-editor .ext-notes-search-row {
padding: 4px 8px;
}
.surface-editor .notes-filter-row {
.ext-editor .ext-notes-filter-row {
padding: 2px 8px 4px;
display: flex;
gap: 4px;
}
.surface-editor .notes-filter-select {
.ext-editor .ext-notes-filter-select {
font-size: 11px;
flex: 1;
min-width: 0;

View File

@@ -56,7 +56,7 @@
// Wrap in surface container for CSS scoping
const surface = document.createElement('div');
surface.className = 'surface-editor';
surface.className = 'ext-editor';
surface.id = 'editorSurface';
mount.appendChild(surface);
@@ -84,7 +84,7 @@
// Build body + bootstrap
const body = document.createElement('div');
body.className = 'editor-body';
body.className = 'ext-editor-body';
body.id = 'editorBody';
surface.appendChild(body);
@@ -113,32 +113,32 @@
function _buildTopbar(wsName) {
const el = document.createElement('div');
el.className = 'editor-topbar';
el.className = 'ext-editor-topbar';
el.id = 'editorTopbar';
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>' +
'Back' +
'</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>' +
'<div class="editor-ws-selector" id="editorWsSelector">' +
'<button class="editor-ws-selector-btn" id="editorWsSelectorBtn">' +
'<div class="ext-editor-ws-selector" id="editorWsSelector">' +
'<button class="ext-editor-ws-selector-btn" id="editorWsSelectorBtn">' +
'<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>' +
'</button>' +
'<div class="editor-ws-dropdown" id="editorWsDropdown">' +
'<div id="editorWsList" class="editor-ws-list"></div>' +
'<div class="editor-ws-dropdown-divider"></div>' +
'<button class="editor-ws-dropdown-item editor-ws-new" id="editorWsNewBtn">' +
'<div class="ext-editor-ws-dropdown" id="editorWsDropdown">' +
'<div id="editorWsList" class="ext-editor-ws-list"></div>' +
'<div class="ext-editor-ws-dropdown-divider"></div>' +
'<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>' +
'New Workspace' +
'</button>' +
'</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>' +
'<span id="editorBranchName" class="editor-topbar-branch-text">main</span>' +
'<span id="editorBranchName" class="ext-editor-topbar-branch-text">main</span>' +
'</div>' +
'<div style="flex:1;"></div>' +
'<button class="icon-btn" id="editorRefreshBtn" title="Refresh files">' +
@@ -151,11 +151,11 @@
function _buildBootstrap() {
const el = document.createElement('div');
el.className = 'editor-bootstrap';
el.className = 'ext-editor-bootstrap';
el.id = 'editorBootstrap';
el.style.display = 'none';
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;">' +
'<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>' +
@@ -168,8 +168,8 @@
'<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>' +
'<input type="text" id="editorBootstrapName" class="editor-bootstrap-input" placeholder="Workspace name" value="workspace">' +
'<button id="editorBootstrapBtn" class="editor-bootstrap-btn">Create Workspace</button>' +
'<input type="text" id="editorBootstrapName" class="ext-editor-bootstrap-input" placeholder="Workspace name" value="workspace">' +
'<button id="editorBootstrapBtn" class="ext-editor-bootstrap-btn">Create Workspace</button>' +
'</div>';
return el;
}
@@ -218,7 +218,7 @@
}
workspaces.forEach(ws => {
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.addEventListener('click', () => { window.location.href = base + '/s/editor?ws=' + ws.id; });
listEl.appendChild(item);
@@ -243,10 +243,10 @@
listEl.innerHTML = '';
workspaces.forEach(ws => {
const item = document.createElement('button');
item.className = 'editor-bootstrap-ws-item';
item.className = 'ext-editor-bootstrap-ws-item';
item.innerHTML =
'<span class="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-name">' + esc(ws.name || ws.id?.slice(0, 8)) + '</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; });
listEl.appendChild(item);
});

View File

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

View File

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

View File

@@ -2,16 +2,16 @@
Uses CSS custom properties from the platform theme system (variables.css).
See EXTENSION-SURFACES.md for the full property reference. */
.hello-dashboard { max-width: 720px; margin: 0 auto; padding: 40px 24px; }
.hello-header { margin-bottom: 32px; }
.hello-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; }
.hello-subtitle code,
.hello-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; }
.hello-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; }
.hello-card-value { font-size: 20px; font-weight: 600; color: var(--text); margin-bottom: 4px; }
.hello-card-detail { font-size: 12px; color: var(--text-3); }
.hello-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 { max-width: 720px; margin: 0 auto; padding: 40px 24px; }
.ext-hello-dashboard-header { margin-bottom: 32px; }
.ext-hello-dashboard-header h1 { font-size: 28px; font-weight: 700; color: var(--text); margin: 0 0 8px 0; }
.ext-hello-dashboard-subtitle { font-size: 14px; color: var(--text-2); margin: 0; }
.ext-hello-dashboard-subtitle code,
.ext-hello-dashboard-card-value code { background: var(--bg-raised); padding: 2px 6px; border-radius: 4px; font-size: 13px; }
.ext-hello-dashboard-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; }
.ext-hello-dashboard-card { background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 16px; }
.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; }
.ext-hello-dashboard-card-value { font-size: 20px; font-weight: 600; color: var(--text); margin-bottom: 4px; }
.ext-hello-dashboard-card-detail { font-size: 12px; color: var(--text-3); }
.ext-hello-dashboard-actions { display: flex; gap: 12px; margin-bottom: 24px; }
.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';
mount.innerHTML =
'<div class="hello-dashboard">' +
'<div class="hello-header">' +
'<div class="ext-hello-dashboard">' +
'<div class="ext-hello-dashboard-header">' +
'<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 class="hello-cards">' +
'<div class="ext-hello-dashboard-cards">' +
card('Platform Access', (typeof UI !== 'undefined' ? '\u2713 Connected' : '\u2717 Unavailable'),
'API, Theme, UI primitives available', 'var(--accent)') +
card('Theme', isDark ? '\uD83C\uDF19 Dark' : '\u2600\uFE0F Light',
@@ -33,11 +33,11 @@
card('Route', '<code>' + esc(manifest.route || '/s/hello-dashboard') + '</code>',
'Registered via surface manifest', '') +
'</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--secondary sw-btn--md" id="helloApi">Test API</button>' +
'</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>';
// Wire toast button
@@ -69,10 +69,10 @@
function card(title, value, detail, color) {
var style = color ? ' style="color:' + color + ';"' : '';
return '<div class="hello-card">' +
'<div class="hello-card-title">' + esc(title) + '</div>' +
'<div class="hello-card-value"' + style + '>' + value + '</div>' +
'<div class="hello-card-detail">' + esc(detail) + '</div>' +
return '<div class="ext-hello-dashboard-card">' +
'<div class="ext-hello-dashboard-card-title">' + esc(title) + '</div>' +
'<div class="ext-hello-dashboard-card-value"' + style + '>' + value + '</div>' +
'<div class="ext-hello-dashboard-card-detail">' + esc(detail) + '</div>' +
'</div>';
}

View File

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

View File

@@ -24,6 +24,7 @@
var mount = document.getElementById('extension-mount');
if (!mount) return;
mount.classList.add('ext-icd-test-runner-root');
// ─── Boot Preact SDK (v0.37.14) ───────────────────────────
// 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 () {
sw.pipe.render(1, function (ctx) {
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;
}, { source: '_test-render-dom' });
@@ -348,7 +348,7 @@
element: el, message: null,
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 () {

View File

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

View File

@@ -132,16 +132,16 @@
}
return html`
<div class="tag-input">
<div class="ext-notes-tag-input">
${(tags || []).map(function(tag) {
return html`
<span key=${tag} class="tag-pill">
<span key=${tag} class="ext-notes-tag-pill">
${tag}
<span class="tag-pill__remove" onClick=${function() { removeTag(tag); }}>×</span>
<span class="ext-notes-tag-pill__remove" onClick=${function() { removeTag(tag); }}>×</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)…'}
value=${input}
onInput=${handleInput}
@@ -149,9 +149,9 @@
onFocus=${function() { if (input.trim()) setShowAC(true); }}
onBlur=${function() { setTimeout(function() { setShowAC(false); }, 150); }} />
${showAC && suggestions.length > 0 && html`
<div class="tag-autocomplete">
<div class="ext-notes-tag-autocomplete">
${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>`;
})}
</div>
@@ -169,17 +169,17 @@
if (!tags || tags.length === 0) return null;
return html`
<div class="tag-filter">
<div class="tag-filter__label">
<div class="ext-notes-tag-filter">
<div class="ext-notes-tag-filter__label">
<span>Tags</span>
${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 class="tag-filter__pills">
<div class="ext-notes-tag-filter__pills">
${tags.map(function(tag) {
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); }}>
${tag}
</span>
@@ -216,23 +216,23 @@
}
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}
draggable="true"
onDragStart=${handleDragStart}
onDragEnd=${handleDragEnd}>
<div class="note-card__title">
${note.pinned ? html`<span class="note-card__pin">📌</span>` : null}
<div class="ext-notes-note-card__title">
${note.pinned ? html`<span class="ext-notes-note-card__pin">📌</span>` : null}
${note.title || 'Untitled'}
</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`
<div class="note-card__tags">
${showTags.map(function(t) { return html`<span key=${t} class="tag-pill">${t}</span>`; })}
${overflow > 0 && html`<span class="tag-pill tag-pill--overflow">+${overflow}</span>`}
<div class="ext-notes-note-card__tags">
${showTags.map(function(t) { return html`<span key=${t} class="ext-notes-tag-pill">${t}</span>`; })}
${overflow > 0 && html`<span class="ext-notes-tag-pill ext-notes-tag-pill--overflow">+${overflow}</span>`}
</div>
`}
${dateStr && html`<div class="note-card__date">${dateStr}</div>`}
${dateStr && html`<div class="ext-notes-note-card__date">${dateStr}</div>`}
</div>
`;
}
@@ -254,10 +254,10 @@
}, [onClose]);
return html`
<div ref=${menuRef} class="folder-context-menu" style="top: ${y}px; left: ${x}px">
<div class="folder-context-menu__item" onClick=${onAddSub}>Add subfolder</div>
<div class="folder-context-menu__item" onClick=${onRename}>Rename</div>
<div class="folder-context-menu__item folder-context-menu__item--danger" onClick=${onDelete}>Delete</div>
<div ref=${menuRef} class="ext-notes-folder-context-menu" style="top: ${y}px; left: ${x}px">
<div class="ext-notes-folder-context-menu__item" onClick=${onAddSub}>Add subfolder</div>
<div class="ext-notes-folder-context-menu__item" onClick=${onRename}>Rename</div>
<div class="ext-notes-folder-context-menu__item ext-notes-folder-context-menu__item--danger" onClick=${onDelete}>Delete</div>
</div>
`;
}
@@ -310,25 +310,25 @@
return html`
<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"
onClick=${function() { onSelect(node.folder.id); }}
onContextMenu=${handleContext}
onDragOver=${handleDragOver}
onDragLeave=${handleDragLeave}
onDrop=${handleDrop}>
<span class="folder-tree__toggle" onClick=${handleToggle}>
<span class="ext-notes-folder-tree__toggle" onClick=${handleToggle}>
${hasChildren ? (expanded ? '▼' : '▶') : '·'}
</span>
<span class="folder-tree__icon">${isActive ? '📂' : '📁'}</span>
<span class="ext-notes-folder-tree__icon">${isActive ? '📂' : '📁'}</span>
${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); }}
onBlur=${handleRename}
onKeyDown=${function(e) { if (e.key === 'Enter') handleRename(); if (e.key === 'Escape') setEditing(false); }}
onClick=${function(e) { e.stopPropagation(); }}
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>
${menu && html`<${FolderContextMenu} x=${menu.x} y=${menu.y}
@@ -399,18 +399,18 @@
var unfiledDrop = makeDropHandlers(setDropUnfiled);
return html`
<div class="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">
<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}
onDragOver=${allDrop.onDragOver} onDragLeave=${allDrop.onDragLeave} onDrop=${allDrop.onDrop}>
<span class="folder-tree__icon">📋</span>
<span class="folder-tree__name">All Notes</span>
<span class="ext-notes-folder-tree__icon">📋</span>
<span class="ext-notes-folder-tree__name">All Notes</span>
</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}
onDragOver=${unfiledDrop.onDragOver} onDragLeave=${unfiledDrop.onDragLeave} onDrop=${unfiledDrop.onDrop}>
<span class="folder-tree__icon">📄</span>
<span class="folder-tree__name">Unfiled</span>
<span class="ext-notes-folder-tree__icon">📄</span>
<span class="ext-notes-folder-tree__name">Unfiled</span>
</div>
${tree.map(function(node) {
return html`<${FolderNode} key=${node.folder.id} node=${node} depth=${0}
@@ -419,7 +419,7 @@
onDelete=${onDeleteFolder} onCreateSub=${onCreateFolder}
onDropNote=${onDropNote} />`;
})}
<div class="folder-tree__add" onClick=${function() { onCreateFolder(''); }}>
<div class="ext-notes-folder-tree__add" onClick=${function() { onCreateFolder(''); }}>
+ New Folder
</div>
</div>
@@ -446,18 +446,18 @@
if (!backlinks.length) return null;
return html`
<div class="backlinks-panel">
<div class="backlinks-panel__header" onClick=${function() { setCollapsed(!collapsed); }}>
<span class="backlinks-panel__toggle">${collapsed ? '▶' : '▼'}</span>
<div class="ext-notes-backlinks-panel">
<div class="ext-notes-backlinks-panel__header" onClick=${function() { setCollapsed(!collapsed); }}>
<span class="ext-notes-backlinks-panel__toggle">${collapsed ? '▶' : '▼'}</span>
<span>Backlinks</span>
<span class="backlinks-panel__count">${backlinks.length}</span>
<span class="ext-notes-backlinks-panel__count">${backlinks.length}</span>
</div>
${!collapsed && backlinks.map(function(bl) {
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); }}>
<span class="backlinks-panel__title">${bl.source_title}</span>
<span class="backlinks-panel__context">via [[${bl.link_text}]]</span>
<span class="ext-notes-backlinks-panel__title">${bl.source_title}</span>
<span class="ext-notes-backlinks-panel__context">via [[${bl.link_text}]]</span>
</div>
`;
})}
@@ -547,10 +547,10 @@
function SidebarTabs({ activeTab, onTabChange, noteSelected }) {
return html`
<div class="sidebar-tabs">
<button class="sidebar-tabs__tab ${activeTab === 'notes' ? 'sidebar-tabs__tab--active' : ''}"
<div class="ext-ext-notes-sidebar-tabs">
<button class="ext-ext-notes-sidebar-tabs__tab ${activeTab === 'notes' ? 'ext-ext-notes-sidebar-tabs__tab--active' : ''}"
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>
</div>
`;
@@ -563,14 +563,14 @@
function SidebarOutline({ headings, activeHeadingIdx, onScrollToHeading }) {
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`
<div class="sidebar-outline">
<div class="ext-ext-notes-sidebar-outline">
${headings.map(function(h, i) {
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"
onClick=${function() { onScrollToHeading(h); }}>
${h.text}
@@ -772,8 +772,8 @@
for (var i = 0; i < outgoingLinks.length; i++) {
var link = outgoingLinks[i];
if (!link.target_id) {
var search = 'class="wikilink" data-wikilink="' + _escAttr(link.link_text) + '"';
var replace = 'class="wikilink wikilink--unresolved" data-wikilink="' + _escAttr(link.link_text) + '"';
var search = 'class="ext-notes-wikilink" 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);
}
}
@@ -787,7 +787,7 @@
if (!el || viewMode === 'edit') return;
function handleClick(e) {
var target = e.target;
if (target.classList && target.classList.contains('wikilink')) {
if (target.classList && target.classList.contains('ext-notes-wikilink')) {
e.preventDefault();
var linkText = target.getAttribute('data-wikilink');
if (linkText && onNavigateToTitle) onNavigateToTitle(linkText);
@@ -939,44 +939,44 @@
}, [folders]);
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 ─────────────────────
function renderEditorBody() {
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') {
var editor = hasCM
? html`<div class="notes-editor__cm" ref=${cmContainerRef} />`
: html`<textarea class="notes-editor__textarea" ref=${textareaRef}
? html`<div class="ext-notes-editor__cm" ref=${cmContainerRef} />`
: html`<textarea class="ext-notes-editor__textarea" ref=${textareaRef}
value=${body} onInput=${handleBodyChange}
onKeyDown=${handleKeyDown}
placeholder="Start writing in Markdown…" />`;
return [
editor,
html`<div class="notes-preview" ref=${previewRef} dangerouslySetInnerHTML=${{ __html: previewHtml }} />`
html`<div class="ext-notes-preview" ref=${previewRef} dangerouslySetInnerHTML=${{ __html: previewHtml }} />`
];
}
// edit mode
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}
onKeyDown=${handleKeyDown}
placeholder="Start writing in Markdown…" />`;
}
return html`
<div class="notes-editor">
<div class="notes-editor__header">
<input class="notes-editor__title-input" type="text"
<div class="ext-notes-editor">
<div class="ext-notes-editor__header">
<input class="ext-notes-editor__title-input" type="text"
value=${title} onInput=${handleTitleChange}
placeholder="Note title" />
<div class="notes-editor__actions">
<select class="notes-editor__folder-select"
<div class="ext-notes-editor__actions">
<select class="ext-notes-editor__folder-select"
value=${note.folder_id || ''}
onChange=${async function(e) {
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>`;
})}
</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')}
</span>
<button class="notes-toggle" onClick=${function() {
<button class="ext-notes-toggle" onClick=${function() {
var next = viewMode === 'rendered' ? 'edit' : viewMode === 'edit' ? 'split' : 'rendered';
_modeStore.set('editor_mode', next);
setViewMode(next);
}}>
${viewMode === 'rendered' ? 'Edit' : viewMode === 'edit' ? 'Split' : 'Read'}
</button>
<button class="notes-btn" onClick=${handleExport} title="Export as .md">Export</button>
<button class="notes-btn" onClick=${handlePin}
<button class="ext-notes-btn" onClick=${handleExport} title="Export as .md">Export</button>
<button class="ext-notes-btn" onClick=${handlePin}
title=${note.pinned ? 'Unpin' : 'Pin'}>
${note.pinned ? '📌' : '📍'}
</button>
<button class="notes-btn notes-btn--danger" onClick=${handleArchive}
<button class="ext-notes-btn ext-notes-btn--danger" onClick=${handleArchive}
title="Archive">🗑</button>
</div>
</div>
<${TagInput} tags=${noteTags} allTags=${allTags} onChange=${handleTagsChange} />
<div class="notes-editor__content">
<div class="notes-editor__body ${viewMode === 'split' ? 'notes-editor__body--split' : ''}">
<div class="ext-notes-editor__content">
<div class="ext-notes-editor__body ${viewMode === 'split' ? 'ext-notes-editor__body--split' : ''}">
${renderEditorBody()}
</div>
</div>
@@ -1405,17 +1405,17 @@
}, []);
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) {
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;
return html`
<div class="graph-pane">
<div class="graph-toolbar">
<div class="ext-notes-graph-pane">
<div class="ext-notes-graph-toolbar">
<label>
<input type="checkbox" checked=${hideOrphans}
onChange=${function(e) { setHideOrphans(e.target.checked); }} />
@@ -1430,14 +1430,14 @@
onMouseUp=${handleMouseUp}
onMouseLeave=${handleMouseUp} />
${hoveredNode && html`
<div class="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" style=${{ left: (mousePos.x + 14) + 'px', top: (mousePos.y - 10) + 'px' }}>
<div class="ext-notes-graph-tooltip__title">${hoveredNode.title}</div>
${hoveredNode.tags && hoveredNode.tags.length > 0 && html`
<div class="graph-tooltip__tags">
${hoveredNode.tags.map(function(t) { return html`<span class="tag-pill" key=${t}>${t}</span>`; })}
<div class="ext-notes-graph-tooltip__tags">
${hoveredNode.tags.map(function(t) { return html`<span class="ext-notes-tag-pill" key=${t}>${t}</span>`; })}
</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>
@@ -1736,8 +1736,8 @@
<${Button} variant=${showGraph ? 'primary' : 'secondary'} size="sm"
onClick=${function() { setShowGraph(!showGraph); }}>Graph<//>
<//>
<div class="notes-app">
<div class="notes-sidebar">
<div class="ext-notes-app">
<div class="ext-notes-sidebar">
<${SidebarTabs} activeTab=${sidebarTab} onTabChange=${setSidebarTab} noteSelected=${!!activeNote} />
${sidebarTab === 'notes' && html`
<${FolderTree} folders=${folders}
@@ -1749,14 +1749,14 @@
onDeleteFolder=${handleDeleteFolder}
onDropNote=${handleDropNote} />
<${TagFilter} tags=${allTags} activeTag=${activeTag} onSelect=${handleTagFilter} />
<div class="notes-search">
<div class="ext-notes-search">
<input type="text" placeholder="Search notes…"
value=${searchTerm} onInput=${handleSearch} />
</div>
<div class="notes-list">
${loading && html`<div class="notes-list__loading"><${Spinner} size="md" /></div>`}
<div class="ext-notes-list">
${loading && html`<div class="ext-notes-list__loading"><${Spinner} size="md" /></div>`}
${!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.')}
</div>
`}

View File

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

View File

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

View File

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

View File

@@ -58,25 +58,25 @@
mount.innerHTML = '';
// Header
var header = el('div', { className: 'sdkr-header' }, [
var header = el('div', { className: 'ext-sdk-test-runner-header' }, [
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);
// 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. ' +
'SDK fail + ICD pass = SDK bug. Both fail = ICD bug.'));
// ── 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' });
fixRow.appendChild(el('span', { className: 'sdkr-fixture-label' }, 'FIXTURES:'));
var fixRow = el('div', { className: 'ext-sdk-test-runner-fixture-row' });
fixRow.appendChild(el('span', { className: 'ext-sdk-test-runner-fixture-label' }, 'FIXTURES:'));
// 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 —'));
['openrouter', 'openai', 'anthropic', 'venice'].forEach(function (p) {
provSelect.appendChild(el('option', { value: p }, p));
@@ -85,18 +85,18 @@
// API key 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)'
});
fixRow.appendChild(keyInput);
// Fixture status
var fixStatus = el('span', { className: 'sdkr-fixture-status' }, '');
var fixStatus = el('span', { className: 'ext-sdk-test-runner-fixture-status' }, '');
fixRow.appendChild(fixStatus);
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. ' +
'Without a key, those tests are skipped. Key is never stored.');
fixturePanel.appendChild(fixNote);
@@ -107,16 +107,16 @@
T._fixtureInput = { provSelect: provSelect, keyInput: keyInput, statusEl: fixStatus };
// Controls row
var controlRow = el('div', { className: 'sdkr-controls' });
var controlRow = el('div', { className: 'ext-sdk-test-runner-controls' });
// Domain filter checkboxes
var filterBox = el('div', { className: 'sdkr-filters' });
filterBox.appendChild(el('span', { className: 'sdkr-filter-label' }, 'Domains: '));
var filterBox = el('div', { className: 'ext-sdk-test-runner-filters' });
filterBox.appendChild(el('span', { className: 'ext-sdk-test-runner-filter-label' }, 'Domains: '));
var domainNames = Object.keys(T.domains).sort();
var domainChecks = {};
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' });
domainChecks[name] = cb;
label.appendChild(cb);
@@ -126,11 +126,11 @@
controlRow.appendChild(filterBox);
// Buttons
var btnRow = el('div', { className: 'sdkr-btn-row' });
var runBtn = el('button', { className: 'sdkr-btn sdkr-btn-primary', onClick: onRun }, 'Run All');
var abortBtn = el('button', { className: 'sdkr-btn sdkr-btn-danger', onClick: function () { T.abort(); } }, 'Abort');
var btnRow = el('div', { className: 'ext-sdk-test-runner-btn-row' });
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: 'ext-sdk-test-runner-btn ext-sdk-test-runner-btn-danger', onClick: function () { T.abort(); } }, 'Abort');
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(abortBtn);
btnRow.appendChild(exportBtn);
@@ -138,12 +138,12 @@
mount.appendChild(controlRow);
// Summary bar
var summaryBar = el('div', { className: 'sdkr-summary' });
var summaryBar = el('div', { className: 'ext-sdk-test-runner-summary' });
mount.appendChild(summaryBar);
// Results table
var tableWrap = el('div', { className: 'sdkr-table-wrap' });
var table = el('table', { className: 'sdkr-table' });
var tableWrap = el('div', { className: 'ext-sdk-test-runner-table-wrap' });
var table = el('table', { className: 'ext-sdk-test-runner-table' });
var thead = el('thead', {}, [
el('tr', {}, [
el('th', {}, '#'),
@@ -176,7 +176,7 @@
];
items.forEach(function (item) {
var badge = el('span', {
className: 'sdkr-badge',
className: 'ext-sdk-test-runner-badge',
style: {
background: item[1] > 0 ? COLORS[item[0]] : 'var(--bg2, #333)',
color: item[1] > 0 ? '#fff' : 'var(--text3, #888)'
@@ -184,7 +184,7 @@
}, LABELS[item[0]] + ': ' + item[1]);
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)' }
}, 'Total: ' + total));
}
@@ -197,19 +197,19 @@
if (entry.rawErr) detail += (detail ? ' | ' : '') + 'ICD: ' + esc(entry.rawErr);
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', {}, esc(entry.domain)),
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('span', {
className: 'sdkr-verdict',
className: 'ext-sdk-test-runner-verdict',
style: { color: COLORS[v] || '#fff' }
}, LABELS[v] || v)
]),
el('td', { className: 'sdkr-ms' }, String(entry.durationMs)),
el('td', { className: 'sdkr-detail' }, detail)
el('td', { className: 'ext-sdk-test-runner-ms' }, String(entry.durationMs)),
el('td', { className: 'ext-sdk-test-runner-detail' }, detail)
]);
tbody.appendChild(row);
updateSummary();

View File

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

View File

@@ -88,7 +88,7 @@
return html`
<${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} />`}
<${FormField} label="Title" required>
<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)}
placeholder="Details (optional)" />
<//>
<div class="task-form__row">
<div class="ext-tasks-form__row">
<${FormField} label="Status">
<select value=${status} onChange=${e => setStatus(e.target.value)}>
${STATUSES.map(s => html`<option value=${s}>${STATUS_LABELS[s] || s}</option>`)}
@@ -110,7 +110,7 @@
</select>
<//>
</div>
<div class="task-form__row">
<div class="ext-tasks-form__row">
<${FormField} label="Due Date">
<input type="date" value=${dueDate} onInput=${e => setDueDate(e.target.value)} />
<//>
@@ -119,7 +119,7 @@
placeholder="comma-separated" />
<//>
</div>
<div class="task-form__actions">
<div class="ext-tasks-form__actions">
<${Button} variant="ghost" type="button" onClick=${onClose}>Cancel<//>
<${Button} variant="primary" type="submit" disabled=${saving}>
${saving ? 'Saving…' : (task ? 'Update' : 'Create')}
@@ -155,45 +155,45 @@
}
return html`
<div class="task-list">
<div class="task-list__filters">
<select value=${filter} onChange=${e => setFilter(e.target.value)} class="task-filter">
<div class="ext-tasks-list">
<div class="ext-tasks-list__filters">
<select value=${filter} onChange=${e => setFilter(e.target.value)} class="ext-tasks-filter">
<option value="all">All</option>
${STATUSES.map(s => html`<option value=${s}>${STATUS_LABELS[s] || s}</option>`)}
</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>
${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`
<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) {
return html`
<div class="task-card" key=${t.id}>
<div class="task-card__header">
<span class="task-card__priority" style="color:${PRIORITY_COLORS[t.priority] || 'var(--text-3)'}">
<div class="ext-tasks-card" key=${t.id}>
<div class="ext-tasks-card__header">
<span class="ext-tasks-card__priority" style="color:${PRIORITY_COLORS[t.priority] || 'var(--text-3)'}">
</span>
<span class="task-card__title" onClick=${() => onEdit(t)}>${t.title}</span>
<span class="task-card__status task-card__status--${t.status}">
<span class="ext-tasks-card__title" onClick=${() => onEdit(t)}>${t.title}</span>
<span class="ext-tasks-card__status ext-tasks-card__status--${t.status}">
${STATUS_LABELS[t.status] || t.status}
</span>
</div>
${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">
${t.due_date && html`<span class="task-card__due">Due: ${t.due_date}</span>`}
${t.tags && html`<span class="task-card__tags">${t.tags}</span>`}
<div class="task-card__actions">
<div class="ext-tasks-card__footer">
${t.due_date && html`<span class="ext-tasks-card__due">Due: ${t.due_date}</span>`}
${t.tags && html`<span class="ext-tasks-card__tags">${t.tags}</span>`}
<div class="ext-tasks-card__actions">
${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>
`}
<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>
</div>
</div>
@@ -216,25 +216,25 @@
}
return html`
<div class="task-board">
${loading && html`<div class="task-list__loading"><${Spinner} size="md" /></div>`}
<div class="ext-tasks-board">
${loading && html`<div class="ext-tasks-list__loading"><${Spinner} size="md" /></div>`}
${!loading && STATUSES.map(function(status) {
var col = items.filter(function(t) { return t.status === status; });
return html`
<div class="task-board__col" key=${status}>
<div class="task-board__col-header">
<span class="task-board__col-title">${STATUS_LABELS[status] || status}</span>
<span class="task-board__col-count">${col.length}</span>
<div class="ext-tasks-board__col" key=${status}>
<div class="ext-tasks-board__col-header">
<span class="ext-tasks-board__col-title">${STATUS_LABELS[status] || status}</span>
<span class="ext-tasks-board__col-count">${col.length}</span>
</div>
<div class="task-board__col-body">
<div class="ext-tasks-board__col-body">
${col.map(function(t) {
return html`
<div class="task-board__card" key=${t.id} onClick=${() => onEdit(t)}>
<div class="task-board__card-title">
<span class="task-card__priority" style="color:${PRIORITY_COLORS[t.priority] || 'var(--text-3)'}">●</span>
<div class="ext-tasks-board__card" key=${t.id} onClick=${() => onEdit(t)}>
<div class="ext-tasks-board__card-title">
<span class="ext-tasks-card__priority" style="color:${PRIORITY_COLORS[t.priority] || 'var(--text-3)'}">●</span>
${t.title}
</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>
`;
})}
@@ -307,22 +307,22 @@
var Topbar = sw.shell?.Topbar;
return html`
<div class="tasks-app">
<div class="ext-tasks-app">
${Topbar ? html`
<${Topbar} title="Tasks">
${stats && html`<span class="tasks-stats">${stats.total || 0} total</span>`}
<div class="tasks-view-tabs">
${stats && html`<span class="ext-tasks-stats">${stats.total || 0} total</span>`}
<div class="ext-tasks-view-tabs">
${tabs.map(function(tab) {
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>`;
})}
</div>
<${Button} variant="primary" size="sm" onClick=${() => setEditTask({})}>+ New Task<//>
<//>
` : html`
<div class="tasks-header">
<h1 class="tasks-title">Tasks</h1>
<div class="ext-tasks-header">
<h1 class="ext-tasks-title">Tasks</h1>
<${Button} variant="primary" size="sm" onClick=${() => setEditTask({})}>+ New Task<//>
</div>
`}
@@ -381,7 +381,7 @@
}, []);
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', {

View File

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

View File

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

View File

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

View File

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

View File

@@ -103,7 +103,7 @@ export function createMarkdown(renderers) {
renderer(token) {
const target = _escapeAttr(token.target);
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>`;
},
}],