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/app-shell.css +3 -3
- package/dist/247420.css +55 -46
- package/dist/247420.js +82 -19
- package/package.json +7 -1
- package/scripts/lint-glyphs.mjs +105 -0
- package/scripts/lint-null-children.mjs +129 -0
- package/src/community-app.js +3 -3
- package/src/components/chat-message-parts.js +4 -3
- package/src/components/chat.js +5 -4
- package/src/components/content.js +9 -5
- package/src/components/editor-primitives.js +2 -2
- package/src/components/files-modals.js +5 -4
- package/src/components/freddie/helpers.js +1 -1
- package/src/components/freddie.js +1 -9
- package/src/components/interaction-primitives.js +6 -6
- package/src/components/sessions.js +15 -0
- package/src/components/shell.js +8 -8
- package/src/components/theme-toggle.js +2 -2
- package/src/components.js +1 -1
- package/src/css/app-shell/catalog-theme.css +4 -8
- package/src/css/app-shell/data-density.css +4 -7
- package/src/css/app-shell/hero-content.css +4 -6
- package/src/css/app-shell/states-interactions.css +25 -0
- package/src/i18n.js +58 -0
- package/src/index.js +7 -2
- package/src/kits/os/freddie/pages-chat.js +4 -4
- package/src/kits/os/freddie/pages-tools.js +1 -1
- package/src/kits/os/terminal-app.js +1 -1
- package/src/kits/slides/deck-stage-state.js +3 -3
- package/src/lint.js +10 -0
- package/src/page-html.js +121 -4
- package/src/theme.js +7 -7
- package/src/uid.js +16 -0
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
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) { /*
|
|
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
|
|
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
|
|
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({
|
|
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
|
+
}
|