anentrypoint-design 0.0.146 → 0.0.147

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.146",
3
+ "version": "0.0.147",
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",
@@ -217,7 +217,7 @@ export function Chat({ title = 'chat', sub, messages = [], composer, header } =
217
217
  return h('div', { class: 'chat' },
218
218
  header || h('div', { class: 'chat-head', role: 'banner' },
219
219
  h('span', { class: 'dot', 'aria-hidden': 'true' }),
220
- h('h2', { style: 'margin:0;font-size:inherit' }, title),
220
+ h('h2', { class: 'ds-chat-title' }, title),
221
221
  sub ? h('span', { class: 'sub', 'aria-label': `subtitle: ${sub}` }, ' · ' + sub) : null,
222
222
  h('span', { class: 'spread' }),
223
223
  h('span', { class: 'sub', 'aria-live': 'polite' }, String(messages.length).padStart(2, '0') + ' msgs')
@@ -280,7 +280,7 @@ export function AICat({ name = 'aicat', messages = [], thinking, composer, statu
280
280
  return h('div', { class: 'chat' },
281
281
  h('div', { class: 'chat-head', role: 'banner' },
282
282
  h('span', { class: 'dot', 'aria-hidden': 'true' }),
283
- h('h2', { style: 'margin:0;font-size:inherit' }, name),
283
+ h('h2', { class: 'ds-chat-title' }, name),
284
284
  h('span', { class: 'sub', 'aria-label': `status: ${status}` }, ' · ' + status),
285
285
  h('span', { class: 'spread' }),
286
286
  h('span', { class: 'sub', 'aria-live': 'polite' }, String(messages.length).padStart(2, '0') + ' turns')
@@ -81,7 +81,7 @@ export function ChannelItem({ id, name, type = 'text', active, voiceActive, voic
81
81
  ),
82
82
  voiceActive && participants.length ? h('div', { class: 'cm-ch-voice-users' },
83
83
  ...participants.map(p => h('div', { class: 'cm-ch-voice-user' + (p.speaking ? ' speaking' : '') },
84
- h('div', { class: 'cm-ch-voice-user-avatar', style: p.color ? `background:${p.color}` : '' }, (p.identity || '?').slice(0, 1).toUpperCase()),
84
+ h('div', { class: 'cm-ch-voice-user-avatar', style: p.color ? `--avatar-bg:${p.color}` : null }, (p.identity || '?').slice(0, 1).toUpperCase()),
85
85
  h('span', { class: 'cm-ch-voice-user-name' }, p.identity)
86
86
  ))
87
87
  ) : null
@@ -115,7 +115,7 @@ export function ChannelCategory({ id, name, channels = [], collapsed, activeId,
115
115
  export function VoiceUser({ identity, speaking, color } = {}) {
116
116
  const initial = (identity || '?').slice(0, 1).toUpperCase();
117
117
  return h('div', { class: 'cm-voice-user' + (speaking ? ' speaking' : '') },
118
- h('div', { class: 'cm-voice-user-avatar', style: color ? `background:${color}` : '' }, initial),
118
+ h('div', { class: 'cm-voice-user-avatar', style: color ? `--avatar-bg:${color}` : null }, initial),
119
119
  h('span', { class: 'cm-voice-user-name' }, identity)
120
120
  );
121
121
  }
@@ -134,7 +134,7 @@ export function UserPanel({ name, tag, color, muted, deafened, onMute, onDeafen,
134
134
  }
135
135
  };
136
136
  return h('div', { class: 'cm-user-panel' },
137
- h('div', { class: 'cm-user-avatar', style: color ? `background:${color}` : '' },
137
+ h('div', { class: 'cm-user-avatar', style: color ? `--avatar-bg:${color}` : null },
138
138
  h('span', { class: 'cm-user-status-dot' }),
139
139
  initial
140
140
  ),
@@ -186,7 +186,7 @@ export function ChannelSidebar({ serverName, channels = [], categories = [], act
186
186
  export function MemberItem({ identity, name, color, status = 'online' } = {}) {
187
187
  const initial = (name || identity || '?').slice(0, 1).toUpperCase();
188
188
  return h('div', { class: 'cm-member-item' },
189
- h('div', { class: 'cm-member-avatar', style: color ? `background:${color}` : '' },
189
+ h('div', { class: 'cm-member-avatar', style: color ? `--avatar-bg:${color}` : null },
190
190
  h('span', { class: 'cm-member-status' + (status === 'online' ? ' online' : '') }),
191
191
  initial
192
192
  ),
@@ -17,12 +17,16 @@ export function Panel({ title, count, right, style = '', children, kind }) {
17
17
  );
18
18
  }
19
19
 
20
+ // Card — semantic alias of Panel; behaves identically.
21
+ export const Card = Panel;
22
+
20
23
  export function Row({ code, title, sub, meta, active, state = 'default', onClick, key, style, href, kind, cols, leading, trailing, target, selected }) {
21
24
  // Support legacy active/selected props for backward compatibility
22
25
  const isActive = state === 'active' || (state === 'default' && (active || selected));
23
26
  const isLink = kind === 'link' || (href != null && !onClick);
24
27
  const isButton = !isLink && !!onClick;
25
- const cls = 'row' + (isActive ? ' active' : '') + (cols ? ' row-grid' : '');
28
+ const stateCls = state === 'disabled' ? ' row-state-disabled' : (state === 'error' ? ' row-state-error' : '');
29
+ const cls = 'row' + (isActive ? ' active' : '') + stateCls + (cols ? ' row-grid' : '');
26
30
  const props = { key, class: cls, style: cols ? `${style ? style + ';' : ''}grid-template-columns:${cols}` : style };
27
31
  if (isLink) {
28
32
  props.href = href || '#';
@@ -36,7 +40,7 @@ export function Row({ code, title, sub, meta, active, state = 'default', onClick
36
40
  if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onClick(e); }
37
41
  };
38
42
  }
39
- if (isActive && (isLink || isButton)) props['aria-current'] = 'true';
43
+ if (isActive && (isLink || isButton)) props['aria-current'] = isActive ? 'page' : null;
40
44
  return h(isLink ? 'a' : 'div', props,
41
45
  leading != null ? leading : (code != null ? h('span', { class: 'code' }, code) : null),
42
46
  h('span', { class: 'title' }, title, sub ? h('span', { class: 'sub' }, sub) : null),
@@ -63,7 +67,7 @@ export function Hero({ eyebrow, title, body, accent, badge, badgeCount, actions
63
67
  body,
64
68
  accent ? h('span', { class: 'ds-hero-accent' }, ' ' + accent) : null
65
69
  ) : null,
66
- actions ? h('div', { class: 'ds-hero-actions', style: 'display:flex;gap:10px;flex-wrap:wrap;margin-top:8px' }, ...(Array.isArray(actions) ? actions : [actions])) : null,
70
+ actions ? h('div', { class: 'ds-hero-actions' }, ...(Array.isArray(actions) ? actions : [actions])) : null,
67
71
  badge ? Panel({ title: badge, count: badgeCount, kind: 'inline', children: [] }) : null
68
72
  );
69
73
  }
@@ -244,28 +248,31 @@ export function SearchInput({ value = '', placeholder = 'search…', onInput, on
244
248
  });
245
249
  }
246
250
 
247
- export function TextField({ label, value = '', type = 'text', placeholder = '', onInput, onChange, name, key, hint, multiline, rows = 4 }) {
251
+ export function TextField({ label, value = '', type = 'text', placeholder = '', onInput, onChange, name, key, hint, multiline, rows = 4, maxLength }) {
248
252
  const input = multiline
249
253
  ? h('textarea', {
250
254
  key: 'i', name, rows, placeholder, value,
255
+ maxlength: maxLength != null ? maxLength : null,
251
256
  oninput: onInput ? (e) => onInput(e.target.value, e) : null,
252
257
  onchange: onChange ? (e) => onChange(e.target.value, e) : null
253
258
  })
254
259
  : h('input', {
255
260
  key: 'i', type, name, placeholder, value,
261
+ maxlength: maxLength != null ? maxLength : null,
256
262
  oninput: onInput ? (e) => onInput(e.target.value, e) : null,
257
263
  onchange: onChange ? (e) => onChange(e.target.value, e) : null
258
264
  });
259
265
  return h('label', { key, class: 'ds-field' },
260
266
  label != null ? h('span', { key: 'l', class: 'ds-field-label' }, label) : null,
261
267
  input,
268
+ maxLength != null ? h('span', { key: 'c', class: 'ds-field-count' }, String(value.length) + '/' + maxLength) : null,
262
269
  hint != null ? h('span', { key: 'h', class: 'lede ds-field-hint' }, hint) : null
263
270
  );
264
271
  }
265
272
 
266
273
  export function Select({ label, value = '', options = [], onChange, name, key, placeholder, hint }) {
267
274
  const opts = [];
268
- if (placeholder != null) opts.push(h('option', { key: '_ph', value: '' }, placeholder));
275
+ if (placeholder != null) opts.push(h('option', { key: '_ph', value: '', disabled: true, selected: value === '' || value == null }, placeholder));
269
276
  for (const o of options) {
270
277
  const id = typeof o === 'string' ? o : (o.value != null ? o.value : o.id);
271
278
  const lab = typeof o === 'string' ? o : (o.label != null ? o.label : (o.id || o.value));
@@ -299,8 +306,9 @@ export function EventList({ items, events, emptyText = 'no events', rankPad = 3
299
306
  );
300
307
  }
301
308
 
302
- export function Form({ fields = [], submit = 'submit', onSubmit }) {
303
- return h('form', { class: 'row-form', onsubmit: (ev) => { ev.preventDefault(); onSubmit && onSubmit(ev); } },
309
+ export function Form({ fields = [], submit = 'submit', onSubmit, columns = 1 }) {
310
+ const cols = columns > 1 ? String(columns) : null;
311
+ return h('form', { class: 'row-form', 'data-columns': cols, onsubmit: (ev) => { ev.preventDefault(); onSubmit && onSubmit(ev); } },
304
312
  ...fields.map((f, i) => f.kind === 'textarea'
305
313
  ? h('textarea', { key: i, name: f.name, placeholder: f.placeholder || '', rows: f.rows || 4 })
306
314
  : h('input', { key: i, name: f.name, type: f.type || 'text', placeholder: f.placeholder || '', value: f.value || '', required: f.required ? 'true' : null })),
@@ -308,7 +316,8 @@ export function Form({ fields = [], submit = 'submit', onSubmit }) {
308
316
  }
309
317
 
310
318
  export function Spinner({ size = 'base', tone = 'accent', label = 'loading', key } = {}) {
311
- const sizeClass = size === 'sm' ? 'ds-spinner-sm' : size === 'lg' ? 'ds-spinner-lg' : '';
319
+ const SIZE_CLASS = { xs: 'ds-spinner-xs', sm: 'ds-spinner-sm', base: '', lg: 'ds-spinner-lg', xl: 'ds-spinner-xl' };
320
+ const sizeClass = SIZE_CLASS[size] != null ? SIZE_CLASS[size] : '';
312
321
  return h('div', {
313
322
  key, class: 'ds-spinner ' + sizeClass + ' tone-' + tone,
314
323
  role: 'status', 'aria-live': 'polite', 'aria-label': label
@@ -113,7 +113,7 @@ export function TreeItem({ label, glyph, tag, depth = 0, selected = false, expan
113
113
  },
114
114
  h('div', {
115
115
  class: 'ds-ep-tree-row',
116
- style: 'padding-left:' + (depth * 12 + 6) + 'px',
116
+ style: 'padding-left:calc(' + depth + ' * var(--tree-indent,12px) + var(--tree-base-indent,6px))',
117
117
  tabindex: selected ? '0' : '-1',
118
118
  onclick: () => onSelect && onSelect(),
119
119
  onkeydown: onRowKeyDown
@@ -286,7 +286,7 @@ export function SplitPanel({ orientation = 'horizontal', initial = '50%', min =
286
286
  class: 'ds-ep-split ' + (isH ? 'horiz' : 'vert'),
287
287
  ref: (el) => { rootEl = el; }
288
288
  },
289
- h('div', { class: 'ds-ep-split-pane', style: sizeProp + ':' + initStyle + ';flex:0 0 auto' }, first),
289
+ h('div', { class: 'ds-ep-split-pane', style: '--split-size:' + initStyle + ';flex:0 0 auto' }, first),
290
290
  ResizeHandle({ axis: isH ? 'horizontal' : 'vertical', onResize }),
291
291
  h('div', { class: 'ds-ep-split-pane grow', style: 'flex:1 1 0;min-' + sizeProp + ':0' }, second)
292
292
  );
@@ -73,7 +73,7 @@ export function RadioGroup({ legend, name, value, options = [], onChange, orient
73
73
  const inputs = root.querySelectorAll('input[type="radio"]');
74
74
  if (inputs[next]) inputs[next].focus();
75
75
  };
76
- return h('fieldset', { key, class: 'ds-radio-group ' + (isHoriz ? 'horiz' : 'vert'), role: 'radiogroup', onkeydown: onKeyDown },
76
+ return h('fieldset', { key, class: 'ds-radio-group ' + (isHoriz ? 'horiz' : 'vert'), role: 'radiogroup', 'aria-orientation': isHoriz ? 'horizontal' : 'vertical', onkeydown: onKeyDown },
77
77
  legend != null ? h('legend', { key: 'lg', class: 'ds-field-label' }, legend) : null,
78
78
  ...options.map((o, i) => {
79
79
  const v = typeof o === 'string' ? o : o.value;
@@ -110,7 +110,7 @@ function cloneWithProps(node, extra) {
110
110
  return { ...node, props: { ...(node.props || {}), ...extra } };
111
111
  }
112
112
 
113
- export function Field({ label, hint, error, required, htmlFor, children, key } = {}) {
113
+ export function Field({ label, hint, error, required, requiredMarker = '*', htmlFor, children, key } = {}) {
114
114
  const autoId = htmlFor || uid('ds-field');
115
115
  const hintId = hint != null ? autoId + '-hint' : null;
116
116
  const errorId = error != null ? autoId + '-err' : null;
@@ -128,12 +128,12 @@ export function Field({ label, hint, error, required, htmlFor, children, key } =
128
128
  return h('div', { key, class: 'ds-field-wrap' },
129
129
  label != null ? h('label', { key: 'l', class: 'ds-field-label', for: autoId },
130
130
  label,
131
- required ? h('span', { key: 'r', class: 'ds-field-required', 'aria-hidden': 'true' }, ' *') : null
131
+ required ? h('span', { key: 'r', class: 'ds-field-required', 'aria-hidden': 'true' }, ' ' + requiredMarker) : null
132
132
  ) : null,
133
133
  required ? h('span', { key: 'sr', class: 'sr-only' }, 'required') : null,
134
134
  ...decorated,
135
135
  error != null
136
- ? h('div', { key: 'e', id: errorId, class: 'ds-field-error', role: 'alert' }, error)
136
+ ? h('div', { key: 'e', id: errorId, class: 'ds-field-error', role: 'alert', 'aria-live': 'polite', 'aria-atomic': 'true' }, error)
137
137
  : (hint != null ? h('div', { key: 'h', id: hintId, class: 'ds-field-hint' }, hint) : null)
138
138
  );
139
139
  }
@@ -149,20 +149,33 @@ const RULES = {
149
149
 
150
150
  export function useFormValidation(schema = {}) {
151
151
  const errors = {};
152
+ const isPromise = (x) => x != null && typeof x.then === 'function';
153
+ // Runs rules for one field. Returns the error string/null synchronously when
154
+ // no rule yields a Promise; returns a Promise resolving to that value when
155
+ // any rule (e.g. an async custom validator) does.
152
156
  const validateField = (name, value) => {
153
157
  const rules = schema[name] || [];
154
- for (const r of rules) {
158
+ const settle = (out, idx) => {
159
+ if (out) { errors[name] = rules[idx].message || out; return errors[name]; }
160
+ // No error from this rule — continue with the rest.
161
+ return run(idx + 1);
162
+ };
163
+ const run = (i) => {
164
+ if (i >= rules.length) { delete errors[name]; return null; }
165
+ const r = rules[i];
155
166
  const fn = RULES[r.rule];
156
- if (!fn) continue;
167
+ if (!fn) return run(i + 1);
157
168
  const out = fn(value, r);
158
- if (out) { errors[name] = r.message || out; return errors[name]; }
159
- }
160
- delete errors[name];
161
- return null;
169
+ if (isPromise(out)) return out.then((res) => settle(res, i));
170
+ return settle(out, i);
171
+ };
172
+ return run(0);
162
173
  };
163
174
  const validate = (values = {}) => {
164
- for (const name of Object.keys(schema)) validateField(name, values[name]);
165
- return { valid: Object.keys(errors).length === 0, errors: { ...errors } };
175
+ const names = Object.keys(schema);
176
+ const results = names.map((name) => validateField(name, values[name]));
177
+ const finish = () => ({ valid: Object.keys(errors).length === 0, errors: { ...errors } });
178
+ return results.some(isPromise) ? Promise.all(results).then(finish) : finish();
166
179
  };
167
180
  return { errors, validate, validateField };
168
181
  }
@@ -0,0 +1,101 @@
1
+ // Freddie page runtime — gives each page a self-contained state + fetch +
2
+ // rerender loop so the consumer's thin router (e.g. freddie src/web/app.js)
3
+ // only has to call `page(host)` and mount the returned vnode. No router
4
+ // changes required downstream: each page boots its own micro render loop via
5
+ // a ref callback that uses the SDK's own applyDiff.
6
+
7
+ import * as webjsx from '../../../vendor/webjsx/index.js';
8
+ const h = webjsx.createElement;
9
+ const applyDiff = webjsx.applyDiff;
10
+
11
+ // Same-origin JSON fetch helper. Returns parsed JSON or throws with a
12
+ // readable message carrying the HTTP status so page error states can show it.
13
+ export async function api(path, opts = {}) {
14
+ const res = await fetch(path, {
15
+ headers: { 'content-type': 'application/json', ...(opts.headers || {}) },
16
+ ...opts,
17
+ body: opts.body != null && typeof opts.body !== 'string' ? JSON.stringify(opts.body) : opts.body,
18
+ });
19
+ let json = null;
20
+ const text = await res.text();
21
+ try { json = text ? JSON.parse(text) : null; } catch { json = { _raw: text }; }
22
+ if (!res.ok) {
23
+ const msg = (json && (json.error?.message || json.error || json.message)) || text || ('HTTP ' + res.status);
24
+ const err = new Error(typeof msg === 'string' ? msg : JSON.stringify(msg));
25
+ err.status = res.status;
26
+ err.body = json;
27
+ throw err;
28
+ }
29
+ return json;
30
+ }
31
+
32
+ // makePage(setup) -> (host) => vnode
33
+ // setup(ctx) is called once per mount with:
34
+ // ctx.state — mutable page state object (seeded from initial)
35
+ // ctx.set(p) — shallow-merge into state and rerender
36
+ // ctx.rerender()— force a rerender
37
+ // ctx.host — the consumer host object passed by the router
38
+ // ctx.api — the fetch helper above
39
+ // setup MUST return a render() function: () => vnode (sync) using ctx.state.
40
+ // Optionally setup may kick off async loads that call ctx.set(...) on arrival
41
+ // and may register intervals via ctx.interval(fn, ms) (auto-cleared on unmount).
42
+ export function makePage(setup, { initial = {} } = {}) {
43
+ return function pageRenderer(host) {
44
+ const state = { loading: true, error: null, ...initial };
45
+ const timers = [];
46
+ let elRef = null;
47
+ let render = () => h('div', {});
48
+ const ctx = {
49
+ state, host, api,
50
+ set(patch) { Object.assign(state, patch); ctx.rerender(); },
51
+ rerender() { if (elRef) { try { applyDiff(elRef, wrap()); } catch (e) { console.warn('[freddie page rerender]', e); } } },
52
+ interval(fn, ms) { const id = setInterval(fn, ms); timers.push(id); return id; },
53
+ cleanup() { for (const id of timers) clearInterval(id); timers.length = 0; },
54
+ };
55
+ function wrap() {
56
+ let body;
57
+ try { body = render(); }
58
+ catch (e) {
59
+ body = h('div', { class: 'ds-alert ds-alert-error', role: 'alert' },
60
+ h('span', { class: 'ds-alert-icon' }, '✕'),
61
+ h('div', { class: 'ds-alert-content' },
62
+ h('div', { class: 'ds-alert-title' }, 'page render error'),
63
+ h('pre', { class: 'fd-pre' }, String(e && e.stack || e))));
64
+ }
65
+ return h('div', { class: 'fd-page-inner' }, ...(Array.isArray(body) ? body : [body]));
66
+ }
67
+ const ref = (el) => {
68
+ if (!el) { ctx.cleanup(); return; }
69
+ if (elRef === el) return;
70
+ elRef = el;
71
+ const r = setup(ctx);
72
+ if (typeof r === 'function') render = r;
73
+ ctx.rerender();
74
+ };
75
+ return h('div', { class: 'fd-page-root', ref });
76
+ };
77
+ }
78
+
79
+ // Standard loading + error scaffolding helpers so every page is consistent.
80
+ export function loadingState(label = 'loading…') {
81
+ return h('div', { class: 'fd-loading', role: 'status', 'aria-live': 'polite' },
82
+ h('div', { class: 'ds-spinner tone-accent', 'aria-hidden': 'true' },
83
+ h('span'), h('span'), h('span')),
84
+ h('span', { class: 'dim' }, label));
85
+ }
86
+
87
+ export function errorState(err, onRetry) {
88
+ const msg = String(err && err.message || err);
89
+ return h('div', { class: 'ds-alert ds-alert-error', role: 'alert' },
90
+ h('span', { class: 'ds-alert-icon' }, '✕'),
91
+ h('div', { class: 'ds-alert-content' },
92
+ h('div', { class: 'ds-alert-title' }, 'failed to load'),
93
+ h('div', { class: 'ds-alert-message' }, msg),
94
+ onRetry ? h('button', { class: 'btn', onclick: onRetry, style: 'margin-top:8px' }, 'retry') : null));
95
+ }
96
+
97
+ export function emptyState(text = 'nothing here yet', glyph = '◌') {
98
+ return h('div', { class: 'fd-empty', role: 'status' },
99
+ h('div', { class: 'fd-empty-glyph', 'aria-hidden': 'true' }, glyph),
100
+ h('div', { class: 'dim' }, text));
101
+ }