anentrypoint-design 0.0.477 → 0.0.480
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/chat.css +4 -4
- package/colors_and_type.css +33 -3
- package/community.css +22 -8
- package/dist/247420.css +324 -49
- package/dist/247420.js +123 -55
- package/dist/index.html +109 -32
- package/package.json +1 -1
- package/src/community-app.js +4 -2
- package/src/components/community/navigation.js +2 -2
- package/src/components/community/presence.js +10 -4
- package/src/components/community/views.js +8 -2
- package/src/components/content/avatar.js +30 -0
- package/src/components/content/hero.js +9 -2
- package/src/components/content.js +2 -2
- package/src/components/freddie/pages-missing.js +1 -1
- package/src/components/overlay-primitives/emoji-picker.js +15 -3
- package/src/components/shell/app-shell.js +15 -4
- package/src/components/shell/workspace-shell.js +1 -1
- package/src/components/theme-toggle.js +8 -3
- package/src/copy-code.js +103 -0
- package/src/css/app-shell/base.css +18 -0
- package/src/css/app-shell/catalog-theme.css +20 -2
- package/src/css/app-shell/chat-polish.css +12 -0
- package/src/css/app-shell/collab.css +5 -0
- package/src/css/app-shell/data-density.css +4 -0
- package/src/css/app-shell/files.css +7 -1
- package/src/css/app-shell/hero-content.css +14 -1
- package/src/css/app-shell/kits-appended.css +13 -0
- package/src/css/app-shell/loading-alerts.css +14 -12
- package/src/css/app-shell/otp-input.css +2 -2
- package/src/css/app-shell/panel-row.css +15 -0
- package/src/css/app-shell/plugins-config.css +3 -0
- package/src/css/app-shell/primitives.css +13 -4
- package/src/css/app-shell/responsive.css +13 -0
- package/src/css/app-shell/responsive2-workspace.css +4 -1
- package/src/css/app-shell/row-print.css +34 -0
- package/src/css/app-shell/states-interactions.css +62 -4
- package/src/css/app-shell/topbar.css +1 -0
- package/src/kits/os/app-panes.css +3 -2
- package/src/kits/os/icons.js +16 -0
- package/src/kits/os/shell-geometry.js +2 -2
- package/src/kits/os/shell.js +24 -3
- package/src/kits/os/theme.css +272 -78
- package/src/kits/os/wm.css +19 -6
- package/src/kits/os/wm.js +38 -1
- package/src/kits/spoint/host-join-lobby.css +3 -3
- 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/src/web-components/freddie-chat.js +2 -2
- package/types/components.d.ts +6 -0
package/src/kits/os/wm.css
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/* ============================================================
|
|
2
2
|
Window Manager visuals — paired with createWM() behavior
|
|
3
3
|
Tokens: --os-* (defined by desktop/theme.css)
|
|
4
|
-
Bible: tonal surfaces,
|
|
4
|
+
Bible: tonal surfaces, inset rail for focus, subtle hairline border +
|
|
5
|
+
elevation shadow on every window (stronger on wm-focused) so windows
|
|
6
|
+
read as distinct layers against a dark desktop background.
|
|
5
7
|
============================================================ */
|
|
6
8
|
|
|
7
9
|
.wm-root {
|
|
@@ -21,14 +23,18 @@
|
|
|
21
23
|
max-height: 100%;
|
|
22
24
|
background: var(--os-bg-1);
|
|
23
25
|
color: var(--os-fg);
|
|
24
|
-
border:
|
|
26
|
+
border: 1px solid color-mix(in oklab, var(--os-fg) 14%, transparent);
|
|
25
27
|
border-radius: var(--os-radius, var(--r-2, 10px));
|
|
26
28
|
display: flex;
|
|
27
29
|
flex-direction: column;
|
|
28
30
|
pointer-events: auto;
|
|
29
31
|
overflow: hidden;
|
|
32
|
+
box-shadow: var(--shadow-2, 0 4px 12px rgba(0, 0, 0, .3));
|
|
33
|
+
}
|
|
34
|
+
.wm-win.wm-focused {
|
|
35
|
+
border-color: color-mix(in oklab, var(--os-fg) 28%, transparent);
|
|
36
|
+
box-shadow: inset 4px 0 0 var(--os-accent), var(--shadow-3, 0 12px 32px rgba(0, 0, 0, .4));
|
|
30
37
|
}
|
|
31
|
-
.wm-win.wm-focused { box-shadow: inset 4px 0 0 var(--os-accent); }
|
|
32
38
|
/* Promote to a compositor layer only for the duration of a drag/resize so the
|
|
33
39
|
per-frame left/top/width/height writes don't force a main-thread layout each
|
|
34
40
|
pointermove. Released on pointerup (class removed) to avoid the permanent
|
|
@@ -60,10 +66,10 @@
|
|
|
60
66
|
text-transform: lowercase;
|
|
61
67
|
}
|
|
62
68
|
|
|
63
|
-
.wm-btns { display: flex; gap: 6px; }
|
|
69
|
+
.wm-btns { display: flex; align-items: center; gap: 6px; }
|
|
64
70
|
.wm-btn {
|
|
65
|
-
width:
|
|
66
|
-
height:
|
|
71
|
+
width: 26px;
|
|
72
|
+
height: 26px;
|
|
67
73
|
border: none;
|
|
68
74
|
background: var(--os-bg-3);
|
|
69
75
|
color: var(--os-fg-2);
|
|
@@ -79,6 +85,13 @@
|
|
|
79
85
|
touch-action: manipulation;
|
|
80
86
|
}
|
|
81
87
|
.wm-btn:hover { background: var(--panel-hover, var(--os-bg-1)); color: var(--os-fg); }
|
|
88
|
+
/* Close gets its own hover color (red) so the destructive action is
|
|
89
|
+
distinguishable from minimize/maximize at a glance, not just by position. */
|
|
90
|
+
.wm-btn[title='close']:hover,
|
|
91
|
+
.wm-btn[aria-label='close']:hover {
|
|
92
|
+
background: var(--color-danger, #C41C0C);
|
|
93
|
+
color: var(--paper, #fff);
|
|
94
|
+
}
|
|
82
95
|
|
|
83
96
|
.wm-body {
|
|
84
97
|
flex: 1;
|
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,
|
|
@@ -62,12 +62,12 @@
|
|
|
62
62
|
background: var(--bg);
|
|
63
63
|
color: var(--fg);
|
|
64
64
|
border: 1px solid var(--bw-rule, var(--rule));
|
|
65
|
-
border-radius: var(--r-1,
|
|
66
|
-
padding:
|
|
65
|
+
border-radius: var(--r-1, 8px);
|
|
66
|
+
padding: var(--space-2) var(--space-2-5);
|
|
67
67
|
font: var(--fs-body) var(--ff-mono);
|
|
68
68
|
outline: none;
|
|
69
69
|
}
|
|
70
|
-
.sp-lobby-input:focus { border-color: var(--accent); }
|
|
70
|
+
.sp-lobby-input:focus-visible { border-color: var(--accent); box-shadow: var(--focus-ring-inset); }
|
|
71
71
|
.sp-lobby-link { font-size: var(--fs-small, 11px); }
|
|
72
72
|
.sp-lobby-code {
|
|
73
73
|
font-family: var(--ff-display);
|
|
@@ -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) {
|
|
@@ -10,8 +10,8 @@ class FreddieChat extends DsChat {
|
|
|
10
10
|
constructor() {
|
|
11
11
|
super();
|
|
12
12
|
this._title = 'freddie';
|
|
13
|
-
this._sub = '';
|
|
14
|
-
this._placeholder = 'message freddie
|
|
13
|
+
this._sub = '/tools · /tool name {json} · /run …';
|
|
14
|
+
this._placeholder = 'message freddie…';
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
connectedCallback() {
|
package/types/components.d.ts
CHANGED
|
@@ -141,6 +141,8 @@ export interface TopbarProps {
|
|
|
141
141
|
active?: string;
|
|
142
142
|
onNav?: (...args: any[]) => any;
|
|
143
143
|
search?: any;
|
|
144
|
+
/** @default true */
|
|
145
|
+
themeToggle?: boolean;
|
|
144
146
|
}
|
|
145
147
|
export declare function Topbar(props?: TopbarProps): VNode;
|
|
146
148
|
|
|
@@ -2037,6 +2039,10 @@ export interface PageViewProps {
|
|
|
2037
2039
|
title?: string;
|
|
2038
2040
|
/** @default '' */
|
|
2039
2041
|
html?: string;
|
|
2042
|
+
/** @default '' */
|
|
2043
|
+
author?: string;
|
|
2044
|
+
/** @default 0 */
|
|
2045
|
+
updatedAt?: number;
|
|
2040
2046
|
/** @default false */
|
|
2041
2047
|
isAdmin?: boolean;
|
|
2042
2048
|
onEdit?: (...args: any[]) => any;
|