anentrypoint-design 0.0.108 → 0.0.110

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anentrypoint-design",
3
- "version": "0.0.108",
3
+ "version": "0.0.110",
4
4
  "description": "247420 design system SDK — webjsx + modified ripple-ui, single-file ESM bundle for reproducible use of the AnEntrypoint design.",
5
5
  "type": "module",
6
6
  "main": "./dist/247420.js",
@@ -0,0 +1,70 @@
1
+ // ThemeToggle — segmented auto/paper/ink radio bound to src/theme.js.
2
+ //
3
+ // Usage:
4
+ // import { ThemeToggle } from 'anentrypoint-design';
5
+ // ThemeToggle() // segmented control
6
+ // ThemeToggle({ compact: true }) // single cycling glyph button
7
+ //
8
+ // Reads current mode from the theme controller; clicks call applyTheme()
9
+ // which persists, updates <html data-theme>, and notifies listeners.
10
+
11
+ import * as webjsx from '../../vendor/webjsx/index.js';
12
+ import { applyTheme, getTheme, resolvedTheme, onThemeChange } from '../theme.js';
13
+
14
+ const h = webjsx.createElement;
15
+
16
+ const MODES = [
17
+ ['auto', '⌗', 'auto'],
18
+ ['paper', '☀', 'light'],
19
+ ['ink', '☾', 'dark'],
20
+ ];
21
+
22
+ // Track instances so an OS-theme change while in 'auto' re-renders the
23
+ // glyph in the compact variant (the segmented variant doesn't need it).
24
+ const refresh = new Set();
25
+ let _bound = false;
26
+ function bindOnce() {
27
+ if (_bound) return;
28
+ _bound = true;
29
+ onThemeChange(() => { for (const cb of refresh) cb(); });
30
+ }
31
+
32
+ export function ThemeToggle({ compact = false, onChange } = {}) {
33
+ bindOnce();
34
+ const current = getTheme();
35
+
36
+ if (compact) {
37
+ const resolved = resolvedTheme();
38
+ const glyph = current === 'auto' ? '⌗' : (resolved === 'ink' ? '☾' : '☀');
39
+ const label = current === 'auto' ? `auto (${resolved})` : (current === 'ink' ? 'dark' : 'light');
40
+ return h('button', {
41
+ class: 'btn ds-theme-toggle',
42
+ type: 'button',
43
+ 'aria-label': 'theme: ' + label,
44
+ title: 'theme: ' + label + ' — click to cycle',
45
+ onclick: () => {
46
+ const next = current === 'auto' ? 'paper' : (current === 'paper' ? 'ink' : 'auto');
47
+ applyTheme(next);
48
+ if (onChange) try { onChange(next); } catch {}
49
+ }
50
+ }, h('span', { class: 'glyph' }, glyph), ' ', label);
51
+ }
52
+
53
+ return h('div', {
54
+ class: 'ds-theme-toggle ds-segmented',
55
+ role: 'radiogroup',
56
+ 'aria-label': 'theme'
57
+ }, ...MODES.map(([mode, glyph, label]) =>
58
+ h('button', {
59
+ key: mode,
60
+ type: 'button',
61
+ role: 'radio',
62
+ 'aria-checked': current === mode ? 'true' : 'false',
63
+ class: 'ds-seg-btn' + (current === mode ? ' is-on' : ''),
64
+ onclick: () => {
65
+ applyTheme(mode);
66
+ if (onChange) try { onChange(mode); } catch {}
67
+ }
68
+ }, h('span', { class: 'glyph' }, glyph), ' ', label)
69
+ ));
70
+ }
package/src/components.js CHANGED
@@ -42,6 +42,8 @@ export {
42
42
  ChatHeader, VoiceStrip, CommunityShell
43
43
  } from './components/community.js';
44
44
 
45
+ export { ThemeToggle } from './components/theme-toggle.js';
46
+
45
47
  export {
46
48
  FREDDIE_PAGES,
47
49
  home, chat, voice, sessions, projects, agents, analytics,
package/src/index.js CHANGED
@@ -12,6 +12,7 @@ import { renderMarkdown, ensureReady as ensureMarkdownReady } from './markdown.j
12
12
  import { ensurePrism, highlightAllUnder } from './highlight.js';
13
13
  import { renderPageHtml } from './page-html.js';
14
14
  import { mountKit } from './bootstrap.js';
15
+ import * as theme from './theme.js';
15
16
  import { registerChatElement, DsChat } from './web-components/ds-chat.js';
16
17
 
17
18
  let _installed = false;
@@ -35,7 +36,9 @@ export function mount(rootEl, viewFn, { autoScope = true } = {}) {
35
36
  if (!rootEl) throw new Error('mount: rootEl required');
36
37
  if (typeof viewFn !== 'function') throw new Error('mount: viewFn required');
37
38
  if (autoScope && rootEl.classList && !rootEl.classList.contains(scope.slice(1))) {
38
- rootEl.classList.add(scope.slice(1));
39
+ const cls = scope.slice(1);
40
+ const inheritedFromAncestor = rootEl.closest && rootEl.closest('.' + cls);
41
+ if (!inheritedFromAncestor) rootEl.classList.add(cls);
39
42
  }
40
43
  // Auto-inject styles (idempotent) so single-line consumers don't need
41
44
  // to remember installStyles() before mount.
@@ -60,8 +63,10 @@ export {
60
63
  renderMarkdown, ensureMarkdownReady,
61
64
  ensurePrism, highlightAllUnder,
62
65
  registerChatElement, DsChat,
63
- renderPageHtml
66
+ renderPageHtml,
67
+ theme
64
68
  };
69
+ export { applyTheme, getTheme, resolvedTheme, onThemeChange, initTheme } from './theme.js';
65
70
  export const h = webjsx.createElement;
66
71
  export const applyDiff = webjsx.applyDiff;
67
72
 
package/src/theme.js ADDED
@@ -0,0 +1,99 @@
1
+ // 247420 design system — theme controller.
2
+ //
3
+ // Three modes:
4
+ // 'auto' — follow OS (prefers-color-scheme). Live-updates on OS change.
5
+ // 'paper' — force light.
6
+ // 'ink' — force dark.
7
+ //
8
+ // Writes the chosen mode to <html data-theme="..."> so CSS rules in
9
+ // system.css / colors_and_type.css resolve correctly. Persists to
10
+ // localStorage under '247420:theme'. Auto-initializes on import in a
11
+ // browser context; safe no-op on server.
12
+
13
+ const KEY = '247420:theme';
14
+ const VALID = new Set(['auto', 'paper', 'ink']);
15
+ const listeners = new Set();
16
+ let _mq = null;
17
+ let _current = 'auto';
18
+
19
+ function isBrowser() {
20
+ return typeof document !== 'undefined' && typeof window !== 'undefined';
21
+ }
22
+
23
+ function readStored() {
24
+ try {
25
+ const v = window.localStorage.getItem(KEY);
26
+ return VALID.has(v) ? v : null;
27
+ } catch { return null; }
28
+ }
29
+
30
+ function writeStored(mode) {
31
+ try { window.localStorage.setItem(KEY, mode); } catch {}
32
+ }
33
+
34
+ function writeAttr(mode) {
35
+ if (!isBrowser()) return;
36
+ document.documentElement.setAttribute('data-theme', mode);
37
+ }
38
+
39
+ function ensureMq() {
40
+ if (_mq || !isBrowser() || !window.matchMedia) return;
41
+ _mq = window.matchMedia('(prefers-color-scheme: dark)');
42
+ const onChange = () => {
43
+ if (_current === 'auto') {
44
+ // Re-emit so listeners can re-render derived UI even though
45
+ // data-theme stays "auto" — the CSS @media handles the swap.
46
+ for (const cb of listeners) {
47
+ try { cb({ mode: 'auto', resolved: _mq.matches ? 'ink' : 'paper' }); } catch {}
48
+ }
49
+ }
50
+ };
51
+ if (_mq.addEventListener) _mq.addEventListener('change', onChange);
52
+ else if (_mq.addListener) _mq.addListener(onChange);
53
+ }
54
+
55
+ export function applyTheme(mode) {
56
+ if (!VALID.has(mode)) mode = 'auto';
57
+ _current = mode;
58
+ writeAttr(mode);
59
+ writeStored(mode);
60
+ ensureMq();
61
+ const resolved = mode === 'auto'
62
+ ? (_mq && _mq.matches ? 'ink' : 'paper')
63
+ : mode;
64
+ for (const cb of listeners) {
65
+ try { cb({ mode, resolved }); } catch {}
66
+ }
67
+ return mode;
68
+ }
69
+
70
+ export function getTheme() {
71
+ return _current;
72
+ }
73
+
74
+ export function resolvedTheme() {
75
+ if (_current !== 'auto') return _current;
76
+ ensureMq();
77
+ return _mq && _mq.matches ? 'ink' : 'paper';
78
+ }
79
+
80
+ export function onThemeChange(cb) {
81
+ listeners.add(cb);
82
+ return () => listeners.delete(cb);
83
+ }
84
+
85
+ // Auto-init on browser import. Picks stored value, else falls back to
86
+ // whatever data-theme is already on <html> (set by page-html.js), else 'auto'.
87
+ export function initTheme() {
88
+ if (!isBrowser()) return 'auto';
89
+ const stored = readStored();
90
+ const fromAttr = document.documentElement.getAttribute('data-theme');
91
+ const initial = stored || (VALID.has(fromAttr) ? fromAttr : 'auto');
92
+ applyTheme(initial);
93
+ return initial;
94
+ }
95
+
96
+ if (isBrowser()) {
97
+ // Run on next microtask so SSR-injected attributes settle first.
98
+ Promise.resolve().then(() => { try { initTheme(); } catch {} });
99
+ }