anentrypoint-design 0.0.413 → 0.0.415
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.js +63 -13
- package/dist/app-shell.css +40 -594
- package/dist/colors_and_type.css +607 -47
- package/dist/index.html +227 -277
- package/dist/preview/buttons.html +39 -28
- package/dist/preview/colors-core.html +18 -8
- package/dist/preview/colors-lore.html +26 -17
- package/dist/preview/colors-semantic.html +24 -18
- package/dist/preview/dateline.html +6 -5
- package/dist/preview/dropzone.html +4 -3
- package/dist/preview/file-grid.html +10 -9
- package/dist/preview/file-row.html +14 -13
- package/dist/preview/file-toolbar.html +4 -3
- package/dist/preview/file-viewer.html +4 -3
- package/dist/preview/header.html +26 -24
- package/dist/preview/icons-unicode.html +8 -5
- package/dist/preview/index-row.html +17 -9
- package/dist/preview/inputs.html +4 -3
- package/dist/preview/manifesto.html +34 -17
- package/dist/preview/motion-default.js +77 -57
- package/dist/preview/rules.html +4 -3
- package/dist/preview/spacing.html +13 -12
- package/dist/preview/stamps-lore.html +5 -4
- package/dist/preview/stamps.html +4 -3
- package/dist/preview/theme-ink.html +7 -6
- package/dist/preview/type-display.html +14 -5
- package/dist/preview/type-mono.html +6 -5
- package/dist/preview/type-prose.html +12 -10
- package/dist/preview/type-scale.html +4 -3
- package/dist/preview/wordmarks.html +10 -7
- package/dist/site/content/globals/site.yaml +5 -3
- package/dist/site/content/pages/freddie.yaml +2 -2
- package/dist/site/content/pages/home.yaml +55 -55
- package/dist/site/theme.mjs +96 -348
- package/dist/slides/index.html +13 -13
- package/dist/src/bootstrap.js +10 -1
- package/dist/src/components/chat.js +43 -195
- package/dist/src/components/community.js +18 -166
- package/dist/src/components/content.js +30 -210
- package/dist/src/components/files-modals.js +14 -106
- package/dist/src/components/files.js +15 -116
- package/dist/src/components/freddie/helpers.js +53 -8
- package/dist/src/components/freddie.js +35 -25
- package/dist/src/components/shell.js +16 -114
- package/dist/src/components/theme-toggle.js +69 -25
- package/dist/src/components.js +92 -13
- package/dist/src/deck-stage.js +6 -4
- package/dist/src/highlight.js +51 -7
- package/dist/src/index.js +58 -12
- package/dist/src/kits/os/about-app.js +2 -3
- package/dist/src/kits/os/app-panes.css +46 -70
- package/dist/src/kits/os/browser-app.js +28 -1
- package/dist/src/kits/os/files-app.js +7 -0
- package/dist/src/kits/os/freddie/pages-chat.js +90 -75
- package/dist/src/kits/os/freddie/pages-core.js +9 -9
- package/dist/src/kits/os/freddie/pages-os.js +8 -8
- package/dist/src/kits/os/freddie/pages-tools.js +14 -14
- package/dist/src/kits/os/freddie/routes.js +21 -19
- package/dist/src/kits/os/freddie-dashboard.css +42 -21
- package/dist/src/kits/os/freddie-dashboard.js +6 -6
- package/dist/src/kits/os/icons.js +38 -16
- package/dist/src/kits/os/index.js +11 -0
- package/dist/src/kits/os/launcher.css +26 -10
- package/dist/src/kits/os/launcher.js +3 -0
- package/dist/src/kits/os/shell.js +72 -108
- package/dist/src/kits/os/terminal-app.js +1 -1
- package/dist/src/kits/os/theme.css +1330 -150
- package/dist/src/kits/os/wm.css +68 -15
- package/dist/src/kits/os/wm.js +93 -17
- package/dist/src/markdown.js +121 -12
- package/dist/src/motion.js +18 -2
- package/dist/src/page-html.js +62 -134
- package/dist/src/theme.js +106 -13
- package/dist/src/web-components/ds-chat.js +80 -9
- package/dist/ui_kits/aicat/app.js +54 -6
- package/dist/ui_kits/aicat/index.html +2 -0
- package/dist/ui_kits/blog/index.html +33 -17
- package/dist/ui_kits/chat/app.js +90 -22
- package/dist/ui_kits/chat/index.html +2 -0
- package/dist/ui_kits/community/app.js +86 -24
- package/dist/ui_kits/community/index.html +1 -0
- package/dist/ui_kits/dashboard/app.js +123 -34
- package/dist/ui_kits/dashboard/index.html +1 -0
- package/dist/ui_kits/docs/index.html +43 -33
- package/dist/ui_kits/error_404/app.js +78 -23
- package/dist/ui_kits/file_browser/README.md +1 -1
- package/dist/ui_kits/file_browser/app.js +45 -10
- package/dist/ui_kits/file_browser/index.html +1 -0
- package/dist/ui_kits/gallery/app.js +130 -51
- package/dist/ui_kits/homepage/app.js +156 -138
- package/dist/ui_kits/project_page/app.js +142 -125
- package/dist/ui_kits/project_page/index.html +2 -1
- package/dist/ui_kits/search/app.js +52 -15
- package/dist/ui_kits/settings/app.js +166 -39
- package/dist/ui_kits/signin/app.js +175 -49
- package/dist/ui_kits/signin/index.html +1 -0
- package/dist/ui_kits/slide_deck/app.js +83 -49
- package/dist/ui_kits/system_primer/app.js +57 -40
- package/dist/ui_kits/terminal/app.js +160 -43
- package/package.json +1 -1
- package/src/page-html/client-script.js +40 -11
- package/src/page-html/page-styles.js +22 -1
- package/src/page-html.js +10 -0
package/dist/src/kits/os/wm.css
CHANGED
|
@@ -8,28 +8,40 @@
|
|
|
8
8
|
position: fixed;
|
|
9
9
|
inset: 0;
|
|
10
10
|
pointer-events: none;
|
|
11
|
-
z-index:
|
|
11
|
+
z-index: var(--z-window);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
.wm-win {
|
|
15
15
|
position: absolute;
|
|
16
16
|
min-width: 200px;
|
|
17
17
|
min-height: 120px;
|
|
18
|
+
/* Never exceed the .wm-root desktop area — a window sized on a large
|
|
19
|
+
monitor must stay recoverable when the viewport shrinks. */
|
|
20
|
+
max-width: 100%;
|
|
21
|
+
max-height: 100%;
|
|
18
22
|
background: var(--os-bg-1);
|
|
19
23
|
color: var(--os-fg);
|
|
20
24
|
border: none;
|
|
21
|
-
border-radius: var(--r-
|
|
25
|
+
border-radius: var(--os-radius, var(--r-2, 10px));
|
|
22
26
|
display: flex;
|
|
23
27
|
flex-direction: column;
|
|
24
28
|
pointer-events: auto;
|
|
25
29
|
overflow: hidden;
|
|
26
30
|
}
|
|
27
31
|
.wm-win.wm-focused { box-shadow: inset 4px 0 0 var(--os-accent); }
|
|
32
|
+
/* Promote to a compositor layer only for the duration of a drag/resize so the
|
|
33
|
+
per-frame left/top/width/height writes don't force a main-thread layout each
|
|
34
|
+
pointermove. Released on pointerup (class removed) to avoid the permanent
|
|
35
|
+
will-change memory/perf anti-pattern. */
|
|
36
|
+
.wm-win.wm-dragging { will-change: left, top; }
|
|
37
|
+
.wm-win.wm-resizing { will-change: width, height; }
|
|
28
38
|
|
|
29
39
|
.wm-bar {
|
|
30
40
|
display: flex;
|
|
31
41
|
align-items: center;
|
|
32
|
-
|
|
42
|
+
min-height: 36px;
|
|
43
|
+
box-sizing: border-box;
|
|
44
|
+
padding: 8px 12px;
|
|
33
45
|
background: var(--os-bg-2);
|
|
34
46
|
cursor: move;
|
|
35
47
|
user-select: none;
|
|
@@ -39,14 +51,16 @@
|
|
|
39
51
|
}
|
|
40
52
|
.wm-title {
|
|
41
53
|
flex: 1;
|
|
54
|
+
min-width: 0;
|
|
42
55
|
font: 12px var(--os-font);
|
|
56
|
+
color: var(--os-fg-2);
|
|
43
57
|
white-space: nowrap;
|
|
44
58
|
overflow: hidden;
|
|
45
59
|
text-overflow: ellipsis;
|
|
46
60
|
text-transform: lowercase;
|
|
47
61
|
}
|
|
48
62
|
|
|
49
|
-
.wm-btns { display: flex; gap:
|
|
63
|
+
.wm-btns { display: flex; gap: 6px; }
|
|
50
64
|
.wm-btn {
|
|
51
65
|
width: 22px;
|
|
52
66
|
height: 22px;
|
|
@@ -60,10 +74,11 @@
|
|
|
60
74
|
display: inline-flex;
|
|
61
75
|
align-items: center;
|
|
62
76
|
justify-content: center;
|
|
63
|
-
border-radius: var(--r-1, 6px);
|
|
77
|
+
border-radius: var(--os-radius-sm, var(--r-1, 6px));
|
|
64
78
|
transition: background 100ms ease, color 100ms ease;
|
|
79
|
+
touch-action: manipulation;
|
|
65
80
|
}
|
|
66
|
-
.wm-btn:hover { background: var(--panel-hover, var(--os-bg-
|
|
81
|
+
.wm-btn:hover { background: var(--panel-hover, var(--os-bg-1)); color: var(--os-fg); }
|
|
67
82
|
|
|
68
83
|
.wm-body {
|
|
69
84
|
flex: 1;
|
|
@@ -76,20 +91,45 @@
|
|
|
76
91
|
position: absolute;
|
|
77
92
|
right: 0;
|
|
78
93
|
bottom: 0;
|
|
79
|
-
width:
|
|
80
|
-
height:
|
|
94
|
+
width: 16px;
|
|
95
|
+
height: 16px;
|
|
96
|
+
padding: 2px;
|
|
97
|
+
box-sizing: border-box;
|
|
81
98
|
cursor: nwse-resize;
|
|
82
99
|
background: transparent;
|
|
83
100
|
color: var(--os-fg-3);
|
|
84
101
|
opacity: 0.4;
|
|
85
|
-
font: 10px var(--os-mono);
|
|
86
|
-
line-height: 1;
|
|
87
|
-
display: flex;
|
|
88
|
-
align-items: flex-end;
|
|
89
|
-
justify-content: flex-end;
|
|
90
102
|
touch-action: none;
|
|
91
103
|
}
|
|
92
|
-
.wm-resize
|
|
104
|
+
.wm-resize:hover { opacity: 0.7; }
|
|
105
|
+
/* Resize grip drawn with CSS (two diagonal hairlines) instead of a decorative
|
|
106
|
+
* unicode content glyph; keeps the affordance, drops the glyph tell. */
|
|
107
|
+
.wm-resize::after {
|
|
108
|
+
content: '';
|
|
109
|
+
position: absolute;
|
|
110
|
+
right: 3px;
|
|
111
|
+
bottom: 3px;
|
|
112
|
+
width: 9px;
|
|
113
|
+
height: 9px;
|
|
114
|
+
background:
|
|
115
|
+
linear-gradient(135deg, transparent 0 45%, currentColor 45% 55%, transparent 55% 100%),
|
|
116
|
+
linear-gradient(135deg, transparent 0 70%, currentColor 70% 80%, transparent 80% 100%);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* Edge + corner resize hit-zones. Invisible (no glyph) — they only widen the
|
|
120
|
+
* grab area beyond the SE .wm-resize grip. `width`/`height` are the grab
|
|
121
|
+
* thickness; corners overlap edges and win via later source order + size. */
|
|
122
|
+
.wm-edge { position: absolute; touch-action: none; z-index: var(--z-raised); }
|
|
123
|
+
.wm-edge[data-dir='n'] { top: -3px; left: 8px; right: 8px; height: 8px; cursor: ns-resize; }
|
|
124
|
+
.wm-edge[data-dir='s'] { bottom: -3px; left: 8px; right: 8px; height: 8px; cursor: ns-resize; }
|
|
125
|
+
.wm-edge[data-dir='e'] { right: -3px; top: 8px; bottom: 8px; width: 8px; cursor: ew-resize; }
|
|
126
|
+
.wm-edge[data-dir='w'] { left: -3px; top: 8px; bottom: 8px; width: 8px; cursor: ew-resize; }
|
|
127
|
+
.wm-edge[data-dir='ne'] { top: -3px; right: -3px; width: 14px; height: 14px; cursor: nesw-resize; }
|
|
128
|
+
.wm-edge[data-dir='nw'] { top: -3px; left: -3px; width: 14px; height: 14px; cursor: nwse-resize; }
|
|
129
|
+
.wm-edge[data-dir='sw'] { bottom: -3px; left: -3px; width: 14px; height: 14px; cursor: nesw-resize; }
|
|
130
|
+
|
|
131
|
+
.wm-win.wm-min .wm-edge,
|
|
132
|
+
.wm-win.wm-max .wm-edge { display: none; }
|
|
93
133
|
|
|
94
134
|
.wm-win.wm-min .wm-body,
|
|
95
135
|
.wm-win.wm-min .wm-resize { display: none; }
|
|
@@ -102,7 +142,19 @@
|
|
|
102
142
|
border-radius: 0;
|
|
103
143
|
}
|
|
104
144
|
|
|
105
|
-
|
|
145
|
+
/* Touch: lift resize hit-zones and window buttons toward the 44px floor
|
|
146
|
+
(wm.css must be self-sufficient; theme.css only covers .ds-247420). */
|
|
147
|
+
@media (pointer: coarse) {
|
|
148
|
+
.wm-edge[data-dir='n'], .wm-edge[data-dir='s'] { height: 20px; }
|
|
149
|
+
.wm-edge[data-dir='e'], .wm-edge[data-dir='w'] { width: 20px; }
|
|
150
|
+
.wm-edge[data-dir='ne'], .wm-edge[data-dir='nw'], .wm-edge[data-dir='sw'] { width: 32px; height: 32px; }
|
|
151
|
+
.wm-resize { width: 32px; height: 32px; }
|
|
152
|
+
.wm-btn { min-width: 44px; min-height: 44px; }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* Full-screen windows on phones AND short landscape phones — a width-only
|
|
156
|
+
query misses 812x375 landscape, which cannot host floating chrome. */
|
|
157
|
+
@media (max-width: 767px), ((max-height: 500px) and (orientation: landscape) and (pointer: coarse)) {
|
|
106
158
|
.wm-win {
|
|
107
159
|
left: 0 !important;
|
|
108
160
|
top: 0 !important;
|
|
@@ -112,4 +164,5 @@
|
|
|
112
164
|
border: none;
|
|
113
165
|
}
|
|
114
166
|
.wm-resize { display: none; }
|
|
167
|
+
.wm-edge { display: none; }
|
|
115
168
|
}
|
package/dist/src/kits/os/wm.js
CHANGED
|
@@ -20,14 +20,40 @@ export function renderWindow(opts = {}) {
|
|
|
20
20
|
callbacks = {},
|
|
21
21
|
} = opts;
|
|
22
22
|
|
|
23
|
+
// Keep at least MIN px of the window horizontally inside the container and
|
|
24
|
+
// the titlebar (BAR px) vertically reachable, so a window can always be
|
|
25
|
+
// grabbed by pointer (persisted bounds from a larger viewport included).
|
|
26
|
+
const MIN_VISIBLE = 60;
|
|
27
|
+
const BAR_H = 36;
|
|
28
|
+
function clampBounds(b, p) {
|
|
29
|
+
const pw = p ? p.clientWidth : window.innerWidth;
|
|
30
|
+
const ph = p ? p.clientHeight : window.innerHeight;
|
|
31
|
+
const out = { ...b };
|
|
32
|
+
if (typeof out.w === 'number') out.w = Math.min(out.w, pw);
|
|
33
|
+
if (typeof out.h === 'number') out.h = Math.min(out.h, ph);
|
|
34
|
+
if (typeof out.x === 'number') {
|
|
35
|
+
const w = typeof out.w === 'number' ? out.w : MIN_VISIBLE;
|
|
36
|
+
out.x = Math.max(MIN_VISIBLE - w, Math.min(out.x, pw - MIN_VISIBLE));
|
|
37
|
+
}
|
|
38
|
+
if (typeof out.y === 'number') out.y = Math.max(0, Math.min(out.y, ph - BAR_H));
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
23
42
|
const el = document.createElement('div');
|
|
24
43
|
el.className = 'wm-win';
|
|
25
44
|
el.dataset.kind = kind;
|
|
26
45
|
if (instanceId) el.dataset.instanceId = instanceId;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
46
|
+
// Floating window chrome is a dialog surface: role="dialog" (not
|
|
47
|
+
// aria-modal, since sibling windows stay operable — this is a
|
|
48
|
+
// non-modal multi-window desktop, not a blocking modal) + aria-label
|
|
49
|
+
// from the titlebar text so AT announces which window has focus.
|
|
50
|
+
el.setAttribute('role', 'dialog');
|
|
51
|
+
el.setAttribute('aria-label', title);
|
|
52
|
+
const b0 = clampBounds(bounds, null);
|
|
53
|
+
el.style.left = b0.x + 'px';
|
|
54
|
+
el.style.top = b0.y + 'px';
|
|
55
|
+
el.style.width = b0.w + 'px';
|
|
56
|
+
el.style.height = b0.h + 'px';
|
|
31
57
|
|
|
32
58
|
const bar = document.createElement('div');
|
|
33
59
|
bar.className = 'wm-bar';
|
|
@@ -46,27 +72,72 @@ export function renderWindow(opts = {}) {
|
|
|
46
72
|
bodyEl.className = 'wm-body';
|
|
47
73
|
setBodyContent(bodyEl, body);
|
|
48
74
|
|
|
49
|
-
|
|
50
|
-
resize.
|
|
75
|
+
// Resize affordances: one grip per edge + corner. `data-dir` carries the
|
|
76
|
+
// direction (n/s/e/w/ne/nw/se/sw) to the consumer's resize math. The SE
|
|
77
|
+
// corner keeps the visible diagonal grip glyph (.wm-resize); the other
|
|
78
|
+
// seven are invisible hit-zones (.wm-edge) styled in wm.css.
|
|
79
|
+
// NOT KEYBOARD ACCESSIBLE: these grips only wire pointerdown (see below);
|
|
80
|
+
// resize math is owned entirely by the consumer's pointermove handler
|
|
81
|
+
// (module comment at top of file), so there is no keydown-driven delta to
|
|
82
|
+
// wire without reaching into consumer-owned drag state. role="separator"
|
|
83
|
+
// + aria-orientation give a screen reader a name for the affordance even
|
|
84
|
+
// though it cannot be operated without a pointer -- an honest partial
|
|
85
|
+
// label, not a claim of full keyboard support.
|
|
86
|
+
const ORIENT = { n: 'horizontal', s: 'horizontal', ne: 'horizontal', nw: 'horizontal', se: 'horizontal', sw: 'horizontal', e: 'vertical', w: 'vertical' };
|
|
87
|
+
const DIRS = ['n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw'];
|
|
88
|
+
const grips = DIRS.map(dir => {
|
|
89
|
+
const g = document.createElement('div');
|
|
90
|
+
g.className = dir === 'se' ? 'wm-resize' : 'wm-edge';
|
|
91
|
+
g.dataset.dir = dir;
|
|
92
|
+
g.setAttribute('role', 'separator');
|
|
93
|
+
g.setAttribute('aria-orientation', ORIENT[dir] || 'horizontal');
|
|
94
|
+
g.setAttribute('aria-label', 'resize ' + dir + ' (pointer only)');
|
|
95
|
+
return g;
|
|
96
|
+
});
|
|
51
97
|
|
|
52
|
-
el.append(bar, bodyEl,
|
|
98
|
+
el.append(bar, bodyEl, ...grips);
|
|
53
99
|
|
|
54
100
|
minBtn.addEventListener('click', e => { e.stopPropagation(); callbacks.onMinimize && callbacks.onMinimize(); });
|
|
55
101
|
maxBtn.addEventListener('click', e => { e.stopPropagation(); callbacks.onMaximize && callbacks.onMaximize(); });
|
|
56
102
|
closeBtn.addEventListener('click', e => { e.stopPropagation(); callbacks.onClose && callbacks.onClose(); });
|
|
57
103
|
|
|
58
|
-
|
|
104
|
+
const focus = () => callbacks.onFocus && callbacks.onFocus();
|
|
105
|
+
|
|
106
|
+
el.addEventListener('pointerdown', () => focus());
|
|
107
|
+
|
|
108
|
+
// Basic focus trap: while this window carries .wm-focused, Tab/Shift+Tab
|
|
109
|
+
// cycles only within its own focusable set instead of escaping to a
|
|
110
|
+
// sibling window or the page behind it. Scoped to keydown on `el` itself
|
|
111
|
+
// (additive listener, no DOM structure change) and gated on the class the
|
|
112
|
+
// consumer already toggles via setFocused/applyFocused below, so an
|
|
113
|
+
// unfocused window is completely untouched by this handler.
|
|
114
|
+
el.addEventListener('keydown', e => {
|
|
115
|
+
if (e.key !== 'Tab' || !el.classList.contains('wm-focused')) return;
|
|
116
|
+
const focusable = el.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
|
117
|
+
if (!focusable.length) return;
|
|
118
|
+
const first = focusable[0];
|
|
119
|
+
const last = focusable[focusable.length - 1];
|
|
120
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
last.focus();
|
|
123
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
124
|
+
e.preventDefault();
|
|
125
|
+
first.focus();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
59
128
|
|
|
60
129
|
bar.addEventListener('pointerdown', e => {
|
|
61
130
|
if (e.target.closest('.wm-btn')) return;
|
|
62
|
-
|
|
131
|
+
e.stopPropagation();
|
|
132
|
+
focus();
|
|
63
133
|
if (callbacks.onDragStart) callbacks.onDragStart(e, { x: el.offsetLeft, y: el.offsetTop, w: el.offsetWidth, h: el.offsetHeight });
|
|
64
134
|
});
|
|
65
135
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
136
|
+
grips.forEach(g => g.addEventListener('pointerdown', e => {
|
|
137
|
+
e.stopPropagation();
|
|
138
|
+
focus();
|
|
139
|
+
if (callbacks.onResizeStart) callbacks.onResizeStart(e, { x: el.offsetLeft, y: el.offsetTop, w: el.offsetWidth, h: el.offsetHeight, dir: g.dataset.dir });
|
|
140
|
+
}));
|
|
70
141
|
|
|
71
142
|
applyFocused(el, focused);
|
|
72
143
|
applyMaximized(el, maximized);
|
|
@@ -77,10 +148,14 @@ export function renderWindow(opts = {}) {
|
|
|
77
148
|
setTitle(t) { titleEl.textContent = t; },
|
|
78
149
|
setBody(b) { setBodyContent(bodyEl, b); },
|
|
79
150
|
setBounds(b) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
151
|
+
const c = clampBounds({
|
|
152
|
+
...b,
|
|
153
|
+
w: typeof b.w === 'number' ? b.w : el.offsetWidth,
|
|
154
|
+
}, el.offsetParent);
|
|
155
|
+
if (typeof c.x === 'number') el.style.left = c.x + 'px';
|
|
156
|
+
if (typeof c.y === 'number') el.style.top = c.y + 'px';
|
|
157
|
+
if (typeof b.w === 'number') el.style.width = c.w + 'px';
|
|
158
|
+
if (typeof b.h === 'number') el.style.height = c.h + 'px';
|
|
84
159
|
},
|
|
85
160
|
setFocused(v) { applyFocused(el, v); },
|
|
86
161
|
setMaximized(v) { applyMaximized(el, v); },
|
|
@@ -97,6 +172,7 @@ function mkBtn(label, ttl) {
|
|
|
97
172
|
b.className = 'wm-btn';
|
|
98
173
|
b.textContent = label;
|
|
99
174
|
b.title = ttl;
|
|
175
|
+
b.setAttribute('aria-label', ttl);
|
|
100
176
|
return b;
|
|
101
177
|
}
|
|
102
178
|
|
package/dist/src/markdown.js
CHANGED
|
@@ -1,39 +1,148 @@
|
|
|
1
1
|
// Markdown — lazy-loads marked + DOMPurify on first call. Stub-safe:
|
|
2
2
|
// if loading fails, we fall back to a simple escape-and-linebreak pass so
|
|
3
|
-
// the chat doesn't go blank.
|
|
3
|
+
// the chat doesn't go blank. FAIL-CLOSED: on any doubt about whether the
|
|
4
|
+
// sanitizer is actually active (load failure, parse/sanitize throw, or a
|
|
5
|
+
// purifier that doesn't look like a real DOMPurify instance), the render
|
|
6
|
+
// path returns escaped plaintext rather than ever risking raw HTML reaching
|
|
7
|
+
// innerHTML.
|
|
8
|
+
|
|
9
|
+
import { escapeHtml } from './html-escape.js';
|
|
4
10
|
|
|
5
11
|
let _ready = null;
|
|
6
12
|
let _marked = null;
|
|
7
13
|
let _purify = null;
|
|
14
|
+
// A failed load is NOT cached forever: we drop _ready so a later render retries,
|
|
15
|
+
// guarded by a small backoff so an offline session doesn't hammer the CDN.
|
|
16
|
+
let _failedAt = 0;
|
|
17
|
+
const RETRY_BACKOFF_MS = 30000;
|
|
18
|
+
|
|
19
|
+
// Pin to exact semver so the CDN cannot silently swap code under us.
|
|
20
|
+
// SRI cannot be applied to dynamic ESM imports in browsers (no importmap
|
|
21
|
+
// integrity support at design time); pinning the version is the best available
|
|
22
|
+
// mitigation for CDN-supply-chain risk on these two dependencies. Both are
|
|
23
|
+
// overridable (see configureMarkdownCdn below) for a consumer that wants to
|
|
24
|
+
// self-host, mirror-pin, or route through a CSP-allowlisted proxy; the
|
|
25
|
+
// defaults below are byte-for-byte unchanged from before, so a zero-config
|
|
26
|
+
// consumer's behavior is untouched.
|
|
27
|
+
const DEFAULT_MARKED_URL = 'https://cdn.jsdelivr.net/npm/marked@15.0.12/+esm';
|
|
28
|
+
const DEFAULT_PURIFY_URL = 'https://cdn.jsdelivr.net/npm/dompurify@3.2.6/+esm';
|
|
29
|
+
|
|
30
|
+
let _markedUrl = DEFAULT_MARKED_URL;
|
|
31
|
+
let _purifyUrl = DEFAULT_PURIFY_URL;
|
|
32
|
+
|
|
33
|
+
// Optional override for where marked/DOMPurify are fetched from. Additive:
|
|
34
|
+
// call before the first render() to take effect (ensureReady() reads these
|
|
35
|
+
// module-level vars lazily on first invocation only, same as before). Every
|
|
36
|
+
// existing consumer that never calls this keeps hitting the pinned jsDelivr
|
|
37
|
+
// URLs above, byte-for-byte. Passing null/undefined for a key resets that
|
|
38
|
+
// one URL back to its default without touching the other.
|
|
39
|
+
export function configureMarkdownCdn({ markedUrl, purifyUrl } = {}) {
|
|
40
|
+
if (markedUrl !== undefined) _markedUrl = markedUrl || DEFAULT_MARKED_URL;
|
|
41
|
+
if (purifyUrl !== undefined) _purifyUrl = purifyUrl || DEFAULT_PURIFY_URL;
|
|
42
|
+
// Force a fresh load on the next render so a runtime override (e.g. a
|
|
43
|
+
// consumer switching to a self-hosted mirror after boot) actually takes.
|
|
44
|
+
_ready = null;
|
|
45
|
+
_failedAt = 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Read-only introspection of the URLs actually in effect (defaults or
|
|
49
|
+
// override) — useful for a consumer's own SRI/CSP audit tooling.
|
|
50
|
+
export function getMarkdownCdnConfig() {
|
|
51
|
+
return { markedUrl: _markedUrl, purifyUrl: _purifyUrl };
|
|
52
|
+
}
|
|
8
53
|
|
|
9
|
-
|
|
10
|
-
|
|
54
|
+
// True while the markdown stack is unavailable (escaped-fallback rendering).
|
|
55
|
+
// Consumers (markdown-cache) use this to avoid caching degraded output.
|
|
56
|
+
export function isDegraded() {
|
|
57
|
+
return !_marked || !_purify || typeof _purify.sanitize !== 'function';
|
|
58
|
+
}
|
|
11
59
|
|
|
12
60
|
export async function ensureReady() {
|
|
13
61
|
if (_ready) return _ready;
|
|
62
|
+
if (_failedAt && Date.now() - _failedAt < RETRY_BACKOFF_MS) return false;
|
|
14
63
|
_ready = (async () => {
|
|
15
64
|
try {
|
|
16
|
-
const [{ marked }, DOMPurifyMod] = await Promise.all([import(
|
|
65
|
+
const [{ marked }, DOMPurifyMod] = await Promise.all([import(_markedUrl), import(_purifyUrl)]);
|
|
66
|
+
const purify = DOMPurifyMod.default || DOMPurifyMod;
|
|
67
|
+
// Fail closed if either module didn't resolve to something usable —
|
|
68
|
+
// a CDN that 200s with an empty/HTML error-page body can satisfy the
|
|
69
|
+
// dynamic import yet hand back a shape with no .parse/.sanitize.
|
|
70
|
+
if (!marked || typeof marked.parse !== 'function') throw new Error('marked module missing parse()');
|
|
71
|
+
if (!purify || typeof purify.sanitize !== 'function') throw new Error('DOMPurify module missing sanitize()');
|
|
17
72
|
_marked = marked;
|
|
18
|
-
_purify =
|
|
73
|
+
_purify = purify;
|
|
74
|
+
_failedAt = 0;
|
|
19
75
|
return true;
|
|
20
76
|
} catch (err) {
|
|
21
77
|
console.warn('[247420] markdown loader failed:', err);
|
|
78
|
+
// Reset the cached promise so a later render retries (after backoff).
|
|
79
|
+
// Also drop any partial module refs so isDegraded()/renderMarkdown
|
|
80
|
+
// never treat a half-initialized state as ready.
|
|
81
|
+
_marked = null;
|
|
82
|
+
_purify = null;
|
|
83
|
+
_ready = null;
|
|
84
|
+
_failedAt = Date.now();
|
|
22
85
|
return false;
|
|
23
86
|
}
|
|
24
87
|
})();
|
|
25
88
|
return _ready;
|
|
26
89
|
}
|
|
27
90
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
91
|
+
// Fail-closed plaintext fallback, always wrapped in the same safe container
|
|
92
|
+
// shape the real sanitized output would use (a plain block the caller can
|
|
93
|
+
// innerHTML directly) — never bare unwrapped text relying on caller discipline.
|
|
94
|
+
function escapedFallback(src) {
|
|
95
|
+
return escapeHtml(src).replace(/\n/g, '<br>');
|
|
32
96
|
}
|
|
33
97
|
|
|
98
|
+
// The single HTML-entity escape for the whole SDK now lives in html-escape.js
|
|
99
|
+
// (full set incl. quotes, so it is safe in attribute contexts too).
|
|
100
|
+
// Re-exported here for backward compatibility with existing importers of
|
|
101
|
+
// escapeHtml from this module. page-html.js re-exports this as `escape`.
|
|
102
|
+
export { escapeHtml };
|
|
103
|
+
|
|
34
104
|
export async function renderMarkdown(src) {
|
|
35
105
|
const ok = await ensureReady();
|
|
36
|
-
if (!ok) return
|
|
37
|
-
|
|
38
|
-
|
|
106
|
+
if (!ok) return escapedFallback(src);
|
|
107
|
+
// Fail-closed around the parse+sanitize call itself too, not just the
|
|
108
|
+
// loader: a CDN module that resolved but throws mid-parse (a malformed
|
|
109
|
+
// remote payload, a marked/DOMPurify version mismatch) must never let a
|
|
110
|
+
// partially-produced `raw` string reach the caller unsanitized, and must
|
|
111
|
+
// never propagate an unhandled rejection that a caller might swallow into
|
|
112
|
+
// a raw-HTML fallback of their own.
|
|
113
|
+
try {
|
|
114
|
+
const raw = _marked.parse(String(src));
|
|
115
|
+
if (typeof _purify.sanitize !== 'function') throw new Error('purifier unavailable mid-render');
|
|
116
|
+
return _purify.sanitize(raw, { FORCE_BODY: true });
|
|
117
|
+
} catch (err) {
|
|
118
|
+
console.warn('[247420] markdown render failed, falling back to escaped text:', err);
|
|
119
|
+
// Treat this exactly like a load failure: drop refs and start the
|
|
120
|
+
// backoff so the next call retries a fresh load rather than re-hitting
|
|
121
|
+
// whatever made this one throw.
|
|
122
|
+
_marked = null;
|
|
123
|
+
_purify = null;
|
|
124
|
+
_ready = null;
|
|
125
|
+
_failedAt = Date.now();
|
|
126
|
+
return escapedFallback(src);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Sanitize already-rendered HTML before it touches innerHTML. For any surface
|
|
131
|
+
// that injects host/user-authored HTML (e.g. a wiki page body), this is the
|
|
132
|
+
// single XSS gate — DOMPurify strips scripts/handlers. If the purifier hasn't
|
|
133
|
+
// loaded, we safe-fail by escaping (raw tags show as text, never execute).
|
|
134
|
+
export async function sanitizeHtml(html) {
|
|
135
|
+
const ok = await ensureReady();
|
|
136
|
+
if (!ok) return escapeHtml(html);
|
|
137
|
+
try {
|
|
138
|
+
if (typeof _purify.sanitize !== 'function') throw new Error('purifier unavailable mid-sanitize');
|
|
139
|
+
return _purify.sanitize(String(html), { FORCE_BODY: true });
|
|
140
|
+
} catch (err) {
|
|
141
|
+
console.warn('[247420] sanitizeHtml failed, falling back to escaped text:', err);
|
|
142
|
+
_marked = null;
|
|
143
|
+
_purify = null;
|
|
144
|
+
_ready = null;
|
|
145
|
+
_failedAt = Date.now();
|
|
146
|
+
return escapeHtml(html);
|
|
147
|
+
}
|
|
39
148
|
}
|
package/dist/src/motion.js
CHANGED
|
@@ -11,12 +11,28 @@ export function installMotion() {
|
|
|
11
11
|
style.textContent = `
|
|
12
12
|
@media (prefers-reduced-motion: no-preference) {
|
|
13
13
|
.ds-247420 [data-anim="in"] {
|
|
14
|
-
opacity: 0; transform: translateY(
|
|
15
|
-
|
|
14
|
+
opacity: 0; transform: translateY(14px);
|
|
15
|
+
/* Physical signature reveal — a slight spring landing, not a flat fade. */
|
|
16
|
+
transition: opacity var(--dur-reveal, 560ms) var(--ease, cubic-bezier(.2,0,0,1)),
|
|
17
|
+
transform var(--dur-reveal, 560ms) var(--ease-spring, cubic-bezier(0.34,1.56,0.64,1));
|
|
16
18
|
}
|
|
17
19
|
.ds-247420 [data-anim="ready"] {
|
|
18
20
|
opacity: 1; transform: translateY(0);
|
|
19
21
|
}
|
|
22
|
+
}
|
|
23
|
+
/* [data-motion="reduced"] is the in-app user override (motion-toggle.js) —
|
|
24
|
+
applies the exact same reduced-motion treatment as the OS-level
|
|
25
|
+
prefers-reduced-motion media query above, independent of the OS setting.
|
|
26
|
+
Selector applies regardless of the media query's own match state, so it
|
|
27
|
+
correctly overrides the animated block above on any OS. */
|
|
28
|
+
:root[data-motion="reduced"] .ds-247420 [data-anim="in"],
|
|
29
|
+
.ds-247420[data-motion="reduced"] [data-anim="in"] {
|
|
30
|
+
opacity: 1 !important; transform: translateY(0) !important;
|
|
31
|
+
transition: none !important;
|
|
32
|
+
}
|
|
33
|
+
:root[data-motion="reduced"] .ds-247420 [data-anim="ready"],
|
|
34
|
+
.ds-247420[data-motion="reduced"] [data-anim="ready"] {
|
|
35
|
+
transition: none !important;
|
|
20
36
|
}`.trim();
|
|
21
37
|
document.head.appendChild(style);
|
|
22
38
|
}
|