@sylwellsoftware/fray 0.7.0 → 1.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 (112) hide show
  1. package/README.md +282 -207
  2. package/colors/README.md +27 -23
  3. package/colors/gray/colors.css +1 -42
  4. package/colors/green/colors.css +1 -42
  5. package/colors/iceblue/colors.css +5 -43
  6. package/colors/ocean/colors.css +1 -42
  7. package/colors/orange/colors.css +1 -42
  8. package/colors/purple/colors.css +1 -42
  9. package/colors/red/colors.css +1 -42
  10. package/colors/yellow/colors.css +1 -42
  11. package/dist/Components/Placeholder.d.ts +1 -2
  12. package/dist/Components/Placeholder.d.ts.map +1 -1
  13. package/dist/Components/component.d.ts +15 -7
  14. package/dist/Components/component.d.ts.map +1 -1
  15. package/dist/Components/data/descriptionList.d.ts +3 -1
  16. package/dist/Components/data/descriptionList.d.ts.map +1 -1
  17. package/dist/Components/data/listview/listview.d.ts +1 -2
  18. package/dist/Components/data/listview/listview.d.ts.map +1 -1
  19. package/dist/Components/data/table/DataTable.d.ts +1 -1
  20. package/dist/Components/data/table/DataTable.d.ts.map +1 -1
  21. package/dist/Components/data/table/FilterPanel.d.ts +2 -4
  22. package/dist/Components/data/table/FilterPanel.d.ts.map +1 -1
  23. package/dist/Components/data/table/TableHeader.d.ts +2 -0
  24. package/dist/Components/data/table/TableHeader.d.ts.map +1 -1
  25. package/dist/Components/data/table/TableHeaderCell.d.ts +1 -1
  26. package/dist/Components/data/table/TableHeaderCell.d.ts.map +1 -1
  27. package/dist/Components/data/treeview/treeitem.d.ts +1 -0
  28. package/dist/Components/data/treeview/treeitem.d.ts.map +1 -1
  29. package/dist/Components/data/treeview/treeview.d.ts +5 -1
  30. package/dist/Components/data/treeview/treeview.d.ts.map +1 -1
  31. package/dist/Components/dialog/dialog.d.ts +6 -2
  32. package/dist/Components/dialog/dialog.d.ts.map +1 -1
  33. package/dist/Components/layout/groupPanel.d.ts +21 -0
  34. package/dist/Components/layout/groupPanel.d.ts.map +1 -0
  35. package/dist/Components/layout/header.d.ts +19 -0
  36. package/dist/Components/layout/header.d.ts.map +1 -0
  37. package/dist/Components/layout/panel.d.ts +7 -3
  38. package/dist/Components/layout/panel.d.ts.map +1 -1
  39. package/dist/Components/layout/sidebar.d.ts +4 -0
  40. package/dist/Components/layout/sidebar.d.ts.map +1 -1
  41. package/dist/Components/layout/splitView.d.ts +1 -1
  42. package/dist/Components/layout/splitView.d.ts.map +1 -1
  43. package/dist/Components/layout/tabpanel/tab.d.ts +1 -0
  44. package/dist/Components/layout/tabpanel/tab.d.ts.map +1 -1
  45. package/dist/Components/layout/tabpanel/tabline.d.ts +1 -2
  46. package/dist/Components/layout/tabpanel/tabline.d.ts.map +1 -1
  47. package/dist/Components/layout/tabpanel/tabpanel.d.ts +1 -1
  48. package/dist/Components/layout/tabpanel/tabpanel.d.ts.map +1 -1
  49. package/dist/Components/lineinputs/CheckableControl.d.ts +7 -0
  50. package/dist/Components/lineinputs/CheckableControl.d.ts.map +1 -0
  51. package/dist/Components/lineinputs/LabeledInputControl.d.ts +7 -0
  52. package/dist/Components/lineinputs/LabeledInputControl.d.ts.map +1 -0
  53. package/dist/Components/lineinputs/SelectControl.d.ts +7 -0
  54. package/dist/Components/lineinputs/SelectControl.d.ts.map +1 -0
  55. package/dist/Components/lineinputs/checkbox/Checkbox.d.ts +11 -5
  56. package/dist/Components/lineinputs/checkbox/Checkbox.d.ts.map +1 -1
  57. package/dist/Components/lineinputs/checkbox/QuadCheckbox.d.ts +1 -2
  58. package/dist/Components/lineinputs/checkbox/QuadCheckbox.d.ts.map +1 -1
  59. package/dist/Components/lineinputs/checkbox/TriCheckbox.d.ts +1 -2
  60. package/dist/Components/lineinputs/checkbox/TriCheckbox.d.ts.map +1 -1
  61. package/dist/Components/lineinputs/dropdown.d.ts +7 -6
  62. package/dist/Components/lineinputs/dropdown.d.ts.map +1 -1
  63. package/dist/Components/lineinputs/label.d.ts +14 -0
  64. package/dist/Components/lineinputs/label.d.ts.map +1 -0
  65. package/dist/Components/lineinputs/radio.d.ts +20 -7
  66. package/dist/Components/lineinputs/radio.d.ts.map +1 -1
  67. package/dist/Components/lineinputs/textbox.d.ts +7 -6
  68. package/dist/Components/lineinputs/textbox.d.ts.map +1 -1
  69. package/dist/Components/lineinputs/toggle.d.ts +10 -3
  70. package/dist/Components/lineinputs/toggle.d.ts.map +1 -1
  71. package/dist/Components/menu/button.d.ts +6 -3
  72. package/dist/Components/menu/button.d.ts.map +1 -1
  73. package/dist/Components/menu/toolbar.d.ts +1 -1
  74. package/dist/Components/menu/toolbar.d.ts.map +1 -1
  75. package/dist/Components/status/progressBar.d.ts +1 -1
  76. package/dist/Components/status/progressBar.d.ts.map +1 -1
  77. package/dist/Components/theme/stylesheetPicker.d.ts +6 -7
  78. package/dist/Components/theme/stylesheetPicker.d.ts.map +1 -1
  79. package/dist/index.d.ts +4 -1
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +2613 -1825
  82. package/dist/index.js.map +1 -1
  83. package/dist/{jsx-dev-runtime-Kf1Dhu-y.js → jsx-dev-runtime-8s_xIag_.js} +501 -484
  84. package/dist/jsx-dev-runtime-8s_xIag_.js.map +1 -0
  85. package/dist/jsx-dev-runtime.js +1 -1
  86. package/dist/jsx-runtime.js +1 -1
  87. package/dist/routing/RouteLink.d.ts +1 -0
  88. package/dist/routing/RouteLink.d.ts.map +1 -1
  89. package/dist/routing/RouteQuery.d.ts +1 -0
  90. package/dist/routing/RouteQuery.d.ts.map +1 -1
  91. package/dist/routing/RouteScope.d.ts +1 -0
  92. package/dist/routing/RouteScope.d.ts.map +1 -1
  93. package/dist/routing/RouteValue.d.ts +1 -0
  94. package/dist/routing/RouteValue.d.ts.map +1 -1
  95. package/dist/runtime.d.ts +5 -13
  96. package/dist/runtime.d.ts.map +1 -1
  97. package/dist/styling/styleRegistry.d.ts +0 -4
  98. package/dist/styling/styleRegistry.d.ts.map +1 -1
  99. package/dist/styling/theme.d.ts +14 -2
  100. package/dist/styling/theme.d.ts.map +1 -1
  101. package/package.json +5 -6
  102. package/styles/structural.css +943 -399
  103. package/themes/README.md +30 -72
  104. package/themes/base.css +313 -0
  105. package/themes/java/theme.css +14 -423
  106. package/themes/minimal/theme.css +3 -419
  107. package/themes/shiny/theme.css +121 -508
  108. package/dist/jsx-dev-runtime-Kf1Dhu-y.js.map +0 -1
  109. package/dist/styling/baseStyleDefinitions.d.ts +0 -75
  110. package/dist/styling/baseStyleDefinitions.d.ts.map +0 -1
  111. package/themes/dark.css +0 -98
  112. package/themes/light.css +0 -98
package/README.md CHANGED
@@ -27,10 +27,12 @@ Presentation should be equally direct. Native HTML already defines buttons,
27
27
  inputs, tables, lists, progress, dialogs, and landmarks, so Fray uses those
28
28
  elements when their semantics match. Components that need another boundary use
29
29
  readable light-DOM host names rather than framework identity classes.
30
- Application classes remain available for meaningful reusable traits and
31
- consumer styling, while Fray's current structural implementation uses explicit
32
- host and part metadata for its own layout and documented exceptional theme
33
- rules.
30
+ Within a component host, Fray uses classes for owned parts, purely visual
31
+ states with no semantic equivalent, and meaningful reusable traits; those
32
+ classes do not identify components. Native and ARIA state attributes remain the
33
+ sole semantic state source and are targeted directly by CSS. Renderer markers,
34
+ diagnostics, interoperability, and actual data use `data-*`; Fray does not use
35
+ `data-*` as its routine component-CSS hook.
34
36
 
35
37
  ## Design model
36
38
 
@@ -92,15 +94,17 @@ Responsibility stays at the narrowest layer that understands it:
92
94
 
93
95
  ## Set up a browser application
94
96
 
95
- Fray ships ESM, TypeScript declarations, automatic/classic JSX runtimes, one
96
- generated structural stylesheet, replaceable theme treatments, and replaceable
97
- color palettes. Glue is a peer dependency.
97
+ Fray ships ESM, TypeScript declarations, automatic/classic JSX runtimes,
98
+ variable defaults, replaceable theme treatments, and replaceable color
99
+ palettes. Glue is a peer dependency. Applications collect structural CSS from
100
+ the component classes they actually declare.
98
101
 
99
102
  ```ts
100
103
  import {Emitter} from '@sylwellsoftware/glue'
101
104
  import {
102
105
  Button,
103
106
  Component,
107
+ Header,
104
108
  Panel,
105
109
  Sidebar,
106
110
  Textbox,
@@ -108,7 +112,7 @@ import {
108
112
  createFrayRuntime,
109
113
  h,
110
114
  } from '@sylwellsoftware/fray'
111
- import '@sylwellsoftware/fray/styles/structural.css'
115
+ import '@sylwellsoftware/fray/themes/base.css'
112
116
  import '@sylwellsoftware/fray/colors/iceblue/colors.css'
113
117
  import '@sylwellsoftware/fray/themes/minimal/theme.css'
114
118
 
@@ -118,7 +122,10 @@ class App extends Component {
118
122
  static dependencies = [Button, Panel, Textbox, Toolbar]
119
123
 
120
124
  render() {
121
- return h(Panel, {header: 'Profile'},
125
+ return h(Panel, {
126
+ className: 'fray-fill-horizontal fray-fill-vertical',
127
+ header: 'Profile',
128
+ },
122
129
  h(Textbox, {label: 'Name', valueEmitter: name}),
123
130
  h(Toolbar, {label: 'Profile actions'},
124
131
  h(Button, {label: 'Save', onClick: () => save(name.get())})))
@@ -126,6 +133,7 @@ class App extends Component {
126
133
  }
127
134
 
128
135
  const runtime = createFrayRuntime()
136
+ runtime.registerStyles(App).injectStyles(document)
129
137
  runtime.mount(runtime.create(App), document.querySelector('#app')!)
130
138
 
131
139
  function save(value: string) {
@@ -133,6 +141,17 @@ function save(value: string) {
133
141
  }
134
142
  ```
135
143
 
144
+ Root sizing is application-owned. Add `fray-fill-horizontal` to a rendered
145
+ application root to claim exactly `100vw`, `fray-fill-vertical` to claim
146
+ exactly `100vh`, or both for a fullscreen application. Each axis supplies its
147
+ own root overflow fallback and zero minimum; descendant islands are bounded
148
+ and scroll on only the opted-in axes. Other components retain their intrinsic
149
+ minimums and their own structural overflow contracts. Either modifier also
150
+ establishes the theme's `--font-family`, `--font-size`, and `--line-height` on
151
+ the application root so native content and Fray controls inherit the published
152
+ typography without application CSS. A root with neither modifier keeps its
153
+ ordinary embedded/content-sized behavior, including its host page's typography.
154
+
136
155
  The prebuilt structural file targets Fray's default `fray-` hosts. Applications
137
156
  with custom components or configured host names may instead register their root
138
157
  dependencies and call `runtime.injectStyles(document)`; collection remains
@@ -380,11 +399,11 @@ not a framework identity class. The default application runtime therefore
380
399
  produces DOM such as:
381
400
 
382
401
  ```html
383
- <fray-panel class="panellike" data-fray-component="panel">
384
- <fray-textbox data-fray-component="textbox">
385
- <input type="text">
402
+ <fray-panel data-fray class="island" data-fray-component="panel">
403
+ <fray-textbox data-fray data-fray-component="textbox">
404
+ <input data-fray type="text">
386
405
  </fray-textbox>
387
- <button data-fray-component="button">Save</button>
406
+ <button data-fray data-fray-component="button">Save</button>
388
407
  </fray-panel>
389
408
  ```
390
409
 
@@ -393,48 +412,44 @@ Native semantics remain native: `Button` renders `button`, `Toggle` renders
393
412
  `thead`/`th`. `Tab` is a declarative child consumed by `TabPanel` and has no
394
413
  independent root. The
395
414
  `data-fray-component` keeps diagnostics unambiguous. It is not a structural or
396
- theme selector. Fray may merge public presentation traits such as `panellike`
397
- with an application-supplied `class`/`className`; those traits describe a
398
- reusable capability, not component identity.
399
-
400
- Element naming is an immutable application-runtime setting:
401
-
402
- ```ts
403
- const defaultNames = createFrayRuntime()
404
- // <fray-panel>, <fray-list-view>, ...
405
-
406
- const productNames = createFrayRuntime({
407
- elementNames: {prefix: 'acme'},
408
- })
409
- // <acme-panel>, <acme-list-view>, ...
410
-
411
- const prefixlessNames = createFrayRuntime({
412
- elementNames: {prefix: null},
413
- })
414
- // <layout-panel>, <list-view>, <text-box>, ...
415
+ theme selector. Fray may merge public presentation traits such as `island`
416
+ and `colored` with an application-supplied `class`/`className`; those traits
417
+ describe a reusable capability, not component identity.
418
+
419
+ Pass `island` to a fixed-host component when that surface should be visually
420
+ separated from the page. Fray adds the reusable `island` class to its host;
421
+ applications may use the same class on deliberate native surface boundaries.
422
+ The component-owned rule consumes `--island-*` variables, so an island theme
423
+ can add gutters, an edge, and elevation while a flat theme can leave the
424
+ modifier visually neutral. Islands are one surface layer: nesting an `island`
425
+ component below another island is rejected. Application-authored native island
426
+ classes must follow the same no-nesting invariant. A surface island never
427
+ creates a nested theme or palette scope.
428
+
429
+ Apply `colored` only to an element that supplies an ordered dark/base/light
430
+ triplet through `--c1`, `--c2`, and `--c3`. The shared component rule paints
431
+ that triplet as a gradient; named themes can add depth through
432
+ `--colored-shadow`. Shiny supplies its glossy shadow while the base and other
433
+ themes remain flat.
434
+
435
+ Built-in host names are fixed public DOM: Fray adds its one required custom
436
+ element hyphen and removes internal word separators from the component stem.
437
+ For example, `Panel`, `ListView`, and `ThemePicker` render as `<fray-panel>`,
438
+ `<fray-listview>`, and `<fray-themepicker>`. They cannot be prefixed, renamed,
439
+ or made prefix-free at runtime. This lets distributed stylesheets target hosts
440
+ directly and predictably.
441
+
442
+ Every element created by Fray's renderer also receives the boolean `data-fray`
443
+ attribute. Themes can therefore target native Fray output without affecting
444
+ other UI libraries: `button[data-fray]`, `input[data-fray]`, and
445
+ `dialog[data-fray]`. The marker is renderer-owned and cannot be removed through
446
+ component props. `data-fray-component` remains diagnostic metadata, not an
447
+ ordinary styling selector.
415
448
 
416
- const selectedOverrides = createFrayRuntime({
417
- elementNames: {
418
- prefix: null,
419
- overrides: {'panel': 'change-panel'},
420
- },
421
- })
422
- // <change-panel>, with standalone names for the other components
423
- ```
424
-
425
- HTML custom-element names must contain a hyphen, so prefixless mode uses each
426
- component's standards-valid standalone name rather than invalid names such as
427
- `<panel>` or `<listview>`. Prefixes and overrides must be lowercase kebab-case;
428
- an exact override must itself be a non-reserved custom-element name.
429
-
430
- Each runtime owns its element-name mapping and structural-style registry.
431
449
  Create the runtime once at application startup, then create and mount the root
432
- through that runtime. Nested components inherit it automatically. A separate
433
- compiled Fray build is unnecessary. Two runtimes with different mappings can
434
- coexist in one document without their component-host selectors colliding.
435
- Styles still live in the document's global cascade because these hosts are
436
- deliberately unregistered light-DOM elements, not Web Components or Shadow DOM
437
- boundaries.
450
+ through that runtime. Styles live in the document's global cascade because the
451
+ hosts are deliberately unregistered light-DOM elements, not Web Components or
452
+ Shadow DOM boundaries.
438
453
 
439
454
  ## Reactive templates
440
455
 
@@ -449,8 +464,8 @@ canonical vnode operation, but it is not the built-in component authoring
449
464
  format. The workspace lint gate rejects new `h()` templates under
450
465
  `packages/fray/src/Components`.
451
466
 
452
- Configured custom hosts are also available inside a TSX component through its
453
- protected `Host` template component:
467
+ Custom hosts are available inside a TSX component through its protected `Host`
468
+ template component:
454
469
 
455
470
  ```tsx
456
471
  interface BadgeProps extends ComponentProps {
@@ -466,12 +481,11 @@ class Badge extends Component<BadgeProps> {
466
481
  }
467
482
 
468
483
  static override hostName = 'badge'
469
- static override standaloneHostName = 'ui-badge'
470
484
  }
471
485
  ```
472
486
 
473
- At runtime that template produces `<fray-badge>` by default,
474
- `<acme-badge>` under an `acme` prefix, or `<ui-badge>` in prefixless mode.
487
+ At runtime that template produces `<fray-badge>`. `hostName` is a stable
488
+ lowercase kebab-case component identity; Fray derives the fixed host spelling.
475
489
  Native-root components such as `Button` use their native tag directly in TSX
476
490
  instead of `Host`.
477
491
 
@@ -481,7 +495,7 @@ Glue values have explicit behavior at each template boundary:
481
495
  | --- | --- |
482
496
  | `{emitter}` | Render the current value and patch only that child range on emission. |
483
497
  | `<Child source={emitter} />` | Pass the emitter object unchanged; the child owns how it consumes it. |
484
- | `prop={live(emitter)}` | Subscribe a scalar DOM or component prop one way to the emitter's current value. |
498
+ | `prop={live(emitter)}` | Subscribe a DOM property or a component-declared live prop one way to the emitter's current value. |
485
499
  | `<input bind:value={emitter} />` | Bind a writable string emitter and native `value` two ways. |
486
500
  | `<input bind:checked={emitter} />` | Bind a writable boolean emitter and native `checked` two ways. |
487
501
  | `this.read(emitter)` | Read during `render()` and rerender the component while that dependency is used. |
@@ -494,6 +508,16 @@ The component still owns and disposes emitters it creates. Direct rendering
494
508
  uses only an emitter's value; use `snapshot()` when loading and error state
495
509
  must affect the markup.
496
510
 
511
+ Component props do not implicitly unwrap emitters. A raw emitter prop passes
512
+ the emitter object to the component, while `live(emitter)` passes its current
513
+ value and subscribes at the renderer boundary. Every class component has an
514
+ explicit live-prop contract and rejects `live()` outside its allowlist in both
515
+ typed templates and at runtime. `live()` is reserved for small render-time
516
+ state such as availability, validation, and busy/pressed state. Identity,
517
+ callbacks, initial values, structural collections, and emitter ownership are
518
+ ordinary props. A data prop documented as accepting a readable emitter is a
519
+ separate input-source contract, not a `live()` prop.
520
+
497
521
  This complete example uses mutable and derived emitters, direct emitter
498
522
  children, raw emitter props, native two-way bindings, a one-way live property,
499
523
  conditional tracked state, and a `LiveQuery` passed to a child:
@@ -619,7 +643,7 @@ class ChangeApp extends Component {
619
643
  static dependencies = [Button, Panel, Preview, Results, Textbox]
620
644
  }
621
645
 
622
- const runtime = createFrayRuntime({elementNames: {prefix: 'acme'}})
646
+ const runtime = createFrayRuntime()
623
647
  runtime.registerStyles(ChangeApp).injectStyles(document)
624
648
  runtime.mount(runtime.create(ChangeApp), document.querySelector('#app')!)
625
649
  ```
@@ -690,8 +714,10 @@ or `defaultValue` in new code.
690
714
 
691
715
  ## Stable component reference
692
716
 
693
- All components also accept `children`, `className` (`class` is an alias), and a
694
- sibling-local `key` through the common component props.
717
+ All components also accept `children`, `className` (`class` is an alias), an
718
+ `island` surface modifier, and a sibling-local `key` through the common
719
+ component props. The modifier is static presentation input, not a `live()`
720
+ property.
695
721
 
696
722
  | Component | Important props | User callback | State/emitter behavior |
697
723
  | --- | --- | --- | --- |
@@ -699,23 +725,56 @@ sibling-local `key` through the common component props.
699
725
  | `Toolbar` | `label`, `orientation`, `id`, `children` | None | Stateless named toolbar; orientation is horizontal or vertical. |
700
726
  | `Textbox` | `label` or `ariaLabel`, value props, `disabled`, `required`, `readOnly`, `error`, native text constraints, `inputRef` | `onInput(value, event)`, `onChange(value, event)` | String `valueEmitter`; external emitter changes patch the native input without replacing it. |
701
727
  | `Dropdown<T>` | `options`, `label` or `ariaLabel`, value props, `disabled`, `required`, `error`, `placeholder`, `name` | `onChange(value, event)` | Typed string/number `valueEmitter`; `options` may be an array or readable emitter. |
702
- | `RadioButton` | `label`, `name`, `value`, `checked`, `disabled`, `required` | `onChange(checked, event)` | Native radio input with a label; use a shared `name` to associate standalone buttons. |
703
- | `RadioGroup<T>` | `options`, `label` or `ariaLabel`, value props, `name`, `disabled`, `required` | `onChange(value, event)` | Native radio inputs with one selected `valueEmitter` option. |
704
- | `Toggle<T>` | `options`, `label` or `ariaLabel`, value props, `disabled`, `required` | `onChange(value, event)` | One selected value; arrow keys, Home, and End move and select within the radio group. |
705
- | `Checkbox<T>` | `symbols`, `label`, value props, `disabled`, `required`, `name` | `onChange(value, event)` | Two-state semantic value by default; click/Space advances and arrow keys move in either direction. |
728
+ | `RadioButton` | `label`, `name`, `value`, `checked`, `disabled`, `required`, `error` | `onChange(checked, event)` | Native radio input with a labelled fixed shell; `checked`, `disabled`, `required`, and `error` support `live()`. |
729
+ | `RadioGroup<T>` | Plain-array `options`, `label` or `ariaLabel`, value props, `name`, `disabled`, `required`, `error` | `onChange(value, event)` | Native radio inputs with one selected `valueEmitter`; `disabled`, `required`, and `error` support `live()`. |
730
+ | `Toggle<T>` | `options`, `label` or `ariaLabel`, value props, `disabled`, `required`, `error` | `onChange(value, event)` | One selected value; `disabled`, `required`, and `error` support `live()`. |
731
+ | `Checkbox<T>` | `symbols`, `label`, value props, `disabled`, `required`, `error`, `name` | `onChange(value, event)` | Two-state semantic value by default; `disabled`, `required`, and `error` support `live()`. |
706
732
  | `TriCheckbox` | Checkbox props except `symbols` | `onChange(value, event)` | Cycles deny → neutral → prefer using `FilterMode`. |
707
733
  | `QuadCheckbox` | Checkbox props except `symbols` | `onChange(value, event)` | Cycles deny → neutral → prefer → require using `FilterMode`. |
708
- | `Panel` | `header`, `toolbar`, `orientation`, `disabled`, `id`, `children` | None | Stateless labelled section when a header exists; `disabled` describes the region but does not mutate descendant controls. |
709
- | `Sidebar` | `header`, `toolbar`, `ariaLabel`, `id`, `children` | None | Native complementary region with fixed header/toolbar parts and independently scrolling content. |
710
- | `SplitView` | `primary`, `secondary`, `direction`, `primarySize`, pane labels | None | Stateless, non-resizable two-pane layout with explicit overflow ownership. |
734
+ | `Header` | `id`, `headingId`, `level`, `children` | None | Styled heading surface using a native `h1`–`h6`; level defaults to `2`. |
735
+ | `GroupPanel` | `header`, `id`, `children` | None | Labelled bordered control group with a full-height vertical Header. |
736
+ | `Panel` | `header`, `toolbar`, `orientation`, `disabled`, `id`, `children` | None | Stateless labelled section that composes Header when header content exists; `disabled` describes the region but does not mutate descendant controls. |
737
+ | `Sidebar` | `header`, `toolbar`, `ariaLabel`, `id`, `children` | None | Fixed `fray-sidebar` host containing a native complementary region with fixed header/toolbar parts and independently scrolling content. |
738
+ | `SplitView` | `primary`, `secondary`, `direction`, `primarySize`, pane labels | None | Stateless, non-resizable two-pane flex layout with explicit overflow ownership and keyboard-focusable panes; `primarySize` is a flex-basis value. |
711
739
  | `DescriptionList` / `DescriptionItem` | list `label`; item `term`, `value` or children | None | Native `dl`/`dt`/`dd` record summary with responsive term/value wrapping. |
712
- | `ProgressBar` | `label`, `value` or `valueEmitter`, `max`, `valueText` | None | Labelled native progress; a null value is indeterminate. |
740
+ | `ProgressBar` | `label`, `value` or `valueEmitter`, `max`, `valueText` | None | Native progress semantics with a clipped-label visual surface; a null value is indeterminate. |
713
741
  | `ThemePicker` | `label` or `ariaLabel`, theme `options`, value props, `targetDocument`, `disabled` | `onChange(value, option, event)` | String `valueEmitter`; replaces only the theme stylesheet link. |
714
742
  | `ColorPicker` | `label` or `ariaLabel`, color `options`, value props, `targetDocument`, `disabled` | `onChange(value, option, event)` | String `valueEmitter`; replaces only the color stylesheet link. |
715
743
  | `Tab` | `id`, `label`, `disabled`, optional literal `route`, `children` | None | Declarative content marker consumed by `TabPanel`; a route annotation binds tab activation to the current route scope. |
716
744
  | `TabLine` | `tabs`, `label`, `baseId`, value props | `onChange(id, event)` | Active-tab `valueEmitter`; arrow keys skip disabled tabs, with Home/End support. |
717
745
  | `TabPanel` | `tabs` or `Tab` children, `label`, `id`, value props | `onChange(id, event)` | Owns or consumes the active-tab emitter, wires the selected tabpanel, and contextually registers annotated tabs when a router is present. |
718
746
 
747
+ ### Live binding and data-source contracts
748
+
749
+ Use `live()` only for the props listed here. All other component props reject
750
+ `live()` in TSX, `h()`, and at runtime. Form-control errors render an alert,
751
+ set invalid state, and associate the control or group with that message.
752
+
753
+ | Component | `live()` props | Dedicated reactive input/state props |
754
+ | --- | --- | --- |
755
+ | `Button` | `disabled`, `pressed`, `busy` | None |
756
+ | `Textbox` | `disabled`, `required`, `readOnly`, `error` | `valueEmitter` |
757
+ | `Dropdown` | `disabled`, `required`, `error` | `options` may be an array or readable emitter; `valueEmitter` |
758
+ | `RadioButton` | `checked`, `disabled`, `required`, `error` | None |
759
+ | `RadioGroup` | `disabled`, `required`, `error` | `valueEmitter`; `options` is always an ordinary array |
760
+ | `Toggle` | `disabled`, `required`, `error` | `valueEmitter`; `options` is always an ordinary array |
761
+ | `Checkbox`, `TriCheckbox`, `QuadCheckbox` | `disabled`, `required`, `error` | `valueEmitter` |
762
+ | `ThemePicker`, `ColorPicker` | `disabled` | `valueEmitter` |
763
+ | `Dialog` | `showCloseButton` | `valueEmitter` controls open state |
764
+ | `Panel` | `disabled` | None |
765
+ | `ProgressBar` | None | `valueEmitter` |
766
+ | `ListView` | None | `items` may be an array or readable emitter; selection emitters are outputs |
767
+ | `TreeView` | None | `nodes` may be an array or readable emitter; selected/expanded emitters are outputs; `itemLabelClassName` and `itemLabelStyle` decorate each block label beside its expander |
768
+ | `DataTable` | None | `data` may be an array or readable emitter; `dataSource`/`rest` and selection emitters are explicit source/state contracts |
769
+ | `FilterPanel` | None | `options` may be an array or readable emitter |
770
+ | Layout, tab, description, toolbar, placeholder, and routing components | None | Their ordinary structural/configuration props require an owner rerender when changed |
771
+
772
+ Readable data sources carry their own fetch state and error through Glue's
773
+ `getFetchState()` and `getError()` APIs. `ListView`, `DataTable`, `FilterPanel`,
774
+ and `TreeView` surface those source failures as data-loading errors. This is
775
+ separate from a control's `error` prop, which represents validation or other
776
+ application-level input feedback.
777
+
719
778
  Invalid option arrays, duplicate tab IDs, unsupported orientations, malformed
720
779
  emitters, and non-function callbacks fail with descriptive errors.
721
780
 
@@ -770,6 +829,80 @@ h(Toggle, {
770
829
  })
771
830
  ```
772
831
 
832
+ ### RadioGroup
833
+
834
+ `RadioGroup` deliberately has a narrow reactive contract. Its selectable
835
+ `options` are ordinary structural input, its `valueEmitter` is a stable raw
836
+ writable state channel, and only `disabled`, `required`, and `error` accept one-way
837
+ `live()` bindings.
838
+
839
+ | Prop | Role | `live()` support |
840
+ | --- | --- | --- |
841
+ | `options` | Selectable value/label tuples supplied by the owner | No |
842
+ | `disabled` | Current availability state | Yes, with a readable boolean emitter |
843
+ | `required` | Current form-requirement state | Yes, with a readable boolean emitter |
844
+ | `error` | Current validation message and invalid state | Yes, with a readable error emitter |
845
+ | `valueEmitter` | Stable writable selected-value channel | Pass the emitter raw; do not wrap it |
846
+ | `value`, `defaultValue`, `initialValue` | Initial value when no `valueEmitter` is supplied | No |
847
+ | `id`, `label`, `ariaLabel`, `name` | Ordinary identity and presentation input | No `live()` binding |
848
+ | `onChange` | Action callback | No |
849
+
850
+ Static and live availability state can be combined without changing the option
851
+ contract:
852
+
853
+ ```tsx
854
+ const selectedView = new Emitter<'list' | 'grid'>('list')
855
+ const unavailable = new Emitter(false)
856
+ const mustChoose = new Emitter(true)
857
+
858
+ <RadioGroup
859
+ label="View"
860
+ options={[
861
+ ['list', 'List'],
862
+ ['grid', 'Grid'],
863
+ ]}
864
+ valueEmitter={selectedView}
865
+ disabled={live(unavailable)}
866
+ required={live(mustChoose)}
867
+ />
868
+ ```
869
+
870
+ `live(optionsEmitter)` and a raw options emitter are both unsupported. When an
871
+ application genuinely owns a changing option vocabulary, its owning class
872
+ component must make that structural rerender explicit:
873
+
874
+ ```tsx
875
+ interface ViewChooserProps extends ComponentProps {
876
+ options: ReadableEmitter<readonly RadioOption[]>
877
+ }
878
+
879
+ class ViewChooser extends Component<ViewChooserProps> {
880
+ render() {
881
+ return <RadioGroup
882
+ label="View"
883
+ options={this.read(this.props.options)}
884
+ />
885
+ }
886
+ }
887
+ ```
888
+
889
+ Calling `optionsEmitter.get()` directly in `render()` only reads a snapshot and
890
+ does not subscribe. `this.read(optionsEmitter)` rerenders the owner when the
891
+ array changes; normal vnode reconciliation then supplies the new ordinary
892
+ array prop to `RadioGroup`. The owner remains responsible for deciding what an
893
+ option removal means for its selected-value emitter. Fray does not silently
894
+ select, clear, or otherwise rewrite that state.
895
+
896
+ `label` is a `FrayChild`, so it may still contain an emitter that is rendered
897
+ and subscribed as child content. That fine-grained child behavior is distinct
898
+ from making the `label` property itself a `live()` binding. The same is true of
899
+ an individual `RadioOption` label: its rendered content may be reactive without
900
+ making the option array or its selectable values live.
901
+
902
+ Because `live()` subscriptions belong to a parent render record, pass live
903
+ props through JSX or `h()`. Direct `new RadioGroup(...)` construction accepts
904
+ resolved booleans and ordinary arrays only.
905
+
773
906
  ### Checkbox variants
774
907
 
775
908
  ```ts
@@ -781,6 +914,18 @@ h(QuadCheckbox, {label: 'Required tags', defaultValue: FilterMode.Require})
781
914
  `FilterMode` values are `Deny`, `Neutral`, `Prefer`, and `Require`. The basic
782
915
  checkbox uses neutral/prefer, while the variants expose the additional states.
783
916
 
917
+ ### Header
918
+
919
+ ```tsx
920
+ <Header id="portfolio-header" headingId="portfolio-title" level={2}>
921
+ Portfolio summary
922
+ </Header>
923
+ ```
924
+
925
+ `level` is an integer from `1` through `6` and defaults to `2`. Header children
926
+ must be valid native heading content. The custom host owns presentation while
927
+ the nested native heading owns document semantics.
928
+
784
929
  ### Panel
785
930
 
786
931
  ```ts
@@ -803,10 +948,10 @@ h(Sidebar, {
803
948
  }, h('ul', null, h('li', null, 'Release automation')))
804
949
  ```
805
950
 
806
- The surrounding grid or flex layout must bound the Sidebar's height. Its
807
- header and toolbar remain fixed while the dedicated content part owns vertical
808
- scrolling and is keyboard-focusable. Supply `ariaLabel` when there is no visible
809
- `header`.
951
+ The surrounding grid or flex layout must bound the Sidebar's height. Its native
952
+ complementary region is inside the fixed Sidebar host; Header and toolbar remain
953
+ fixed while the dedicated content part owns vertical scrolling and is keyboard-
954
+ focusable. Supply `ariaLabel` when there is no visible `header`.
810
955
 
811
956
  ### Declarative tabs
812
957
 
@@ -837,149 +982,79 @@ content and IDs.
837
982
 
838
983
  ## Styling and accessibility
839
984
 
840
- Fray's styling system has three physically and conceptually separate layers:
841
-
842
- | Layer | Shipped path | Responsibility | Runtime behavior |
843
- | --- | --- | --- | --- |
844
- | Structure | `styles/structural.css` | Generated component layout, flow, sizing, positioning, accessibility mechanics, stable hooks, and variable consumption | Loaded once; remains stable during presentation changes |
845
- | Theme | `themes/<name>/theme.css` | Typography, spacing, geometry, depth, surface treatment, semantic family mappings, and exceptional pseudo/native rendering | Loaded separately and independently replaceable |
846
- | Colors | `colors/<name>/colors.css` | Primary, secondary, and neutral ramps plus contrast/color primitives; no UI-semantic roles | Loaded separately and independently replaceable |
847
-
848
- Component authors place only structure and mechanics in `static css`: display,
849
- flow, sizing, positioning, overflow, stable state hooks, and consumption of
850
- semantic variables. Reusable `static baseStyles` mappings apply named
851
- structural rules to component selectors, while `static dependencies` let the
852
- collector traverse a complete application tree, deduplicate definitions, and
853
- generate one artifact. Literal palettes and treatment-specific shadows,
854
- gradients, radii, and decoration do not belong in component CSS.
855
-
856
- The initial supported treatments are `shiny`, `java`, and `minimal`. The color
857
- catalog contains `iceblue`, `ocean`, `green`, `gray`, `orange`, `purple`, `red`,
858
- and `yellow`. The older top-level `themes/light.css` and `themes/dark.css`
859
- remain compatibility bundles; new applications should use the separated
860
- contract.
861
-
862
- These three treatments adapt the useful intent of earlier styling experiments
863
- rather than preserving their CSS literally. Application-specific selectors,
864
- duplicated declarations, and mixed structural/presentation rules were removed;
865
- the characteristic restrained Minimal, classic raised Java, and layered glossy
866
- Shiny treatments were rebuilt on the current component hooks and variables.
867
-
868
- ### Hierarchical custom properties
869
-
870
- CSS custom properties are Fray's primary theme integration protocol:
985
+ A Fray application loads four ordered styling inputs:
986
+
987
+ | Order | Input | Responsibility |
988
+ | --- | --- | --- |
989
+ | 1 | `themes/base.css` | Default palette anchors, palette-ramp derivation, and semantic custom-property defaults only |
990
+ | 2 | Runtime-collected component CSS | Rules from the `static css` declarations of the component classes the application declares |
991
+ | 3 | `colors/<name>/colors.css` | Palette anchors and endpoints only |
992
+ | 4 | `themes/<name>/theme.css` | Intentional custom-property overrides; `color-scheme` is the only ordinary declaration allowed |
993
+
994
+ Application layout remains in application-owned CSS and is not a fifth Fray
995
+ layer. Named color and theme files never import the base file; load order is
996
+ explicit at the application entry point.
997
+
998
+ Component classes own every selector and every declaration that consumes a
999
+ Fray variable. Shared rules live in meaningful abstract component classes when
1000
+ those components share DOM or behavior. For example, `Dropdown` inherits the
1001
+ labeled-input contract and then the select-shell contract. The collector walks
1002
+ that class chain base-to-derived, resolves each class's own `&` selectors
1003
+ against the concrete host, and then traverses dependencies declared at every
1004
+ level. The older named `baseStyles` recipe system has been removed.
1005
+
1006
+ A component's `static css` should contain the absolute minimum needed for its
1007
+ contract. Every selector, declaration, pseudo-element, and variable must have a
1008
+ specific purpose that cannot be expressed more simply. A base-class edit must
1009
+ be reviewed against all descendants because its CSS is compiled into each
1010
+ concrete descendant that is used.
1011
+
1012
+ The checked-in `styles/structural.css` is a complete generated artifact for
1013
+ non-tree-shaken/diagnostic use. Normal applications should register their root
1014
+ component and inject the resulting usage-specific stylesheet:
871
1015
 
872
- ```text
873
- colors.css
874
- --palette-primary-* / --palette-secondary-* / --palette-neutral-*
875
-
876
-
877
- theme.css
878
- global UI roles (font, spacing, shape, surface)
879
-
880
-
881
- generic families (header, button, input, panel, selection)
882
-
883
-
884
- optional variants (table header, tab button, toggle button,
885
- dropdown trigger, dialog header)
886
-
887
-
888
- structural CSS and custom components
1016
+ ```ts
1017
+ const runtime = createFrayRuntime()
1018
+ runtime.registerStyles(App).injectStyles(document)
1019
+ runtime.mount(runtime.create(App), root)
889
1020
  ```
890
1021
 
891
- Components request the narrowest useful variable and explicitly fall back
892
- toward its generic family. A theme can therefore change all header-like or
893
- button-like elements with a few assignments, then override only the variants
894
- that should look different. `frayThemeVariableCatalog` exports this contract in
895
- machine-readable form, including every variable's layer, family, value kind,
896
- purpose, and optional fallback.
1022
+ ### Custom properties
897
1023
 
898
- A custom component can apply the public trait matching the treatment it needs
899
- and optionally consume the same variable hierarchy:
1024
+ `base.css` derives full primary, secondary, and neutral ramps from each
1025
+ palette's `500` anchor plus light/dark mix endpoints. A color file therefore
1026
+ sets anchors and endpoints, while a theme maps or overrides semantic families
1027
+ such as `--button-*`, `--input-*`, `--panel-*`, and `--selection-*`.
1028
+ `frayThemeVariableCatalog` exposes the supported vocabulary.
900
1029
 
901
- ```css
902
- acme-grid.datacomponentlike > header {
903
- color: var(--table-header-color, var(--header-color));
904
- background: var(--table-header-background, var(--header-background));
905
- }
906
- ```
1030
+ `--application-background` owns the canvas behind an axis-filling application
1031
+ root and its islands. Its base value is the white palette endpoint, which
1032
+ Shiny and Minimal both retain.
1033
+
1034
+ The explicit `island` modifier consumes `--island-margin`,
1035
+ `--island-padding`, `--island-background`, `--island-border`,
1036
+ `--island-radius`, and `--island-shadow`. Base and Minimal keep its layout and
1037
+ elevation neutral; Shiny uses it for Bank2-style separated surfaces. Themes do
1038
+ not infer island boundaries from component type or nesting, and islands cannot
1039
+ contain other islands.
907
1040
 
908
- Complete traits use the `like` suffix. A component whose outer and content
909
- regions are distinct uses `shell` and `inner`, for example `buttonshell` with
910
- `buttoninner`, or `datacomponentshell` with `datacomponentinner`. Other public
911
- families include `inputlike`, `headerlike`, `coloredlike`, `panellike`, and
912
- `toolbarlike`; the panel and toolbar families also expose
913
- `panelshell`/`panelinner` and `toolbarshell`/`toolbarinner`. Wrappers are not
914
- introduced solely to carry a split trait.
915
-
916
- Themes directly target native elements, public traits, native pseudo-parts,
917
- and native/ARIA state. They never target `data-fray-component` or `data-part`.
918
- Every theme seeds inherited variables with a zero-specificity
919
- `:root`/`[data-theme]` boundary rule. Presentation selectors use `@scope` with
920
- nested theme roots and `[data-theme-exclude]` limits, a named cascade layer,
921
- and low-specificity `:where()` selectors. Shiny's highlights and
922
- select/progress decoration follow that contract and yield to native
923
- representation under forced colors.
924
-
925
- `coloredlike` defaults to the primary palette. Components can provide
926
- `--colored-base`, `--colored-light`, `--colored-dark`, and
927
- `--colored-contrast`; the active theme decides whether those inputs become a
928
- flat color, gradient, other polish, or no special treatment.
1041
+ The reusable `colored` trait consumes a required `--c1`/`--c2`/`--c3`
1042
+ dark/base/light triplet and `--colored-shadow`. Its base treatment is the
1043
+ shared three-stop gradient; Shiny adds depth without changing the
1044
+ application-owned triplet.
929
1045
 
930
1046
  ### Runtime selection
931
1047
 
932
1048
  `replaceFrayStylesheet` maintains one
933
1049
  `link[data-fray-stylesheet="theme"]` and one
934
1050
  `link[data-fray-stylesheet="colors"]`. Replacing either link also sets the
935
- corresponding `data-theme` or `data-color` root attribute. The
936
- `ThemePicker` and `ColorPicker` controls expose the same operation through the
937
- normal Fray value-control contract.
938
-
939
- The default option catalogs resolve URLs against Fray's published package
940
- layout for direct ESM/CDN use. A bundled application should ask its bundler to
941
- emit each selectable CSS file as an asset and supply those resulting URLs:
942
-
943
- ```tsx
944
- import {ColorPicker, Component, ThemePicker} from '@sylwellsoftware/fray'
945
- import iceblueHref from '@sylwellsoftware/fray/colors/iceblue/colors.css?url'
946
- import purpleHref from '@sylwellsoftware/fray/colors/purple/colors.css?url'
947
- import minimalHref from '@sylwellsoftware/fray/themes/minimal/theme.css?url'
948
- import shinyHref from '@sylwellsoftware/fray/themes/shiny/theme.css?url'
949
-
950
- const themes = [
951
- {value: 'shiny', label: 'Shiny', href: shinyHref},
952
- {value: 'minimal', label: 'Minimal', href: minimalHref},
953
- ]
954
- const colors = [
955
- {value: 'iceblue', label: 'Ice blue', href: iceblueHref},
956
- {value: 'purple', label: 'Purple', href: purpleHref},
957
- ]
958
-
959
- class AppearanceControls extends Component {
960
- render() {
961
- return <aside aria-label="Appearance">
962
- <ThemePicker label="Theme" options={themes} defaultValue="shiny" />
963
- <ColorPicker label="Colors" options={colors} defaultValue="iceblue" />
964
- </aside>
965
- }
966
-
967
- static dependencies = [ColorPicker, ThemePicker]
968
- }
969
- ```
970
-
971
- The `?url` syntax above is supported by Vite; use the equivalent emitted-asset
972
- mechanism for another bundler. The structural stylesheet is not replaced.
973
-
974
- See [`themes/README.md`](themes/README.md) for the complete architecture and
975
- variable families, and [`colors/README.md`](colors/README.md) for the palette
976
- contract.
1051
+ corresponding root data attribute. `ThemePicker` and `ColorPicker` expose the
1052
+ same operation through the normal value-control contract. The base file and
1053
+ runtime-injected structural stylesheet stay loaded while those two links are
1054
+ replaced.
977
1055
 
978
- Stable examples are tested with axe in Chromium, Firefox, and WebKit and have
979
- no serious or critical automated violations. Browser tests also cover keyboard
980
- operation, labelled roles, reduced motion, 200% configured text sizing, and
981
- forced-colors focus visibility. A formal manual screen-reader pass is still a
982
- release-candidate requirement; automated checks are not a substitute for it.
1056
+ See [the theme contract](themes/README.md) and
1057
+ [the palette contract](colors/README.md) for authoring details.
983
1058
 
984
1059
  ## Data workflows
985
1060
 
@@ -1043,8 +1118,8 @@ For a tree derived from domain state, a callback updates that real source; the
1043
1118
  tree derivation then rebuilds and `deriveTreeNode` resolves the fresh node.
1044
1119
  `updateTreeNode` is the equivalent pure path-copy operation.
1045
1120
 
1046
- Generic `FilterState` keeps `neutral`, `prefer`, `require`, and `deny` semantic
1047
- values separate from glyphs and transport. `filterByState`,
1121
+ Generic `FilterState` keeps `neutral`, `prefer`, `require`, and `deny`
1122
+ semantic values separate from glyphs and transport. `filterByState`,
1048
1123
  `deriveFilterPredicate`, and `deriveFilteredItems` consume caller-supplied
1049
1124
  dimension matchers. `serializeFilterState`/`parseFilterState` round-trip
1050
1125
  validated version-1 plain data without owning URL or storage access. Unknown
@@ -1063,5 +1138,5 @@ React/Vue adapters, a broad design system, a virtualized production data grid,
1063
1138
  legacy-browser support, or a stable `1.0` API. See the [workspace
1064
1139
  overview](../../README.md), [API surface](../../docs/API_SURFACE.md),
1065
1140
  [architecture overview](../../docs/architecture.md),
1066
- [changelog](../../CHANGELOG.md), [contribution guide](../../CONTRIBUTING.md),
1141
+ [changelog](CHANGELOG.md), [contribution guide](../../CONTRIBUTING.md),
1067
1142
  and [security policy](../../SECURITY.md).