anentrypoint-design 0.0.190 → 0.0.192
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app-shell.css +9 -5
- package/chat.css +32 -1
- package/dist/247420.css +41 -6
- package/dist/247420.js +12 -12
- package/package.json +1 -1
- package/src/components/agent-chat.js +33 -14
- package/src/components/chat.js +6 -2
- package/src/components/content.js +9 -2
- package/src/components/shell.js +21 -1
- package/src/kits/os/app-panes.css +9 -3
- package/src/kits/os/browser-app.js +28 -1
- package/src/kits/os/theme.css +48 -15
- package/src/kits/os/wm.css +13 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anentrypoint-design",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.192",
|
|
4
4
|
"description": "247420 design system SDK — webjsx + modified ripple-ui, single-file ESM bundle for reproducible use of the AnEntrypoint design.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/247420.js",
|
|
@@ -101,27 +101,43 @@ export function AgentChat(props = {}) {
|
|
|
101
101
|
// True when streaming but the live assistant turn already shows content/parts,
|
|
102
102
|
// so its inline typing dots have stopped — a long silent tool call would
|
|
103
103
|
// otherwise read as frozen. We append a standalone "working" indicator below.
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
// A message carries content (text/parts) when it has a non-empty content
|
|
105
|
+
// string OR at least one part. Used for the empty-shell skip + working tail
|
|
106
|
+
// so an interleaved turn (parts-only, no m.content) is not treated as empty.
|
|
107
|
+
const msgHasBody = (m) => !!(m.content || (Array.isArray(m.parts) && m.parts.length));
|
|
108
|
+
const showWorkingTail = busy && lastMsg && lastMsg.role === 'assistant' && msgHasBody(lastMsg);
|
|
106
109
|
const rows = messages.map((m, i) => {
|
|
107
110
|
const isAssistant = m.role === 'assistant';
|
|
108
111
|
const isStreaming = busy && i === lastIdx && isAssistant;
|
|
109
112
|
const hasParts = Array.isArray(m.parts) && m.parts.length > 0;
|
|
110
|
-
const emptyStreaming = isStreaming && !m
|
|
113
|
+
const emptyStreaming = isStreaming && !msgHasBody(m);
|
|
111
114
|
// A finished assistant message with no content and no parts is an empty
|
|
112
115
|
// shell (e.g. an aborted turn) — render nothing rather than a blank bubble.
|
|
113
|
-
if (!isStreaming && isAssistant && !m
|
|
116
|
+
if (!isStreaming && isAssistant && !msgHasBody(m)) return null;
|
|
117
|
+
// Render order follows m.parts so text and tool cards INTERLEAVE in arrival
|
|
118
|
+
// order (text -> tool -> text -> tool). A message's parts may be bare
|
|
119
|
+
// strings (legacy) OR structured {kind,...} objects (md/tool/tool_result/
|
|
120
|
+
// code/...) passed straight through to ChatMessage.renderPart — this is what
|
|
121
|
+
// lets an orchestration host render the kit's collapsible ToolCallNode
|
|
122
|
+
// inline instead of flattening tools to the end of the turn.
|
|
114
123
|
const parts = [];
|
|
115
|
-
if (
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
+
if (hasParts) {
|
|
125
|
+
for (const p of m.parts) {
|
|
126
|
+
const part = (p && typeof p === 'object' && p.kind) ? p : { kind: 'text', text: String(p) };
|
|
127
|
+
// While a turn is still streaming, render its prose as cheap inline text
|
|
128
|
+
// rather than full markdown: MdNode re-parses + re-sanitizes the WHOLE
|
|
129
|
+
// accumulated source and swaps the entire bubble innerHTML on every frame
|
|
130
|
+
// (O(n^2) over the turn, with a visible reflow). Downgrade md -> text
|
|
131
|
+
// mid-stream; the settled turn below renders real markdown once.
|
|
132
|
+
if (isStreaming && part.kind === 'md') parts.push({ kind: 'text', text: part.text });
|
|
133
|
+
else parts.push(part);
|
|
134
|
+
}
|
|
124
135
|
}
|
|
136
|
+
// m.content is the legacy/simple path (user messages, hosts that don't build
|
|
137
|
+
// interleaved parts). Only prepend it when the parts array doesn't already
|
|
138
|
+
// carry prose, so a parts-driven turn isn't double-rendered.
|
|
139
|
+
const partsHaveProse = parts.some(p => p.kind === 'md' || p.kind === 'text');
|
|
140
|
+
if (m.content && !partsHaveProse) parts.unshift({ kind: isAssistant ? 'md' : 'text', text: m.content });
|
|
125
141
|
return ChatMessage({
|
|
126
142
|
key: m.id || String(i),
|
|
127
143
|
who: isAssistant ? 'them' : 'you',
|
|
@@ -172,7 +188,10 @@ export function AgentChat(props = {}) {
|
|
|
172
188
|
h('div', { class: 'agentchat-head', role: 'banner' },
|
|
173
189
|
h('h2', { class: 'agentchat-title' }, name + (selectedModel ? ' · ' + selectedModel : '')),
|
|
174
190
|
h('span', { class: 'agentchat-sub', 'aria-live': 'polite' },
|
|
175
|
-
|
|
191
|
+
// Derive the busy label from the same status prop the controls use, so a
|
|
192
|
+
// reconnecting-while-streaming state reads one word everywhere instead of
|
|
193
|
+
// the head saying "streaming…" while the controls say "reconnecting…".
|
|
194
|
+
busy ? (status || 'streaming…') : (messages.length ? messages.length + (messages.length === 1 ? ' message' : ' messages') : ''))),
|
|
176
195
|
h('div', { class: 'agentchat-thread', ref: threadRef(messages.length), role: 'log', 'aria-label': 'conversation' },
|
|
177
196
|
emptyState,
|
|
178
197
|
...rows.filter(Boolean),
|
package/src/components/chat.js
CHANGED
|
@@ -92,8 +92,12 @@ function MdNode(p) {
|
|
|
92
92
|
function CodeNode(p) {
|
|
93
93
|
const refSink = (el) => {
|
|
94
94
|
if (!el) return;
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
// Key on the full code, not its length: two different blocks of the same
|
|
96
|
+
// length (e.g. an edit that swaps a line) would otherwise share a key and
|
|
97
|
+
// skip re-highlighting, leaving stale syntax coloring.
|
|
98
|
+
const codeKey = (p.lang || '') + '|' + (p.code || '');
|
|
99
|
+
if (el.dataset.codeKey === codeKey) return;
|
|
100
|
+
el.dataset.codeKey = codeKey;
|
|
97
101
|
highlightCodeBlockCached(el);
|
|
98
102
|
};
|
|
99
103
|
return h('div', { class: 'chat-bubble chat-code', ref: refSink },
|
|
@@ -20,7 +20,7 @@ export function Panel({ title, count, right, style = '', children, kind }) {
|
|
|
20
20
|
// Card — semantic alias of Panel; behaves identically.
|
|
21
21
|
export const Card = Panel;
|
|
22
22
|
|
|
23
|
-
export function Row({ code, rank, title, sub, meta, active, state = 'default', onClick, key, style, href, kind, cols, leading, trailing, target, selected, rail }) {
|
|
23
|
+
export function Row({ code, rank, title, sub, meta, active, state = 'default', onClick, key, style, href, kind, cols, leading, trailing, target, selected, rail, expanded }) {
|
|
24
24
|
// `rank` is an alias for `code` (the leading monospace index); callers use
|
|
25
25
|
// either name. `rail` renders a thin colour bar at the row's leading edge as
|
|
26
26
|
// a status indicator (tone: green | purple | flame | <any token>).
|
|
@@ -45,6 +45,10 @@ export function Row({ code, rank, title, sub, meta, active, state = 'default', o
|
|
|
45
45
|
props.onkeydown = (e) => {
|
|
46
46
|
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onClick(e); }
|
|
47
47
|
};
|
|
48
|
+
// When the row is a disclosure toggle (host passes a boolean `expanded`),
|
|
49
|
+
// announce its open/closed state so AT users hear "expanded/collapsed".
|
|
50
|
+
// Omitted entirely for plain action buttons (expanded === undefined).
|
|
51
|
+
if (expanded === true || expanded === false) props['aria-expanded'] = expanded ? 'true' : 'false';
|
|
48
52
|
}
|
|
49
53
|
if (isDisabled) props['aria-disabled'] = 'true';
|
|
50
54
|
if (isActive && (isLink || isButton)) props['aria-current'] = isActive ? 'page' : null;
|
|
@@ -333,7 +337,10 @@ export function EventList({ items, events, emptyText = 'no events', rankPad = 3
|
|
|
333
337
|
active: it.active,
|
|
334
338
|
onClick: it.onClick,
|
|
335
339
|
kind: it.kind,
|
|
336
|
-
rail: it.rail
|
|
340
|
+
rail: it.rail,
|
|
341
|
+
// Forward a disclosure state when the host marks the row as a toggle,
|
|
342
|
+
// so a clickable event row announces aria-expanded.
|
|
343
|
+
expanded: it.expanded
|
|
337
344
|
}))
|
|
338
345
|
);
|
|
339
346
|
}
|
package/src/components/shell.js
CHANGED
|
@@ -131,8 +131,28 @@ const ICON_PATHS = {
|
|
|
131
131
|
'skip-forward': '<path d="M5 5v14l9-7z"/><path d="M19 5v14"/>',
|
|
132
132
|
'chevron-left': '<path d="m15 6-6 6 6 6"/>',
|
|
133
133
|
trash: '<path d="M4 7h16M9 7V5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2M6 7l1 13a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1l1-13"/>',
|
|
134
|
-
'external-link': '<path d="M14 4h6v6M20 4l-9 9M19 13v6a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h6"/>'
|
|
134
|
+
'external-link': '<path d="M14 4h6v6M20 4l-9 9M19 13v6a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h6"/>',
|
|
135
|
+
// theme-toggle icons (replace decorative sun/moon/contrast text glyphs)
|
|
136
|
+
sun: '<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/>',
|
|
137
|
+
moon: '<path d="M21 12.8A9 9 0 1 1 11.2 3 7 7 0 0 0 21 12.8z"/>',
|
|
138
|
+
contrast: '<circle cx="12" cy="12" r="9"/><path d="M12 3v18a9 9 0 0 0 0-18z" fill="currentColor"/>',
|
|
139
|
+
// file-browser icons (replace folder/file emoji + arrow glyphs in fs apps)
|
|
140
|
+
folder: '<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>',
|
|
141
|
+
upload: '<path d="M12 16V4M7 9l5-5 5 5"/><path d="M5 20h14"/>',
|
|
142
|
+
download: '<path d="M12 4v12M7 11l5 5 5-5"/><path d="M5 20h14"/>',
|
|
143
|
+
'corner-up-left': '<path d="M9 14 4 9l5-5"/><path d="M4 9h11a5 5 0 0 1 5 5v6"/>'
|
|
135
144
|
};
|
|
145
|
+
// Raw-DOM consumers (no webjsx render in scope) need the SVG as a markup string
|
|
146
|
+
// rather than an h() vnode. Same path table, same viewBox/stroke contract as
|
|
147
|
+
// Icon(); use innerHTML = iconMarkup(name). Keeps the icon paths upstream so
|
|
148
|
+
// raw-DOM call sites never reintroduce decorative glyph literals.
|
|
149
|
+
export function iconMarkup(name, { size = 16 } = {}) {
|
|
150
|
+
const inner = ICON_PATHS[name];
|
|
151
|
+
if (!inner) return '';
|
|
152
|
+
return '<svg class="ds-icon ds-icon-' + name + '" width="' + size + '" height="' + size +
|
|
153
|
+
'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="var(--ds-icon-stroke, 1.6)"' +
|
|
154
|
+
' stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' + inner + '</svg>';
|
|
155
|
+
}
|
|
136
156
|
export function Icon(name, { size = 16 } = {}) {
|
|
137
157
|
const inner = ICON_PATHS[name];
|
|
138
158
|
if (!inner) return h('span', { class: 'glyph', 'aria-hidden': 'true' }, '');
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* App-pane chrome for kit components rendered inside .wm-win bodies.
|
|
2
2
|
Bible tokens only — no hardcoded color, font, or radius. Focus = inset rail; no shadows, no gradients. */
|
|
3
3
|
|
|
4
|
-
/*
|
|
4
|
+
/* --- terminal-app --- */
|
|
5
5
|
.terminal-app {
|
|
6
6
|
display: flex;
|
|
7
7
|
flex-direction: column;
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
}
|
|
49
49
|
.terminal-app-slot > * { width: 100%; height: 100%; }
|
|
50
50
|
|
|
51
|
-
/*
|
|
51
|
+
/* --- browser-app --- */
|
|
52
52
|
.browser-app {
|
|
53
53
|
display: flex;
|
|
54
54
|
flex-direction: column;
|
|
@@ -79,6 +79,9 @@
|
|
|
79
79
|
}
|
|
80
80
|
.browser-app-btn:hover { background: var(--os-accent-soft); }
|
|
81
81
|
.browser-app-btn:focus-visible { box-shadow: inset 0 0 0 1px var(--os-accent); }
|
|
82
|
+
/* Inert back/forward when there's no history in that direction. */
|
|
83
|
+
.browser-app-btn:disabled { opacity: 0.4; cursor: default; pointer-events: none; }
|
|
84
|
+
.browser-app-btn:disabled:hover { background: var(--os-bg-3); }
|
|
82
85
|
.browser-app-url {
|
|
83
86
|
flex: 1 1 auto;
|
|
84
87
|
min-width: 0;
|
|
@@ -110,8 +113,11 @@
|
|
|
110
113
|
text-transform: lowercase;
|
|
111
114
|
min-height: 18px;
|
|
112
115
|
}
|
|
116
|
+
/* Loading: a thin indeterminate bar under the toolbar. Error: red status row. */
|
|
117
|
+
.browser-app-loading .browser-app-bar { box-shadow: inset 0 -2px 0 var(--os-accent); }
|
|
118
|
+
.browser-app[data-error] .browser-app-status { color: var(--os-danger, #c0392b); }
|
|
113
119
|
|
|
114
|
-
/*
|
|
120
|
+
/* --- validator-app --- */
|
|
115
121
|
.validator-app {
|
|
116
122
|
display: flex;
|
|
117
123
|
flex-direction: column;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// Browser-pane paint surface — URL bar + iframe slot + status row. Consumer owns iframe.
|
|
2
2
|
// renderBrowserPane({initialUrl, callbacks: {onNavigate, onReload, onBack, onForward}})
|
|
3
|
-
// -> {node, slot, setUrl, setStatus, dispose}.
|
|
3
|
+
// -> {node, slot, setUrl, setStatus, setNav, setLoading, setError, dispose}.
|
|
4
4
|
// slot is the container the consumer should append its iframe to.
|
|
5
|
+
// setNav({canBack,canForward}) disables the back/forward buttons when history
|
|
6
|
+
// has no entry in that direction; setLoading(bool) toggles a loading state on
|
|
7
|
+
// the bar; setError(msg|null) surfaces a load failure in the status row.
|
|
5
8
|
|
|
6
9
|
export function renderBrowserPane(opts = {}) {
|
|
7
10
|
const { initialUrl = 'about:blank', callbacks = {} } = opts;
|
|
@@ -21,8 +24,14 @@ export function renderBrowserPane(opts = {}) {
|
|
|
21
24
|
return b;
|
|
22
25
|
};
|
|
23
26
|
const backBtn = mkBtn('<', 'back');
|
|
27
|
+
backBtn.setAttribute('aria-label', 'Back');
|
|
24
28
|
const fwdBtn = mkBtn('>', 'forward');
|
|
29
|
+
fwdBtn.setAttribute('aria-label', 'Forward');
|
|
25
30
|
const reloadBtn = mkBtn('reload', 'reload');
|
|
31
|
+
reloadBtn.setAttribute('aria-label', 'Reload');
|
|
32
|
+
// Start with no history in either direction until the consumer says otherwise.
|
|
33
|
+
backBtn.disabled = true;
|
|
34
|
+
fwdBtn.disabled = true;
|
|
26
35
|
|
|
27
36
|
const urlInput = document.createElement('input');
|
|
28
37
|
urlInput.type = 'text';
|
|
@@ -44,6 +53,8 @@ export function renderBrowserPane(opts = {}) {
|
|
|
44
53
|
|
|
45
54
|
const status = document.createElement('div');
|
|
46
55
|
status.className = 'browser-app-status';
|
|
56
|
+
status.setAttribute('role', 'status');
|
|
57
|
+
status.setAttribute('aria-live', 'polite');
|
|
47
58
|
status.textContent = '';
|
|
48
59
|
|
|
49
60
|
node.append(bar, slot, status);
|
|
@@ -53,6 +64,22 @@ export function renderBrowserPane(opts = {}) {
|
|
|
53
64
|
get slot() { return slot; },
|
|
54
65
|
setUrl(u) { urlInput.value = u; },
|
|
55
66
|
setStatus(s) { status.textContent = s; },
|
|
67
|
+
// Reflect history availability so disabled buttons read as inert.
|
|
68
|
+
setNav({ canBack = false, canForward = false } = {}) {
|
|
69
|
+
backBtn.disabled = !canBack;
|
|
70
|
+
fwdBtn.disabled = !canForward;
|
|
71
|
+
},
|
|
72
|
+
// Loading: tint the bar + announce; the consumer flips it off on load/error.
|
|
73
|
+
setLoading(on) {
|
|
74
|
+
node.classList.toggle('browser-app-loading', !!on);
|
|
75
|
+
if (on) { node.removeAttribute('data-error'); status.textContent = 'loading...'; }
|
|
76
|
+
},
|
|
77
|
+
// Error: persistent failure surfaced in the live status row.
|
|
78
|
+
setError(msg) {
|
|
79
|
+
node.classList.remove('browser-app-loading');
|
|
80
|
+
if (msg) { node.setAttribute('data-error', '1'); status.textContent = msg; }
|
|
81
|
+
else { node.removeAttribute('data-error'); }
|
|
82
|
+
},
|
|
56
83
|
dispose() {},
|
|
57
84
|
};
|
|
58
85
|
}
|
package/src/kits/os/theme.css
CHANGED
|
@@ -139,6 +139,18 @@ html, body {
|
|
|
139
139
|
}
|
|
140
140
|
.os-menu .os-btn:hover { background: var(--panel-hover, var(--os-bg-2)); color: var(--os-fg); }
|
|
141
141
|
|
|
142
|
+
/* Collapsible "System" apps group inside the apps menu. thebird's os-shell.js
|
|
143
|
+
* builds the group structurally and toggles only the list's display; the static
|
|
144
|
+
* layout (column flow + indent + arrow alignment) lives here so no visual rule
|
|
145
|
+
* leaks back into thebird JS (zero-design-CSS contract). */
|
|
146
|
+
.os-menu-group { display: flex; flex-direction: column; gap: 2px; }
|
|
147
|
+
.os-menu-system-toggle { display: flex; align-items: center; gap: 6px; }
|
|
148
|
+
.os-menu-system-arrow { display: inline-flex; align-items: center; transition: transform 120ms ease; }
|
|
149
|
+
.os-menu-system-arrow .ds-icon { width: 14px; height: 14px; }
|
|
150
|
+
.os-menu-system-list { display: none; flex-direction: column; padding-left: 14px; }
|
|
151
|
+
.os-menu-system-toggle[aria-expanded="true"] + .os-menu-system-list { display: flex; }
|
|
152
|
+
@media (prefers-reduced-motion: reduce) { .os-menu-system-arrow { transition: none; } }
|
|
153
|
+
|
|
142
154
|
.os-clock {
|
|
143
155
|
color: var(--os-fg-2);
|
|
144
156
|
font-variant-numeric: tabular-nums;
|
|
@@ -223,13 +235,19 @@ html, body {
|
|
|
223
235
|
height: 14px !important;
|
|
224
236
|
color: var(--os-fg-3);
|
|
225
237
|
opacity: 0.4;
|
|
226
|
-
font: 10px var(--os-mono);
|
|
227
|
-
display: flex;
|
|
228
|
-
align-items: flex-end;
|
|
229
|
-
justify-content: flex-end;
|
|
230
|
-
line-height: 1;
|
|
231
238
|
}
|
|
232
|
-
.
|
|
239
|
+
/* Resize grip via CSS gradient (matches wm.css); no decorative content glyph. */
|
|
240
|
+
.wm-resize::after {
|
|
241
|
+
content: '';
|
|
242
|
+
position: absolute;
|
|
243
|
+
right: 3px;
|
|
244
|
+
bottom: 3px;
|
|
245
|
+
width: 8px;
|
|
246
|
+
height: 8px;
|
|
247
|
+
background:
|
|
248
|
+
linear-gradient(135deg, transparent 0 45%, currentColor 45% 55%, transparent 55% 100%),
|
|
249
|
+
linear-gradient(135deg, transparent 0 70%, currentColor 70% 80%, transparent 80% 100%);
|
|
250
|
+
}
|
|
233
251
|
.wm-body { background: var(--os-bg-1) !important; }
|
|
234
252
|
|
|
235
253
|
.os-side-rail {
|
|
@@ -474,6 +492,17 @@ html, body {
|
|
|
474
492
|
--os-bar-h: 34px !important;
|
|
475
493
|
--os-bar-h-mobile: 34px !important;
|
|
476
494
|
--os-dock-h: 34px !important;
|
|
495
|
+
/* Brand identity, scoped so it never leaks to other portfolio consumers.
|
|
496
|
+
* The base theme's auto/paper/ink presets let --accent drift to the bright
|
|
497
|
+
* --green-2 (#3A9A34) in ink mode; thebird's brand accent is the deep green
|
|
498
|
+
* #247420 (--green) in EVERY resolved mode, and its paper is the warm
|
|
499
|
+
* #F5F0E4 variant. Re-assert both here rather than forcing the consumer to
|
|
500
|
+
* opt into [data-theme="thebird"], which the OS boots past with
|
|
501
|
+
* data-theme="auto". --accent-bright is pinned too so the ink-mode
|
|
502
|
+
* --accent: var(--accent-bright, ...) chain still resolves to brand green. */
|
|
503
|
+
--accent: var(--green) !important;
|
|
504
|
+
--accent-bright: var(--green) !important;
|
|
505
|
+
--paper: #F5F0E4;
|
|
477
506
|
}
|
|
478
507
|
/* Window titlebar matches the 34px bar height; clamp so menubar/taskbar/titlebar
|
|
479
508
|
* heights are uniform. */
|
|
@@ -511,11 +540,14 @@ html.ds-247420 { touch-action: pan-x pan-y; overscroll-behavior: none; -webkit-t
|
|
|
511
540
|
.ds-247420 .wm-snap-preview {
|
|
512
541
|
position: fixed;
|
|
513
542
|
pointer-events: none;
|
|
543
|
+
display: none;
|
|
514
544
|
z-index: 9050;
|
|
515
545
|
background: color-mix(in oklab, var(--os-accent) 20%, transparent);
|
|
516
546
|
border: 2px solid var(--os-accent);
|
|
517
547
|
border-radius: 8px;
|
|
518
|
-
transition: all
|
|
548
|
+
/* Animate only geometry, never `all` — `transition: all` animated display/
|
|
549
|
+
* opacity too, producing a one-frame ghost on show (jank-pass fix). */
|
|
550
|
+
transition: left 80ms ease, top 80ms ease, width 80ms ease, height 80ms ease;
|
|
519
551
|
}
|
|
520
552
|
@media (prefers-reduced-motion: reduce) {
|
|
521
553
|
.ds-247420 .wm-snap-preview { transition: none; }
|
|
@@ -807,11 +839,16 @@ html.ds-247420 { touch-action: pan-x pan-y; overscroll-behavior: none; -webkit-t
|
|
|
807
839
|
.ds-247420 .fsb-list { flex: 1; min-height: 0; overflow: auto; padding: .3em 0; }
|
|
808
840
|
.ds-247420 .fsb-row { display: flex; align-items: center; gap: .6em; padding: .35em .7em; cursor: pointer; border: 0; }
|
|
809
841
|
.ds-247420 .fsb-row:hover { background: color-mix(in oklab, var(--fg) 6%, transparent); }
|
|
842
|
+
.ds-247420 .fsb-row:focus-visible { outline: 2px solid var(--os-accent); outline-offset: -2px; background: color-mix(in oklab, var(--fg) 6%, transparent); }
|
|
810
843
|
.ds-247420 .fsb-icon { width: 1.3em; text-align: center; flex: 0 0 auto; }
|
|
811
844
|
.ds-247420 .fsb-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
812
845
|
.ds-247420 .fsb-size { opacity: .55; font-size: 11px; font-variant-numeric: tabular-nums; flex: 0 0 auto; }
|
|
813
846
|
.ds-247420 .fsb-actions { display: flex; gap: .3em; flex: 0 0 auto; opacity: 0; }
|
|
814
|
-
|
|
847
|
+
/* Reveal row actions on hover OR keyboard focus-within OR coarse-pointer (touch),
|
|
848
|
+
* so they aren't hover-only dead controls for keyboard/touch users. */
|
|
849
|
+
.ds-247420 .fsb-row:hover .fsb-actions,
|
|
850
|
+
.ds-247420 .fsb-row:focus-within .fsb-actions { opacity: 1; }
|
|
851
|
+
@media (hover: none) { .ds-247420 .fsb-actions { opacity: 1; } }
|
|
815
852
|
.ds-247420 .fsb-act { all: unset; cursor: pointer; padding: .1em .35em; border-radius: 5px; opacity: .7; font-size: 12px; }
|
|
816
853
|
.ds-247420 .fsb-act:hover { opacity: 1; background: color-mix(in oklab, var(--fg) 12%, transparent); }
|
|
817
854
|
.ds-247420 .fsb-empty { opacity: .5; padding: 1.5em; text-align: center; font-style: italic; }
|
|
@@ -829,13 +866,9 @@ html.ds-247420 { touch-action: pan-x pan-y; overscroll-behavior: none; -webkit-t
|
|
|
829
866
|
.ds-247420 .wm-context-menu-item { display: block; width: 100%; padding: 7px 10px; border: none; border-radius: 4px; background: none; color: inherit; font-family: inherit; text-align: left; cursor: pointer; font-size: 13px; }
|
|
830
867
|
.ds-247420 .wm-context-menu-item:hover { background: color-mix(in oklab, var(--fg) 8%, transparent); }
|
|
831
868
|
|
|
832
|
-
/*
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
/* ---- wm alt-tab switcher (was: docs/wm.js switcherEl cssText) ---- */
|
|
836
|
-
.ds-247420 .wm-switcher { position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); padding: 16px 20px; background: var(--panel-2, #1a1a1a); border: 1px solid var(--os-accent, #247420); border-radius: 8px; z-index: 9600; font-family: var(--ff-ui); color: var(--fg, #fff); min-width: 240px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); }
|
|
837
|
-
.ds-247420 .wm-switcher-row { padding: 8px 12px; border-radius: 4px; margin: 2px 0; cursor: pointer; }
|
|
838
|
-
.ds-247420 .wm-switcher-row.is-active { background: color-mix(in oklab, var(--os-accent) 25%, transparent); }
|
|
869
|
+
/* (legacy duplicate .wm-snap-preview + .wm-switcher-row/.is-active blocks
|
|
870
|
+
* removed — superseded by the canonical .wm-snap-preview + .wm-switcher /
|
|
871
|
+
* .wm-switcher-item.active rules above; the JS now emits .wm-switcher-item.) */
|
|
839
872
|
|
|
840
873
|
/* ---- todo app (was: docs/apps.js todo-app cssText) ---- */
|
|
841
874
|
.ds-247420 .tb-todo-bar { display: flex; gap: 6px; margin-bottom: 8px; }
|
package/src/kits/os/wm.css
CHANGED
|
@@ -92,15 +92,22 @@
|
|
|
92
92
|
background: transparent;
|
|
93
93
|
color: var(--os-fg-3);
|
|
94
94
|
opacity: 0.4;
|
|
95
|
-
font: 10px var(--os-mono);
|
|
96
|
-
line-height: 1;
|
|
97
|
-
display: flex;
|
|
98
|
-
align-items: flex-end;
|
|
99
|
-
justify-content: flex-end;
|
|
100
95
|
touch-action: none;
|
|
101
96
|
}
|
|
102
97
|
.wm-resize:hover { opacity: 0.7; }
|
|
103
|
-
|
|
98
|
+
/* Resize grip drawn with CSS (two diagonal hairlines) instead of a decorative
|
|
99
|
+
* unicode content glyph; keeps the affordance, drops the glyph tell. */
|
|
100
|
+
.wm-resize::after {
|
|
101
|
+
content: '';
|
|
102
|
+
position: absolute;
|
|
103
|
+
right: 3px;
|
|
104
|
+
bottom: 3px;
|
|
105
|
+
width: 9px;
|
|
106
|
+
height: 9px;
|
|
107
|
+
background:
|
|
108
|
+
linear-gradient(135deg, transparent 0 45%, currentColor 45% 55%, transparent 55% 100%),
|
|
109
|
+
linear-gradient(135deg, transparent 0 70%, currentColor 70% 80%, transparent 80% 100%);
|
|
110
|
+
}
|
|
104
111
|
|
|
105
112
|
.wm-win.wm-min .wm-body,
|
|
106
113
|
.wm-win.wm-min .wm-resize { display: none; }
|