anentrypoint-design 0.0.477 → 0.0.480

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +101 -0
  2. package/chat.css +4 -4
  3. package/colors_and_type.css +33 -3
  4. package/community.css +22 -8
  5. package/dist/247420.css +324 -49
  6. package/dist/247420.js +123 -55
  7. package/dist/index.html +109 -32
  8. package/package.json +1 -1
  9. package/src/community-app.js +4 -2
  10. package/src/components/community/navigation.js +2 -2
  11. package/src/components/community/presence.js +10 -4
  12. package/src/components/community/views.js +8 -2
  13. package/src/components/content/avatar.js +30 -0
  14. package/src/components/content/hero.js +9 -2
  15. package/src/components/content.js +2 -2
  16. package/src/components/freddie/pages-missing.js +1 -1
  17. package/src/components/overlay-primitives/emoji-picker.js +15 -3
  18. package/src/components/shell/app-shell.js +15 -4
  19. package/src/components/shell/workspace-shell.js +1 -1
  20. package/src/components/theme-toggle.js +8 -3
  21. package/src/copy-code.js +103 -0
  22. package/src/css/app-shell/base.css +18 -0
  23. package/src/css/app-shell/catalog-theme.css +20 -2
  24. package/src/css/app-shell/chat-polish.css +12 -0
  25. package/src/css/app-shell/collab.css +5 -0
  26. package/src/css/app-shell/data-density.css +4 -0
  27. package/src/css/app-shell/files.css +7 -1
  28. package/src/css/app-shell/hero-content.css +14 -1
  29. package/src/css/app-shell/kits-appended.css +13 -0
  30. package/src/css/app-shell/loading-alerts.css +14 -12
  31. package/src/css/app-shell/otp-input.css +2 -2
  32. package/src/css/app-shell/panel-row.css +15 -0
  33. package/src/css/app-shell/plugins-config.css +3 -0
  34. package/src/css/app-shell/primitives.css +13 -4
  35. package/src/css/app-shell/responsive.css +13 -0
  36. package/src/css/app-shell/responsive2-workspace.css +4 -1
  37. package/src/css/app-shell/row-print.css +34 -0
  38. package/src/css/app-shell/states-interactions.css +62 -4
  39. package/src/css/app-shell/topbar.css +1 -0
  40. package/src/kits/os/app-panes.css +3 -2
  41. package/src/kits/os/icons.js +16 -0
  42. package/src/kits/os/shell-geometry.js +2 -2
  43. package/src/kits/os/shell.js +24 -3
  44. package/src/kits/os/theme.css +272 -78
  45. package/src/kits/os/wm.css +19 -6
  46. package/src/kits/os/wm.js +38 -1
  47. package/src/kits/spoint/host-join-lobby.css +3 -3
  48. package/src/page-html/client-script.js +96 -28
  49. package/src/page-html.js +3 -2
  50. package/src/theme.js +6 -2
  51. package/src/web-components/ds-chat.js +9 -0
  52. package/src/web-components/freddie-chat.js +2 -2
  53. package/types/components.d.ts +6 -0
@@ -1,7 +1,9 @@
1
1
  /* ============================================================
2
2
  Window Manager visuals — paired with createWM() behavior
3
3
  Tokens: --os-* (defined by desktop/theme.css)
4
- Bible: tonal surfaces, no borders for chrome, inset rail for focus.
4
+ Bible: tonal surfaces, inset rail for focus, subtle hairline border +
5
+ elevation shadow on every window (stronger on wm-focused) so windows
6
+ read as distinct layers against a dark desktop background.
5
7
  ============================================================ */
6
8
 
7
9
  .wm-root {
@@ -21,14 +23,18 @@
21
23
  max-height: 100%;
22
24
  background: var(--os-bg-1);
23
25
  color: var(--os-fg);
24
- border: none;
26
+ border: 1px solid color-mix(in oklab, var(--os-fg) 14%, transparent);
25
27
  border-radius: var(--os-radius, var(--r-2, 10px));
26
28
  display: flex;
27
29
  flex-direction: column;
28
30
  pointer-events: auto;
29
31
  overflow: hidden;
32
+ box-shadow: var(--shadow-2, 0 4px 12px rgba(0, 0, 0, .3));
33
+ }
34
+ .wm-win.wm-focused {
35
+ border-color: color-mix(in oklab, var(--os-fg) 28%, transparent);
36
+ box-shadow: inset 4px 0 0 var(--os-accent), var(--shadow-3, 0 12px 32px rgba(0, 0, 0, .4));
30
37
  }
31
- .wm-win.wm-focused { box-shadow: inset 4px 0 0 var(--os-accent); }
32
38
  /* Promote to a compositor layer only for the duration of a drag/resize so the
33
39
  per-frame left/top/width/height writes don't force a main-thread layout each
34
40
  pointermove. Released on pointerup (class removed) to avoid the permanent
@@ -60,10 +66,10 @@
60
66
  text-transform: lowercase;
61
67
  }
62
68
 
63
- .wm-btns { display: flex; gap: 6px; }
69
+ .wm-btns { display: flex; align-items: center; gap: 6px; }
64
70
  .wm-btn {
65
- width: 22px;
66
- height: 22px;
71
+ width: 26px;
72
+ height: 26px;
67
73
  border: none;
68
74
  background: var(--os-bg-3);
69
75
  color: var(--os-fg-2);
@@ -79,6 +85,13 @@
79
85
  touch-action: manipulation;
80
86
  }
81
87
  .wm-btn:hover { background: var(--panel-hover, var(--os-bg-1)); color: var(--os-fg); }
88
+ /* Close gets its own hover color (red) so the destructive action is
89
+ distinguishable from minimize/maximize at a glance, not just by position. */
90
+ .wm-btn[title='close']:hover,
91
+ .wm-btn[aria-label='close']:hover {
92
+ background: var(--color-danger, #C41C0C);
93
+ color: var(--paper, #fff);
94
+ }
82
95
 
83
96
  .wm-body {
84
97
  flex: 1;
package/src/kits/os/wm.js CHANGED
@@ -11,6 +11,38 @@
11
11
 
12
12
  import { icons } from './icons.js';
13
13
 
14
+ // Shared aria-live announcer for window open/close/focus-change events.
15
+ // Visually hidden, one instance per document, lazily created so importing
16
+ // this module has no side effect until a window actually renders. Screen
17
+ // readers get no other signal that a floating, non-modal window opened,
18
+ // closed, or changed focus -- there is no page navigation or route change
19
+ // to announce it implicitly, unlike a normal document flow.
20
+ let _announcer = null;
21
+ function getAnnouncer() {
22
+ if (_announcer && _announcer.isConnected) return _announcer;
23
+ _announcer = document.getElementById('wm-announcer');
24
+ if (_announcer) return _announcer;
25
+ _announcer = document.createElement('div');
26
+ _announcer.id = 'wm-announcer';
27
+ _announcer.setAttribute('aria-live', 'polite');
28
+ _announcer.setAttribute('aria-atomic', 'true');
29
+ _announcer.className = 'sr-only';
30
+ // Inline fallback in case the consuming page's stylesheet doesn't define
31
+ // .sr-only (this module has no guaranteed CSS import of its own) --
32
+ // standard clip-based visually-hidden-but-AT-visible technique.
33
+ _announcer.style.cssText = 'position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;';
34
+ document.body.appendChild(_announcer);
35
+ return _announcer;
36
+ }
37
+ function announce(text) {
38
+ const a = getAnnouncer();
39
+ // Clear-then-set on a microtask forces a re-announcement even if the
40
+ // text is identical to what's already there (e.g. focusing the same
41
+ // window twice in a row) -- aria-live only fires on a DOM mutation.
42
+ a.textContent = '';
43
+ requestAnimationFrame(() => { a.textContent = text; });
44
+ }
45
+
14
46
  export function renderWindow(opts = {}) {
15
47
  const {
16
48
  title = 'window',
@@ -127,11 +159,15 @@ export function renderWindow(opts = {}) {
127
159
  return;
128
160
  }
129
161
  disarmClose();
162
+ announce('closed ' + titleEl.textContent);
130
163
  callbacks.onClose && callbacks.onClose();
131
164
  });
132
165
  closeBtn.addEventListener('blur', disarmClose);
133
166
 
134
- const focus = () => callbacks.onFocus && callbacks.onFocus();
167
+ const focus = () => {
168
+ if (!el.classList.contains('wm-focused')) announce(titleEl.textContent + ' focused');
169
+ callbacks.onFocus && callbacks.onFocus();
170
+ };
135
171
 
136
172
  el.addEventListener('pointerdown', () => focus());
137
173
 
@@ -172,6 +208,7 @@ export function renderWindow(opts = {}) {
172
208
  applyFocused(el, focused);
173
209
  applyMaximized(el, maximized);
174
210
  applyMinimized(el, minimized);
211
+ announce('opened ' + title);
175
212
 
176
213
  return {
177
214
  el,
@@ -62,12 +62,12 @@
62
62
  background: var(--bg);
63
63
  color: var(--fg);
64
64
  border: 1px solid var(--bw-rule, var(--rule));
65
- border-radius: var(--r-1, 6px);
66
- padding: 8px 10px;
65
+ border-radius: var(--r-1, 8px);
66
+ padding: var(--space-2) var(--space-2-5);
67
67
  font: var(--fs-body) var(--ff-mono);
68
68
  outline: none;
69
69
  }
70
- .sp-lobby-input:focus { border-color: var(--accent); }
70
+ .sp-lobby-input:focus-visible { border-color: var(--accent); box-shadow: var(--focus-ring-inset); }
71
71
  .sp-lobby-link { font-size: var(--fs-small, 11px); }
72
72
  .sp-lobby-code {
73
73
  font-family: var(--ff-display);
@@ -80,9 +80,64 @@ function examplesNode(examples) {
80
80
  });
81
81
  }
82
82
 
83
- function panelNode(panel, idx) {
84
- const items = Array.isArray(panel.items) ? panel.items : [];
85
- if (!items.length) return null;
83
+ // Category glyph per homepage panel id — the same line-icon vocabulary
84
+ // icons.js already provides, so kits/previews/decks/docs read as distinct
85
+ // categories at a glance instead of by text label alone. Falls back to no
86
+ // icon for panel ids outside this table (e.g. one-off feature panels).
87
+ const PANEL_ICON = {
88
+ kits: 'grid',
89
+ file_browser: 'folder',
90
+ desktop_os: 'square',
91
+ web_components: 'page',
92
+ api_exports: 'link',
93
+ decks: 'screen',
94
+ docs: 'file-text',
95
+ previews: 'eye',
96
+ features: 'info',
97
+ };
98
+
99
+ // Filter state for the kits search box above the "ui kits" panel. Reuses
100
+ // ui_kits/search's own query-bar pattern (plain module-level state object,
101
+ // mutated on input, driving a re-render) rather than inventing a second
102
+ // filtering mechanism -- this homepage already ships that kit, so the
103
+ // homepage's own kit listing gets the same affordance instead of being the
104
+ // one surface on the site without a way to search kits by name.
105
+ const kitsFilterState = { q: '' };
106
+
107
+ function panelNode(panel, idx, rerender) {
108
+ let items = Array.isArray(panel.items) ? panel.items : [];
109
+ const isKits = panel.id === 'kits';
110
+ const q = isKits ? kitsFilterState.q.trim().toLowerCase() : '';
111
+ if (isKits && q) {
112
+ items = items.filter((it) => {
113
+ const hay = (String(it.title || it.name || '') + ' ' + String(it.sub || it.desc || '')).toLowerCase();
114
+ return hay.includes(q);
115
+ });
116
+ }
117
+ const iconName = panel.id && PANEL_ICON[panel.id];
118
+ const titleText = panel.title || panel.name || '';
119
+ const titleNode = iconName && titleText
120
+ ? h('span', { class: 'ds-panel-title-glyph' }, C.Icon(iconName, { size: 15 }), h('span', {}, titleText))
121
+ : titleText;
122
+ const filterInput = isKits && rerender ? h('div', { class: 'ds-kits-filter' },
123
+ h('input', {
124
+ type: 'search', class: 'input ds-kits-filter-input',
125
+ placeholder: 'filter kits by name or description…',
126
+ value: kitsFilterState.q,
127
+ 'aria-label': 'filter ui kits',
128
+ oninput: (e) => { kitsFilterState.q = e.target.value; rerender(); },
129
+ })) : null;
130
+ if (!items.length) {
131
+ if (isKits && q) {
132
+ return h('div', { class: 'ds-kits-panel-wrap' }, filterInput,
133
+ C.Panel({ id: panel.id || null, title: titleNode, count: 0, children:
134
+ h('div', { class: 'ds-empty-state' },
135
+ h('div', { class: 'ds-empty-state-glyph' }, '( )'),
136
+ h('p', { class: 'ds-empty-state-msg' }, 'no kits match ', h('code', {}, '"' + kitsFilterState.q.trim() + '"')),
137
+ ) }));
138
+ }
139
+ return filterInput ? h('div', { class: 'ds-kits-panel-wrap' }, filterInput) : null;
140
+ }
86
141
  const rows = items.map((it, i) => {
87
142
  const code = it.code == null ? '' : String(it.code).trim();
88
143
  const kids = [];
@@ -92,7 +147,8 @@ function panelNode(panel, idx) {
92
147
  kids.push(h('span', { key: 'a', class: 'ds-row-arrow' }, it.meta || '->'));
93
148
  return h('a', { key: i, class: 'row', href: it.href || '#' }, ...kids);
94
149
  });
95
- return C.Panel({ id: panel.id || null, title: panel.title || panel.name || '', count: panel.count || items.length, children: rows });
150
+ const panelEl = C.Panel({ id: panel.id || null, title: titleNode, count: items.length, children: rows });
151
+ return filterInput ? h('div', { class: 'ds-kits-panel-wrap' }, filterInput, panelEl) : panelEl;
96
152
  }
97
153
 
98
154
  function marqueeNode(marquee) {
@@ -196,8 +252,14 @@ const bodyNode = data.bodyHtml ? C.Section({ children: h('div', { class: 'page-b
196
252
  function footerNode() {
197
253
  const year = new Date().getFullYear();
198
254
  const links = (data.navItems || []).filter(([, href]) => /^https?:/.test(String(href)));
255
+ // Names the actual maintaining entity, not just the project's numeric
256
+ // brand. This is a personal/community project (MIT, copyright holder
257
+ // "AnEntrypoint" per LICENSE) with no separate legal org behind it, so the
258
+ // footer says that plainly instead of leaving "247420" to stand in for an
259
+ // entity it isn't.
260
+ const entity = (data.seoAuthor || '247420 · a design system by AnEntrypoint').toLowerCase();
199
261
  return h('footer', { class: 'ds-page-footer' },
200
- h('span', { class: 'ds-page-footer-copy' }, String(year) + ' · ' + data.siteName),
262
+ h('span', { class: 'ds-page-footer-copy' }, String(year) + ' · ' + entity),
201
263
  links.length ? h('nav', { class: 'ds-page-footer-links', 'aria-label': 'footer' },
202
264
  ...links.map(([label, href], i) => h('a', { key: i, href }, String(label).replace(' ->', '')))
203
265
  ) : null,
@@ -215,40 +277,46 @@ function tierNode(tier, children) {
215
277
  return h('section', { key: tier.key, class: 'ds-tier ds-tier-' + tier.key, id: tier.key, 'aria-labelledby': labelId }, head, ...kids);
216
278
  }
217
279
 
218
- const panelsById = new Map((data.panels || []).map((p) => [p.id || p.title || p.name || '', p]));
219
- const takePanel = (id) => { const p = panelsById.get(id); if (p) panelsById.delete(id); return p ? panelNode(p) : null; };
220
-
221
280
  const TIERS = [
222
281
  { key: 'open', label: 'open', lede: 'browse and try the system running.', ids: ['kits', 'previews', 'decks'], extra: () => [examplesNode(data.examples)] },
223
282
  { key: 'ships', label: 'ships', lede: 'what the package contains.', ids: ['file_browser', 'desktop_os', 'web_components', 'api_exports'], extra: () => [] },
224
283
  { key: 'read', label: 'read', lede: 'understand the rules behind it.', ids: ['docs', 'features'], extra: () => [quickstartNode(data.quickstart)] },
225
284
  ];
226
285
 
227
- const tierNodes = TIERS.map((t) => {
228
- const kids = [...t.ids.map(takePanel), ...t.extra()].filter(Boolean);
229
- if (t.key === 'open' && kids.length) {
230
- const lead = kids[0];
231
- if (lead.props) lead.props.class = (lead.props.class || '') + ' ds-tier-lead';
232
- }
233
- return tierNode(t, kids);
234
- });
235
- const leftoverPanels = [...panelsById.values()].map(panelNode);
286
+ // Builds the whole main-content tree fresh each render so the kits filter
287
+ // (module-level kitsFilterState) can drive a real re-render via mount()'s
288
+ // own returned render callback -- same reactive shape mountKit() gives every
289
+ // other kit on the site, applied here to the static-page renderer.
290
+ function buildMainChildren(rerender) {
291
+ const panelsById = new Map((data.panels || []).map((p) => [p.id || p.title || p.name || '', p]));
292
+ const takePanel = (id) => { const p = panelsById.get(id); if (p) panelsById.delete(id); return p ? panelNode(p, 0, rerender) : null; };
293
+
294
+ const tierNodes = TIERS.map((t) => {
295
+ const kids = [...t.ids.map(takePanel), ...t.extra()].filter(Boolean);
296
+ if (t.key === 'open' && kids.length) {
297
+ const lead = kids[0];
298
+ if (lead.props) lead.props.class = (lead.props.class || '') + ' ds-tier-lead';
299
+ }
300
+ return tierNode(t, kids);
301
+ });
302
+ const leftoverPanels = [...panelsById.values()].map((p) => panelNode(p, 0, rerender));
236
303
 
237
- const mainChildren = [
238
- heroNode(data.hero),
239
- marqueeNode(data.marquee),
240
- ...data.sections.map(sectionNode),
241
- ...tierNodes,
242
- ...leftoverPanels,
243
- bodyNode,
244
- footerNode(),
245
- ].filter(Boolean);
304
+ return [
305
+ heroNode(data.hero),
306
+ marqueeNode(data.marquee),
307
+ ...data.sections.map(sectionNode),
308
+ ...tierNodes,
309
+ ...leftoverPanels,
310
+ bodyNode,
311
+ footerNode(),
312
+ ].filter(Boolean);
313
+ }
246
314
 
247
- mount(document.getElementById('app'), () => C.AppShell({
315
+ mount(document.getElementById('app'), (rerender) => C.AppShell({
248
316
  topbar: C.Topbar({ brand: data.siteName, items: data.navItems, active: data.title }),
249
317
  crumb: C.Crumb({ leaf: data.title }),
250
318
  side: sideNode(data.sidebar),
251
- main: h('div', { class: 'app-stage' }, ...mainChildren),
319
+ main: h('div', { class: 'app-stage' }, ...buildMainChildren(rerender)),
252
320
  status: C.Status({
253
321
  left: data.statusLeft || [data.siteName.toLowerCase(), data.slug],
254
322
  right: data.statusRight || ['live'],
package/src/page-html.js CHANGED
@@ -82,13 +82,14 @@ export function renderPageHtml({
82
82
  quickstart: quickstart || null,
83
83
  statusLeft: Array.isArray(statusLeft) ? statusLeft : null,
84
84
  statusRight: Array.isArray(statusRight) ? statusRight : null,
85
+ seoAuthor: seo && seo.author ? seo.author : null,
85
86
  };
86
87
 
87
88
  const seoTags = seo ? renderSeoTags({ title, siteName, seo }) : '';
88
89
  const faviconTags = renderFaviconTags({ faviconHref, faviconGlyph });
89
90
 
90
91
  return `<!doctype html>
91
- <html lang="en" class="ds-247420">
92
+ <html lang="en" class="ds-247420" data-theme="${theme}">
92
93
  <head>
93
94
  <meta charset="utf-8">
94
95
  <meta name="viewport" content="width=device-width, initial-scale=1">
@@ -105,7 +106,7 @@ ${PAGE_INLINE_STYLES}
105
106
  <script id="__site__" type="application/json">${JSON.stringify(pageData).replace(/</g, '\\u003c')}</script>
106
107
  ${headExtra}
107
108
  </head>
108
- <body data-theme="${theme}">
109
+ <body>
109
110
  <div id="app"></div>
110
111
  <script type="module">
111
112
  ${CLIENT_SCRIPT}${clientScriptExtra}
package/src/theme.js CHANGED
@@ -43,7 +43,11 @@ function writeStored(mode) {
43
43
 
44
44
  function writeAttr(mode) {
45
45
  if (!isBrowser()) return;
46
- document.body.setAttribute('data-theme', mode);
46
+ // data-theme must live on the same element carrying the .ds-247420 scope
47
+ // class (<html>, per every shipped page template) -- the bundled CSS
48
+ // selectors are `.ds-247420[data-theme="ink"]`, which cannot match if
49
+ // the attribute sits on <body> instead of <html>.
50
+ document.documentElement.setAttribute('data-theme', mode);
47
51
  }
48
52
 
49
53
  function ensureMq() {
@@ -169,7 +173,7 @@ export function initDirection() {
169
173
  export function initTheme() {
170
174
  if (!isBrowser()) return 'auto';
171
175
  const stored = readStored();
172
- const fromAttr = document.body.getAttribute('data-theme');
176
+ const fromAttr = document.documentElement.getAttribute('data-theme');
173
177
  const initial = stored || (VALID.has(fromAttr) ? fromAttr : 'auto');
174
178
  applyTheme(initial);
175
179
  // Restore persisted accent/density (no-op if none stored — keeps the
@@ -28,6 +28,7 @@ class DsChat extends HTMLElement {
28
28
  this._placeholder = val || '';
29
29
  } else if (name === 'title') {
30
30
  this._title = val || 'chat';
31
+ if (!this.hasAttribute('aria-label') || this._ariaLabelAuto) { this.setAttribute('aria-label', this._title); this._ariaLabelAuto = true; }
31
32
  } else if (name === 'sub') {
32
33
  this._sub = val || '';
33
34
  } else if (name === 'disabled') {
@@ -47,6 +48,14 @@ class DsChat extends HTMLElement {
47
48
  get disabled() { return this._disabled; }
48
49
  connectedCallback() {
49
50
  this.classList.add('ds-247420');
51
+ // Accessible name for the custom element itself: <ds-chat> has no
52
+ // implicit ARIA role/name, so a screen reader landmark/element list
53
+ // shows an unlabeled item without this. role="region" + aria-label
54
+ // (falling back to the chat title) makes it identifiable on its own,
55
+ // separate from the inner .chat-thread's role="log"/aria-live, which
56
+ // announces individual streamed messages rather than naming the widget.
57
+ if (!this.hasAttribute('role')) this.setAttribute('role', 'region');
58
+ if (!this.hasAttribute('aria-label')) this.setAttribute('aria-label', this._title || 'chat');
50
59
  this._render();
51
60
  }
52
61
  _send(text) {
@@ -10,8 +10,8 @@ class FreddieChat extends DsChat {
10
10
  constructor() {
11
11
  super();
12
12
  this._title = 'freddie';
13
- this._sub = '';
14
- this._placeholder = 'message freddie · /tools · /tool name {json} · /run …';
13
+ this._sub = '/tools · /tool name {json} · /run …';
14
+ this._placeholder = 'message freddie…';
15
15
  }
16
16
 
17
17
  connectedCallback() {
@@ -141,6 +141,8 @@ export interface TopbarProps {
141
141
  active?: string;
142
142
  onNav?: (...args: any[]) => any;
143
143
  search?: any;
144
+ /** @default true */
145
+ themeToggle?: boolean;
144
146
  }
145
147
  export declare function Topbar(props?: TopbarProps): VNode;
146
148
 
@@ -2037,6 +2039,10 @@ export interface PageViewProps {
2037
2039
  title?: string;
2038
2040
  /** @default '' */
2039
2041
  html?: string;
2042
+ /** @default '' */
2043
+ author?: string;
2044
+ /** @default 0 */
2045
+ updatedAt?: number;
2040
2046
  /** @default false */
2041
2047
  isAdmin?: boolean;
2042
2048
  onEdit?: (...args: any[]) => any;