anentrypoint-design 0.0.477 → 0.0.478
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/README.md +101 -0
- package/dist/247420.css +139 -0
- package/dist/247420.js +124 -56
- package/dist/index.html +109 -32
- package/package.json +1 -1
- package/src/components/content/hero.js +9 -2
- package/src/components/freddie/pages-missing.js +1 -1
- package/src/components/shell/app-shell.js +15 -4
- package/src/copy-code.js +103 -0
- package/src/css/app-shell/catalog-theme.css +6 -0
- package/src/css/app-shell/files.css +6 -0
- package/src/css/app-shell/hero-content.css +13 -0
- package/src/css/app-shell/kits-appended.css +13 -0
- package/src/css/app-shell/panel-row.css +15 -0
- package/src/css/app-shell/row-print.css +34 -0
- package/src/css/app-shell/states-interactions.css +42 -0
- package/src/css/app-shell/topbar.css +1 -0
- package/src/kits/os/theme.css +21 -0
- package/src/kits/os/wm.js +38 -1
- package/src/page-html/client-script.js +96 -28
- package/src/page-html.js +3 -2
- package/src/theme.js +6 -2
- package/src/web-components/ds-chat.js +9 -0
- package/types/components.d.ts +2 -0
|
@@ -71,5 +71,39 @@
|
|
|
71
71
|
.ds-live-log { height: auto; max-height: none; overflow: visible; }
|
|
72
72
|
.ds-stat, .kpi-card { border: 1px solid var(--rule); }
|
|
73
73
|
.ds-session-row { break-inside: avoid; }
|
|
74
|
+
|
|
75
|
+
/* Receipt table (Receipt() in src/components/content/panel.js, `.kv`) — the
|
|
76
|
+
project_page kit's "receipt" section, and any other kv-table consumer.
|
|
77
|
+
On screen it inherits panel/token colors that can render as near-white
|
|
78
|
+
text on a printer's assumed-white page background if the browser's
|
|
79
|
+
"background graphics" print option is off (the common default); force
|
|
80
|
+
the LIGHT-theme ink/paper tokens (real print-page colors, not a raw
|
|
81
|
+
literal — --ink/--paper are the same "true black text on true white
|
|
82
|
+
page" values the light theme already defines) and visible cell borders
|
|
83
|
+
so it stays legible without relying on background-graphics printing at
|
|
84
|
+
all. break-inside:avoid keeps a receipt from splitting a row across a
|
|
85
|
+
page boundary. */
|
|
86
|
+
.kv { border-collapse: collapse; width: 100%; color: var(--ink); background: var(--paper); }
|
|
87
|
+
.kv tr { break-inside: avoid; }
|
|
88
|
+
.kv td { border: 1px solid var(--paper-3); padding: 4px 8px; color: var(--ink); }
|
|
89
|
+
|
|
90
|
+
/* Any button/interactive-only affordance still present inside otherwise-
|
|
91
|
+
printable content (copy buttons, expand/collapse toggles, filter pills,
|
|
92
|
+
the receipt/doc section's own action row) is chrome, not content. */
|
|
93
|
+
.btn, .btn-primary, .btn-ghost, .row-act, .ds-filter-pills,
|
|
94
|
+
.app-search, input[type="search"] { display: none; }
|
|
95
|
+
|
|
96
|
+
/* Force real ink-on-paper for body copy: panel/fg tokens are tuned for
|
|
97
|
+
screen surfaces (including dark theme, where --panel-text can resolve
|
|
98
|
+
near-white) and a printer defaults to a white page regardless of
|
|
99
|
+
data-theme, so an unconverted dark-theme page would print near-invisible
|
|
100
|
+
text without this override. Same --ink/--paper light-theme pair as the
|
|
101
|
+
receipt table above, not a second raw literal. */
|
|
102
|
+
/* No !important: source order already wins here (this rule is the last
|
|
103
|
+
word inside its own @media print block, same as every other override in
|
|
104
|
+
this file), and an unconditional !important on `body` would beat even a
|
|
105
|
+
future print-specific consumer override with higher specificity. */
|
|
106
|
+
html body, .app-main, .page-body, .ds-lede { color: var(--ink); background: var(--paper); }
|
|
107
|
+
a { color: var(--ink); text-decoration: underline; }
|
|
74
108
|
}
|
|
75
109
|
|
|
@@ -825,3 +825,45 @@ input[type="password"]:not(:placeholder-shown) + .input-clear {
|
|
|
825
825
|
max-width: none;
|
|
826
826
|
}
|
|
827
827
|
}
|
|
828
|
+
|
|
829
|
+
/* Stacking host for the imperative toast()/<Toast> component
|
|
830
|
+
(src/components/editor-primitives/toast.js). One fixed-position column;
|
|
831
|
+
individual toasts are appended/removed as siblings inside it rather than
|
|
832
|
+
each toast fixed-positioning itself, so more than one toast in flight
|
|
833
|
+
stacks instead of overlapping at the same bottom-right point. */
|
|
834
|
+
.ds-ep-toast-host {
|
|
835
|
+
position: fixed; z-index: var(--z-toast);
|
|
836
|
+
bottom: 24px; right: 24px;
|
|
837
|
+
display: flex; flex-direction: column-reverse; gap: var(--space-2);
|
|
838
|
+
pointer-events: none;
|
|
839
|
+
}
|
|
840
|
+
.ds-ep-toast {
|
|
841
|
+
pointer-events: auto;
|
|
842
|
+
display: flex; align-items: center; gap: var(--space-3);
|
|
843
|
+
background: var(--bg-2); color: var(--fg);
|
|
844
|
+
border: 1px solid var(--rule);
|
|
845
|
+
border-radius: var(--r-2);
|
|
846
|
+
padding: var(--space-3) var(--space-4);
|
|
847
|
+
max-width: 320px;
|
|
848
|
+
box-shadow: var(--shadow-overlay);
|
|
849
|
+
font-size: var(--fs-sm);
|
|
850
|
+
}
|
|
851
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
852
|
+
.ds-ep-toast { animation: toast-slide-in 0.3s ease-out; }
|
|
853
|
+
.ds-ep-toast.leaving { animation: toast-slide-in 0.2s ease-in reverse; }
|
|
854
|
+
}
|
|
855
|
+
.ds-ep-toast.kind-error { border-color: var(--danger, var(--warn)); }
|
|
856
|
+
.ds-ep-toast.kind-success { border-color: var(--green-2); }
|
|
857
|
+
.ds-ep-toast.kind-warning { border-color: var(--amber); }
|
|
858
|
+
.ds-ep-toast-msg { flex: 1; }
|
|
859
|
+
.ds-ep-toast-action {
|
|
860
|
+
flex-shrink: 0;
|
|
861
|
+
background: transparent; border: 0; color: var(--accent-ink);
|
|
862
|
+
font: inherit; font-weight: 600; cursor: pointer; padding: 0;
|
|
863
|
+
}
|
|
864
|
+
.ds-ep-toast-action:hover { text-decoration: underline; }
|
|
865
|
+
|
|
866
|
+
@media (max-width: 480px) {
|
|
867
|
+
.ds-ep-toast-host { bottom: 16px; right: 16px; left: 16px; }
|
|
868
|
+
.ds-ep-toast { max-width: none; }
|
|
869
|
+
}
|
|
@@ -80,6 +80,7 @@ body.canvas-host { background: transparent !important; }
|
|
|
80
80
|
.app-topbar > .brand { flex: 0 0 auto; }
|
|
81
81
|
.app-topbar > .app-search { flex: 1 1 auto; }
|
|
82
82
|
.app-topbar > nav { margin-left: auto; }
|
|
83
|
+
.app-topbar > .app-topbar-theme { flex: 0 0 auto; }
|
|
83
84
|
.app-topbar nav { display: flex; gap: 4px; font-size: var(--fs-sm); flex-wrap: wrap; }
|
|
84
85
|
.app-topbar nav a { flex: 0 0 auto; }
|
|
85
86
|
.app-topbar nav a {
|
package/src/kits/os/theme.css
CHANGED
|
@@ -58,6 +58,8 @@ html, body {
|
|
|
58
58
|
color: var(--os-fg);
|
|
59
59
|
font: var(--fs-tiny) var(--os-font);
|
|
60
60
|
height: var(--os-bar-h);
|
|
61
|
+
/* prefers-reduced-transparency handled below (near bottom of this file):
|
|
62
|
+
drops the blur + backing translucency to a solid --os-bg-2 fill. */
|
|
61
63
|
padding: 0 var(--space-2-75);
|
|
62
64
|
gap: var(--space-1-75);
|
|
63
65
|
box-shadow: none;
|
|
@@ -1099,6 +1101,25 @@ html.ds-247420 { touch-action: pan-x pan-y; overscroll-behavior: none; -webkit-t
|
|
|
1099
1101
|
.ds-247420 .tb-sess-overlay { backdrop-filter: none; -webkit-backdrop-filter: none; }
|
|
1100
1102
|
}
|
|
1101
1103
|
|
|
1104
|
+
/* -----------------------------------------------------------------------
|
|
1105
|
+
prefers-reduced-transparency — distinct from prefers-reduced-motion
|
|
1106
|
+
above: a user can want full animation but zero see-through/blur panel
|
|
1107
|
+
chrome (vestibular-motion sensitivity vs. low-vision/legibility need are
|
|
1108
|
+
different accessibility needs, so this is its own media query, not folded
|
|
1109
|
+
into the motion one). Every backdrop-blur/translucent-panel effect in this
|
|
1110
|
+
kit drops to a solid, fully-opaque backing fill — the blur adds nothing
|
|
1111
|
+
once nothing shows through it, so it is dropped alongside the alpha for
|
|
1112
|
+
one less GPU compositing pass, not just left as dead blur-on-opaque.
|
|
1113
|
+
----------------------------------------------------------------------- */
|
|
1114
|
+
@media (prefers-reduced-transparency: reduce) {
|
|
1115
|
+
.os-menubar, .os-taskbar {
|
|
1116
|
+
background: var(--os-bg-2);
|
|
1117
|
+
backdrop-filter: none;
|
|
1118
|
+
-webkit-backdrop-filter: none;
|
|
1119
|
+
}
|
|
1120
|
+
.ds-247420 .tb-sess-overlay { background: var(--scrim-strong); backdrop-filter: none; -webkit-backdrop-filter: none; }
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1102
1123
|
/* -----------------------------------------------------------------------
|
|
1103
1124
|
* Freddie chat — jank pass 3 (2026-05-31)
|
|
1104
1125
|
* Pins layout, spacing, focus, scrollbar, role-distinct bubbles, focus ring,
|
package/src/kits/os/wm.js
CHANGED
|
@@ -11,6 +11,38 @@
|
|
|
11
11
|
|
|
12
12
|
import { icons } from './icons.js';
|
|
13
13
|
|
|
14
|
+
// Shared aria-live announcer for window open/close/focus-change events.
|
|
15
|
+
// Visually hidden, one instance per document, lazily created so importing
|
|
16
|
+
// this module has no side effect until a window actually renders. Screen
|
|
17
|
+
// readers get no other signal that a floating, non-modal window opened,
|
|
18
|
+
// closed, or changed focus -- there is no page navigation or route change
|
|
19
|
+
// to announce it implicitly, unlike a normal document flow.
|
|
20
|
+
let _announcer = null;
|
|
21
|
+
function getAnnouncer() {
|
|
22
|
+
if (_announcer && _announcer.isConnected) return _announcer;
|
|
23
|
+
_announcer = document.getElementById('wm-announcer');
|
|
24
|
+
if (_announcer) return _announcer;
|
|
25
|
+
_announcer = document.createElement('div');
|
|
26
|
+
_announcer.id = 'wm-announcer';
|
|
27
|
+
_announcer.setAttribute('aria-live', 'polite');
|
|
28
|
+
_announcer.setAttribute('aria-atomic', 'true');
|
|
29
|
+
_announcer.className = 'sr-only';
|
|
30
|
+
// Inline fallback in case the consuming page's stylesheet doesn't define
|
|
31
|
+
// .sr-only (this module has no guaranteed CSS import of its own) --
|
|
32
|
+
// standard clip-based visually-hidden-but-AT-visible technique.
|
|
33
|
+
_announcer.style.cssText = 'position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;';
|
|
34
|
+
document.body.appendChild(_announcer);
|
|
35
|
+
return _announcer;
|
|
36
|
+
}
|
|
37
|
+
function announce(text) {
|
|
38
|
+
const a = getAnnouncer();
|
|
39
|
+
// Clear-then-set on a microtask forces a re-announcement even if the
|
|
40
|
+
// text is identical to what's already there (e.g. focusing the same
|
|
41
|
+
// window twice in a row) -- aria-live only fires on a DOM mutation.
|
|
42
|
+
a.textContent = '';
|
|
43
|
+
requestAnimationFrame(() => { a.textContent = text; });
|
|
44
|
+
}
|
|
45
|
+
|
|
14
46
|
export function renderWindow(opts = {}) {
|
|
15
47
|
const {
|
|
16
48
|
title = 'window',
|
|
@@ -127,11 +159,15 @@ export function renderWindow(opts = {}) {
|
|
|
127
159
|
return;
|
|
128
160
|
}
|
|
129
161
|
disarmClose();
|
|
162
|
+
announce('closed ' + titleEl.textContent);
|
|
130
163
|
callbacks.onClose && callbacks.onClose();
|
|
131
164
|
});
|
|
132
165
|
closeBtn.addEventListener('blur', disarmClose);
|
|
133
166
|
|
|
134
|
-
const focus = () =>
|
|
167
|
+
const focus = () => {
|
|
168
|
+
if (!el.classList.contains('wm-focused')) announce(titleEl.textContent + ' focused');
|
|
169
|
+
callbacks.onFocus && callbacks.onFocus();
|
|
170
|
+
};
|
|
135
171
|
|
|
136
172
|
el.addEventListener('pointerdown', () => focus());
|
|
137
173
|
|
|
@@ -172,6 +208,7 @@ export function renderWindow(opts = {}) {
|
|
|
172
208
|
applyFocused(el, focused);
|
|
173
209
|
applyMaximized(el, maximized);
|
|
174
210
|
applyMinimized(el, minimized);
|
|
211
|
+
announce('opened ' + title);
|
|
175
212
|
|
|
176
213
|
return {
|
|
177
214
|
el,
|
|
@@ -80,9 +80,64 @@ function examplesNode(examples) {
|
|
|
80
80
|
});
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
// Category glyph per homepage panel id — the same line-icon vocabulary
|
|
84
|
+
// icons.js already provides, so kits/previews/decks/docs read as distinct
|
|
85
|
+
// categories at a glance instead of by text label alone. Falls back to no
|
|
86
|
+
// icon for panel ids outside this table (e.g. one-off feature panels).
|
|
87
|
+
const PANEL_ICON = {
|
|
88
|
+
kits: 'grid',
|
|
89
|
+
file_browser: 'folder',
|
|
90
|
+
desktop_os: 'square',
|
|
91
|
+
web_components: 'page',
|
|
92
|
+
api_exports: 'link',
|
|
93
|
+
decks: 'screen',
|
|
94
|
+
docs: 'file-text',
|
|
95
|
+
previews: 'eye',
|
|
96
|
+
features: 'info',
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Filter state for the kits search box above the "ui kits" panel. Reuses
|
|
100
|
+
// ui_kits/search's own query-bar pattern (plain module-level state object,
|
|
101
|
+
// mutated on input, driving a re-render) rather than inventing a second
|
|
102
|
+
// filtering mechanism -- this homepage already ships that kit, so the
|
|
103
|
+
// homepage's own kit listing gets the same affordance instead of being the
|
|
104
|
+
// one surface on the site without a way to search kits by name.
|
|
105
|
+
const kitsFilterState = { q: '' };
|
|
106
|
+
|
|
107
|
+
function panelNode(panel, idx, rerender) {
|
|
108
|
+
let items = Array.isArray(panel.items) ? panel.items : [];
|
|
109
|
+
const isKits = panel.id === 'kits';
|
|
110
|
+
const q = isKits ? kitsFilterState.q.trim().toLowerCase() : '';
|
|
111
|
+
if (isKits && q) {
|
|
112
|
+
items = items.filter((it) => {
|
|
113
|
+
const hay = (String(it.title || it.name || '') + ' ' + String(it.sub || it.desc || '')).toLowerCase();
|
|
114
|
+
return hay.includes(q);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const iconName = panel.id && PANEL_ICON[panel.id];
|
|
118
|
+
const titleText = panel.title || panel.name || '';
|
|
119
|
+
const titleNode = iconName && titleText
|
|
120
|
+
? h('span', { class: 'ds-panel-title-glyph' }, C.Icon(iconName, { size: 15 }), h('span', {}, titleText))
|
|
121
|
+
: titleText;
|
|
122
|
+
const filterInput = isKits && rerender ? h('div', { class: 'ds-kits-filter' },
|
|
123
|
+
h('input', {
|
|
124
|
+
type: 'search', class: 'input ds-kits-filter-input',
|
|
125
|
+
placeholder: 'filter kits by name or description…',
|
|
126
|
+
value: kitsFilterState.q,
|
|
127
|
+
'aria-label': 'filter ui kits',
|
|
128
|
+
oninput: (e) => { kitsFilterState.q = e.target.value; rerender(); },
|
|
129
|
+
})) : null;
|
|
130
|
+
if (!items.length) {
|
|
131
|
+
if (isKits && q) {
|
|
132
|
+
return h('div', { class: 'ds-kits-panel-wrap' }, filterInput,
|
|
133
|
+
C.Panel({ id: panel.id || null, title: titleNode, count: 0, children:
|
|
134
|
+
h('div', { class: 'ds-empty-state' },
|
|
135
|
+
h('div', { class: 'ds-empty-state-glyph' }, '( )'),
|
|
136
|
+
h('p', { class: 'ds-empty-state-msg' }, 'no kits match ', h('code', {}, '"' + kitsFilterState.q.trim() + '"')),
|
|
137
|
+
) }));
|
|
138
|
+
}
|
|
139
|
+
return filterInput ? h('div', { class: 'ds-kits-panel-wrap' }, filterInput) : null;
|
|
140
|
+
}
|
|
86
141
|
const rows = items.map((it, i) => {
|
|
87
142
|
const code = it.code == null ? '' : String(it.code).trim();
|
|
88
143
|
const kids = [];
|
|
@@ -92,7 +147,8 @@ function panelNode(panel, idx) {
|
|
|
92
147
|
kids.push(h('span', { key: 'a', class: 'ds-row-arrow' }, it.meta || '->'));
|
|
93
148
|
return h('a', { key: i, class: 'row', href: it.href || '#' }, ...kids);
|
|
94
149
|
});
|
|
95
|
-
|
|
150
|
+
const panelEl = C.Panel({ id: panel.id || null, title: titleNode, count: items.length, children: rows });
|
|
151
|
+
return filterInput ? h('div', { class: 'ds-kits-panel-wrap' }, filterInput, panelEl) : panelEl;
|
|
96
152
|
}
|
|
97
153
|
|
|
98
154
|
function marqueeNode(marquee) {
|
|
@@ -196,8 +252,14 @@ const bodyNode = data.bodyHtml ? C.Section({ children: h('div', { class: 'page-b
|
|
|
196
252
|
function footerNode() {
|
|
197
253
|
const year = new Date().getFullYear();
|
|
198
254
|
const links = (data.navItems || []).filter(([, href]) => /^https?:/.test(String(href)));
|
|
255
|
+
// Names the actual maintaining entity, not just the project's numeric
|
|
256
|
+
// brand. This is a personal/community project (MIT, copyright holder
|
|
257
|
+
// "AnEntrypoint" per LICENSE) with no separate legal org behind it, so the
|
|
258
|
+
// footer says that plainly instead of leaving "247420" to stand in for an
|
|
259
|
+
// entity it isn't.
|
|
260
|
+
const entity = (data.seoAuthor || '247420 · a design system by AnEntrypoint').toLowerCase();
|
|
199
261
|
return h('footer', { class: 'ds-page-footer' },
|
|
200
|
-
h('span', { class: 'ds-page-footer-copy' }, String(year) + ' · ' +
|
|
262
|
+
h('span', { class: 'ds-page-footer-copy' }, String(year) + ' · ' + entity),
|
|
201
263
|
links.length ? h('nav', { class: 'ds-page-footer-links', 'aria-label': 'footer' },
|
|
202
264
|
...links.map(([label, href], i) => h('a', { key: i, href }, String(label).replace(' ->', '')))
|
|
203
265
|
) : null,
|
|
@@ -215,40 +277,46 @@ function tierNode(tier, children) {
|
|
|
215
277
|
return h('section', { key: tier.key, class: 'ds-tier ds-tier-' + tier.key, id: tier.key, 'aria-labelledby': labelId }, head, ...kids);
|
|
216
278
|
}
|
|
217
279
|
|
|
218
|
-
const panelsById = new Map((data.panels || []).map((p) => [p.id || p.title || p.name || '', p]));
|
|
219
|
-
const takePanel = (id) => { const p = panelsById.get(id); if (p) panelsById.delete(id); return p ? panelNode(p) : null; };
|
|
220
|
-
|
|
221
280
|
const TIERS = [
|
|
222
281
|
{ key: 'open', label: 'open', lede: 'browse and try the system running.', ids: ['kits', 'previews', 'decks'], extra: () => [examplesNode(data.examples)] },
|
|
223
282
|
{ key: 'ships', label: 'ships', lede: 'what the package contains.', ids: ['file_browser', 'desktop_os', 'web_components', 'api_exports'], extra: () => [] },
|
|
224
283
|
{ key: 'read', label: 'read', lede: 'understand the rules behind it.', ids: ['docs', 'features'], extra: () => [quickstartNode(data.quickstart)] },
|
|
225
284
|
];
|
|
226
285
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
return
|
|
234
|
-
|
|
235
|
-
const
|
|
286
|
+
// Builds the whole main-content tree fresh each render so the kits filter
|
|
287
|
+
// (module-level kitsFilterState) can drive a real re-render via mount()'s
|
|
288
|
+
// own returned render callback -- same reactive shape mountKit() gives every
|
|
289
|
+
// other kit on the site, applied here to the static-page renderer.
|
|
290
|
+
function buildMainChildren(rerender) {
|
|
291
|
+
const panelsById = new Map((data.panels || []).map((p) => [p.id || p.title || p.name || '', p]));
|
|
292
|
+
const takePanel = (id) => { const p = panelsById.get(id); if (p) panelsById.delete(id); return p ? panelNode(p, 0, rerender) : null; };
|
|
293
|
+
|
|
294
|
+
const tierNodes = TIERS.map((t) => {
|
|
295
|
+
const kids = [...t.ids.map(takePanel), ...t.extra()].filter(Boolean);
|
|
296
|
+
if (t.key === 'open' && kids.length) {
|
|
297
|
+
const lead = kids[0];
|
|
298
|
+
if (lead.props) lead.props.class = (lead.props.class || '') + ' ds-tier-lead';
|
|
299
|
+
}
|
|
300
|
+
return tierNode(t, kids);
|
|
301
|
+
});
|
|
302
|
+
const leftoverPanels = [...panelsById.values()].map((p) => panelNode(p, 0, rerender));
|
|
236
303
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
].filter(Boolean);
|
|
304
|
+
return [
|
|
305
|
+
heroNode(data.hero),
|
|
306
|
+
marqueeNode(data.marquee),
|
|
307
|
+
...data.sections.map(sectionNode),
|
|
308
|
+
...tierNodes,
|
|
309
|
+
...leftoverPanels,
|
|
310
|
+
bodyNode,
|
|
311
|
+
footerNode(),
|
|
312
|
+
].filter(Boolean);
|
|
313
|
+
}
|
|
246
314
|
|
|
247
|
-
mount(document.getElementById('app'), () => C.AppShell({
|
|
315
|
+
mount(document.getElementById('app'), (rerender) => C.AppShell({
|
|
248
316
|
topbar: C.Topbar({ brand: data.siteName, items: data.navItems, active: data.title }),
|
|
249
317
|
crumb: C.Crumb({ leaf: data.title }),
|
|
250
318
|
side: sideNode(data.sidebar),
|
|
251
|
-
main: h('div', { class: 'app-stage' }, ...
|
|
319
|
+
main: h('div', { class: 'app-stage' }, ...buildMainChildren(rerender)),
|
|
252
320
|
status: C.Status({
|
|
253
321
|
left: data.statusLeft || [data.siteName.toLowerCase(), data.slug],
|
|
254
322
|
right: data.statusRight || ['live'],
|
package/src/page-html.js
CHANGED
|
@@ -82,13 +82,14 @@ export function renderPageHtml({
|
|
|
82
82
|
quickstart: quickstart || null,
|
|
83
83
|
statusLeft: Array.isArray(statusLeft) ? statusLeft : null,
|
|
84
84
|
statusRight: Array.isArray(statusRight) ? statusRight : null,
|
|
85
|
+
seoAuthor: seo && seo.author ? seo.author : null,
|
|
85
86
|
};
|
|
86
87
|
|
|
87
88
|
const seoTags = seo ? renderSeoTags({ title, siteName, seo }) : '';
|
|
88
89
|
const faviconTags = renderFaviconTags({ faviconHref, faviconGlyph });
|
|
89
90
|
|
|
90
91
|
return `<!doctype html>
|
|
91
|
-
<html lang="en" class="ds-247420">
|
|
92
|
+
<html lang="en" class="ds-247420" data-theme="${theme}">
|
|
92
93
|
<head>
|
|
93
94
|
<meta charset="utf-8">
|
|
94
95
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
@@ -105,7 +106,7 @@ ${PAGE_INLINE_STYLES}
|
|
|
105
106
|
<script id="__site__" type="application/json">${JSON.stringify(pageData).replace(/</g, '\\u003c')}</script>
|
|
106
107
|
${headExtra}
|
|
107
108
|
</head>
|
|
108
|
-
<body
|
|
109
|
+
<body>
|
|
109
110
|
<div id="app"></div>
|
|
110
111
|
<script type="module">
|
|
111
112
|
${CLIENT_SCRIPT}${clientScriptExtra}
|
package/src/theme.js
CHANGED
|
@@ -43,7 +43,11 @@ function writeStored(mode) {
|
|
|
43
43
|
|
|
44
44
|
function writeAttr(mode) {
|
|
45
45
|
if (!isBrowser()) return;
|
|
46
|
-
|
|
46
|
+
// data-theme must live on the same element carrying the .ds-247420 scope
|
|
47
|
+
// class (<html>, per every shipped page template) -- the bundled CSS
|
|
48
|
+
// selectors are `.ds-247420[data-theme="ink"]`, which cannot match if
|
|
49
|
+
// the attribute sits on <body> instead of <html>.
|
|
50
|
+
document.documentElement.setAttribute('data-theme', mode);
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
function ensureMq() {
|
|
@@ -169,7 +173,7 @@ export function initDirection() {
|
|
|
169
173
|
export function initTheme() {
|
|
170
174
|
if (!isBrowser()) return 'auto';
|
|
171
175
|
const stored = readStored();
|
|
172
|
-
const fromAttr = document.
|
|
176
|
+
const fromAttr = document.documentElement.getAttribute('data-theme');
|
|
173
177
|
const initial = stored || (VALID.has(fromAttr) ? fromAttr : 'auto');
|
|
174
178
|
applyTheme(initial);
|
|
175
179
|
// Restore persisted accent/density (no-op if none stored — keeps the
|
|
@@ -28,6 +28,7 @@ class DsChat extends HTMLElement {
|
|
|
28
28
|
this._placeholder = val || '';
|
|
29
29
|
} else if (name === 'title') {
|
|
30
30
|
this._title = val || 'chat';
|
|
31
|
+
if (!this.hasAttribute('aria-label') || this._ariaLabelAuto) { this.setAttribute('aria-label', this._title); this._ariaLabelAuto = true; }
|
|
31
32
|
} else if (name === 'sub') {
|
|
32
33
|
this._sub = val || '';
|
|
33
34
|
} else if (name === 'disabled') {
|
|
@@ -47,6 +48,14 @@ class DsChat extends HTMLElement {
|
|
|
47
48
|
get disabled() { return this._disabled; }
|
|
48
49
|
connectedCallback() {
|
|
49
50
|
this.classList.add('ds-247420');
|
|
51
|
+
// Accessible name for the custom element itself: <ds-chat> has no
|
|
52
|
+
// implicit ARIA role/name, so a screen reader landmark/element list
|
|
53
|
+
// shows an unlabeled item without this. role="region" + aria-label
|
|
54
|
+
// (falling back to the chat title) makes it identifiable on its own,
|
|
55
|
+
// separate from the inner .chat-thread's role="log"/aria-live, which
|
|
56
|
+
// announces individual streamed messages rather than naming the widget.
|
|
57
|
+
if (!this.hasAttribute('role')) this.setAttribute('role', 'region');
|
|
58
|
+
if (!this.hasAttribute('aria-label')) this.setAttribute('aria-label', this._title || 'chat');
|
|
50
59
|
this._render();
|
|
51
60
|
}
|
|
52
61
|
_send(text) {
|
package/types/components.d.ts
CHANGED