anentrypoint-design 0.0.413 → 0.0.415
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/247420.js +63 -13
- package/dist/app-shell.css +40 -594
- package/dist/colors_and_type.css +607 -47
- package/dist/index.html +227 -277
- package/dist/preview/buttons.html +39 -28
- package/dist/preview/colors-core.html +18 -8
- package/dist/preview/colors-lore.html +26 -17
- package/dist/preview/colors-semantic.html +24 -18
- package/dist/preview/dateline.html +6 -5
- package/dist/preview/dropzone.html +4 -3
- package/dist/preview/file-grid.html +10 -9
- package/dist/preview/file-row.html +14 -13
- package/dist/preview/file-toolbar.html +4 -3
- package/dist/preview/file-viewer.html +4 -3
- package/dist/preview/header.html +26 -24
- package/dist/preview/icons-unicode.html +8 -5
- package/dist/preview/index-row.html +17 -9
- package/dist/preview/inputs.html +4 -3
- package/dist/preview/manifesto.html +34 -17
- package/dist/preview/motion-default.js +77 -57
- package/dist/preview/rules.html +4 -3
- package/dist/preview/spacing.html +13 -12
- package/dist/preview/stamps-lore.html +5 -4
- package/dist/preview/stamps.html +4 -3
- package/dist/preview/theme-ink.html +7 -6
- package/dist/preview/type-display.html +14 -5
- package/dist/preview/type-mono.html +6 -5
- package/dist/preview/type-prose.html +12 -10
- package/dist/preview/type-scale.html +4 -3
- package/dist/preview/wordmarks.html +10 -7
- package/dist/site/content/globals/site.yaml +5 -3
- package/dist/site/content/pages/freddie.yaml +2 -2
- package/dist/site/content/pages/home.yaml +55 -55
- package/dist/site/theme.mjs +96 -348
- package/dist/slides/index.html +13 -13
- package/dist/src/bootstrap.js +10 -1
- package/dist/src/components/chat.js +43 -195
- package/dist/src/components/community.js +18 -166
- package/dist/src/components/content.js +30 -210
- package/dist/src/components/files-modals.js +14 -106
- package/dist/src/components/files.js +15 -116
- package/dist/src/components/freddie/helpers.js +53 -8
- package/dist/src/components/freddie.js +35 -25
- package/dist/src/components/shell.js +16 -114
- package/dist/src/components/theme-toggle.js +69 -25
- package/dist/src/components.js +92 -13
- package/dist/src/deck-stage.js +6 -4
- package/dist/src/highlight.js +51 -7
- package/dist/src/index.js +58 -12
- package/dist/src/kits/os/about-app.js +2 -3
- package/dist/src/kits/os/app-panes.css +46 -70
- package/dist/src/kits/os/browser-app.js +28 -1
- package/dist/src/kits/os/files-app.js +7 -0
- package/dist/src/kits/os/freddie/pages-chat.js +90 -75
- package/dist/src/kits/os/freddie/pages-core.js +9 -9
- package/dist/src/kits/os/freddie/pages-os.js +8 -8
- package/dist/src/kits/os/freddie/pages-tools.js +14 -14
- package/dist/src/kits/os/freddie/routes.js +21 -19
- package/dist/src/kits/os/freddie-dashboard.css +42 -21
- package/dist/src/kits/os/freddie-dashboard.js +6 -6
- package/dist/src/kits/os/icons.js +38 -16
- package/dist/src/kits/os/index.js +11 -0
- package/dist/src/kits/os/launcher.css +26 -10
- package/dist/src/kits/os/launcher.js +3 -0
- package/dist/src/kits/os/shell.js +72 -108
- package/dist/src/kits/os/terminal-app.js +1 -1
- package/dist/src/kits/os/theme.css +1330 -150
- package/dist/src/kits/os/wm.css +68 -15
- package/dist/src/kits/os/wm.js +93 -17
- package/dist/src/markdown.js +121 -12
- package/dist/src/motion.js +18 -2
- package/dist/src/page-html.js +62 -134
- package/dist/src/theme.js +106 -13
- package/dist/src/web-components/ds-chat.js +80 -9
- package/dist/ui_kits/aicat/app.js +54 -6
- package/dist/ui_kits/aicat/index.html +2 -0
- package/dist/ui_kits/blog/index.html +33 -17
- package/dist/ui_kits/chat/app.js +90 -22
- package/dist/ui_kits/chat/index.html +2 -0
- package/dist/ui_kits/community/app.js +86 -24
- package/dist/ui_kits/community/index.html +1 -0
- package/dist/ui_kits/dashboard/app.js +123 -34
- package/dist/ui_kits/dashboard/index.html +1 -0
- package/dist/ui_kits/docs/index.html +43 -33
- package/dist/ui_kits/error_404/app.js +78 -23
- package/dist/ui_kits/file_browser/README.md +1 -1
- package/dist/ui_kits/file_browser/app.js +45 -10
- package/dist/ui_kits/file_browser/index.html +1 -0
- package/dist/ui_kits/gallery/app.js +130 -51
- package/dist/ui_kits/homepage/app.js +156 -138
- package/dist/ui_kits/project_page/app.js +142 -125
- package/dist/ui_kits/project_page/index.html +2 -1
- package/dist/ui_kits/search/app.js +52 -15
- package/dist/ui_kits/settings/app.js +166 -39
- package/dist/ui_kits/signin/app.js +175 -49
- package/dist/ui_kits/signin/index.html +1 -0
- package/dist/ui_kits/slide_deck/app.js +83 -49
- package/dist/ui_kits/system_primer/app.js +57 -40
- package/dist/ui_kits/terminal/app.js +160 -43
- package/package.json +1 -1
- package/src/page-html/client-script.js +40 -11
- package/src/page-html/page-styles.js +22 -1
- package/src/page-html.js +10 -0
package/dist/src/page-html.js
CHANGED
|
@@ -15,57 +15,53 @@
|
|
|
15
15
|
// theme: 'auto' | 'light' | 'ink',
|
|
16
16
|
// cssHref, headExtra,
|
|
17
17
|
// })
|
|
18
|
+
//
|
|
19
|
+
// This module is a barrel over ./page-html/: the server-side markdown+href
|
|
20
|
+
// helpers, the <head> tag builders, the inline <style> block, and the client
|
|
21
|
+
// mount script string. The public export surface here is unchanged — no
|
|
22
|
+
// consumer import needs to move.
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function inlineMd(s) {
|
|
24
|
-
return s
|
|
25
|
-
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
26
|
-
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
|
27
|
-
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
|
28
|
-
}
|
|
24
|
+
import { escape, inlineMd, slugify, renderMarkdown, joinHref } from './page-html/markdown.js';
|
|
25
|
+
import { renderSeoTags, renderFaviconTags, renderCssLink } from './page-html/head-tags.js';
|
|
26
|
+
import { PAGE_INLINE_STYLES } from './page-html/page-styles.js';
|
|
27
|
+
import { CLIENT_SCRIPT } from './page-html/client-script.js';
|
|
29
28
|
|
|
30
|
-
export
|
|
31
|
-
const lines = String(md || '').split('\n');
|
|
32
|
-
const out = [];
|
|
33
|
-
let inCode = false, inList = false;
|
|
34
|
-
for (const line of lines) {
|
|
35
|
-
if (line.startsWith('```')) { if (inCode) { out.push('</pre>'); inCode = false; } else { out.push('<pre>'); inCode = true; } continue; }
|
|
36
|
-
if (inCode) { out.push(escape(line)); continue; }
|
|
37
|
-
if (line.startsWith('# ')) out.push(`<h1>${escape(line.slice(2))}</h1>`);
|
|
38
|
-
else if (line.startsWith('## ')) out.push(`<h2>${escape(line.slice(3))}</h2>`);
|
|
39
|
-
else if (line.startsWith('### ')) out.push(`<h3>${escape(line.slice(4))}</h3>`);
|
|
40
|
-
else if (line.startsWith('- ')) { if (!inList) { out.push('<ul>'); inList = true; } out.push(`<li>${inlineMd(escape(line.slice(2)))}</li>`); }
|
|
41
|
-
else { if (inList) { out.push('</ul>'); inList = false; } if (line.trim()) out.push(`<p>${inlineMd(escape(line))}</p>`); }
|
|
42
|
-
}
|
|
43
|
-
if (inList) out.push('</ul>');
|
|
44
|
-
if (inCode) out.push('</pre>');
|
|
45
|
-
return out.join('\n');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Join a basePath prefix to a nav href. Absolute URLs and hash links pass
|
|
49
|
-
// through unchanged; leading-slash paths get the prefix.
|
|
50
|
-
function joinHref(basePath, href) {
|
|
51
|
-
if (!href) return '#';
|
|
52
|
-
const h = String(href);
|
|
53
|
-
if (/^([a-z]+:|#|\/\/)/i.test(h)) return h;
|
|
54
|
-
if (!basePath) return h;
|
|
55
|
-
const base = basePath.replace(/\/+$/, '');
|
|
56
|
-
if (h.startsWith('/')) return base + h;
|
|
57
|
-
return base + '/' + h.replace(/^\.?\//, '');
|
|
58
|
-
}
|
|
29
|
+
export { escape, inlineMd, slugify, renderMarkdown };
|
|
59
30
|
|
|
60
31
|
export function renderPageHtml({
|
|
61
32
|
title = '247420', slug = 'index', siteName = '247420',
|
|
62
33
|
navItems = [], basePath = '',
|
|
63
34
|
hero, sections, examples, body,
|
|
64
|
-
theme = 'auto', cssHref, headExtra = ''
|
|
35
|
+
theme = 'auto', cssHref, headExtra = '',
|
|
36
|
+
// Extended affordances (all optional, all backward compatible — a call
|
|
37
|
+
// site that omits them gets byte-identical output to before these were
|
|
38
|
+
// added). See design/site/theme.mjs and thebird/site/theme.mjs for
|
|
39
|
+
// consumers of the full surface.
|
|
40
|
+
seo = null, // { description, keywords, author, twitter, locale, lang, image, url, glyph, ldJson:boolean }
|
|
41
|
+
sidebar = null, // { sections: [{ group, items: [{glyph,label,href}] }] } -> C.Side
|
|
42
|
+
marquee = null, // { items: [...strings], sep }
|
|
43
|
+
panels = null, // [{ id, title, count, items: [{code,title,sub,meta,href}] }] -> C.Panel + RowLink rows
|
|
44
|
+
quickstart = null, // { heading, lines: [{ kind, text }] } -> cli block panel
|
|
45
|
+
statusLeft = null, // override the default [siteName.toLowerCase(), slug] status-bar left cluster
|
|
46
|
+
statusRight = null, // override the default ['live'] status-bar right cluster
|
|
47
|
+
faviconHref = null, // static favicon URL (e.g. './favicon.svg'); takes precedence over faviconGlyph
|
|
48
|
+
faviconGlyph = null, // single-character/emoji favicon rendered as an inline data: SVG
|
|
49
|
+
clientScriptExtra = '', // raw JS appended after the mount() call in the client <script type="module">
|
|
50
|
+
version = null, // pin BOTH the CSS href and the JS importmap to this exact version
|
|
51
|
+
// instead of @latest (e.g. '0.0.320'); omitted -> default @latest behavior.
|
|
52
|
+
// Fleet policy is @latest everywhere so a published fix reaches every
|
|
53
|
+
// consumer without redeploying it: passing this pins EVERY page this
|
|
54
|
+
// call generates, and a pinned page silently stops receiving fixes.
|
|
65
55
|
} = {}) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
56
|
+
if (version != null && process.env.ANENTRYPOINT_ALLOW_PIN !== '1') {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`renderPageHtml({version: '${version}'}) pins every generated page to one release, ` +
|
|
59
|
+
'which opts the whole surface out of published fixes. Fleet policy is @latest. ' +
|
|
60
|
+
'Set ANENTRYPOINT_ALLOW_PIN=1 to override deliberately.'
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
const pkgVersion = version || 'latest';
|
|
64
|
+
const cssLink = renderCssLink({ cssHref, pkgVersion });
|
|
69
65
|
|
|
70
66
|
// Resolve nav hrefs server-side against basePath. Client receives final URLs.
|
|
71
67
|
const navResolved = (Array.isArray(navItems) ? navItems : []).map(([label, href]) =>
|
|
@@ -80,22 +76,40 @@ export function renderPageHtml({
|
|
|
80
76
|
sections: Array.isArray(sections) ? sections : [],
|
|
81
77
|
examples: Array.isArray(examples) ? examples : [],
|
|
82
78
|
bodyHtml: body ? renderMarkdown(body) : '',
|
|
79
|
+
sidebar: sidebar || null,
|
|
80
|
+
marquee: marquee || null,
|
|
81
|
+
panels: Array.isArray(panels) ? panels : [],
|
|
82
|
+
quickstart: quickstart || null,
|
|
83
|
+
statusLeft: Array.isArray(statusLeft) ? statusLeft : null,
|
|
84
|
+
statusRight: Array.isArray(statusRight) ? statusRight : null,
|
|
83
85
|
};
|
|
84
86
|
|
|
87
|
+
const seoTags = seo ? renderSeoTags({ title, siteName, seo }) : '';
|
|
88
|
+
const faviconTags = renderFaviconTags({ faviconHref, faviconGlyph });
|
|
89
|
+
|
|
90
|
+
// Theme attribute co-location is CORRECT here: dist/247420.css keys every
|
|
91
|
+
// theme block off the COMPOUND selector `.ds-247420[data-theme="X"]`
|
|
92
|
+
// (verified in dist/247420.css ~L229). That selector requires BOTH the class
|
|
93
|
+
// and the data-theme on the SAME node, so `<html class="ds-247420"
|
|
94
|
+
// data-theme=...>` is what the CSS expects for SSR. The AGENTS.md
|
|
95
|
+
// "descendant-selector" warning is about the dashboard's RUNTIME controller,
|
|
96
|
+
// which splits them (.ds-247420 on <html>, data-theme on <body>) and relies
|
|
97
|
+
// on inheritance — a different mechanism. Do NOT move data-theme to <body>
|
|
98
|
+
// here or the theme blocks stop matching.
|
|
85
99
|
return `<!doctype html>
|
|
86
100
|
<html lang="en" class="ds-247420" data-theme="${theme}">
|
|
87
101
|
<head>
|
|
88
102
|
<meta charset="utf-8">
|
|
89
103
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
90
104
|
<title>${escape(title)} — ${escape(siteName)}</title>
|
|
105
|
+
${seoTags}
|
|
106
|
+
${faviconTags}
|
|
91
107
|
${cssLink}
|
|
92
108
|
<script type="importmap">
|
|
93
|
-
{ "imports": { "anentrypoint-design": "https://unpkg.com/anentrypoint-design
|
|
109
|
+
{ "imports": { "anentrypoint-design": "https://unpkg.com/anentrypoint-design@${pkgVersion}/dist/247420.js" } }
|
|
94
110
|
</script>
|
|
95
111
|
<style>
|
|
96
|
-
|
|
97
|
-
.page-body h1 { margin-top: 0 } .page-body h2 { margin-top: 32px } .page-body h3 { margin-top: 24px }
|
|
98
|
-
.page-body pre { margin: 12px 0; background: var(--panel-2); padding: 12px; border-radius: 8px; overflow-x: auto }
|
|
112
|
+
${PAGE_INLINE_STYLES}
|
|
99
113
|
</style>
|
|
100
114
|
<script id="__site__" type="application/json">${JSON.stringify(pageData).replace(/</g, '\\u003c')}</script>
|
|
101
115
|
${headExtra}
|
|
@@ -103,93 +117,7 @@ ${headExtra}
|
|
|
103
117
|
<body>
|
|
104
118
|
<div id="app"></div>
|
|
105
119
|
<script type="module">
|
|
106
|
-
|
|
107
|
-
const data = JSON.parse(document.getElementById('__site__').textContent);
|
|
108
|
-
const RAILS = ['rail-green', 'rail-purple', 'rail-mascot', 'rail-sun', 'rail-flame', 'rail-sky'];
|
|
109
|
-
|
|
110
|
-
function heroNode(hero) {
|
|
111
|
-
if (!hero) return null;
|
|
112
|
-
return C.Hero({
|
|
113
|
-
eyebrow: hero.eyebrow,
|
|
114
|
-
title: hero.heading || hero.title || data.title,
|
|
115
|
-
body: hero.body || hero.subheading || '',
|
|
116
|
-
accent: hero.accent,
|
|
117
|
-
badge: Array.isArray(hero.badges) && hero.badges[0] ? hero.badges[0].label : undefined,
|
|
118
|
-
actions: Array.isArray(hero.ctas) ? hero.ctas.map((c, i) => h('a', { key: i, class: i === 0 ? 'btn btn-accent' : 'btn btn-ghost', href: c.href || '#' }, c.label || c.cta || 'go')) : null,
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function sectionNode(sec, idx) {
|
|
123
|
-
const rail = RAILS[idx % RAILS.length];
|
|
124
|
-
const features = sec.features || sec.items || [];
|
|
125
|
-
const rows = features.map((f, i) => {
|
|
126
|
-
const kids = [h('span', { key: 't', class: 'title' }, String(f.name || ''))];
|
|
127
|
-
if (f.desc) kids.push(h('div', { key: 'd', class: 'sub', innerHTML: String(f.desc).replace(/\`([^\`]+)\`/g, '<code>$1</code>') }));
|
|
128
|
-
if (f.benefit) kids.push(h('div', { key: 'b', class: 'row-benefit' }, String(f.benefit)));
|
|
129
|
-
return h('div', { key: i, class: 'row ' + rail }, ...kids);
|
|
130
|
-
});
|
|
131
|
-
return C.Section({
|
|
132
|
-
title: sec.name || sec.title || sec.id,
|
|
133
|
-
children: [
|
|
134
|
-
sec.lede ? h('p', { class: 'ds-lede' }, sec.lede) : null,
|
|
135
|
-
...rows,
|
|
136
|
-
sec.body && sec.body.length >= 240 ? h('div', { class: 'page-body', innerHTML: __md(sec.body) }) : null,
|
|
137
|
-
].filter(Boolean),
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function examplesNode(examples) {
|
|
142
|
-
if (!examples || !examples.length) return null;
|
|
143
|
-
return C.Section({
|
|
144
|
-
title: 'explore',
|
|
145
|
-
children: examples.map((e, i) => {
|
|
146
|
-
const rail = RAILS[(i + 1) % RAILS.length];
|
|
147
|
-
const kids = [
|
|
148
|
-
h('span', { key: 'c', class: 'code' }, String(i + 1).padStart(2, '0')),
|
|
149
|
-
h('span', { key: 't', class: 'title' }, String(e.label || e.name || e.href || '')),
|
|
150
|
-
];
|
|
151
|
-
if (e.desc) kids.push(h('span', { key: 'm', class: 'meta dim' }, ' — ' + e.desc));
|
|
152
|
-
kids.push(h('span', { key: 'a', class: 'ds-row-arrow' }, '↗'));
|
|
153
|
-
return h('a', { key: i, class: 'row ' + rail, href: e.href || '#' }, ...kids);
|
|
154
|
-
}),
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// minimal client-side markdown renderer matching server-side renderer (idempotent for already-html bodies)
|
|
159
|
-
function __md(md) {
|
|
160
|
-
const lines = String(md || '').split('\\n');
|
|
161
|
-
const out = []; let inCode = false, inList = false;
|
|
162
|
-
const esc = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
163
|
-
const inl = (s) => s.replace(/\`([^\`]+)\`/g, '<code>$1</code>').replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>').replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href="$2">$1</a>');
|
|
164
|
-
for (const line of lines) {
|
|
165
|
-
if (line.startsWith('\`\`\`')) { if (inCode) { out.push('</pre>'); inCode = false; } else { out.push('<pre>'); inCode = true; } continue; }
|
|
166
|
-
if (inCode) { out.push(esc(line)); continue; }
|
|
167
|
-
if (line.startsWith('# ')) out.push('<h1>' + esc(line.slice(2)) + '</h1>');
|
|
168
|
-
else if (line.startsWith('## ')) out.push('<h2>' + esc(line.slice(3)) + '</h2>');
|
|
169
|
-
else if (line.startsWith('### ')) out.push('<h3>' + esc(line.slice(4)) + '</h3>');
|
|
170
|
-
else if (line.startsWith('- ')) { if (!inList) { out.push('<ul>'); inList = true; } out.push('<li>' + inl(esc(line.slice(2))) + '</li>'); }
|
|
171
|
-
else { if (inList) { out.push('</ul>'); inList = false; } if (line.trim()) out.push('<p>' + inl(esc(line)) + '</p>'); }
|
|
172
|
-
}
|
|
173
|
-
if (inList) out.push('</ul>');
|
|
174
|
-
if (inCode) out.push('</pre>');
|
|
175
|
-
return out.join('\\n');
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const bodyNode = data.bodyHtml ? C.Section({ children: h('div', { class: 'page-body', innerHTML: data.bodyHtml }) }) : null;
|
|
179
|
-
|
|
180
|
-
const mainChildren = [
|
|
181
|
-
heroNode(data.hero),
|
|
182
|
-
...data.sections.map(sectionNode),
|
|
183
|
-
examplesNode(data.examples),
|
|
184
|
-
bodyNode,
|
|
185
|
-
].filter(Boolean);
|
|
186
|
-
|
|
187
|
-
mount(document.getElementById('app'), () => C.AppShell({
|
|
188
|
-
topbar: C.Topbar({ brand: data.siteName, items: data.navItems, active: data.title }),
|
|
189
|
-
crumb: C.Crumb({ leaf: data.title }),
|
|
190
|
-
main: h('div', { class: 'app-stage' }, ...mainChildren),
|
|
191
|
-
status: C.Status({ left: [data.siteName.toLowerCase(), data.slug], right: ['live'] }),
|
|
192
|
-
}));
|
|
120
|
+
${CLIENT_SCRIPT}${clientScriptExtra}
|
|
193
121
|
</script>
|
|
194
122
|
</body>
|
|
195
123
|
</html>`;
|
package/dist/src/theme.js
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
// 247420 design system — theme controller.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// 'auto'
|
|
5
|
-
// 'paper'
|
|
6
|
-
// 'ink'
|
|
3
|
+
// Theme modes (data-theme):
|
|
4
|
+
// 'auto' — follow OS (prefers-color-scheme). Live-updates on OS change.
|
|
5
|
+
// 'paper' — force light.
|
|
6
|
+
// 'ink' — force dark.
|
|
7
|
+
// 'thebird' — warm-paper brand preset (named theme).
|
|
8
|
+
// Accents (data-accent): 'green' | 'purple' | 'mascot'.
|
|
9
|
+
// Density (data-density): 'compact' | 'comfortable' | 'spacious'.
|
|
7
10
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// browser
|
|
11
|
+
// Each is one attribute on <html> the canonical theme (colors_and_type.css)
|
|
12
|
+
// reads. Adding a theme = one [data-theme="X"] block in colors_and_type.css
|
|
13
|
+
// plus its name in THEMES below. Persists to localStorage; auto-inits on
|
|
14
|
+
// browser import; safe no-op on server.
|
|
12
15
|
|
|
13
16
|
const KEY = '247420:theme';
|
|
14
|
-
const
|
|
17
|
+
const ACCENT_KEY = '247420:accent';
|
|
18
|
+
const DENSITY_KEY = '247420:density';
|
|
19
|
+
const LOCALE_KEY = '247420:locale';
|
|
20
|
+
// 'auto' is a mode, not a [data-theme] preset block — it stays in VALID for the
|
|
21
|
+
// controller but is the OS-follow path. The named presets are the rest.
|
|
22
|
+
const VALID = new Set(['auto', 'paper', 'ink', 'thebird', 'github-dark']);
|
|
23
|
+
const VALID_ACCENT = new Set(['green', 'purple', 'mascot']);
|
|
24
|
+
const VALID_DENSITY = new Set(['compact', 'comfortable', 'spacious']);
|
|
15
25
|
const listeners = new Set();
|
|
16
26
|
let _mq = null;
|
|
17
27
|
let _current = 'auto';
|
|
@@ -28,7 +38,7 @@ function readStored() {
|
|
|
28
38
|
}
|
|
29
39
|
|
|
30
40
|
function writeStored(mode) {
|
|
31
|
-
try { window.localStorage.setItem(KEY, mode); } catch {}
|
|
41
|
+
try { window.localStorage.setItem(KEY, mode); } catch { /* swallow: persistence is best-effort, theme still applies in-memory */ }
|
|
32
42
|
}
|
|
33
43
|
|
|
34
44
|
function writeAttr(mode) {
|
|
@@ -44,7 +54,7 @@ function ensureMq() {
|
|
|
44
54
|
// Re-emit so listeners can re-render derived UI even though
|
|
45
55
|
// data-theme stays "auto" — the CSS @media handles the swap.
|
|
46
56
|
for (const cb of listeners) {
|
|
47
|
-
try { cb({ mode: 'auto', resolved: _mq.matches ? 'ink' : 'paper' }); } catch {}
|
|
57
|
+
try { cb({ mode: 'auto', resolved: _mq.matches ? 'ink' : 'paper' }); } catch { /* swallow: a listener's error must not block notifying the rest */ }
|
|
48
58
|
}
|
|
49
59
|
}
|
|
50
60
|
};
|
|
@@ -62,7 +72,7 @@ export function applyTheme(mode) {
|
|
|
62
72
|
? (_mq && _mq.matches ? 'ink' : 'paper')
|
|
63
73
|
: mode;
|
|
64
74
|
for (const cb of listeners) {
|
|
65
|
-
try { cb({ mode, resolved }); } catch {}
|
|
75
|
+
try { cb({ mode, resolved }); } catch { /* swallow: a listener's error must not block notifying the rest */ }
|
|
66
76
|
}
|
|
67
77
|
return mode;
|
|
68
78
|
}
|
|
@@ -82,6 +92,80 @@ export function onThemeChange(cb) {
|
|
|
82
92
|
return () => listeners.delete(cb);
|
|
83
93
|
}
|
|
84
94
|
|
|
95
|
+
// ---- Accent + density: independent attribute controllers ----
|
|
96
|
+
|
|
97
|
+
function readStoredKey(key, valid) {
|
|
98
|
+
try { const v = window.localStorage.getItem(key); return valid.has(v) ? v : null; } catch { return null; }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function applyAccent(accent) {
|
|
102
|
+
if (!isBrowser()) return accent;
|
|
103
|
+
if (VALID_ACCENT.has(accent)) {
|
|
104
|
+
document.documentElement.setAttribute('data-accent', accent);
|
|
105
|
+
try { window.localStorage.setItem(ACCENT_KEY, accent); } catch { /* swallow: persistence is best-effort, accent still applies in-memory */ }
|
|
106
|
+
} else {
|
|
107
|
+
// No accent attribute = the theme's default accent (green).
|
|
108
|
+
document.documentElement.removeAttribute('data-accent');
|
|
109
|
+
try { window.localStorage.removeItem(ACCENT_KEY); } catch { /* swallow: persistence is best-effort, accent still applies in-memory */ }
|
|
110
|
+
}
|
|
111
|
+
return accent;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function getAccent() {
|
|
115
|
+
if (!isBrowser()) return null;
|
|
116
|
+
return document.documentElement.getAttribute('data-accent');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function applyDensity(density) {
|
|
120
|
+
if (!isBrowser()) return density;
|
|
121
|
+
if (VALID_DENSITY.has(density)) {
|
|
122
|
+
document.documentElement.setAttribute('data-density', density);
|
|
123
|
+
try { window.localStorage.setItem(DENSITY_KEY, density); } catch { /* swallow: persistence is best-effort, density still applies in-memory */ }
|
|
124
|
+
}
|
|
125
|
+
return density;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function getDensity() {
|
|
129
|
+
if (!isBrowser()) return null;
|
|
130
|
+
return document.documentElement.getAttribute('data-density');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---- Direction: derived from an active locale, not user-toggled ----
|
|
134
|
+
//
|
|
135
|
+
// Unlike theme/accent/density (explicit user picks), direction is DERIVED
|
|
136
|
+
// from whichever locale the consumer's i18n catalog has active — mirrors the
|
|
137
|
+
// data-theme pattern (one attribute the CSS reads) but the input is a BCP-47
|
|
138
|
+
// locale tag, not a direct rtl/ltr choice. Intl.Locale(locale).textInfo is
|
|
139
|
+
// the real platform primitive for this (no manual RTL-language list to
|
|
140
|
+
// maintain). Falls back to 'ltr' for a locale the runtime can't resolve
|
|
141
|
+
// (unknown tag, or a runtime without Intl.Locale.textInfo support).
|
|
142
|
+
export function applyDirection(locale) {
|
|
143
|
+
if (!isBrowser()) return 'ltr';
|
|
144
|
+
let dir = 'ltr';
|
|
145
|
+
try {
|
|
146
|
+
const info = new Intl.Locale(locale).textInfo;
|
|
147
|
+
if (info && (info.direction === 'rtl' || info.direction === 'ltr')) dir = info.direction;
|
|
148
|
+
} catch { /* swallow: unresolvable locale tag or no Intl.Locale.textInfo support — default ltr already set */ }
|
|
149
|
+
document.documentElement.setAttribute('dir', dir);
|
|
150
|
+
try { window.localStorage.setItem(LOCALE_KEY, locale); } catch { /* swallow: persistence is best-effort, direction still applies in-memory */ }
|
|
151
|
+
return dir;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function getDirection() {
|
|
155
|
+
if (!isBrowser()) return 'ltr';
|
|
156
|
+
return document.documentElement.getAttribute('dir') || 'ltr';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Restores the last-applied locale's direction on boot (no-op if none
|
|
160
|
+
// stored — leaves whatever dir the page was authored/SSR'd with).
|
|
161
|
+
export function initDirection() {
|
|
162
|
+
if (!isBrowser()) return 'ltr';
|
|
163
|
+
let stored = null;
|
|
164
|
+
try { stored = window.localStorage.getItem(LOCALE_KEY); } catch { /* swallow: no stored locale, use SSR/authored dir as-is */ }
|
|
165
|
+
if (stored) return applyDirection(stored);
|
|
166
|
+
return getDirection();
|
|
167
|
+
}
|
|
168
|
+
|
|
85
169
|
// Auto-init on browser import. Picks stored value, else falls back to
|
|
86
170
|
// whatever data-theme is already on <html> (set by page-html.js), else 'auto'.
|
|
87
171
|
export function initTheme() {
|
|
@@ -90,10 +174,19 @@ export function initTheme() {
|
|
|
90
174
|
const fromAttr = document.documentElement.getAttribute('data-theme');
|
|
91
175
|
const initial = stored || (VALID.has(fromAttr) ? fromAttr : 'auto');
|
|
92
176
|
applyTheme(initial);
|
|
177
|
+
// Restore persisted accent/density (no-op if none stored — keeps the
|
|
178
|
+
// theme's default accent and the page's authored density).
|
|
179
|
+
const accent = readStoredKey(ACCENT_KEY, VALID_ACCENT);
|
|
180
|
+
if (accent) applyAccent(accent);
|
|
181
|
+
const density = readStoredKey(DENSITY_KEY, VALID_DENSITY);
|
|
182
|
+
if (density) applyDensity(density);
|
|
93
183
|
return initial;
|
|
94
184
|
}
|
|
95
185
|
|
|
96
186
|
if (isBrowser()) {
|
|
97
187
|
// Run on next microtask so SSR-injected attributes settle first.
|
|
98
|
-
Promise.resolve().then(() => {
|
|
188
|
+
Promise.resolve().then(() => {
|
|
189
|
+
try { initTheme(); } catch { /* swallow: deferred init is a progressive enhancement, page already rendered without it */ }
|
|
190
|
+
try { initDirection(); } catch { /* swallow: deferred init is a progressive enhancement, page already rendered without it */ }
|
|
191
|
+
});
|
|
99
192
|
}
|
|
@@ -1,35 +1,106 @@
|
|
|
1
1
|
// <ds-chat> custom element — auto-registers when SDK loads in a browser.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
2
|
+
// Attributes / properties:
|
|
3
|
+
// el.messages = [{who,text,time,name,...}, ...]
|
|
4
|
+
// el.placeholder, el.title, el.sub, el.disabled
|
|
5
|
+
// Emits a bubbling, composed 'send' event with { detail: { text } } when the
|
|
6
|
+
// user submits via the built-in composer.
|
|
4
7
|
|
|
5
8
|
import * as webjsx from '../../vendor/webjsx/index.js';
|
|
6
|
-
import { Chat } from '../components/chat.js';
|
|
9
|
+
import { Chat, ChatComposer } from '../components/chat.js';
|
|
7
10
|
|
|
8
11
|
class DsChat extends HTMLElement {
|
|
9
12
|
constructor() {
|
|
10
13
|
super();
|
|
11
14
|
this._messages = [];
|
|
12
|
-
this._placeholder = 'type, then
|
|
15
|
+
this._placeholder = 'type, then enter';
|
|
16
|
+
this._title = 'chat';
|
|
17
|
+
this._sub = '';
|
|
18
|
+
this._composerValue = '';
|
|
19
|
+
this._disabled = false;
|
|
20
|
+
this._scrollPending = false;
|
|
13
21
|
}
|
|
14
|
-
static get observedAttributes() { return ['messages', 'placeholder']; }
|
|
22
|
+
static get observedAttributes() { return ['messages', 'placeholder', 'title', 'sub', 'disabled']; }
|
|
15
23
|
attributeChangedCallback(name, _old, val) {
|
|
16
24
|
if (name === 'messages') {
|
|
17
25
|
try { this._messages = JSON.parse(val); } catch { this._messages = []; }
|
|
26
|
+
this._scrollPending = true;
|
|
18
27
|
} else if (name === 'placeholder') {
|
|
19
28
|
this._placeholder = val || '';
|
|
29
|
+
} else if (name === 'title') {
|
|
30
|
+
this._title = val || 'chat';
|
|
31
|
+
} else if (name === 'sub') {
|
|
32
|
+
this._sub = val || '';
|
|
33
|
+
} else if (name === 'disabled') {
|
|
34
|
+
this._disabled = val != null && val !== 'false';
|
|
20
35
|
}
|
|
21
36
|
this._render();
|
|
22
37
|
}
|
|
23
|
-
set messages(v) { this._messages = Array.isArray(v) ? v : []; this._render(); }
|
|
38
|
+
set messages(v) { this._messages = Array.isArray(v) ? v : []; this._scrollPending = true; this._render(); }
|
|
24
39
|
get messages() { return this._messages; }
|
|
25
40
|
set placeholder(v) { this._placeholder = v || ''; this._render(); }
|
|
26
41
|
get placeholder() { return this._placeholder; }
|
|
27
|
-
|
|
28
|
-
|
|
42
|
+
set title(v) { this._title = v || 'chat'; this._render(); }
|
|
43
|
+
get title() { return this._title; }
|
|
44
|
+
set sub(v) { this._sub = v || ''; this._render(); }
|
|
45
|
+
get sub() { return this._sub; }
|
|
46
|
+
set disabled(v) { this._disabled = !!v; this._render(); }
|
|
47
|
+
get disabled() { return this._disabled; }
|
|
48
|
+
connectedCallback() {
|
|
49
|
+
this.classList.add('ds-247420');
|
|
50
|
+
this._render();
|
|
51
|
+
}
|
|
52
|
+
_send(text) {
|
|
53
|
+
this._composerValue = '';
|
|
54
|
+
this.dispatchEvent(new CustomEvent('send', { detail: { text }, bubbles: true, composed: true }));
|
|
55
|
+
this._render();
|
|
56
|
+
}
|
|
29
57
|
_render() {
|
|
30
58
|
if (!this.isConnected) return;
|
|
31
|
-
|
|
59
|
+
// The factory captures `value` at construction time, so the click handler
|
|
60
|
+
// it builds sees a stale empty string. Pass a no-op and own send wiring
|
|
61
|
+
// ourselves by reading the live textarea below.
|
|
62
|
+
const self = this;
|
|
63
|
+
const composer = ChatComposer({
|
|
64
|
+
value: this._composerValue,
|
|
65
|
+
placeholder: this._placeholder,
|
|
66
|
+
disabled: this._disabled,
|
|
67
|
+
onInput: (v) => { self._composerValue = v; self._syncSendButton(); },
|
|
68
|
+
onSend: () => { /* superseded by live read below */ },
|
|
69
|
+
});
|
|
70
|
+
const node = Chat({
|
|
71
|
+
title: this._title,
|
|
72
|
+
sub: this._sub,
|
|
73
|
+
messages: this._messages,
|
|
74
|
+
composer,
|
|
75
|
+
});
|
|
32
76
|
webjsx.applyDiff(this, node);
|
|
77
|
+
// Wire send button + Enter-key to read the LIVE textarea value rather than
|
|
78
|
+
// the closure's stale prop. Idempotent — only attach once per composer DOM node.
|
|
79
|
+
const composerEl = this.querySelector('.chat-composer');
|
|
80
|
+
if (composerEl && !composerEl._dsBound) {
|
|
81
|
+
composerEl._dsBound = true;
|
|
82
|
+
const ta = composerEl.querySelector('textarea');
|
|
83
|
+
const btn = composerEl.querySelector('button.send');
|
|
84
|
+
const submit = () => {
|
|
85
|
+
const v = ta ? ta.value.trim() : '';
|
|
86
|
+
if (!v || this._disabled) return;
|
|
87
|
+
if (ta) ta.value = '';
|
|
88
|
+
this._send(v);
|
|
89
|
+
};
|
|
90
|
+
if (btn) btn.addEventListener('click', (e) => { e.preventDefault(); submit(); });
|
|
91
|
+
if (ta) ta.addEventListener('keydown', (e) => {
|
|
92
|
+
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); submit(); }
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
if (this._scrollPending) {
|
|
96
|
+
this._scrollPending = false;
|
|
97
|
+
const thread = this.querySelector('.chat-thread');
|
|
98
|
+
if (thread) thread.scrollTop = thread.scrollHeight;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
_syncSendButton() {
|
|
102
|
+
const btn = this.querySelector('.chat-composer button.send');
|
|
103
|
+
if (btn) btn.disabled = this._disabled || !this._composerValue.trim();
|
|
33
104
|
}
|
|
34
105
|
}
|
|
35
106
|
|
|
@@ -70,8 +70,47 @@ function classifyAndReply(text) {
|
|
|
70
70
|
return REPLIES.text();
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
// `phase` drives the transcript. AICat renders the happy path; `empty` is a
|
|
74
|
+
// fresh session, `error` is a model-side failure, `loading` is history being
|
|
75
|
+
// rehydrated. All three are reachable from the sidebar so they stay live
|
|
76
|
+
// reference surfaces rather than paths only a real outage would reveal.
|
|
77
|
+
const PHASES = ['ready', 'loading', 'empty', 'error'];
|
|
78
|
+
|
|
79
|
+
// Turn-shaped shimmer for history rehydration. Reuses .ds-event-row-skeleton +
|
|
80
|
+
// .ds-skel* (app-shell/files.css) — avatar / body / timestamp, same rhythm.
|
|
81
|
+
function TranscriptSkeleton() {
|
|
82
|
+
return h('div', {},
|
|
83
|
+
...Array.from({ length: 5 }, (_, i) => h('div', { key: 'sk' + i, class: 'ds-event-row-skeleton' },
|
|
84
|
+
h('span', { class: 'ds-skel ds-skel-icon' }),
|
|
85
|
+
h('span', { class: 'ds-skel ds-skel-title' }),
|
|
86
|
+
h('span', { class: 'ds-skel ds-skel-meta' })
|
|
87
|
+
))
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function TranscriptEmpty() {
|
|
92
|
+
return h('div', { class: 'ds-empty-state' },
|
|
93
|
+
h('div', { class: 'ds-empty-state-glyph' }, '( )'),
|
|
94
|
+
h('p', { class: 'ds-empty-state-msg' }, 'fresh session, nothing said yet'),
|
|
95
|
+
h('p', { class: 'ds-empty-state-hint' }, 'ask for code, markdown, an image, a pdf or a file attachment — or pick one of the prompts under "try" on the left and aicat answers in that shape.')
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function TranscriptError() {
|
|
100
|
+
return h('div', { class: 'ds-alert ds-alert-error' },
|
|
101
|
+
h('span', { class: 'ds-alert-icon' }, '!'),
|
|
102
|
+
h('div', { class: 'ds-alert-content' },
|
|
103
|
+
h('div', { class: 'ds-alert-title' }, 'aicat stopped mid-reply'),
|
|
104
|
+
h('div', { class: 'ds-alert-message' }, 'the model returned a truncated response, so the last turn is incomplete rather than wrong-but-whole. your prompt is still in the box — resending replays it against a fresh context.'),
|
|
105
|
+
h('div', { class: 'ds-alert-retry' },
|
|
106
|
+
h('button', { class: 'btn', onclick: () => { state.phase = 'ready'; kit.render(); } }, 'resend last turn')
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
73
112
|
const state = {
|
|
74
|
-
draft: '', thinking: false, mood: 'idle',
|
|
113
|
+
draft: '', thinking: false, mood: 'idle', phase: 'ready',
|
|
75
114
|
messages: [
|
|
76
115
|
{ who: 'them', name: 'aicat', text: 'hi. I am **aicat**. I read fast and I knock things off shelves.', time: '·' },
|
|
77
116
|
{ who: 'them', name: 'aicat', parts: [{ kind: 'md', text: 'try one of these:\n\n- ask for `code` (react/python — pick a flavour)\n- ask for the **token pdf** or the **mascot image**\n- ask me to attach a *config file*\n- or just chat — I respond in markdown.' }], time: '·' }
|
|
@@ -100,14 +139,20 @@ function send(text) {
|
|
|
100
139
|
|
|
101
140
|
function App() {
|
|
102
141
|
return AppShell({
|
|
103
|
-
topbar: Topbar({ brand: '247420', leaf: 'aicat', items: [['index', '../../'], ['chat', '../chat/'], ['source
|
|
142
|
+
topbar: Topbar({ brand: '247420', leaf: 'aicat', items: [['index', '../../'], ['chat', '../chat/'], ['source ->', 'https://github.com/AnEntrypoint/design']] }),
|
|
104
143
|
crumb: Crumb({ trail: ['247420', 'kits'], leaf: 'aicat' }),
|
|
105
144
|
side: Side({
|
|
106
145
|
sections: [
|
|
107
146
|
{ group: 'session', items: [
|
|
108
|
-
{ glyph: '
|
|
109
|
-
{ glyph: '
|
|
147
|
+
{ glyph: '+', label: 'new chat', key: 'new', onClick: (e) => { e.preventDefault(); state.messages = state.messages.slice(0, 2); kit.render(); } },
|
|
148
|
+
{ glyph: '~', label: 'history', count: 7, key: 'h' }
|
|
110
149
|
] },
|
|
150
|
+
// Reachable state switcher for the transcript.
|
|
151
|
+
{ group: 'session state', items: PHASES.map((p) => ({
|
|
152
|
+
glyph: h('span', { class: state.phase === p ? 'ds-dot ds-dot-on' : 'ds-dot ds-dot-off' }),
|
|
153
|
+
label: p, key: 'ph-' + p, active: state.phase === p,
|
|
154
|
+
onClick: (e) => { e.preventDefault(); state.phase = p; kit.render(); }
|
|
155
|
+
})) },
|
|
111
156
|
{ group: 'try', items: PRESETS.map((p, i) => ({
|
|
112
157
|
glyph: '·', label: p.q.length > 22 ? p.q.slice(0, 22) + '…' : p.q, key: 'p' + i,
|
|
113
158
|
onClick: (e) => { e.preventDefault(); send(p.q); }
|
|
@@ -123,7 +168,10 @@ function App() {
|
|
|
123
168
|
status: state.thinking ? 'thinking…' : (state.mood === 'happy' ? 'online · purring' : 'online · idle'),
|
|
124
169
|
face: FACES[state.mood] || FACES.idle
|
|
125
170
|
}),
|
|
126
|
-
|
|
171
|
+
state.phase === 'loading' ? Panel({ title: 'restoring session', children: TranscriptSkeleton() })
|
|
172
|
+
: state.phase === 'error' ? Panel({ title: 'reply failed', children: TranscriptError() })
|
|
173
|
+
: state.phase === 'empty' ? Panel({ title: 'new session', children: TranscriptEmpty() })
|
|
174
|
+
: AICat({
|
|
127
175
|
name: 'aicat',
|
|
128
176
|
status: state.thinking ? 'thinking…' : 'online · purring',
|
|
129
177
|
messages: state.messages, thinking: state.thinking,
|
|
@@ -146,7 +194,7 @@ function App() {
|
|
|
146
194
|
)
|
|
147
195
|
],
|
|
148
196
|
status: Status({
|
|
149
|
-
left: ['aicat', '
|
|
197
|
+
left: ['aicat', '- ' + (state.phase === 'ready' ? state.messages.length : 0) + ' turns', state.thinking ? '- thinking' : '- idle', '- ' + state.phase],
|
|
150
198
|
right: ['247420 / mmxxvi']
|
|
151
199
|
})
|
|
152
200
|
});
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
<link rel="icon" type="image/svg+xml" href="../../favicon.svg">
|
|
12
12
|
<link rel="stylesheet" href="../../colors_and_type.css">
|
|
13
13
|
<link rel="stylesheet" href="../../app-shell.css">
|
|
14
|
+
<link rel="stylesheet" href="../../chat.css">
|
|
15
|
+
<link rel="stylesheet" href="../../editor-primitives.css">
|
|
14
16
|
<script type="importmap">
|
|
15
17
|
{ "imports": {
|
|
16
18
|
"webjsx": "../../vendor/webjsx/index.js",
|