anentrypoint-design 0.0.324 → 0.0.326

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/src/components.js CHANGED
@@ -28,7 +28,7 @@ export {
28
28
  export { AgentChat, MESSAGE_CAP } from './components/agent-chat.js';
29
29
 
30
30
  export {
31
- ConversationList, SessionCard, SessionDashboard, SessionMeta, fmtDuration, AgentListSkeleton
31
+ ConversationList, SessionCard, SessionDashboard, SessionMeta, fmtDuration, fmtTime, fmtAgo, AgentListSkeleton
32
32
  } from './components/sessions.js';
33
33
 
34
34
  export { ContextPane } from './components/context-pane.js';
@@ -101,14 +101,10 @@
101
101
  .ds-shortcuts-hint .ds-kbd { white-space: normal; max-width: 100%; }
102
102
  .ds-kbd-caps { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 4px; }
103
103
  .ds-kbd-sep { color: var(--fg-3); font-size: var(--fs-micro); padding: 0 2px; }
104
- .ds-kbd {
105
- display: inline-block; min-width: 0;
106
- padding: 2px 7px; border-radius: var(--r-0);
107
- background: var(--bg); border: var(--bw-hair) solid var(--rule);
108
- border-bottom-width: 2px;
109
- font-family: var(--ff-mono); font-size: var(--fs-micro); color: var(--fg-2);
110
- white-space: nowrap;
111
- }
104
+ /* .ds-kbd base definition lives in editor-primitives.css (canonical home —
105
+ it owns the fuller ds-kbd-group/ds-kbd-row family this key chip belongs
106
+ to); this file only carries the .ds-shortcuts-hint contextual overrides
107
+ above. */
112
108
 
113
109
  /* ============================================================
114
110
  Theme toggle (segmented + compact) — bound to src/theme.js
@@ -92,13 +92,10 @@
92
92
  .ds-sub-btn:hover { border-color: var(--accent); color: var(--accent-ink); }
93
93
  .ds-sub-btn span { display: block; font-size: 18px; font-weight: 700; color: var(--accent-ink); }
94
94
 
95
- /* SessionRow */
96
- .ds-session-row {
97
- display: flex; align-items: center; gap: 8px; padding: 6px 10px;
98
- flex-wrap: wrap; min-width: 0;
99
- border-bottom: var(--bw-hair) solid var(--bg-3); cursor: pointer;
100
- }
101
- .ds-session-row:hover { background: var(--bg-3); }
95
+ /* SessionRow — base .ds-session-row (layout, hover/focus/active states,
96
+ rail-tone indicators) is canonically defined in chat.css alongside the
97
+ rest of the chat session-list component; this file only carries the
98
+ dev/admin-row sub-parts (id/counts/devcnt/span) that decorate it. */
102
99
  .ds-session-row-id {
103
100
  font-family: var(--ff-mono); font-size: var(--fs-micro); color: var(--accent-ink);
104
101
  flex: 0 1 160px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
@@ -391,12 +391,10 @@ table tr.clickable:focus-visible td { background: var(--bg-2); }
391
391
  .row-form input:focus-visible,
392
392
  .row-form textarea:focus-visible { box-shadow: var(--focus-ring-inset); }
393
393
 
394
- /* Field char counter (TextField maxLength) */
395
- .ds-field-count {
396
- font-size: var(--fs-tiny, 13px);
397
- color: var(--fg-3, var(--fg-2));
398
- text-align: right;
399
- }
394
+ /* Field char counter (TextField maxLength) — canonical definition is with
395
+ the rest of the .ds-field family below (states-interactions.css); this
396
+ duplicate was removed to stop the two bodies fighting on stylesheet load
397
+ order. */
400
398
 
401
399
  /* Multi-column form layout (Form columns prop) */
402
400
  .row-form[data-columns="2"] { grid-template-columns: repeat(2, 1fr); }
@@ -3,6 +3,20 @@
3
3
  Comprehensive improvements for perfect UX across all surfaces
4
4
  ============================================================ */
5
5
 
6
+ /* ------------------------------------------------------------
7
+ Table — clickable row focus ring
8
+ Table()'s onRowClick path (content.js) sets class="clickable"
9
+ role="button" tabindex="0" on the <tr>; the browser's native
10
+ :focus-visible outline on a <tr> renders inconsistently across
11
+ engines (often clipped by table border-collapse), so it needs
12
+ the same explicit outline treatment every other interactive
13
+ primitive in this file gets, via the shared --focus-* tokens.
14
+ -------------------------------------------------------------- */
15
+ tr.clickable:focus-visible {
16
+ outline: var(--focus-w) solid var(--focus-color);
17
+ outline-offset: calc(-1 * var(--focus-offset));
18
+ }
19
+
6
20
  /* ------------------------------------------------------------
7
21
  Component States — Disabled, Loading, Error, Success
8
22
  -------------------------------------------------------------- */
@@ -391,14 +405,25 @@ hr.rule-dotted { border-top: 1px dotted var(--rule-strong); }
391
405
  }
392
406
  .ds-field-label {
393
407
  font-size: var(--fs-sm, 14px);
408
+ font-weight: 500;
394
409
  color: var(--fg-2);
395
410
  line-height: 1.3;
396
411
  }
412
+ .ds-field-required {
413
+ color: var(--danger);
414
+ font-weight: 700;
415
+ margin-left: 2px;
416
+ }
397
417
  .ds-field-hint {
398
418
  font-size: var(--fs-tiny, 13px);
399
419
  color: var(--fg-3);
400
420
  line-height: 1.35;
401
421
  }
422
+ .ds-field-error {
423
+ color: var(--danger);
424
+ font-size: var(--fs-tiny, 13px);
425
+ font-weight: 500;
426
+ }
402
427
  .ds-field-count {
403
428
  font-size: var(--fs-tiny, 13px);
404
429
  color: var(--fg-3);
package/src/i18n.js ADDED
@@ -0,0 +1,58 @@
1
+ // Minimal, dependency-free i18n primitive for the design SDK.
2
+ //
3
+ // Mirrors the shape of thebird's `docs/i18n.js` (catalog registry +
4
+ // setLocale/getLocale + t(key, fallbackText, vars) interpolation) so a
5
+ // consumer app that already has its own i18n runtime (thebird, freddie, ...)
6
+ // can either use this directly or wire an equivalent adapter, and so
7
+ // design's own components CAN be made translatable without design owning a
8
+ // specific consumer's catalog.
9
+ //
10
+ // Scope of this pass: the primitive exists and is exported, plus a couple of
11
+ // representative hardcoded strings in one or two components are migrated as
12
+ // proof (see src/components/chat.js). This is NOT an exhaustive sweep of
13
+ // every hardcoded string in the SDK — that is a much larger, incremental
14
+ // effort threaded through call sites over time, the same way thebird's own
15
+ // t() adoption is incremental app-by-app.
16
+ const STORAGE_KEY = 'ds-locale';
17
+ const FALLBACK_LOCALE = 'en';
18
+
19
+ const catalogs = new Map();
20
+
21
+ export function registerLocale(locale, table) {
22
+ catalogs.set(locale, { ...(catalogs.get(locale) || {}), ...table });
23
+ }
24
+
25
+ registerLocale('en', {});
26
+
27
+ export function getLocale() {
28
+ try {
29
+ const stored = typeof localStorage !== 'undefined' && localStorage.getItem(STORAGE_KEY);
30
+ if (stored && catalogs.has(stored)) return stored;
31
+ } catch { /* swallow: localStorage unavailable, fall through to navigator.language detection */ }
32
+ if (typeof navigator !== 'undefined' && navigator.language) {
33
+ const short = navigator.language.slice(0, 2);
34
+ if (catalogs.has(short)) return short;
35
+ }
36
+ return FALLBACK_LOCALE;
37
+ }
38
+
39
+ export function setLocale(locale) {
40
+ try { if (typeof localStorage !== 'undefined') localStorage.setItem(STORAGE_KEY, locale); } catch { /* swallow: localStorage write failed */ }
41
+ }
42
+
43
+ // t(key, fallbackText, vars?) — looks up key in the active locale's catalog;
44
+ // falls back to fallbackText (the literal already at the call site) so
45
+ // adoption is a non-breaking wrap: t('chat.emptySub', 'Send a message to
46
+ // start the conversation') behaves exactly like the bare literal did until a
47
+ // translation is registered for that key.
48
+ export function t(key, fallbackText, vars) {
49
+ const locale = getLocale();
50
+ const table = catalogs.get(locale) || {};
51
+ let str = (key in table) ? table[key] : fallbackText;
52
+ if (vars) for (const k of Object.keys(vars)) str = str.split('{' + k + '}').join(String(vars[k]));
53
+ return str;
54
+ }
55
+
56
+ export function availableLocales() {
57
+ return [...catalogs.keys()];
58
+ }
package/src/index.js CHANGED
@@ -11,12 +11,14 @@ import * as motion from './motion.js';
11
11
  import * as debug from './debug.js';
12
12
  import { renderMarkdown, ensureReady as ensureMarkdownReady, sanitizeHtml, isDegraded as isMarkdownDegraded, configureMarkdownCdn, getMarkdownCdnConfig } from './markdown.js';
13
13
  import { escapeHtml, escapeJson } from './html-escape.js';
14
+ import { uid, shortUid } from './uid.js';
14
15
  import { ensurePrism, highlightAllUnder, configurePrismCdn, getPrismCdnConfig } from './highlight.js';
15
16
  import { renderPageHtml } from './page-html.js';
16
17
  import { HeroFromPageData } from './components/content.js';
17
18
  import { ThemeToggle } from './components/theme-toggle.js';
18
19
  import { mountKit } from './bootstrap.js';
19
20
  import * as theme from './theme.js';
21
+ import { t, registerLocale, getLocale, setLocale, availableLocales } from './i18n.js';
20
22
  import { registerChatElement, DsChat } from './web-components/ds-chat.js';
21
23
  import { registerFreddieChatElement, FreddieChat } from './web-components/freddie-chat.js';
22
24
 
@@ -83,7 +85,9 @@ export {
83
85
  registerFreddieChatElement, FreddieChat,
84
86
  renderPageHtml, HeroFromPageData,
85
87
  escapeHtml, escapeJson,
86
- theme, ThemeToggle
88
+ uid, shortUid,
89
+ theme, ThemeToggle,
90
+ t, registerLocale, getLocale, setLocale, availableLocales
87
91
  };
88
92
  export { applyTheme, getTheme, resolvedTheme, onThemeChange, initTheme,
89
93
  applyAccent, getAccent, applyDensity, getDensity } from './theme.js';
@@ -101,7 +105,8 @@ export {
101
105
  FREDDIE_PAGES,
102
106
  home, chat, voice, sessions, projects, agents, analytics,
103
107
  models, cron, skills, config, env, tools, batch, gateway, chains,
104
- skillLabel, getRecentPaths, saveRecentPath, renderChatMessages
108
+ skillLabel, getRecentPaths, saveRecentPath, renderChatMessages,
109
+ fmtBytes, fmtFileSize, fmtTime, fmtAgo, fmtDuration
105
110
  } from './components.js';
106
111
 
107
112
  export default {
@@ -19,7 +19,7 @@ function parseSseEvents(text) {
19
19
  if (line.startsWith('event: ')) { curEvent = line.slice(7).trim(); }
20
20
  else if (line.startsWith('data: ')) { curData = line.slice(6).trim(); }
21
21
  else if (line === '' && curEvent) {
22
- try { events.push({ event: curEvent, data: JSON.parse(curData) }); } catch {}
22
+ try { events.push({ event: curEvent, data: JSON.parse(curData) }); } catch { /* swallow: a malformed SSE event is dropped, the stream continues */ }
23
23
  curEvent = null; curData = '';
24
24
  }
25
25
  }
@@ -65,7 +65,7 @@ export function makeChatPage(ctx) {
65
65
  const models = Array.isArray(j?.data) ? j.data.map(m => m.id) : [];
66
66
  providers = [{ id: 'acptoapi', name: 'acptoapi gateway (' + baseUrl + ')', configured: true, models: ['auto', ...models] }, ...providers];
67
67
  }
68
- } catch {}
68
+ } catch { /* swallow: probing the local acptoapi gateway is opt-in best-effort, absence just means no providers shown */ }
69
69
  }
70
70
  const configuredProviders = providers.filter(p => p.configured);
71
71
 
@@ -89,7 +89,7 @@ export function makeChatPage(ctx) {
89
89
  };
90
90
 
91
91
  const cancelInFlight = () => {
92
- if (chatState.abort) { try { chatState.abort.abort(); } catch {} chatState.abort = null; }
92
+ if (chatState.abort) { try { chatState.abort.abort(); } catch { /* swallow: the in-flight request may already be settled, abort() is a no-op then */ } chatState.abort = null; }
93
93
  chatState.busy = false;
94
94
  syncMessages();
95
95
  renderPage();
@@ -140,7 +140,7 @@ export function makeChatPage(ctx) {
140
140
  ? ('agent: ' + running + ' (step ' + (stepN + 1) + ')…')
141
141
  : ('agent thinking' + (stepN ? ' (step ' + stepN + ')' : '') + '…');
142
142
  renderPage();
143
- } catch {}
143
+ } catch { /* swallow: progress-indicator update failing must not abort the agent run */ }
144
144
  };
145
145
  const out = await window.__thebirdRunAgent({ prompt: trimmed, onUpdate });
146
146
  const turnMsgs = (out && Array.isArray(out.messages)) ? out.messages : [];
@@ -91,7 +91,7 @@ export function makeToolsPages(ctx) {
91
91
  submit: 'save',
92
92
  onSubmit: async (ev) => {
93
93
  let v = ev.target.elements.value.value;
94
- try { v = JSON.parse(v); } catch {}
94
+ try { v = JSON.parse(v); } catch { /* swallow: value may be a plain string, not JSON — keep it as-is */ }
95
95
  await h0.pi.config.saveValue(ev.target.elements.key.value, v);
96
96
  rerender();
97
97
  },
@@ -30,7 +30,7 @@ export function renderTerminal(opts = {}) {
30
30
  term.open(slot);
31
31
  if (term._addonManager) {
32
32
  const addons = term._addonManager._addons || [];
33
- for (const a of addons) { try { a.instance && a.instance.fit && a.instance.fit(); } catch (_) {} }
33
+ for (const a of addons) { try { a.instance && a.instance.fit && a.instance.fit(); } catch (_) { /* swallow: an addon's fit() failing must not block mounting the terminal */ } }
34
34
  }
35
35
  }
36
36
 
@@ -21,13 +21,13 @@ export function restoreIndex(deckEl, slides) {
21
21
  if (raw == null) return 0;
22
22
  const n = parseInt(raw, 10);
23
23
  if (Number.isFinite(n) && n >= 0 && n < slides.length) return n;
24
- } catch (e) { /* ignore */ }
24
+ } catch (e) { /* swallow: localStorage unavailable or corrupt saved-slide-index — fall back to slide 0 */ }
25
25
  return 0;
26
26
  }
27
27
 
28
28
  export function persistIndex(deckEl, index) {
29
29
  const storageKey = STORAGE_PREFIX + (location.pathname || '/');
30
- try { localStorage.setItem(storageKey, String(index)); } catch (e) {}
30
+ try { localStorage.setItem(storageKey, String(index)); } catch (e) { /* swallow: persistence is best-effort, deck still shows the current slide */ }
31
31
  }
32
32
 
33
33
  export function collectSlides(deckEl, slot) {
@@ -66,7 +66,7 @@ export function applyIndex(deckEl, { index, prevIndex, slides, countEl, showOver
66
66
  if (countEl) countEl.textContent = String(curr + 1);
67
67
  persistIndex(deckEl, curr);
68
68
  if (broadcast) {
69
- try { window.postMessage({ slideIndexChanged: curr }, '*'); } catch (e) {}
69
+ try { window.postMessage({ slideIndexChanged: curr }, '*'); } catch (e) { /* swallow: cross-frame notification is best-effort, local slide state already updated */ }
70
70
  deckEl.dispatchEvent(new CustomEvent('slidechange', {
71
71
  detail: {
72
72
  index: curr, previousIndex: prev, total: slides.length,
package/src/lint.js ADDED
@@ -0,0 +1,10 @@
1
+ // Public re-export of the two source-hygiene lint gates that external
2
+ // consumers (thebird-owned UI files, freddie's GUI plugins) most plausibly
3
+ // want to run in their OWN build/lint setups: the decorative-glyph guard and
4
+ // the webjsx null-children guard. `scripts/` is intentionally not part of
5
+ // the published package surface (see package.json `files`), so this
6
+ // `src/`-located re-export is the published entry point — the actual lint
7
+ // logic still lives in scripts/lint-glyphs.mjs and scripts/lint-null-children.mjs
8
+ // and is not duplicated here.
9
+ export { lintGlyphsOrThrow } from '../scripts/lint-glyphs.mjs';
10
+ export { lintNullChildrenOrThrow } from '../scripts/lint-null-children.mjs';
package/src/page-html.js CHANGED
@@ -67,15 +67,75 @@ function joinHref(basePath, href) {
67
67
  return base + '/' + h.replace(/^\.?\//, '');
68
68
  }
69
69
 
70
+ // Full SEO/OG/twitter/schema.org meta block, extracted so renderPageHtml
71
+ // consumers (design's own marketing site, thebird's landing) can opt in
72
+ // instead of hand-rolling ~40 lines of <meta> tags per theme.mjs.
73
+ function renderSeoTags({ title, siteName, seo }) {
74
+ const desc = escape(seo.description || '');
75
+ const lang = escape(seo.lang || 'en');
76
+ const url = escape(seo.url || '');
77
+ const image = escape(seo.image || '');
78
+ const author = escape(seo.author || siteName);
79
+ const twitter = escape(seo.twitter || '');
80
+ const locale = escape(seo.locale || 'en_US');
81
+ const keywords = escape(Array.isArray(seo.keywords) ? seo.keywords.join(', ') : (seo.keywords || ''));
82
+ const ogTitle = escape(title);
83
+ const ogDesc = escape(seo.description || siteName);
84
+ let out = `
85
+ <meta name="description" content="${desc}">
86
+ ${keywords ? `<meta name="keywords" content="${keywords}">` : ''}
87
+ <meta name="author" content="${author}">
88
+ <meta name="robots" content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1">
89
+ <meta name="generator" content="anentrypoint-design">
90
+ ${url ? `<link rel="canonical" href="${url}">` : ''}
91
+ <meta property="og:type" content="website">
92
+ <meta property="og:title" content="${ogTitle}">
93
+ <meta property="og:description" content="${ogDesc}">
94
+ ${url ? `<meta property="og:url" content="${url}">` : ''}
95
+ <meta property="og:site_name" content="${escape(siteName)}">
96
+ <meta property="og:locale" content="${locale}">
97
+ ${image ? `<meta property="og:image" content="${image}">` : ''}
98
+ <meta name="twitter:card" content="summary_large_image">
99
+ <meta name="twitter:title" content="${ogTitle}">
100
+ <meta name="twitter:description" content="${ogDesc}">
101
+ ${twitter ? `<meta name="twitter:site" content="${twitter}">` : ''}
102
+ ${image ? `<meta name="twitter:image" content="${image}">` : ''}`;
103
+ if (seo.ldJson !== false && (seo.description || url)) {
104
+ const ld = JSON.stringify({
105
+ '@context': 'https://schema.org', '@type': 'WebSite',
106
+ name: title, url: seo.url || '', description: seo.description || '', inLanguage: seo.lang || 'en',
107
+ }).replace(/</g, '\\u003c');
108
+ out += `\n<script type="application/ld+json">${ld}</script>`;
109
+ }
110
+ return out;
111
+ }
112
+
70
113
  export function renderPageHtml({
71
114
  title = '247420', slug = 'index', siteName = '247420',
72
115
  navItems = [], basePath = '',
73
116
  hero, sections, examples, body,
74
- theme = 'auto', cssHref, headExtra = ''
117
+ theme = 'auto', cssHref, headExtra = '',
118
+ // Extended affordances (all optional, all backward compatible — a call
119
+ // site that omits them gets byte-identical output to before these were
120
+ // added). See design/site/theme.mjs and thebird/site/theme.mjs for
121
+ // consumers of the full surface.
122
+ seo = null, // { description, keywords, author, twitter, locale, lang, image, url, glyph, ldJson:boolean }
123
+ sidebar = null, // { sections: [{ group, items: [{glyph,label,href}] }] } -> C.Side
124
+ marquee = null, // { items: [...strings], sep }
125
+ panels = null, // [{ id, title, count, items: [{code,title,sub,meta,href}] }] -> C.Panel + RowLink rows
126
+ quickstart = null, // { heading, lines: [{ kind, text }] } -> cli block panel
127
+ statusLeft = null, // override the default [siteName.toLowerCase(), slug] status-bar left cluster
128
+ statusRight = null, // override the default ['live'] status-bar right cluster
129
+ faviconHref = null, // static favicon URL (e.g. './favicon.svg'); takes precedence over faviconGlyph
130
+ faviconGlyph = null, // single-character/emoji favicon rendered as an inline data: SVG
131
+ clientScriptExtra = '', // raw JS appended after the mount() call in the client <script type="module">
132
+ version = null, // pin BOTH the CSS href and the JS importmap to this exact version
133
+ // instead of @latest (e.g. '0.0.320'); omitted -> default @latest behavior.
75
134
  } = {}) {
135
+ const pkgVersion = version || 'latest';
76
136
  const cssLink = cssHref
77
137
  ? `<link rel="stylesheet" href="${cssHref}">`
78
- : `<link rel="stylesheet" href="https://unpkg.com/anentrypoint-design@latest/dist/247420.css">`;
138
+ : `<link rel="stylesheet" href="https://unpkg.com/anentrypoint-design@${pkgVersion}/dist/247420.css">`;
79
139
 
80
140
  // Resolve nav hrefs server-side against basePath. Client receives final URLs.
81
141
  const navResolved = (Array.isArray(navItems) ? navItems : []).map(([label, href]) =>
@@ -90,8 +150,21 @@ export function renderPageHtml({
90
150
  sections: Array.isArray(sections) ? sections : [],
91
151
  examples: Array.isArray(examples) ? examples : [],
92
152
  bodyHtml: body ? renderMarkdown(body) : '',
153
+ sidebar: sidebar || null,
154
+ marquee: marquee || null,
155
+ panels: Array.isArray(panels) ? panels : [],
156
+ quickstart: quickstart || null,
157
+ statusLeft: Array.isArray(statusLeft) ? statusLeft : null,
158
+ statusRight: Array.isArray(statusRight) ? statusRight : null,
93
159
  };
94
160
 
161
+ const seoTags = seo ? renderSeoTags({ title, siteName, seo }) : '';
162
+ const faviconTags = faviconHref
163
+ ? `<link rel="icon" href="${escape(faviconHref)}">`
164
+ : (faviconGlyph
165
+ ? `<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Ctext y='26' font-size='26'%3E${encodeURIComponent(faviconGlyph)}%3C/text%3E%3C/svg%3E">`
166
+ : '');
167
+
95
168
  // Theme attribute co-location is CORRECT here: dist/247420.css keys every
96
169
  // theme block off the COMPOUND selector `.ds-247420[data-theme="X"]`
97
170
  // (verified in dist/247420.css ~L229). That selector requires BOTH the class
@@ -107,9 +180,11 @@ export function renderPageHtml({
107
180
  <meta charset="utf-8">
108
181
  <meta name="viewport" content="width=device-width, initial-scale=1">
109
182
  <title>${escape(title)} — ${escape(siteName)}</title>
183
+ ${seoTags}
184
+ ${faviconTags}
110
185
  ${cssLink}
111
186
  <script type="importmap">
112
- { "imports": { "anentrypoint-design": "https://unpkg.com/anentrypoint-design@latest/dist/247420.js" } }
187
+ { "imports": { "anentrypoint-design": "https://unpkg.com/anentrypoint-design@${pkgVersion}/dist/247420.js" } }
113
188
  </script>
114
189
  <style>
115
190
  .app-stage { width: 100%; max-width: var(--stage-wide, min(96vw, 1440px)); margin-inline: auto; padding: var(--space-6, 48px) var(--space-4, 24px) var(--space-8, 96px); display: grid; gap: var(--space-6, 48px); box-sizing: border-box }
@@ -218,6 +293,40 @@ function examplesNode(examples) {
218
293
  });
219
294
  }
220
295
 
296
+ function panelNode(panel, idx) {
297
+ const items = Array.isArray(panel.items) ? panel.items : [];
298
+ if (!items.length) return null;
299
+ const rows = items.map((it, i) => {
300
+ const kids = [
301
+ h('span', { key: 'c', class: 'code' }, String(it.code || String(i + 1).padStart(2, '0'))),
302
+ h('span', { key: 't', class: 'title' }, String(it.title || it.name || '')),
303
+ ];
304
+ if (it.sub || it.desc) kids.push(h('span', { key: 'm', class: 'meta dim' }, ' — ' + (it.sub || it.desc)));
305
+ kids.push(h('span', { key: 'a', class: 'ds-row-arrow' }, it.meta || '->'));
306
+ return h('a', { key: i, class: 'row', href: it.href || '#' }, ...kids);
307
+ });
308
+ return C.Panel({ id: panel.id || null, title: panel.title || panel.name || '', count: panel.count || items.length, children: rows });
309
+ }
310
+
311
+ function marqueeNode(marquee) {
312
+ if (!marquee || !Array.isArray(marquee.items) || !marquee.items.length) return null;
313
+ return C.Marquee ? C.Marquee({ items: marquee.items, sep: marquee.sep || '/' }) : null;
314
+ }
315
+
316
+ function quickstartNode(quickstart) {
317
+ if (!quickstart || !Array.isArray(quickstart.lines) || !quickstart.lines.length) return null;
318
+ const lineNodes = quickstart.lines.map((l, i) => h('div', { key: 'q' + i, class: 'cli' },
319
+ h('span', { class: 'prompt' }, l.kind === 'cmt' ? '#' : '$'),
320
+ h('span', { class: 'cmd' }, l.text)
321
+ ));
322
+ return C.Panel({ title: quickstart.heading || 'quick start', children: h('div', { class: 'ds-quickstart' }, ...lineNodes) });
323
+ }
324
+
325
+ function sideNode(sidebar) {
326
+ if (!sidebar || !Array.isArray(sidebar.sections) || !sidebar.sections.length || !C.Side) return null;
327
+ return C.Side({ sections: sidebar.sections });
328
+ }
329
+
221
330
  // minimal client-side markdown renderer matching server-side renderer (idempotent for already-html bodies)
222
331
  function __slug(s) { return String(s || '').trim().toLowerCase().replace(/[^\\w\\s-]/g, '').replace(/\\s+/g, '-'); }
223
332
  function __md(md) {
@@ -243,7 +352,10 @@ const bodyNode = data.bodyHtml ? C.Section({ children: h('div', { class: 'page-b
243
352
 
244
353
  const mainChildren = [
245
354
  heroNode(data.hero),
355
+ marqueeNode(data.marquee),
246
356
  ...data.sections.map(sectionNode),
357
+ ...(data.panels || []).map(panelNode),
358
+ quickstartNode(data.quickstart),
247
359
  examplesNode(data.examples),
248
360
  bodyNode,
249
361
  ].filter(Boolean);
@@ -251,9 +363,14 @@ const mainChildren = [
251
363
  mount(document.getElementById('app'), () => C.AppShell({
252
364
  topbar: C.Topbar({ brand: data.siteName, items: data.navItems, active: data.title }),
253
365
  crumb: C.Crumb({ leaf: data.title }),
366
+ side: sideNode(data.sidebar),
254
367
  main: h('div', { class: 'app-stage' }, ...mainChildren),
255
- status: C.Status({ left: [data.siteName.toLowerCase(), data.slug], right: ['live'] }),
368
+ status: C.Status({
369
+ left: data.statusLeft || [data.siteName.toLowerCase(), data.slug],
370
+ right: data.statusRight || ['live'],
371
+ }),
256
372
  }));
373
+ ${clientScriptExtra}
257
374
  </script>
258
375
  </body>
259
376
  </html>`;
package/src/theme.js CHANGED
@@ -37,7 +37,7 @@ function readStored() {
37
37
  }
38
38
 
39
39
  function writeStored(mode) {
40
- try { window.localStorage.setItem(KEY, mode); } catch {}
40
+ try { window.localStorage.setItem(KEY, mode); } catch { /* swallow: persistence is best-effort, theme still applies in-memory */ }
41
41
  }
42
42
 
43
43
  function writeAttr(mode) {
@@ -53,7 +53,7 @@ function ensureMq() {
53
53
  // Re-emit so listeners can re-render derived UI even though
54
54
  // data-theme stays "auto" — the CSS @media handles the swap.
55
55
  for (const cb of listeners) {
56
- try { cb({ mode: 'auto', resolved: _mq.matches ? 'ink' : 'paper' }); } catch {}
56
+ try { cb({ mode: 'auto', resolved: _mq.matches ? 'ink' : 'paper' }); } catch { /* swallow: a listener's error must not block notifying the rest */ }
57
57
  }
58
58
  }
59
59
  };
@@ -71,7 +71,7 @@ export function applyTheme(mode) {
71
71
  ? (_mq && _mq.matches ? 'ink' : 'paper')
72
72
  : mode;
73
73
  for (const cb of listeners) {
74
- try { cb({ mode, resolved }); } catch {}
74
+ try { cb({ mode, resolved }); } catch { /* swallow: a listener's error must not block notifying the rest */ }
75
75
  }
76
76
  return mode;
77
77
  }
@@ -101,11 +101,11 @@ export function applyAccent(accent) {
101
101
  if (!isBrowser()) return accent;
102
102
  if (VALID_ACCENT.has(accent)) {
103
103
  document.documentElement.setAttribute('data-accent', accent);
104
- try { window.localStorage.setItem(ACCENT_KEY, accent); } catch {}
104
+ try { window.localStorage.setItem(ACCENT_KEY, accent); } catch { /* swallow: persistence is best-effort, accent still applies in-memory */ }
105
105
  } else {
106
106
  // No accent attribute = the theme's default accent (green).
107
107
  document.documentElement.removeAttribute('data-accent');
108
- try { window.localStorage.removeItem(ACCENT_KEY); } catch {}
108
+ try { window.localStorage.removeItem(ACCENT_KEY); } catch { /* swallow: persistence is best-effort, accent still applies in-memory */ }
109
109
  }
110
110
  return accent;
111
111
  }
@@ -119,7 +119,7 @@ export function applyDensity(density) {
119
119
  if (!isBrowser()) return density;
120
120
  if (VALID_DENSITY.has(density)) {
121
121
  document.documentElement.setAttribute('data-density', density);
122
- try { window.localStorage.setItem(DENSITY_KEY, density); } catch {}
122
+ try { window.localStorage.setItem(DENSITY_KEY, density); } catch { /* swallow: persistence is best-effort, density still applies in-memory */ }
123
123
  }
124
124
  return density;
125
125
  }
@@ -148,5 +148,5 @@ export function initTheme() {
148
148
 
149
149
  if (isBrowser()) {
150
150
  // Run on next microtask so SSR-injected attributes settle first.
151
- Promise.resolve().then(() => { try { initTheme(); } catch {} });
151
+ Promise.resolve().then(() => { try { initTheme(); } catch { /* swallow: deferred init is a progressive enhancement, page already rendered without it */ } });
152
152
  }
package/src/uid.js ADDED
@@ -0,0 +1,16 @@
1
+ // Single source of quasi-unique id generation for the whole SDK and every
2
+ // static-site consumer, instead of each call site pasting its own
3
+ // Math.random().toString(36) one-liner. Prefers crypto.randomUUID() (both
4
+ // browser and modern Node have this); falls back to a Math.random-based
5
+ // id only when crypto.randomUUID is unavailable (older runtimes).
6
+ export function uid() {
7
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') return crypto.randomUUID();
8
+ return Math.random().toString(36).slice(2) + Date.now().toString(36);
9
+ }
10
+
11
+ // Short id variant for call sites that just want a compact suffix (e.g.
12
+ // DOM ids, temp-file names) rather than a full UUID. `len` caps the
13
+ // random-part length (default 8, matching the common `.slice(2, 10)` idiom).
14
+ export function shortUid(len = 8) {
15
+ return uid().replace(/-/g, '').slice(0, len);
16
+ }