@christianirack/brainllm-runtime 0.0.0-stage → 0.1.0

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 (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +4 -2
  3. package/brain-anim.js +21 -0
  4. package/brain-ch.js +254 -0
  5. package/brain-consent.js +38 -0
  6. package/brain-data.js +547 -0
  7. package/brain-dev.js +100 -0
  8. package/brain-env.js +160 -0
  9. package/brain-err.js +242 -0
  10. package/brain-form.js +260 -0
  11. package/brain-guard.js +242 -0
  12. package/brain-hydrate.js +72 -0
  13. package/brain-i18n.js +111 -0
  14. package/brain-inspect.js +745 -0
  15. package/brain-island.js +106 -0
  16. package/brain-lazy.js +88 -0
  17. package/brain-mcp.js +58 -0
  18. package/brain-mock.js +80 -0
  19. package/brain-overlay.js +123 -0
  20. package/brain-persist.js +65 -0
  21. package/brain-router.js +353 -0
  22. package/brain-rpc.js +17 -0
  23. package/brain-schema.js +69 -0
  24. package/brain-wc.js +69 -0
  25. package/brain-why.js +198 -0
  26. package/brain-worker.js +66 -0
  27. package/brain.js +421 -0
  28. package/package.json +20 -4
  29. package/server/server.mjs +882 -0
  30. package/shim/dom.js +448 -0
  31. package/shim/probe.js +111 -0
  32. package/sw/brain-sw.js +103 -0
  33. package/themes/contrast.cssbrain +39 -0
  34. package/themes/dark.cssbrain +39 -0
  35. package/themes/glass.cssbrain +39 -0
  36. package/themes/paper.cssbrain +39 -0
  37. package/ui/button.brain +31 -0
  38. package/ui/center.brain +11 -0
  39. package/ui/checkbox.brain +13 -0
  40. package/ui/container.brain +11 -0
  41. package/ui/dialog.brain +29 -0
  42. package/ui/error-state.brain +53 -0
  43. package/ui/field.brain +34 -0
  44. package/ui/grid.brain +11 -0
  45. package/ui/input.brain +23 -0
  46. package/ui/lang-switch.brain +38 -0
  47. package/ui/menu.brain +64 -0
  48. package/ui/row.brain +12 -0
  49. package/ui/select.brain +29 -0
  50. package/ui/sidebar.brain +17 -0
  51. package/ui/spacer.brain +12 -0
  52. package/ui/stack.brain +10 -0
  53. package/ui/table.brain +73 -0
  54. package/ui/tabs.brain +33 -0
  55. package/ui/toast.brain +45 -0
package/ui/table.brain ADDED
@@ -0,0 +1,73 @@
1
+ <meta props="rows:list columns:list caption:str rowKey:str='id' sortable:bool=true loading:bool=false error:str? empty:str='No data yet.' rowHeight:num=40 height:num=480">
2
+ <script>
3
+ // columns: [{ key: 'name', label: 'Name', sort?: false }] · more than 200 rows → only the visible window is rendered
4
+ const VIRTUAL_FROM = 200
5
+ const OVERSCAN = 10
6
+ let sortKey = sig('')
7
+ let dir = sig(1)
8
+ let top = sig(0)
9
+ const sorted = drv(() => {
10
+ if (!sortKey) return rows
11
+ const k = sortKey
12
+ const d = dir
13
+ return [...rows].sort((a, b) => (a[k] > b[k] ? d : a[k] < b[k] ? -d : 0))
14
+ })
15
+ const virtual = drv(() => sorted.length > VIRTUAL_FROM)
16
+ const first = drv(() => (virtual ? Math.max(0, Math.floor(top / rowHeight) - OVERSCAN) : 0))
17
+ const last = drv(() => (virtual ? Math.min(sorted.length, first + Math.ceil(height / rowHeight) + 2 * OVERSCAN) : sorted.length))
18
+ const visible = drv(() => sorted.slice(first, last))
19
+ const sortBy = (k) => {
20
+ if (sortKey === k) dir = -dir
21
+ else { sortKey = k; dir = 1 }
22
+ }
23
+ const ariaSort = (k) => (sortKey === k ? (dir === 1 ? 'ascending' : 'descending') : 'none')
24
+ const arrow = (k) => (sortKey === k ? (dir === 1 ? '▲' : '▼') : '')
25
+ const onScroll = (e) => { top = e.target.scrollTop }
26
+ </script>
27
+ <div class="wrap" class:virtual={virtual} style:max-height={virtual ? height + 'px' : null} on:scroll={onScroll}>
28
+ <table aria-busy={loading ? 'true' : null} aria-rowcount={sorted.length + 1}>
29
+ <caption>{caption}</caption>
30
+ <thead>
31
+ <tr>
32
+ {#each columns as c (c.key)}
33
+ <th scope="col" aria-sort={ariaSort(c.key)}>
34
+ {#if sortable && c.sort !== false}
35
+ <button type="button" on:click={sortBy(c.key)}>{c.label} <span aria-hidden="true">{arrow(c.key)}</span></button>
36
+ {:else}
37
+ {c.label}
38
+ {/if}
39
+ </th>
40
+ {/each}
41
+ </tr>
42
+ </thead>
43
+ <tbody>
44
+ {#if error}
45
+ <tr><td class="msg bad" colspan={columns.length} role="alert">{error}</td></tr>
46
+ {:else if loading}
47
+ <tr><td class="msg" colspan={columns.length}>Loading…</td></tr>
48
+ {:else}
49
+ {#if first > 0}<tr class="pad" aria-hidden="true" style:height={first * rowHeight + 'px'}></tr>{/if}
50
+ {#each visible as r (r[rowKey])}
51
+ <tr style:height={rowHeight + 'px'}>
52
+ {#each columns as c (c.key)}<td>{r[c.key]}</td>{/each}
53
+ </tr>
54
+ {:empty}
55
+ <tr><td class="msg" colspan={columns.length}>{empty}</td></tr>
56
+ {/each}
57
+ {#if last < sorted.length}<tr class="pad" aria-hidden="true" style:height={(sorted.length - last) * rowHeight + 'px'}></tr>{/if}
58
+ {/if}
59
+ </tbody>
60
+ </table>
61
+ </div>
62
+ <style>
63
+ .wrap { overflow: auto; border: 1px solid var(--glass-edge); border-radius: var(--radius); background: var(--glass); box-shadow: var(--shadow); }
64
+ table { width: 100%; border-collapse: collapse; font: 14px/1.4 var(--font); color: var(--fg); font-variant-numeric: tabular-nums; }
65
+ caption { text-align: left; font-weight: 650; padding: var(--space-3) var(--space-3) var(--space-2); }
66
+ th, td { text-align: left; padding: var(--space-2) var(--space-3); border-bottom: 1px solid var(--line); }
67
+ thead th { position: sticky; top: 0; background: var(--surface); font-weight: 600; color: var(--muted); }
68
+ th button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
69
+ th button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
70
+ .msg { text-align: center; color: var(--muted); padding: var(--space-5); }
71
+ .msg.bad { color: var(--bad); }
72
+ .pad td { padding: 0; border: 0; }
73
+ </style>
package/ui/tabs.brain ADDED
@@ -0,0 +1,33 @@
1
+ <meta props="tabs:list value:str~ label:str">
2
+ <script>
3
+ // tabs: [{ id: 'a', label: 'Overview' }] · the parent renders the panel of `value` in the slot
4
+ let list = ref()
5
+ const current = drv(() => tabs.find((t) => t.id === value) || tabs[0])
6
+ const keys = (e) => {
7
+ const i = tabs.findIndex((t) => t.id === value)
8
+ let j = -1
9
+ if (e.key === 'ArrowRight') j = (i + 1) % tabs.length
10
+ else if (e.key === 'ArrowLeft') j = (i - 1 + tabs.length) % tabs.length
11
+ else if (e.key === 'Home') j = 0
12
+ else if (e.key === 'End') j = tabs.length - 1
13
+ if (j < 0) return
14
+ e.preventDefault()
15
+ value = tabs[j].id
16
+ list.querySelectorAll('[role=tab]')[j].focus()
17
+ }
18
+ </script>
19
+ <div class="tabs">
20
+ <div class="list" role="tablist" aria-label={label} bind:ref={list} on:keydown={keys}>
21
+ {#each tabs as t (t.id)}
22
+ <button type="button" role="tab" aria-selected={t.id === value ? 'true' : 'false'} tabindex={t.id === value ? 0 : -1} on:click={value = t.id}>{t.label}</button>
23
+ {/each}
24
+ </div>
25
+ <div class="panel" role="tabpanel" aria-label={current ? current.label : label} tabindex="0"><slot /></div>
26
+ </div>
27
+ <style>
28
+ .list { display: flex; gap: var(--space-1); padding: var(--space-1); border-radius: var(--radius); background: var(--glass); border: 1px solid var(--glass-edge); width: fit-content; }
29
+ [role=tab] { font: 600 14px/1.2 var(--font); padding: var(--space-2) var(--space-3); border: 0; border-radius: var(--radius-sm); color: var(--muted); background: transparent; cursor: pointer; }
30
+ [role=tab][aria-selected='true'] { color: var(--fg); background: var(--surface); box-shadow: var(--shadow); }
31
+ [role=tab]:focus-visible, .panel:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
32
+ .panel { padding: var(--space-4) 0; }
33
+ </style>
package/ui/toast.brain ADDED
@@ -0,0 +1,45 @@
1
+ <meta props="from:obj ms:num=3000 max:num=3 dismissLabel:str='Dismiss'">
2
+ <script>
3
+ // from: a bus from channels/ (`export default bus('toast')`); send a string or { text, kind: 'ok' | 'error' }
4
+ let items = sig([])
5
+ let next = 0
6
+ const dismiss = (id) => { items = items.filter((x) => x.id !== id) }
7
+ fx(() => {
8
+ let timers = []
9
+ const off = from.on((m) => {
10
+ const t = typeof m === 'object' && m ? m : { text: String(m) }
11
+ next = next + 1
12
+ const id = next
13
+ items = [...items, { id, text: t.text, kind: t.kind || 'info' }].slice(-max)
14
+ const h = setTimeout(() => {
15
+ timers = timers.filter((x) => x !== h)
16
+ dismiss(id)
17
+ }, ms)
18
+ timers = [...timers, h]
19
+ })
20
+ return () => {
21
+ off()
22
+ for (const h of timers) clearTimeout(h)
23
+ }
24
+ })
25
+ </script>
26
+ <div class="toasts" role="status" aria-live="polite">
27
+ {#each items as t (t.id)}
28
+ <div class="toast" class:ok={t.kind === 'ok'} class:bad={t.kind === 'error'}>
29
+ <span>{t.text}</span>
30
+ <button type="button" aria-label={dismissLabel} on:click={dismiss(t.id)}>×</button>
31
+ </div>
32
+ {/each}
33
+ </div>
34
+ <style>
35
+ .toasts { position: fixed; right: 16px; bottom: 16px; display: grid; gap: var(--space-2); z-index: 1000; }
36
+ .toast {
37
+ display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4); min-width: 240px;
38
+ font: 14px/1.4 var(--font); color: var(--fg); background: var(--glass); border: 1px solid var(--glass-edge);
39
+ border-radius: var(--radius); box-shadow: var(--shadow); backdrop-filter: blur(20px) saturate(1.4);
40
+ }
41
+ .toast.ok { border-color: var(--ok); }
42
+ .toast.bad { border-color: var(--bad); }
43
+ .toast span { flex: 1; }
44
+ button { font-size: 18px; line-height: 1; border: 0; color: var(--muted); background: transparent; cursor: pointer; }
45
+ </style>