anentrypoint-design 0.0.146 → 0.0.148
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 +166 -22
- package/colors_and_type.css +21 -1
- package/community.css +25 -4
- package/dist/247420.css +227 -30
- package/dist/247420.js +13 -12
- package/package.json +1 -1
- package/src/components/chat.js +2 -2
- package/src/components/community.js +4 -4
- package/src/components/content.js +17 -8
- package/src/components/editor-primitives.js +2 -2
- package/src/components/form-primitives.js +25 -12
- package/src/components/freddie/runtime.js +101 -0
- package/src/components/freddie.js +614 -27
- package/src/components/overlay-primitives.js +3 -2
- package/src/components/shell.js +40 -5
- package/src/components.js +2 -2
- package/src/kits/os/freddie/pages-chat.js +1 -1
- package/src/kits/os/freddie/pages-core.js +1 -1
- package/src/kits/os/freddie-dashboard.js +3 -3
- package/src/kits/os/shell.js +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anentrypoint-design",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.148",
|
|
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",
|
package/src/components/chat.js
CHANGED
|
@@ -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', {
|
|
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', {
|
|
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 ?
|
|
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 ?
|
|
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 ?
|
|
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 ?
|
|
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
|
|
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'] = '
|
|
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'
|
|
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
|
-
|
|
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
|
|
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:' +
|
|
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:
|
|
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' }, '
|
|
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
|
-
|
|
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)
|
|
167
|
+
if (!fn) return run(i + 1);
|
|
157
168
|
const out = fn(value, r);
|
|
158
|
-
if (out)
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
return
|
|
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
|
-
|
|
165
|
-
|
|
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
|
+
}
|