anentrypoint-design 0.0.437 → 0.0.439
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/dist/247420.css +78 -23
- package/dist/247420.js +23 -23
- package/dist/colors_and_type.css +86 -42
- package/dist/preview/buttons.html +5 -2
- package/dist/preview/colors-core.html +4 -1
- package/dist/preview/colors-lore.html +4 -1
- package/dist/preview/colors-semantic.html +4 -1
- package/dist/preview/dateline.html +4 -1
- package/dist/preview/dropzone.html +4 -1
- package/dist/preview/file-grid.html +4 -1
- package/dist/preview/file-row.html +4 -1
- package/dist/preview/file-toolbar.html +4 -1
- package/dist/preview/file-viewer.html +7 -1
- package/dist/preview/header.html +5 -2
- package/dist/preview/icons-unicode.html +26 -20
- package/dist/preview/index-row.html +4 -1
- package/dist/preview/inputs.html +4 -1
- package/dist/preview/manifesto.html +4 -1
- package/dist/preview/rules.html +4 -1
- package/dist/preview/spacing.html +17 -12
- package/dist/preview/stamps-lore.html +4 -1
- package/dist/preview/stamps.html +4 -1
- package/dist/preview/theme-ink.html +4 -1
- package/dist/preview/type-display.html +4 -1
- package/dist/preview/type-mono.html +4 -1
- package/dist/preview/type-prose.html +5 -2
- package/dist/preview/type-scale.html +40 -15
- package/dist/preview/wordmarks.html +18 -22
- package/dist/src/components/content.js +2 -1
- package/dist/src/components.js +21 -5
- package/dist/src/kits/os/theme.css +38 -5
- package/dist/ui_kits/aicat/app.js +1 -1
- package/dist/ui_kits/gallery/app.js +27 -7
- package/dist/ui_kits/signin/app.js +51 -10
- package/dist/ui_kits/system_primer/app.js +189 -3
- package/dist/ui_kits/terminal/app.js +56 -10
- package/package.json +1 -1
- package/src/components/content/cli.js +7 -1
- package/src/components/content/panel.js +7 -2
- package/src/components/shell/app-shell.js +7 -2
- package/src/components/shell/icons.js +4 -1
- package/src/css/app-shell/kits-appended.css +58 -21
- package/src/css/app-shell/panel-row.css +14 -0
- package/src/css/app-shell/responsive2-workspace.css +5 -1
- package/src/css/app-shell/topbar.css +1 -1
- package/src/kits/os/files-app.js +9 -2
- package/src/kits/os/icons.js +5 -0
- package/src/kits/os/launcher.css +7 -2
- package/src/kits/os/launcher.js +27 -3
- package/src/kits/os/monitor-app.js +8 -2
- package/src/kits/os/shell.js +55 -12
- package/src/kits/os/theme.css +20 -2
- package/src/kits/os/wm.js +41 -11
- package/types/components.d.ts +2 -0
- package/dist/slides/deck-stage-overlay.js +0 -63
- package/dist/slides/deck-stage-state.js +0 -81
- package/dist/slides/deck-stage-style.js +0 -117
- package/dist/slides/deck-stage.js +0 -159
- package/dist/src/kits/os/freddie/helpers.js +0 -59
- package/dist/src/kits/os/validator-app.js +0 -55
package/src/kits/os/icons.js
CHANGED
|
@@ -24,6 +24,11 @@ const OS_PATHS = {
|
|
|
24
24
|
chat: ICON_PATHS.forum,
|
|
25
25
|
tools: '<path d="M14 7l3-3 3 3-3 3-3-3zM7 14l3 3-7 7-3-3 7-7zM5 7l3-3M14 14l6 6"/>',
|
|
26
26
|
freddie: '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2M8 12a4 4 0 008 0"/>',
|
|
27
|
+
// Titlebar window controls: mirrors ICON_PATHS's minus/plus/x visual weight
|
|
28
|
+
// so wm.js's chip buttons render through the same SVG contract as every
|
|
29
|
+
// other icon in this kit instead of raw ASCII glyph text (-/+/x).
|
|
30
|
+
minimize: '<path d="M5 12h14"/>',
|
|
31
|
+
maximize: '<rect x="5" y="5" width="14" height="14" rx="1"/>',
|
|
27
32
|
};
|
|
28
33
|
|
|
29
34
|
// iconMarkup() only resolves names already registered in the shared
|
package/src/kits/os/launcher.css
CHANGED
|
@@ -43,8 +43,9 @@
|
|
|
43
43
|
color: var(--os-fg);
|
|
44
44
|
box-shadow: inset 4px 0 0 var(--os-accent);
|
|
45
45
|
}
|
|
46
|
-
.launcher-add {
|
|
46
|
+
.launcher-add { color: var(--os-fg-2); }
|
|
47
47
|
.launcher-add:hover { color: var(--os-accent); }
|
|
48
|
+
.launcher-btn svg { width: 18px; height: 18px; display: block; fill: none; stroke: currentColor; }
|
|
48
49
|
|
|
49
50
|
.launcher-instances {
|
|
50
51
|
display: flex;
|
|
@@ -65,8 +66,12 @@
|
|
|
65
66
|
gap: var(--space-1, 4px);
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
.launcher-close {
|
|
69
|
+
.launcher-close { opacity: 0.7; width: 20px; height: 20px; color: var(--os-fg-3); }
|
|
70
|
+
.launcher-close svg { width: 12px; height: 12px; }
|
|
69
71
|
.launcher-close:hover { opacity: 1; color: var(--os-red); }
|
|
72
|
+
/* Armed (first-click) confirm state for the second-click-to-confirm destructive
|
|
73
|
+
* close — a visible "are you sure" cue before the instance actually closes. */
|
|
74
|
+
.launcher-close.confirm { opacity: 1; color: var(--os-red); box-shadow: inset 0 0 0 1px var(--os-red); }
|
|
70
75
|
|
|
71
76
|
.launcher-dock + .wm-root { left: calc(var(--os-rail-w, 56px) + env(safe-area-inset-left, 0px)) !important; }
|
|
72
77
|
|
package/src/kits/os/launcher.js
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
// Consumer (thebird) owns instance creation, fs/worker/shell wiring, teardown.
|
|
3
3
|
// renderDock returns a handle whose setInstances/setActive are called from
|
|
4
4
|
// lifecycle code. Visuals are bible-aligned: panel-select bg + accent inset
|
|
5
|
-
// rail for active, tonal hover, lowercase mono labels.
|
|
5
|
+
// rail for active, tonal hover, lowercase mono labels. Add/close controls
|
|
6
|
+
// render through ./icons.js's SVG contract, not raw ASCII glyphs.
|
|
7
|
+
|
|
8
|
+
import { icons } from './icons.js';
|
|
6
9
|
|
|
7
10
|
export function renderDock(opts = {}) {
|
|
8
11
|
const { root = document.body, callbacks = {} } = opts;
|
|
@@ -12,7 +15,7 @@ export function renderDock(opts = {}) {
|
|
|
12
15
|
|
|
13
16
|
const addBtn = document.createElement('button');
|
|
14
17
|
addBtn.className = 'launcher-btn launcher-add';
|
|
15
|
-
addBtn.
|
|
18
|
+
addBtn.innerHTML = icons.plus;
|
|
16
19
|
addBtn.title = 'new instance';
|
|
17
20
|
addBtn.setAttribute('aria-label', 'new instance');
|
|
18
21
|
addBtn.addEventListener('click', () => callbacks.onNewInstance && callbacks.onNewInstance());
|
|
@@ -51,15 +54,36 @@ export function renderDock(opts = {}) {
|
|
|
51
54
|
|
|
52
55
|
const closeBtn = document.createElement('button');
|
|
53
56
|
closeBtn.className = 'launcher-btn launcher-close';
|
|
54
|
-
closeBtn.
|
|
57
|
+
closeBtn.innerHTML = icons.close;
|
|
55
58
|
closeBtn.title = 'close ' + inst.id;
|
|
56
59
|
closeBtn.setAttribute('aria-label', 'close ' + inst.id);
|
|
57
60
|
closeBtn.dataset.role = 'close';
|
|
58
61
|
closeBtn.dataset.instanceId = inst.id;
|
|
62
|
+
// Destructive one-click close gets a second-click-to-confirm: the
|
|
63
|
+
// first click arms the button (visual + aria-label change) instead
|
|
64
|
+
// of firing immediately; a second click within the window commits.
|
|
65
|
+
// Clicking elsewhere, or the arm window elapsing, disarms silently.
|
|
66
|
+
let armed = false;
|
|
67
|
+
let armTimer = null;
|
|
68
|
+
function disarm() {
|
|
69
|
+
armed = false;
|
|
70
|
+
if (armTimer) { clearTimeout(armTimer); armTimer = null; }
|
|
71
|
+
closeBtn.classList.remove('confirm');
|
|
72
|
+
closeBtn.setAttribute('aria-label', 'close ' + inst.id);
|
|
73
|
+
}
|
|
59
74
|
closeBtn.addEventListener('click', e => {
|
|
60
75
|
e.stopPropagation();
|
|
76
|
+
if (!armed) {
|
|
77
|
+
armed = true;
|
|
78
|
+
closeBtn.classList.add('confirm');
|
|
79
|
+
closeBtn.setAttribute('aria-label', 'confirm close ' + inst.id);
|
|
80
|
+
armTimer = setTimeout(disarm, 3000);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
disarm();
|
|
61
84
|
callbacks.onCloseInstance && callbacks.onCloseInstance(inst.id);
|
|
62
85
|
});
|
|
86
|
+
closeBtn.addEventListener('blur', disarm);
|
|
63
87
|
|
|
64
88
|
row.append(selBtn, closeBtn);
|
|
65
89
|
instancesHost.appendChild(row);
|
|
@@ -8,8 +8,14 @@ export function renderMonitorApp(opts = {}) {
|
|
|
8
8
|
node.className = 'app-pane mono';
|
|
9
9
|
node.dataset.component = 'monitor-app';
|
|
10
10
|
|
|
11
|
+
function renderError(err) {
|
|
12
|
+
node.textContent = 'could not read stats: ' + (err && err.message ? err.message : String(err));
|
|
13
|
+
node.classList.add('is-error');
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
async function tick() {
|
|
12
17
|
const s = (await Promise.resolve(getStats())) || {};
|
|
18
|
+
node.classList.remove('is-error');
|
|
13
19
|
const heap = (s.jsHeapMb != null && s.jsHeapLimitMb != null)
|
|
14
20
|
? `js heap: ${Number(s.jsHeapMb).toFixed(1)} MB / ${Number(s.jsHeapLimitMb).toFixed(0)} MB`
|
|
15
21
|
: 'js heap: n/a';
|
|
@@ -24,8 +30,8 @@ export function renderMonitorApp(opts = {}) {
|
|
|
24
30
|
].join('\n');
|
|
25
31
|
}
|
|
26
32
|
|
|
27
|
-
tick().catch(
|
|
28
|
-
const timer = setInterval(() => tick().catch(
|
|
33
|
+
tick().catch(renderError);
|
|
34
|
+
const timer = setInterval(() => tick().catch(renderError), pollMs);
|
|
29
35
|
return {
|
|
30
36
|
node,
|
|
31
37
|
tick,
|
package/src/kits/os/shell.js
CHANGED
|
@@ -42,8 +42,37 @@ export function createDesktopShell({ root = document.body, wm, registry, brand =
|
|
|
42
42
|
osRoot.append(menubar, appsMenu, taskbar);
|
|
43
43
|
document.body.append(sideRail, drawer);
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
// Apps menu keyboard operability (APG menu-button pattern), mirroring the
|
|
46
|
+
// drawer's capture/restore-focus treatment below: opening moves focus onto
|
|
47
|
+
// the first menuitem so Tab/arrow-keys start inside the now-visible menu
|
|
48
|
+
// instead of on a hidden ancestor; closing restores focus to appsBtn (the
|
|
49
|
+
// only trigger) so keyboard position isn't lost. Arrow keys roam the
|
|
50
|
+
// role="menuitem" set (roving focus) per the declared role="menu".
|
|
51
|
+
let menuReturnFocus = null;
|
|
52
|
+
function menuItems() { return [...appsMenu.querySelectorAll('[role="menuitem"]')]; }
|
|
53
|
+
function openMenu() {
|
|
54
|
+
menuReturnFocus = document.activeElement;
|
|
55
|
+
appsMenu.classList.add('open');
|
|
56
|
+
appsBtn.setAttribute('aria-expanded', 'true');
|
|
57
|
+
const items = menuItems();
|
|
58
|
+
if (items.length) items[0].focus();
|
|
59
|
+
}
|
|
60
|
+
function closeMenu() {
|
|
61
|
+
if (!appsMenu.classList.contains('open')) return;
|
|
62
|
+
appsMenu.classList.remove('open');
|
|
63
|
+
appsBtn.setAttribute('aria-expanded', 'false');
|
|
64
|
+
if (menuReturnFocus && typeof menuReturnFocus.focus === 'function') menuReturnFocus.focus();
|
|
65
|
+
menuReturnFocus = null;
|
|
66
|
+
}
|
|
67
|
+
appsMenu.addEventListener('keydown', e => {
|
|
68
|
+
const items = menuItems();
|
|
69
|
+
if (!items.length) return;
|
|
70
|
+
const i = items.indexOf(document.activeElement);
|
|
71
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); items[(i + 1 + items.length) % items.length].focus(); }
|
|
72
|
+
else if (e.key === 'ArrowUp') { e.preventDefault(); items[(i - 1 + items.length) % items.length].focus(); }
|
|
73
|
+
else if (e.key === 'Home') { e.preventDefault(); items[0].focus(); }
|
|
74
|
+
else if (e.key === 'End') { e.preventDefault(); items[items.length - 1].focus(); }
|
|
75
|
+
});
|
|
47
76
|
// Focus management: opening the drawer moves keyboard focus onto its
|
|
48
77
|
// close button (the first reachable control inside the now-visible
|
|
49
78
|
// dialog) so Tab starts inside it, not lost on a now-hidden ancestor;
|
|
@@ -136,26 +165,40 @@ export function createDesktopShell({ root = document.body, wm, registry, brand =
|
|
|
136
165
|
refreshTaskbar();
|
|
137
166
|
}
|
|
138
167
|
|
|
168
|
+
function makeLoadingNode() {
|
|
169
|
+
const n = document.createElement('div');
|
|
170
|
+
n.className = 'app-pane os-app-loading';
|
|
171
|
+
n.textContent = 'loading…';
|
|
172
|
+
return n;
|
|
173
|
+
}
|
|
174
|
+
|
|
139
175
|
function openApp(appId) {
|
|
140
176
|
const app = (typeof registry.get === 'function') ? registry.get(appId) : registry[appId];
|
|
141
177
|
if (!app) throw new Error('unknown app: ' + appId);
|
|
142
178
|
const ctx = { ...(activeContext || {}), registry, openApp, wm };
|
|
143
179
|
const result = app.factory(ctx);
|
|
180
|
+
const isAsync = result && typeof result.then === 'function';
|
|
181
|
+
const sz = app.defaultSize || { w: 520, h: 360 };
|
|
182
|
+
const { w, h, x, y, maximized } = computeSpawnRect(sz, wm.count);
|
|
183
|
+
const titlePrefix = (activeContext && activeContext.titlePrefix) ? activeContext.titlePrefix + ' · ' : '';
|
|
184
|
+
// A slow async factory (network/worker-backed app) must not read as a
|
|
185
|
+
// dead click: spawn the window immediately with a loading placeholder
|
|
186
|
+
// body, then swap in the real content once the factory resolves.
|
|
187
|
+
const win = wm.open({ title: titlePrefix + app.name, body: isAsync ? makeLoadingNode() : result.node, kind: appId, width: w, height: h, x, y, maximized });
|
|
188
|
+
if (activeInstanceId && win.el) {
|
|
189
|
+
win.el.dataset.instanceId = activeInstanceId;
|
|
190
|
+
win.instanceId = activeInstanceId;
|
|
191
|
+
}
|
|
192
|
+
win.appId = appId;
|
|
193
|
+
refreshTaskbar();
|
|
144
194
|
const finish = (r) => {
|
|
145
|
-
|
|
146
|
-
const { w, h, x, y, maximized } = computeSpawnRect(sz, wm.count);
|
|
147
|
-
const titlePrefix = (activeContext && activeContext.titlePrefix) ? activeContext.titlePrefix + ' · ' : '';
|
|
148
|
-
const win = wm.open({ title: titlePrefix + app.name, body: r.node, kind: appId, width: w, height: h, x, y, maximized });
|
|
195
|
+
if (isAsync && typeof win.setBody === 'function') win.setBody(r.node);
|
|
149
196
|
win._app = { id: appId, dispose: r.dispose };
|
|
150
|
-
if (activeInstanceId && win.el) {
|
|
151
|
-
win.el.dataset.instanceId = activeInstanceId;
|
|
152
|
-
win.instanceId = activeInstanceId;
|
|
153
|
-
}
|
|
154
|
-
win.appId = appId;
|
|
155
197
|
refreshTaskbar();
|
|
156
198
|
return win;
|
|
157
199
|
};
|
|
158
|
-
|
|
200
|
+
if (isAsync) return result.then(finish);
|
|
201
|
+
return finish(result);
|
|
159
202
|
}
|
|
160
203
|
|
|
161
204
|
if (newInstBtn) newInstBtn.addEventListener('click', () => onNewInstance && onNewInstance({ instSwitch, setContext, openApp }));
|
package/src/kits/os/theme.css
CHANGED
|
@@ -184,7 +184,10 @@ html, body {
|
|
|
184
184
|
white-space: nowrap;
|
|
185
185
|
overflow: hidden;
|
|
186
186
|
text-overflow: ellipsis;
|
|
187
|
-
text-transform:
|
|
187
|
+
/* No text-transform here: .os-task carries the app/window's own dynamic
|
|
188
|
+
* title (shell.js refreshTaskbar -> t.textContent = w.title), and a
|
|
189
|
+
* user- or app-supplied string (filename, acronym, URL) must render
|
|
190
|
+
* as-authored rather than get silently lowercased by brand-voice CSS. */
|
|
188
191
|
}
|
|
189
192
|
.os-task { min-height: 28px; }
|
|
190
193
|
.os-task:hover { background: var(--panel-hover, var(--os-bg-2)); color: var(--os-fg); }
|
|
@@ -229,7 +232,9 @@ html, body {
|
|
|
229
232
|
letter-spacing: 0.005em;
|
|
230
233
|
text-align: center;
|
|
231
234
|
font-weight: 600;
|
|
232
|
-
text-transform:
|
|
235
|
+
/* No text-transform here: the titlebar renders the window's own dynamic
|
|
236
|
+
* title (renderWindow({title}) / setTitle(t) in wm.js) — a user- or
|
|
237
|
+
* app-supplied filename/acronym/URL must not be silently lowercased. */
|
|
233
238
|
}
|
|
234
239
|
.ds-247420 .wm-btns { gap: var(--space-1-75); order: -1; }
|
|
235
240
|
.ds-247420 .wm-btn {
|
|
@@ -249,6 +254,11 @@ html, body {
|
|
|
249
254
|
transition: background 100ms ease, color 100ms ease;
|
|
250
255
|
}
|
|
251
256
|
.ds-247420 .wm-btn:hover { background: var(--panel-hover, var(--os-bg-2)); color: var(--os-fg); }
|
|
257
|
+
.ds-247420 .wm-btn svg { width: 12px; height: 12px; display: block; fill: none; stroke: currentColor; }
|
|
258
|
+
/* Armed (first-click) confirm state for the window close button's
|
|
259
|
+
* second-click-to-confirm idiom — an unrecoverable close gets a visible cue
|
|
260
|
+
* before it commits, matching the launcher dock's instance-close treatment. */
|
|
261
|
+
.ds-247420 .wm-btn.confirm { background: var(--os-red); color: var(--paper); }
|
|
252
262
|
|
|
253
263
|
.ds-247420 .wm-resize {
|
|
254
264
|
background: transparent;
|
|
@@ -402,6 +412,14 @@ html, body {
|
|
|
402
412
|
.app-pane.mono .row:hover { background: var(--panel-hover, var(--os-bg-2)); }
|
|
403
413
|
.app-pane.mono .row.selected { background: var(--panel-select, var(--os-accent-soft)); box-shadow: inset 4px 0 0 var(--os-accent); }
|
|
404
414
|
.app-pane.mono .head { color: var(--os-accent); margin-bottom: var(--space-2); padding-bottom: var(--space-2); font-weight: 600; letter-spacing: 0.02em; border-bottom: 1px solid var(--os-bg-3); }
|
|
415
|
+
/* Visible fetch-error surface for polling app panes (files/monitor): a swallowed
|
|
416
|
+
* rejection must not leave the pane silently frozen with no user-facing signal. */
|
|
417
|
+
.app-pane.mono .row-error { color: var(--danger, #c0392b); padding: var(--space-2); }
|
|
418
|
+
.app-pane.mono[data-component="monitor-app"].is-error { color: var(--danger, #c0392b); }
|
|
419
|
+
/* Placeholder body shown immediately for a window spawned by an async app
|
|
420
|
+
* factory, swapped for the real content on resolve — see openApp() in
|
|
421
|
+
* shell.js. Prevents a slow factory from reading as a dead click. */
|
|
422
|
+
.os-app-loading { display: flex; align-items: center; justify-content: center; color: var(--os-fg-3); font-style: italic; }
|
|
405
423
|
.app-pane.mono pre { background: var(--os-bg-2); padding: var(--space-2-5) var(--space-2-75); margin: var(--space-2) 0 0 0; max-height: 220px; overflow: auto; white-space: pre-wrap; font: var(--fs-nano) var(--os-mono); border-radius: var(--r-1, 6px); color: var(--os-fg); border: none; }
|
|
406
424
|
.app-shell-pane { margin: 0; padding: var(--space-2); font: var(--fs-micro) var(--os-mono); color: var(--os-fg); background: var(--os-bg-0); height: 100%; overflow: auto; white-space: pre-wrap; box-sizing: border-box; }
|
|
407
425
|
.app-canvas { width: 100%; height: 100%; background: var(--os-bg-0); display: block; cursor: default; }
|
package/src/kits/os/wm.js
CHANGED
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
// Consumer (thebird) owns z-order, focus stack, alt-tab, drag/resize math.
|
|
3
3
|
// renderWindow returns a handle whose setBounds is called from pointermove.
|
|
4
4
|
//
|
|
5
|
-
// Visuals are bible-aligned: mac-less chip buttons (
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
5
|
+
// Visuals are bible-aligned: mac-less chip buttons (SVG minimize/maximize/
|
|
6
|
+
// close icons via ./icons.js, not raw ASCII glyphs — matches every other
|
|
7
|
+
// icon surface in this kit), inset 4px rail for focus, CSS-gradient resize
|
|
8
|
+
// affordance (no glyph), pointer-events:none on .wm-bar with auto on
|
|
9
|
+
// title+close so phone @media auto-maximize can suppress drag without a JS
|
|
10
|
+
// branch.
|
|
11
|
+
|
|
12
|
+
import { icons } from './icons.js';
|
|
9
13
|
|
|
10
14
|
export function renderWindow(opts = {}) {
|
|
11
15
|
const {
|
|
@@ -62,9 +66,9 @@ export function renderWindow(opts = {}) {
|
|
|
62
66
|
titleEl.textContent = title;
|
|
63
67
|
const btns = document.createElement('div');
|
|
64
68
|
btns.className = 'wm-btns';
|
|
65
|
-
const minBtn = mkBtn(
|
|
66
|
-
const maxBtn = mkBtn(
|
|
67
|
-
const closeBtn = mkBtn(
|
|
69
|
+
const minBtn = mkBtn(icons.minimize, 'minimize');
|
|
70
|
+
const maxBtn = mkBtn(icons.maximize, 'maximize');
|
|
71
|
+
const closeBtn = mkBtn(icons.close, 'close');
|
|
68
72
|
btns.append(minBtn, maxBtn, closeBtn);
|
|
69
73
|
bar.append(titleEl, btns);
|
|
70
74
|
|
|
@@ -99,7 +103,33 @@ export function renderWindow(opts = {}) {
|
|
|
99
103
|
|
|
100
104
|
minBtn.addEventListener('click', e => { e.stopPropagation(); callbacks.onMinimize && callbacks.onMinimize(); });
|
|
101
105
|
maxBtn.addEventListener('click', e => { e.stopPropagation(); callbacks.onMaximize && callbacks.onMaximize(); });
|
|
102
|
-
|
|
106
|
+
|
|
107
|
+
// Closing a window is destructive and unrecoverable (no undo), so it goes
|
|
108
|
+
// through the same second-click-to-confirm arm/commit idiom as the
|
|
109
|
+
// launcher dock's instance-close button: first click arms (visual +
|
|
110
|
+
// aria-label cue), a second click within the window commits; losing
|
|
111
|
+
// focus or the timeout elapsing disarms silently.
|
|
112
|
+
let closeArmed = false;
|
|
113
|
+
let closeArmTimer = null;
|
|
114
|
+
function disarmClose() {
|
|
115
|
+
closeArmed = false;
|
|
116
|
+
if (closeArmTimer) { clearTimeout(closeArmTimer); closeArmTimer = null; }
|
|
117
|
+
closeBtn.classList.remove('confirm');
|
|
118
|
+
closeBtn.setAttribute('aria-label', 'close');
|
|
119
|
+
}
|
|
120
|
+
closeBtn.addEventListener('click', e => {
|
|
121
|
+
e.stopPropagation();
|
|
122
|
+
if (!closeArmed) {
|
|
123
|
+
closeArmed = true;
|
|
124
|
+
closeBtn.classList.add('confirm');
|
|
125
|
+
closeBtn.setAttribute('aria-label', 'confirm close');
|
|
126
|
+
closeArmTimer = setTimeout(disarmClose, 3000);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
disarmClose();
|
|
130
|
+
callbacks.onClose && callbacks.onClose();
|
|
131
|
+
});
|
|
132
|
+
closeBtn.addEventListener('blur', disarmClose);
|
|
103
133
|
|
|
104
134
|
const focus = () => callbacks.onFocus && callbacks.onFocus();
|
|
105
135
|
|
|
@@ -163,14 +193,14 @@ export function renderWindow(opts = {}) {
|
|
|
163
193
|
setInstanceId(id) { if (id) el.dataset.instanceId = id; else delete el.dataset.instanceId; },
|
|
164
194
|
setZIndex(z) { el.style.zIndex = String(z); },
|
|
165
195
|
getBounds() { return { x: el.offsetLeft, y: el.offsetTop, w: el.offsetWidth, h: el.offsetHeight }; },
|
|
166
|
-
dispose() { el.remove(); },
|
|
196
|
+
dispose() { if (closeArmTimer) clearTimeout(closeArmTimer); el.remove(); },
|
|
167
197
|
};
|
|
168
198
|
}
|
|
169
199
|
|
|
170
|
-
function mkBtn(
|
|
200
|
+
function mkBtn(svg, ttl) {
|
|
171
201
|
const b = document.createElement('button');
|
|
172
202
|
b.className = 'wm-btn';
|
|
173
|
-
b.
|
|
203
|
+
b.innerHTML = svg;
|
|
174
204
|
b.title = ttl;
|
|
175
205
|
b.setAttribute('aria-label', ttl);
|
|
176
206
|
return b;
|
package/types/components.d.ts
CHANGED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
const ICON_PREV = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 3L5 8l5 5"/></svg>';
|
|
2
|
-
const ICON_NEXT = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 3l5 5-5 5"/></svg>';
|
|
3
|
-
|
|
4
|
-
export function buildOverlay({ onPrev, onNext, onReset }) {
|
|
5
|
-
const overlay = document.createElement('div');
|
|
6
|
-
overlay.className = 'overlay export-hidden';
|
|
7
|
-
overlay.setAttribute('role', 'toolbar');
|
|
8
|
-
overlay.setAttribute('aria-label', 'Deck controls');
|
|
9
|
-
overlay.innerHTML = `
|
|
10
|
-
<button class="btn prev" type="button" aria-label="Previous slide" title="Previous (←)">${ICON_PREV}</button>
|
|
11
|
-
<span class="count" aria-live="polite"><span class="current">1</span><span class="sep">/</span><span class="total">1</span></span>
|
|
12
|
-
<button class="btn next" type="button" aria-label="Next slide" title="Next (→)">${ICON_NEXT}</button>
|
|
13
|
-
<span class="divider"></span>
|
|
14
|
-
<button class="btn reset" type="button" aria-label="Reset to first slide" title="Reset (R)">Reset<span class="kbd">R</span></button>
|
|
15
|
-
`;
|
|
16
|
-
overlay.querySelector('.prev').addEventListener('click', onPrev);
|
|
17
|
-
overlay.querySelector('.next').addEventListener('click', onNext);
|
|
18
|
-
overlay.querySelector('.reset').addEventListener('click', onReset);
|
|
19
|
-
return overlay;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function buildTapzones({ onBack, onForward }) {
|
|
23
|
-
const tapzones = document.createElement('div');
|
|
24
|
-
tapzones.className = 'tapzones export-hidden';
|
|
25
|
-
tapzones.setAttribute('aria-hidden', 'true');
|
|
26
|
-
const tzBack = document.createElement('div');
|
|
27
|
-
tzBack.className = 'tapzone tapzone--back';
|
|
28
|
-
const tzMid = document.createElement('div');
|
|
29
|
-
tzMid.className = 'tapzone tapzone--mid';
|
|
30
|
-
tzMid.style.pointerEvents = 'none';
|
|
31
|
-
const tzFwd = document.createElement('div');
|
|
32
|
-
tzFwd.className = 'tapzone tapzone--fwd';
|
|
33
|
-
tzBack.addEventListener('click', onBack);
|
|
34
|
-
tzFwd.addEventListener('click', onForward);
|
|
35
|
-
tapzones.append(tzBack, tzMid, tzFwd);
|
|
36
|
-
return tapzones;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const KEY_HANDLERS = {
|
|
40
|
-
ArrowRight: (deck) => deck._go(deck._index + 1, 'keyboard'),
|
|
41
|
-
PageDown: (deck) => deck._go(deck._index + 1, 'keyboard'),
|
|
42
|
-
' ': (deck) => deck._go(deck._index + 1, 'keyboard'),
|
|
43
|
-
Spacebar: (deck) => deck._go(deck._index + 1, 'keyboard'),
|
|
44
|
-
ArrowLeft: (deck) => deck._go(deck._index - 1, 'keyboard'),
|
|
45
|
-
PageUp: (deck) => deck._go(deck._index - 1, 'keyboard'),
|
|
46
|
-
Home: (deck) => deck._go(0, 'keyboard'),
|
|
47
|
-
End: (deck) => deck._go(deck._slides.length - 1, 'keyboard'),
|
|
48
|
-
r: (deck) => deck._go(0, 'keyboard'),
|
|
49
|
-
R: (deck) => deck._go(0, 'keyboard')
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
export function handleDeckKey(deck, e) {
|
|
53
|
-
const t = e.target;
|
|
54
|
-
if (t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName))) return false;
|
|
55
|
-
if (e.metaKey || e.ctrlKey || e.altKey) return false;
|
|
56
|
-
const fn = KEY_HANDLERS[e.key];
|
|
57
|
-
if (fn) { fn(deck); return true; }
|
|
58
|
-
if (/^[0-9]$/.test(e.key)) {
|
|
59
|
-
const n = e.key === '0' ? 9 : parseInt(e.key, 10) - 1;
|
|
60
|
-
if (n < deck._slides.length) { deck._go(n, 'keyboard'); return true; }
|
|
61
|
-
}
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
export const STORAGE_PREFIX = 'deck-stage:slide:';
|
|
2
|
-
const VALIDATE_ATTR = 'no_overflowing_text,no_overlapping_text,slide_sized_text';
|
|
3
|
-
const pad2 = (n) => String(n).padStart(2, '0');
|
|
4
|
-
|
|
5
|
-
export function loadNotes(deckEl) {
|
|
6
|
-
const tag = document.getElementById('speaker-notes');
|
|
7
|
-
if (!tag) return [];
|
|
8
|
-
try {
|
|
9
|
-
const parsed = JSON.parse(tag.textContent || '[]');
|
|
10
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
11
|
-
} catch (e) {
|
|
12
|
-
console.warn('[deck-stage] Failed to parse #speaker-notes JSON:', e);
|
|
13
|
-
return [];
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function restoreIndex(deckEl, slides) {
|
|
18
|
-
const storageKey = STORAGE_PREFIX + (location.pathname || '/');
|
|
19
|
-
try {
|
|
20
|
-
const raw = localStorage.getItem(storageKey);
|
|
21
|
-
if (raw == null) return 0;
|
|
22
|
-
const n = parseInt(raw, 10);
|
|
23
|
-
if (Number.isFinite(n) && n >= 0 && n < slides.length) return n;
|
|
24
|
-
} catch (e) { /* ignore */ }
|
|
25
|
-
return 0;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function persistIndex(deckEl, index) {
|
|
29
|
-
const storageKey = STORAGE_PREFIX + (location.pathname || '/');
|
|
30
|
-
try { localStorage.setItem(storageKey, String(index)); } catch (e) {}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function collectSlides(deckEl, slot) {
|
|
34
|
-
const assigned = slot.assignedElements({ flatten: true });
|
|
35
|
-
const slides = assigned.filter((el) => {
|
|
36
|
-
const tag = el.tagName;
|
|
37
|
-
return tag !== 'TEMPLATE' && tag !== 'SCRIPT' && tag !== 'STYLE';
|
|
38
|
-
});
|
|
39
|
-
slides.forEach((slide, i) => {
|
|
40
|
-
const n = i + 1;
|
|
41
|
-
let label = slide.getAttribute('data-label');
|
|
42
|
-
if (!label) {
|
|
43
|
-
const existing = slide.getAttribute('data-screen-label');
|
|
44
|
-
if (existing) label = existing.replace(/^\s*\d+\s*/, '').trim() || existing;
|
|
45
|
-
}
|
|
46
|
-
if (!label) {
|
|
47
|
-
const heading = slide.querySelector('h1, h2, h3, [data-title]');
|
|
48
|
-
if (heading) label = (heading.textContent || '').trim().slice(0, 40);
|
|
49
|
-
}
|
|
50
|
-
if (!label) label = 'Slide';
|
|
51
|
-
slide.setAttribute('data-screen-label', `${pad2(n)} ${label}`);
|
|
52
|
-
if (!slide.hasAttribute('data-om-validate')) slide.setAttribute('data-om-validate', VALIDATE_ATTR);
|
|
53
|
-
slide.setAttribute('data-deck-slide', String(i));
|
|
54
|
-
});
|
|
55
|
-
return slides;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function applyIndex(deckEl, { index, prevIndex, slides, countEl, showOverlay = true, broadcast = true, reason = 'init', flashFn = null } = {}) {
|
|
59
|
-
if (!slides.length) return;
|
|
60
|
-
const prev = prevIndex == null ? -1 : prevIndex;
|
|
61
|
-
const curr = index;
|
|
62
|
-
slides.forEach((s, i) => {
|
|
63
|
-
if (i === curr) s.setAttribute('data-deck-active', '');
|
|
64
|
-
else s.removeAttribute('data-deck-active');
|
|
65
|
-
});
|
|
66
|
-
if (countEl) countEl.textContent = String(curr + 1);
|
|
67
|
-
persistIndex(deckEl, curr);
|
|
68
|
-
if (broadcast) {
|
|
69
|
-
try { window.postMessage({ slideIndexChanged: curr }, '*'); } catch (e) {}
|
|
70
|
-
deckEl.dispatchEvent(new CustomEvent('slidechange', {
|
|
71
|
-
detail: {
|
|
72
|
-
index: curr, previousIndex: prev, total: slides.length,
|
|
73
|
-
slide: slides[curr] || null,
|
|
74
|
-
previousSlide: prev >= 0 ? (slides[prev] || null) : null,
|
|
75
|
-
reason
|
|
76
|
-
},
|
|
77
|
-
bubbles: true, composed: true
|
|
78
|
-
}));
|
|
79
|
-
}
|
|
80
|
-
if (showOverlay && flashFn) flashFn();
|
|
81
|
-
}
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
export const STYLESHEET = `
|
|
2
|
-
:host {
|
|
3
|
-
position: fixed;
|
|
4
|
-
inset: 0;
|
|
5
|
-
display: block;
|
|
6
|
-
background: var(--panel-0, #000);
|
|
7
|
-
color: var(--panel-text, #fff);
|
|
8
|
-
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Arial, sans-serif;
|
|
9
|
-
overflow: hidden;
|
|
10
|
-
}
|
|
11
|
-
.stage { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; }
|
|
12
|
-
.canvas {
|
|
13
|
-
position: relative;
|
|
14
|
-
transform-origin: center center;
|
|
15
|
-
flex-shrink: 0;
|
|
16
|
-
background: var(--panel-1, #fff);
|
|
17
|
-
will-change: transform;
|
|
18
|
-
}
|
|
19
|
-
::slotted(*) {
|
|
20
|
-
position: absolute !important;
|
|
21
|
-
inset: 0 !important;
|
|
22
|
-
width: 100% !important;
|
|
23
|
-
height: 100% !important;
|
|
24
|
-
box-sizing: border-box !important;
|
|
25
|
-
overflow: hidden;
|
|
26
|
-
opacity: 0;
|
|
27
|
-
pointer-events: none;
|
|
28
|
-
visibility: hidden;
|
|
29
|
-
}
|
|
30
|
-
::slotted([data-deck-active]) { opacity: 1; pointer-events: auto; visibility: visible; }
|
|
31
|
-
.tapzones {
|
|
32
|
-
position: fixed; inset: 0; display: flex; z-index: 2147482000; pointer-events: none;
|
|
33
|
-
}
|
|
34
|
-
.tapzone { flex: 1; pointer-events: auto; -webkit-tap-highlight-color: transparent; }
|
|
35
|
-
@media (hover: hover) and (pointer: fine) { .tapzones { display: none; } }
|
|
36
|
-
.overlay {
|
|
37
|
-
position: fixed;
|
|
38
|
-
left: 50%; bottom: 22px;
|
|
39
|
-
transform: translate(-50%, 6px) scale(0.92);
|
|
40
|
-
filter: blur(6px);
|
|
41
|
-
display: flex; align-items: center;
|
|
42
|
-
gap: 4px; padding: 4px;
|
|
43
|
-
background: #000; color: #fff;
|
|
44
|
-
border-radius: 999px;
|
|
45
|
-
font-size: 14px;
|
|
46
|
-
font-feature-settings: "tnum" 1;
|
|
47
|
-
letter-spacing: 0.01em;
|
|
48
|
-
opacity: 0; pointer-events: none;
|
|
49
|
-
transition: opacity 260ms ease, transform 260ms cubic-bezier(.2,.8,.2,1), filter 260ms ease;
|
|
50
|
-
transform-origin: center bottom;
|
|
51
|
-
z-index: 2147483000;
|
|
52
|
-
user-select: none;
|
|
53
|
-
}
|
|
54
|
-
.overlay[data-visible] {
|
|
55
|
-
opacity: 1; pointer-events: auto;
|
|
56
|
-
transform: translate(-50%, 0) scale(1);
|
|
57
|
-
filter: blur(0);
|
|
58
|
-
}
|
|
59
|
-
.btn {
|
|
60
|
-
appearance: none; -webkit-appearance: none;
|
|
61
|
-
background: transparent; border: 0; margin: 0; padding: 0;
|
|
62
|
-
color: inherit; font: inherit; cursor: default;
|
|
63
|
-
display: inline-flex; align-items: center; justify-content: center;
|
|
64
|
-
height: 28px; min-width: 28px;
|
|
65
|
-
border-radius: 999px;
|
|
66
|
-
color: rgba(255,255,255,0.72);
|
|
67
|
-
transition: background 140ms ease, color 140ms ease;
|
|
68
|
-
-webkit-tap-highlight-color: transparent;
|
|
69
|
-
}
|
|
70
|
-
.btn:hover { background: rgba(255,255,255,0.12); color: #fff; }
|
|
71
|
-
.btn:active { background: rgba(255,255,255,0.18); }
|
|
72
|
-
.btn:focus, .btn:focus-visible { outline: none; }
|
|
73
|
-
.btn::-moz-focus-inner { border: 0; }
|
|
74
|
-
.btn svg { width: 14px; height: 14px; display: block; }
|
|
75
|
-
.btn.reset {
|
|
76
|
-
font-size: 13px; font-weight: 500; letter-spacing: 0.02em;
|
|
77
|
-
padding: 0 10px 0 12px; gap: 6px;
|
|
78
|
-
color: rgba(255,255,255,0.72);
|
|
79
|
-
}
|
|
80
|
-
.btn.reset .kbd {
|
|
81
|
-
display: inline-flex; align-items: center; justify-content: center;
|
|
82
|
-
min-width: 16px; height: 16px; padding: 0 4px;
|
|
83
|
-
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
84
|
-
font-size: 12px; line-height: 1;
|
|
85
|
-
color: rgba(255,255,255,0.88);
|
|
86
|
-
background: rgba(255,255,255,0.12);
|
|
87
|
-
border-radius: 4px;
|
|
88
|
-
}
|
|
89
|
-
.count {
|
|
90
|
-
font-variant-numeric: tabular-nums;
|
|
91
|
-
color: #fff; font-weight: 500;
|
|
92
|
-
padding: 0 8px; min-width: 42px;
|
|
93
|
-
text-align: center; font-size: 14px;
|
|
94
|
-
}
|
|
95
|
-
.count .sep { color: rgba(255,255,255,0.45); margin: 0 3px; font-weight: 400; }
|
|
96
|
-
.count .total { color: rgba(255,255,255,0.55); }
|
|
97
|
-
.divider { width: 1px; height: 14px; background: rgba(255,255,255,0.18); margin: 0 2px; }
|
|
98
|
-
@media print {
|
|
99
|
-
:host { position: static; inset: auto; background: none; overflow: visible; color: inherit; }
|
|
100
|
-
.stage { position: static; display: block; }
|
|
101
|
-
.canvas { transform: none !important; width: auto !important; height: auto !important; background: none; will-change: auto; }
|
|
102
|
-
::slotted(*) {
|
|
103
|
-
position: relative !important;
|
|
104
|
-
inset: auto !important;
|
|
105
|
-
width: var(--deck-design-w) !important;
|
|
106
|
-
height: var(--deck-design-h) !important;
|
|
107
|
-
box-sizing: border-box !important;
|
|
108
|
-
opacity: 1 !important;
|
|
109
|
-
visibility: visible !important;
|
|
110
|
-
pointer-events: auto;
|
|
111
|
-
break-after: page; page-break-after: always; break-inside: avoid;
|
|
112
|
-
overflow: hidden;
|
|
113
|
-
}
|
|
114
|
-
::slotted(*:last-child) { break-after: auto; page-break-after: auto; }
|
|
115
|
-
.overlay, .tapzones { display: none !important; }
|
|
116
|
-
}
|
|
117
|
-
`;
|