@sylwellsoftware/fray 0.6.0 → 1.0.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 (117) hide show
  1. package/README.md +426 -205
  2. package/colors/README.md +26 -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 +2 -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/header.d.ts +19 -0
  34. package/dist/Components/layout/header.d.ts.map +1 -0
  35. package/dist/Components/layout/panel.d.ts +7 -3
  36. package/dist/Components/layout/panel.d.ts.map +1 -1
  37. package/dist/Components/layout/sidebar.d.ts +4 -0
  38. package/dist/Components/layout/sidebar.d.ts.map +1 -1
  39. package/dist/Components/layout/splitView.d.ts +1 -1
  40. package/dist/Components/layout/splitView.d.ts.map +1 -1
  41. package/dist/Components/layout/tabpanel/tab.d.ts +3 -0
  42. package/dist/Components/layout/tabpanel/tab.d.ts.map +1 -1
  43. package/dist/Components/layout/tabpanel/tabline.d.ts +1 -2
  44. package/dist/Components/layout/tabpanel/tabline.d.ts.map +1 -1
  45. package/dist/Components/layout/tabpanel/tabpanel.d.ts +10 -2
  46. package/dist/Components/layout/tabpanel/tabpanel.d.ts.map +1 -1
  47. package/dist/Components/lineinputs/CheckableControl.d.ts +7 -0
  48. package/dist/Components/lineinputs/CheckableControl.d.ts.map +1 -0
  49. package/dist/Components/lineinputs/LabeledInputControl.d.ts +7 -0
  50. package/dist/Components/lineinputs/LabeledInputControl.d.ts.map +1 -0
  51. package/dist/Components/lineinputs/SelectControl.d.ts +7 -0
  52. package/dist/Components/lineinputs/SelectControl.d.ts.map +1 -0
  53. package/dist/Components/lineinputs/checkbox/Checkbox.d.ts +11 -6
  54. package/dist/Components/lineinputs/checkbox/Checkbox.d.ts.map +1 -1
  55. package/dist/Components/lineinputs/checkbox/QuadCheckbox.d.ts +1 -2
  56. package/dist/Components/lineinputs/checkbox/QuadCheckbox.d.ts.map +1 -1
  57. package/dist/Components/lineinputs/checkbox/TriCheckbox.d.ts +1 -2
  58. package/dist/Components/lineinputs/checkbox/TriCheckbox.d.ts.map +1 -1
  59. package/dist/Components/lineinputs/dropdown.d.ts +7 -6
  60. package/dist/Components/lineinputs/dropdown.d.ts.map +1 -1
  61. package/dist/Components/lineinputs/label.d.ts +14 -0
  62. package/dist/Components/lineinputs/label.d.ts.map +1 -0
  63. package/dist/Components/lineinputs/radio.d.ts +64 -0
  64. package/dist/Components/lineinputs/radio.d.ts.map +1 -0
  65. package/dist/Components/lineinputs/textbox.d.ts +7 -6
  66. package/dist/Components/lineinputs/textbox.d.ts.map +1 -1
  67. package/dist/Components/lineinputs/toggle.d.ts +10 -3
  68. package/dist/Components/lineinputs/toggle.d.ts.map +1 -1
  69. package/dist/Components/menu/button.d.ts +6 -3
  70. package/dist/Components/menu/button.d.ts.map +1 -1
  71. package/dist/Components/menu/toolbar.d.ts +1 -1
  72. package/dist/Components/menu/toolbar.d.ts.map +1 -1
  73. package/dist/Components/status/progressBar.d.ts +1 -1
  74. package/dist/Components/status/progressBar.d.ts.map +1 -1
  75. package/dist/Components/theme/stylesheetPicker.d.ts +6 -7
  76. package/dist/Components/theme/stylesheetPicker.d.ts.map +1 -1
  77. package/dist/index.d.ts +11 -1
  78. package/dist/index.d.ts.map +1 -1
  79. package/dist/index.js +3378 -1454
  80. package/dist/index.js.map +1 -1
  81. package/dist/jsx-dev-runtime-CRDpvXOH.js +1208 -0
  82. package/dist/jsx-dev-runtime-CRDpvXOH.js.map +1 -0
  83. package/dist/jsx-dev-runtime.js +1 -1
  84. package/dist/jsx-runtime.js +1 -1
  85. package/dist/routing/RouteLink.d.ts +18 -0
  86. package/dist/routing/RouteLink.d.ts.map +1 -0
  87. package/dist/routing/RouteQuery.d.ts +19 -0
  88. package/dist/routing/RouteQuery.d.ts.map +1 -0
  89. package/dist/routing/RouteScope.d.ts +18 -0
  90. package/dist/routing/RouteScope.d.ts.map +1 -0
  91. package/dist/routing/RouteValue.d.ts +19 -0
  92. package/dist/routing/RouteValue.d.ts.map +1 -0
  93. package/dist/routing/navigationAdapter.d.ts +33 -0
  94. package/dist/routing/navigationAdapter.d.ts.map +1 -0
  95. package/dist/routing/route.d.ts +64 -0
  96. package/dist/routing/route.d.ts.map +1 -0
  97. package/dist/routing/router.d.ts +109 -0
  98. package/dist/routing/router.d.ts.map +1 -0
  99. package/dist/runtime.d.ts +10 -13
  100. package/dist/runtime.d.ts.map +1 -1
  101. package/dist/styling/styleRegistry.d.ts +0 -4
  102. package/dist/styling/styleRegistry.d.ts.map +1 -1
  103. package/dist/styling/theme.d.ts +14 -2
  104. package/dist/styling/theme.d.ts.map +1 -1
  105. package/package.json +5 -6
  106. package/styles/structural.css +942 -320
  107. package/themes/README.md +30 -72
  108. package/themes/base.css +311 -0
  109. package/themes/java/theme.css +14 -411
  110. package/themes/minimal/theme.css +3 -407
  111. package/themes/shiny/theme.css +127 -500
  112. package/dist/jsx-dev-runtime-CqunwZ8j.js +0 -1174
  113. package/dist/jsx-dev-runtime-CqunwZ8j.js.map +0 -1
  114. package/dist/styling/baseStyleDefinitions.d.ts +0 -75
  115. package/dist/styling/baseStyleDefinitions.d.ts.map +0 -1
  116. package/themes/dark.css +0 -86
  117. package/themes/light.css +0 -86
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
 
@@ -85,21 +87,24 @@ Responsibility stays at the narrowest layer that understands it:
85
87
  | Domain state, service implementations/providers, endpoint configuration, page composition, theme availability and selection policy | Application |
86
88
  | DOM structure, native events, accessibility, component lifetime, service-scope propagation, visual async states | Fray components/runtime |
87
89
  | Mutable/computed values, query timing/results, fetch state, optional causality | Glue |
88
- | URL/wire serialization and retrieval mechanism | Injected Glue query handler/application adapter |
90
+ | Remote wire serialization and retrieval mechanism | Injected Glue query handler/application adapter |
91
+ | Browser navigation placement, restoration, and routed component integration | Caller-owned Fray router with an injected navigation adapter |
89
92
  | Layout/flow CSS and stable component/part hooks | Fray structural styling |
90
93
  | Look-and-feel treatment and palette | Separately loaded Fray-compatible theme/color CSS |
91
94
 
92
95
  ## Set up a browser application
93
96
 
94
- Fray ships ESM, TypeScript declarations, automatic/classic JSX runtimes, one
95
- generated structural stylesheet, replaceable theme treatments, and replaceable
96
- 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.
97
101
 
98
102
  ```ts
99
103
  import {Emitter} from '@sylwellsoftware/glue'
100
104
  import {
101
105
  Button,
102
106
  Component,
107
+ Header,
103
108
  Panel,
104
109
  Sidebar,
105
110
  Textbox,
@@ -107,7 +112,7 @@ import {
107
112
  createFrayRuntime,
108
113
  h,
109
114
  } from '@sylwellsoftware/fray'
110
- import '@sylwellsoftware/fray/styles/structural.css'
115
+ import '@sylwellsoftware/fray/themes/base.css'
111
116
  import '@sylwellsoftware/fray/colors/iceblue/colors.css'
112
117
  import '@sylwellsoftware/fray/themes/minimal/theme.css'
113
118
 
@@ -117,7 +122,10 @@ class App extends Component {
117
122
  static dependencies = [Button, Panel, Textbox, Toolbar]
118
123
 
119
124
  render() {
120
- return h(Panel, {header: 'Profile'},
125
+ return h(Panel, {
126
+ className: 'fray-fill-horizontal fray-fill-vertical',
127
+ header: 'Profile',
128
+ },
121
129
  h(Textbox, {label: 'Name', valueEmitter: name}),
122
130
  h(Toolbar, {label: 'Profile actions'},
123
131
  h(Button, {label: 'Save', onClick: () => save(name.get())})))
@@ -125,6 +133,7 @@ class App extends Component {
125
133
  }
126
134
 
127
135
  const runtime = createFrayRuntime()
136
+ runtime.registerStyles(App).injectStyles(document)
128
137
  runtime.mount(runtime.create(App), document.querySelector('#app')!)
129
138
 
130
139
  function save(value: string) {
@@ -132,6 +141,14 @@ function save(value: string) {
132
141
  }
133
142
  ```
134
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. A root with neither
150
+ modifier keeps its ordinary embedded/content-sized behavior.
151
+
135
152
  The prebuilt structural file targets Fray's default `fray-` hosts. Applications
136
153
  with custom components or configured host names may instead register their root
137
154
  dependencies and call `runtime.injectStyles(document)`; collection remains
@@ -214,6 +231,164 @@ query state comes from caller-owned endpoint results. Function components stay
214
231
  presentation-oriented and receive rendered values or emitters from a nearby
215
232
  lifecycle-owning class component.
216
233
 
234
+ ## Hierarchical browser routing
235
+
236
+ Routing is optional and application-scoped. Route descriptors name immutable
237
+ relative segments; the mounted component hierarchy supplies their parentage.
238
+ This lets a `TabPanel` discover immediate routes from ordinary `Tab`
239
+ annotations without requiring a duplicate central route tree:
240
+
241
+ ```tsx
242
+ import {Emitter} from '@sylwellsoftware/glue'
243
+ import {
244
+ Component,
245
+ RouteLink,
246
+ RouteQuery,
247
+ RouteUnavailableError,
248
+ RouteValue,
249
+ Tab,
250
+ TabPanel,
251
+ createBrowserRouter,
252
+ createHashNavigation,
253
+ createFrayRuntime,
254
+ defineRoute,
255
+ defineRouteParameter,
256
+ routeTarget,
257
+ stringRouteCodec,
258
+ waitForRouteValue,
259
+ } from '@sylwellsoftware/fray'
260
+
261
+ const routes = {
262
+ changes: defineRoute('changes'),
263
+ security: defineRoute('security'),
264
+ overview: defineRoute('security-overview', 'overview'),
265
+ projects: defineRoute('security-projects', 'projects'),
266
+ }
267
+ const activeApplication = new Emitter('changes')
268
+ const activeSecurityView = new Emitter('overview')
269
+
270
+ class SecurityApplication extends Component {
271
+ render() {
272
+ return <TabPanel valueEmitter={activeSecurityView} label="Security views">
273
+ <Tab id="overview" label="Overview" route={routes.overview}>Summary</Tab>
274
+ <Tab id="projects" label="Projects" route={routes.projects}>Projects</Tab>
275
+ </TabPanel>
276
+ }
277
+
278
+ static dependencies = [Tab, TabPanel]
279
+ }
280
+
281
+ class App extends Component {
282
+ render() {
283
+ return <>
284
+ <nav aria-label="Applications">
285
+ <RouteLink to={routeTarget(routes.security, routes.projects)}>
286
+ Security projects
287
+ </RouteLink>
288
+ </nav>
289
+ <TabPanel valueEmitter={activeApplication} label="Applications">
290
+ <Tab id="changes" label="Changes" route={routes.changes}>Changes</Tab>
291
+ <Tab id="security" label="Security" route={routes.security}>
292
+ <SecurityApplication />
293
+ </Tab>
294
+ </TabPanel>
295
+ </>
296
+ }
297
+
298
+ static dependencies = [RouteLink, SecurityApplication, Tab, TabPanel]
299
+ }
300
+
301
+ const router = createBrowserRouter({adapter: createHashNavigation(window)})
302
+ const runtime = createFrayRuntime({router})
303
+ const app = runtime.mount(runtime.create(App), document.querySelector('#app')!)
304
+
305
+ addEventListener('pagehide', () => {
306
+ app.destroy()
307
+ router.dispose()
308
+ }, {once: true})
309
+ ```
310
+
311
+ The runtime opens the root scope. A selected routed tab opens its own scope for
312
+ nested components, so `/security/projects` restores the outer tab first and
313
+ then discovers and restores the inner tab. Every scope declares when its
314
+ immediate registrations are complete; this distinguishes an unknown child
315
+ from one whose parent has not mounted yet. Duplicate IDs or literal paths and
316
+ multiple parameter routes in one scope fail at completion. Literal routes take
317
+ precedence over the optional parameter route.
318
+
319
+ `RouteLink` is a native anchor. A descriptor resolves against the current
320
+ lineage, which is convenient for sibling destinations. A link outside the
321
+ destination's mounted lineage uses `routeTarget(...)` with the full chain from
322
+ the root. Modified clicks, downloads, and non-`_self` targets retain native
323
+ browser behavior. `router.navigate(target)` pushes by default;
324
+ `router.redirect(target)` and `navigate(target, {history: 'replace'})` replace.
325
+ `router.href(target)`, `router.resolve(descriptor, context)`, and
326
+ `router.isActive(target, exact)` support advanced composition.
327
+
328
+ Dynamic path values use a typed codec and an application-owned nullable
329
+ emitter:
330
+
331
+ ```tsx
332
+ const projectRoute = defineRouteParameter('project', stringRouteCodec, 'project-id')
333
+ const selectedProjectId = new Emitter<string | null>(null)
334
+
335
+ <RouteValue
336
+ route={projectRoute}
337
+ valueEmitter={selectedProjectId}
338
+ resolve={async (projectId, _context, signal) => {
339
+ const projects = await waitForRouteValue(
340
+ projectResults,
341
+ (items) => items.length > 0,
342
+ signal,
343
+ )
344
+ if (!projects.some(({id}) => id === projectId)) {
345
+ throw new RouteUnavailableError(`Unknown project "${projectId}"`)
346
+ }
347
+ return projectId
348
+ }}
349
+ >
350
+ <ProjectView />
351
+ </RouteValue>
352
+ ```
353
+
354
+ Resolvers run in path order, may normalize the decoded value, and receive the
355
+ settled ancestor values plus an `AbortSignal`. A newer navigation or router
356
+ disposal aborts outstanding work. `waitForRouteValue` is a convenience for a
357
+ Glue readable; fetching, authorization, retries, and domain validation remain
358
+ application-owned. Set `scopeChildren` on `RouteValue` only when further route
359
+ levels live beneath the dynamic value. Leaving it off avoids remounting an
360
+ otherwise stable leaf view when selection changes.
361
+
362
+ Explicit query bindings make selected shareable view state routable without
363
+ coupling the originating control to the router:
364
+
365
+ ```tsx
366
+ <RouteQuery
367
+ name="range"
368
+ valueEmitter={historyRange}
369
+ codec={historyRangeCodec}
370
+ defaultValue="12m"
371
+ >
372
+ <HistoryView />
373
+ </RouteQuery>
374
+ ```
375
+
376
+ Defaults are omitted, owned names sort deterministically, foreign query keys
377
+ are preserved, and passive emitter changes replace the current URL. Invalid
378
+ owned values reset to the default and produce a structured `router.issue`.
379
+ Path failures similarly fall back to the deepest resolved parent and replace
380
+ the failed entry. Applications observe `router.transition` for pending/idle
381
+ state and render `router.issue` as localized, accessible feedback. An explicit
382
+ navigation clears the issue.
383
+
384
+ Choose `createHashNavigation(window)` for static hosting; it reserves the URL
385
+ fragment. Choose `createHistoryNavigation(window, {basePath: '/app/'})` for
386
+ ordinary paths; the deployment must serve the application entry point for
387
+ direct requests below that base. `MemoryNavigationAdapter` supplies
388
+ deterministic tests and does not imply server rendering. The application owns
389
+ and disposes the router; destroying a runtime root removes mounted route
390
+ registrations but does not dispose the caller-owned router.
391
+
217
392
  ## Component host elements
218
393
 
219
394
  Fray components with a wrapper render a standards-valid custom host element,
@@ -221,11 +396,11 @@ not a framework identity class. The default application runtime therefore
221
396
  produces DOM such as:
222
397
 
223
398
  ```html
224
- <fray-panel class="panellike" data-fray-component="panel">
225
- <fray-textbox data-fray-component="textbox">
226
- <input type="text">
399
+ <fray-panel data-fray class="island" data-fray-component="panel">
400
+ <fray-textbox data-fray data-fray-component="textbox">
401
+ <input data-fray type="text">
227
402
  </fray-textbox>
228
- <button data-fray-component="button">Save</button>
403
+ <button data-fray data-fray-component="button">Save</button>
229
404
  </fray-panel>
230
405
  ```
231
406
 
@@ -234,48 +409,38 @@ Native semantics remain native: `Button` renders `button`, `Toggle` renders
234
409
  `thead`/`th`. `Tab` is a declarative child consumed by `TabPanel` and has no
235
410
  independent root. The
236
411
  `data-fray-component` keeps diagnostics unambiguous. It is not a structural or
237
- theme selector. Fray may merge public presentation traits such as `panellike`
412
+ theme selector. Fray may merge public presentation traits such as `island`
238
413
  with an application-supplied `class`/`className`; those traits describe a
239
414
  reusable capability, not component identity.
240
415
 
241
- Element naming is an immutable application-runtime setting:
242
-
243
- ```ts
244
- const defaultNames = createFrayRuntime()
245
- // <fray-panel>, <fray-list-view>, ...
246
-
247
- const productNames = createFrayRuntime({
248
- elementNames: {prefix: 'acme'},
249
- })
250
- // <acme-panel>, <acme-list-view>, ...
251
-
252
- const prefixlessNames = createFrayRuntime({
253
- elementNames: {prefix: null},
254
- })
255
- // <layout-panel>, <list-view>, <text-box>, ...
256
-
257
- const selectedOverrides = createFrayRuntime({
258
- elementNames: {
259
- prefix: null,
260
- overrides: {'panel': 'change-panel'},
261
- },
262
- })
263
- // <change-panel>, with standalone names for the other components
264
- ```
265
-
266
- HTML custom-element names must contain a hyphen, so prefixless mode uses each
267
- component's standards-valid standalone name rather than invalid names such as
268
- `<panel>` or `<listview>`. Prefixes and overrides must be lowercase kebab-case;
269
- an exact override must itself be a non-reserved custom-element name.
416
+ Pass `island` to a fixed-host component when that surface should be visually
417
+ separated from the page. Fray adds the reusable `island` class to its host;
418
+ applications may use the same class on deliberate native surface boundaries.
419
+ The component-owned rule consumes `--island-*` variables, so an island theme
420
+ can add gutters, an edge, and elevation while a flat theme can leave the
421
+ modifier visually neutral. Islands are one surface layer: nesting an `island`
422
+ component below another island is rejected. Application-authored native island
423
+ classes must follow the same no-nesting invariant. A surface island never
424
+ creates a nested theme or palette scope.
425
+
426
+ Built-in host names are fixed public DOM: Fray adds its one required custom
427
+ element hyphen and removes internal word separators from the component stem.
428
+ For example, `Panel`, `ListView`, and `ThemePicker` render as `<fray-panel>`,
429
+ `<fray-listview>`, and `<fray-themepicker>`. They cannot be prefixed, renamed,
430
+ or made prefix-free at runtime. This lets distributed stylesheets target hosts
431
+ directly and predictably.
432
+
433
+ Every element created by Fray's renderer also receives the boolean `data-fray`
434
+ attribute. Themes can therefore target native Fray output without affecting
435
+ other UI libraries: `button[data-fray]`, `input[data-fray]`, and
436
+ `dialog[data-fray]`. The marker is renderer-owned and cannot be removed through
437
+ component props. `data-fray-component` remains diagnostic metadata, not an
438
+ ordinary styling selector.
270
439
 
271
- Each runtime owns its element-name mapping and structural-style registry.
272
440
  Create the runtime once at application startup, then create and mount the root
273
- through that runtime. Nested components inherit it automatically. A separate
274
- compiled Fray build is unnecessary. Two runtimes with different mappings can
275
- coexist in one document without their component-host selectors colliding.
276
- Styles still live in the document's global cascade because these hosts are
277
- deliberately unregistered light-DOM elements, not Web Components or Shadow DOM
278
- boundaries.
441
+ through that runtime. Styles live in the document's global cascade because the
442
+ hosts are deliberately unregistered light-DOM elements, not Web Components or
443
+ Shadow DOM boundaries.
279
444
 
280
445
  ## Reactive templates
281
446
 
@@ -290,8 +455,8 @@ canonical vnode operation, but it is not the built-in component authoring
290
455
  format. The workspace lint gate rejects new `h()` templates under
291
456
  `packages/fray/src/Components`.
292
457
 
293
- Configured custom hosts are also available inside a TSX component through its
294
- protected `Host` template component:
458
+ Custom hosts are available inside a TSX component through its protected `Host`
459
+ template component:
295
460
 
296
461
  ```tsx
297
462
  interface BadgeProps extends ComponentProps {
@@ -307,12 +472,11 @@ class Badge extends Component<BadgeProps> {
307
472
  }
308
473
 
309
474
  static override hostName = 'badge'
310
- static override standaloneHostName = 'ui-badge'
311
475
  }
312
476
  ```
313
477
 
314
- At runtime that template produces `<fray-badge>` by default,
315
- `<acme-badge>` under an `acme` prefix, or `<ui-badge>` in prefixless mode.
478
+ At runtime that template produces `<fray-badge>`. `hostName` is a stable
479
+ lowercase kebab-case component identity; Fray derives the fixed host spelling.
316
480
  Native-root components such as `Button` use their native tag directly in TSX
317
481
  instead of `Host`.
318
482
 
@@ -322,7 +486,7 @@ Glue values have explicit behavior at each template boundary:
322
486
  | --- | --- |
323
487
  | `{emitter}` | Render the current value and patch only that child range on emission. |
324
488
  | `<Child source={emitter} />` | Pass the emitter object unchanged; the child owns how it consumes it. |
325
- | `prop={live(emitter)}` | Subscribe a scalar DOM or component prop one way to the emitter's current value. |
489
+ | `prop={live(emitter)}` | Subscribe a DOM property or a component-declared live prop one way to the emitter's current value. |
326
490
  | `<input bind:value={emitter} />` | Bind a writable string emitter and native `value` two ways. |
327
491
  | `<input bind:checked={emitter} />` | Bind a writable boolean emitter and native `checked` two ways. |
328
492
  | `this.read(emitter)` | Read during `render()` and rerender the component while that dependency is used. |
@@ -335,6 +499,16 @@ The component still owns and disposes emitters it creates. Direct rendering
335
499
  uses only an emitter's value; use `snapshot()` when loading and error state
336
500
  must affect the markup.
337
501
 
502
+ Component props do not implicitly unwrap emitters. A raw emitter prop passes
503
+ the emitter object to the component, while `live(emitter)` passes its current
504
+ value and subscribes at the renderer boundary. Every class component has an
505
+ explicit live-prop contract and rejects `live()` outside its allowlist in both
506
+ typed templates and at runtime. `live()` is reserved for small render-time
507
+ state such as availability, validation, and busy/pressed state. Identity,
508
+ callbacks, initial values, structural collections, and emitter ownership are
509
+ ordinary props. A data prop documented as accepting a readable emitter is a
510
+ separate input-source contract, not a `live()` prop.
511
+
338
512
  This complete example uses mutable and derived emitters, direct emitter
339
513
  children, raw emitter props, native two-way bindings, a one-way live property,
340
514
  conditional tracked state, and a `LiveQuery` passed to a child:
@@ -460,7 +634,7 @@ class ChangeApp extends Component {
460
634
  static dependencies = [Button, Panel, Preview, Results, Textbox]
461
635
  }
462
636
 
463
- const runtime = createFrayRuntime({elementNames: {prefix: 'acme'}})
637
+ const runtime = createFrayRuntime()
464
638
  runtime.registerStyles(ChangeApp).injectStyles(document)
465
639
  runtime.mount(runtime.create(ChangeApp), document.querySelector('#app')!)
466
640
  ```
@@ -531,8 +705,10 @@ or `defaultValue` in new code.
531
705
 
532
706
  ## Stable component reference
533
707
 
534
- All components also accept `children`, `className` (`class` is an alias), and a
535
- sibling-local `key` through the common component props.
708
+ All components also accept `children`, `className` (`class` is an alias), an
709
+ `island` surface modifier, and a sibling-local `key` through the common
710
+ component props. The modifier is static presentation input, not a `live()`
711
+ property.
536
712
 
537
713
  | Component | Important props | User callback | State/emitter behavior |
538
714
  | --- | --- | --- | --- |
@@ -540,20 +716,54 @@ sibling-local `key` through the common component props.
540
716
  | `Toolbar` | `label`, `orientation`, `id`, `children` | None | Stateless named toolbar; orientation is horizontal or vertical. |
541
717
  | `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. |
542
718
  | `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. |
543
- | `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. |
544
- | `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. |
719
+ | `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()`. |
720
+ | `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()`. |
721
+ | `Toggle<T>` | `options`, `label` or `ariaLabel`, value props, `disabled`, `required`, `error` | `onChange(value, event)` | One selected value; `disabled`, `required`, and `error` support `live()`. |
722
+ | `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()`. |
545
723
  | `TriCheckbox` | Checkbox props except `symbols` | `onChange(value, event)` | Cycles deny → neutral → prefer using `FilterMode`. |
546
724
  | `QuadCheckbox` | Checkbox props except `symbols` | `onChange(value, event)` | Cycles deny → neutral → prefer → require using `FilterMode`. |
547
- | `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. |
548
- | `Sidebar` | `header`, `toolbar`, `ariaLabel`, `id`, `children` | None | Native complementary region with fixed header/toolbar parts and independently scrolling content. |
549
- | `SplitView` | `primary`, `secondary`, `direction`, `primarySize`, pane labels | None | Stateless, non-resizable two-pane layout with explicit overflow ownership. |
725
+ | `Header` | `id`, `headingId`, `level`, `children` | None | Styled heading surface using a native `h1`–`h6`; level defaults to `2`. |
726
+ | `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. |
727
+ | `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. |
728
+ | `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. |
550
729
  | `DescriptionList` / `DescriptionItem` | list `label`; item `term`, `value` or children | None | Native `dl`/`dt`/`dd` record summary with responsive term/value wrapping. |
551
- | `ProgressBar` | `label`, `value` or `valueEmitter`, `max`, `valueText` | None | Labelled native progress; a null value is indeterminate. |
730
+ | `ProgressBar` | `label`, `value` or `valueEmitter`, `max`, `valueText` | None | Native progress semantics with a clipped-label visual surface; a null value is indeterminate. |
552
731
  | `ThemePicker` | `label` or `ariaLabel`, theme `options`, value props, `targetDocument`, `disabled` | `onChange(value, option, event)` | String `valueEmitter`; replaces only the theme stylesheet link. |
553
732
  | `ColorPicker` | `label` or `ariaLabel`, color `options`, value props, `targetDocument`, `disabled` | `onChange(value, option, event)` | String `valueEmitter`; replaces only the color stylesheet link. |
554
- | `Tab` | `id`, `label`, `disabled`, `children` | None | Declarative content marker consumed by `TabPanel`; not rendered as a tab by itself. |
733
+ | `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. |
555
734
  | `TabLine` | `tabs`, `label`, `baseId`, value props | `onChange(id, event)` | Active-tab `valueEmitter`; arrow keys skip disabled tabs, with Home/End support. |
556
- | `TabPanel` | `tabs` or `Tab` children, `label`, `id`, value props | `onChange(id, event)` | Owns or consumes the active-tab emitter and wires the selected tab to its tabpanel. |
735
+ | `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. |
736
+
737
+ ### Live binding and data-source contracts
738
+
739
+ Use `live()` only for the props listed here. All other component props reject
740
+ `live()` in TSX, `h()`, and at runtime. Form-control errors render an alert,
741
+ set invalid state, and associate the control or group with that message.
742
+
743
+ | Component | `live()` props | Dedicated reactive input/state props |
744
+ | --- | --- | --- |
745
+ | `Button` | `disabled`, `pressed`, `busy` | None |
746
+ | `Textbox` | `disabled`, `required`, `readOnly`, `error` | `valueEmitter` |
747
+ | `Dropdown` | `disabled`, `required`, `error` | `options` may be an array or readable emitter; `valueEmitter` |
748
+ | `RadioButton` | `checked`, `disabled`, `required`, `error` | None |
749
+ | `RadioGroup` | `disabled`, `required`, `error` | `valueEmitter`; `options` is always an ordinary array |
750
+ | `Toggle` | `disabled`, `required`, `error` | `valueEmitter`; `options` is always an ordinary array |
751
+ | `Checkbox`, `TriCheckbox`, `QuadCheckbox` | `disabled`, `required`, `error` | `valueEmitter` |
752
+ | `ThemePicker`, `ColorPicker` | `disabled` | `valueEmitter` |
753
+ | `Dialog` | `showCloseButton` | `valueEmitter` controls open state |
754
+ | `Panel` | `disabled` | None |
755
+ | `ProgressBar` | None | `valueEmitter` |
756
+ | `ListView` | None | `items` may be an array or readable emitter; selection emitters are outputs |
757
+ | `TreeView` | None | `nodes` may be an array or readable emitter; selected/expanded emitters are outputs |
758
+ | `DataTable` | None | `data` may be an array or readable emitter; `dataSource`/`rest` and selection emitters are explicit source/state contracts |
759
+ | `FilterPanel` | None | `options` may be an array or readable emitter |
760
+ | Layout, tab, description, toolbar, placeholder, and routing components | None | Their ordinary structural/configuration props require an owner rerender when changed |
761
+
762
+ Readable data sources carry their own fetch state and error through Glue's
763
+ `getFetchState()` and `getError()` APIs. `ListView`, `DataTable`, `FilterPanel`,
764
+ and `TreeView` surface those source failures as data-loading errors. This is
765
+ separate from a control's `error` prop, which represents validation or other
766
+ application-level input feedback.
557
767
 
558
768
  Invalid option arrays, duplicate tab IDs, unsupported orientations, malformed
559
769
  emitters, and non-function callbacks fail with descriptive errors.
@@ -609,6 +819,80 @@ h(Toggle, {
609
819
  })
610
820
  ```
611
821
 
822
+ ### RadioGroup
823
+
824
+ `RadioGroup` deliberately has a narrow reactive contract. Its selectable
825
+ `options` are ordinary structural input, its `valueEmitter` is a stable raw
826
+ writable state channel, and only `disabled`, `required`, and `error` accept one-way
827
+ `live()` bindings.
828
+
829
+ | Prop | Role | `live()` support |
830
+ | --- | --- | --- |
831
+ | `options` | Selectable value/label tuples supplied by the owner | No |
832
+ | `disabled` | Current availability state | Yes, with a readable boolean emitter |
833
+ | `required` | Current form-requirement state | Yes, with a readable boolean emitter |
834
+ | `error` | Current validation message and invalid state | Yes, with a readable error emitter |
835
+ | `valueEmitter` | Stable writable selected-value channel | Pass the emitter raw; do not wrap it |
836
+ | `value`, `defaultValue`, `initialValue` | Initial value when no `valueEmitter` is supplied | No |
837
+ | `id`, `label`, `ariaLabel`, `name` | Ordinary identity and presentation input | No `live()` binding |
838
+ | `onChange` | Action callback | No |
839
+
840
+ Static and live availability state can be combined without changing the option
841
+ contract:
842
+
843
+ ```tsx
844
+ const selectedView = new Emitter<'list' | 'grid'>('list')
845
+ const unavailable = new Emitter(false)
846
+ const mustChoose = new Emitter(true)
847
+
848
+ <RadioGroup
849
+ label="View"
850
+ options={[
851
+ ['list', 'List'],
852
+ ['grid', 'Grid'],
853
+ ]}
854
+ valueEmitter={selectedView}
855
+ disabled={live(unavailable)}
856
+ required={live(mustChoose)}
857
+ />
858
+ ```
859
+
860
+ `live(optionsEmitter)` and a raw options emitter are both unsupported. When an
861
+ application genuinely owns a changing option vocabulary, its owning class
862
+ component must make that structural rerender explicit:
863
+
864
+ ```tsx
865
+ interface ViewChooserProps extends ComponentProps {
866
+ options: ReadableEmitter<readonly RadioOption[]>
867
+ }
868
+
869
+ class ViewChooser extends Component<ViewChooserProps> {
870
+ render() {
871
+ return <RadioGroup
872
+ label="View"
873
+ options={this.read(this.props.options)}
874
+ />
875
+ }
876
+ }
877
+ ```
878
+
879
+ Calling `optionsEmitter.get()` directly in `render()` only reads a snapshot and
880
+ does not subscribe. `this.read(optionsEmitter)` rerenders the owner when the
881
+ array changes; normal vnode reconciliation then supplies the new ordinary
882
+ array prop to `RadioGroup`. The owner remains responsible for deciding what an
883
+ option removal means for its selected-value emitter. Fray does not silently
884
+ select, clear, or otherwise rewrite that state.
885
+
886
+ `label` is a `FrayChild`, so it may still contain an emitter that is rendered
887
+ and subscribed as child content. That fine-grained child behavior is distinct
888
+ from making the `label` property itself a `live()` binding. The same is true of
889
+ an individual `RadioOption` label: its rendered content may be reactive without
890
+ making the option array or its selectable values live.
891
+
892
+ Because `live()` subscriptions belong to a parent render record, pass live
893
+ props through JSX or `h()`. Direct `new RadioGroup(...)` construction accepts
894
+ resolved booleans and ordinary arrays only.
895
+
612
896
  ### Checkbox variants
613
897
 
614
898
  ```ts
@@ -620,6 +904,18 @@ h(QuadCheckbox, {label: 'Required tags', defaultValue: FilterMode.Require})
620
904
  `FilterMode` values are `Deny`, `Neutral`, `Prefer`, and `Require`. The basic
621
905
  checkbox uses neutral/prefer, while the variants expose the additional states.
622
906
 
907
+ ### Header
908
+
909
+ ```tsx
910
+ <Header id="portfolio-header" headingId="portfolio-title" level={2}>
911
+ Portfolio summary
912
+ </Header>
913
+ ```
914
+
915
+ `level` is an integer from `1` through `6` and defaults to `2`. Header children
916
+ must be valid native heading content. The custom host owns presentation while
917
+ the nested native heading owns document semantics.
918
+
623
919
  ### Panel
624
920
 
625
921
  ```ts
@@ -642,10 +938,10 @@ h(Sidebar, {
642
938
  }, h('ul', null, h('li', null, 'Release automation')))
643
939
  ```
644
940
 
645
- The surrounding grid or flex layout must bound the Sidebar's height. Its
646
- header and toolbar remain fixed while the dedicated content part owns vertical
647
- scrolling and is keyboard-focusable. Supply `ariaLabel` when there is no visible
648
- `header`.
941
+ The surrounding grid or flex layout must bound the Sidebar's height. Its native
942
+ complementary region is inside the fixed Sidebar host; Header and toolbar remain
943
+ fixed while the dedicated content part owns vertical scrolling and is keyboard-
944
+ focusable. Supply `ariaLabel` when there is no visible `header`.
649
945
 
650
946
  ### Declarative tabs
651
947
 
@@ -676,149 +972,74 @@ content and IDs.
676
972
 
677
973
  ## Styling and accessibility
678
974
 
679
- Fray's styling system has three physically and conceptually separate layers:
975
+ A Fray application loads four ordered styling inputs:
976
+
977
+ | Order | Input | Responsibility |
978
+ | --- | --- | --- |
979
+ | 1 | `themes/base.css` | Default palette anchors, palette-ramp derivation, and semantic custom-property defaults only |
980
+ | 2 | Runtime-collected component CSS | Rules from the `static css` declarations of the component classes the application declares |
981
+ | 3 | `colors/<name>/colors.css` | Palette anchors and endpoints only |
982
+ | 4 | `themes/<name>/theme.css` | Intentional custom-property overrides; `color-scheme` is the only ordinary declaration allowed |
983
+
984
+ Application layout remains in application-owned CSS and is not a fifth Fray
985
+ layer. Named color and theme files never import the base file; load order is
986
+ explicit at the application entry point.
987
+
988
+ Component classes own every selector and every declaration that consumes a
989
+ Fray variable. Shared rules live in meaningful abstract component classes when
990
+ those components share DOM or behavior. For example, `Dropdown` inherits the
991
+ labeled-input contract and then the select-shell contract. The collector walks
992
+ that class chain base-to-derived, resolves each class's own `&` selectors
993
+ against the concrete host, and then traverses dependencies declared at every
994
+ level. The older named `baseStyles` recipe system has been removed.
995
+
996
+ A component's `static css` should contain the absolute minimum needed for its
997
+ contract. Every selector, declaration, pseudo-element, and variable must have a
998
+ specific purpose that cannot be expressed more simply. A base-class edit must
999
+ be reviewed against all descendants because its CSS is compiled into each
1000
+ concrete descendant that is used.
1001
+
1002
+ The checked-in `styles/structural.css` is a complete generated artifact for
1003
+ non-tree-shaken/diagnostic use. Normal applications should register their root
1004
+ component and inject the resulting usage-specific stylesheet:
680
1005
 
681
- | Layer | Shipped path | Responsibility | Runtime behavior |
682
- | --- | --- | --- | --- |
683
- | Structure | `styles/structural.css` | Generated component layout, flow, sizing, positioning, accessibility mechanics, stable hooks, and variable consumption | Loaded once; remains stable during presentation changes |
684
- | Theme | `themes/<name>/theme.css` | Typography, spacing, geometry, depth, surface treatment, semantic family mappings, and exceptional pseudo/native rendering | Loaded separately and independently replaceable |
685
- | Colors | `colors/<name>/colors.css` | Primary, secondary, and neutral ramps plus contrast/color primitives; no UI-semantic roles | Loaded separately and independently replaceable |
686
-
687
- Component authors place only structure and mechanics in `static css`: display,
688
- flow, sizing, positioning, overflow, stable state hooks, and consumption of
689
- semantic variables. Reusable `static baseStyles` mappings apply named
690
- structural rules to component selectors, while `static dependencies` let the
691
- collector traverse a complete application tree, deduplicate definitions, and
692
- generate one artifact. Literal palettes and treatment-specific shadows,
693
- gradients, radii, and decoration do not belong in component CSS.
694
-
695
- The initial supported treatments are `shiny`, `java`, and `minimal`. The color
696
- catalog contains `iceblue`, `ocean`, `green`, `gray`, `orange`, `purple`, `red`,
697
- and `yellow`. The older top-level `themes/light.css` and `themes/dark.css`
698
- remain compatibility bundles; new applications should use the separated
699
- contract.
700
-
701
- These three treatments adapt the useful intent of earlier styling experiments
702
- rather than preserving their CSS literally. Application-specific selectors,
703
- duplicated declarations, and mixed structural/presentation rules were removed;
704
- the characteristic restrained Minimal, classic raised Java, and layered glossy
705
- Shiny treatments were rebuilt on the current component hooks and variables.
706
-
707
- ### Hierarchical custom properties
708
-
709
- CSS custom properties are Fray's primary theme integration protocol:
710
-
711
- ```text
712
- colors.css
713
- --palette-primary-* / --palette-secondary-* / --palette-neutral-*
714
-
715
-
716
- theme.css
717
- global UI roles (font, spacing, shape, surface)
718
-
719
-
720
- generic families (header, button, input, panel, selection)
721
-
722
-
723
- optional variants (table header, tab button, toggle button,
724
- dropdown trigger, dialog header)
725
-
726
-
727
- structural CSS and custom components
1006
+ ```ts
1007
+ const runtime = createFrayRuntime()
1008
+ runtime.registerStyles(App).injectStyles(document)
1009
+ runtime.mount(runtime.create(App), root)
728
1010
  ```
729
1011
 
730
- Components request the narrowest useful variable and explicitly fall back
731
- toward its generic family. A theme can therefore change all header-like or
732
- button-like elements with a few assignments, then override only the variants
733
- that should look different. `frayThemeVariableCatalog` exports this contract in
734
- machine-readable form, including every variable's layer, family, value kind,
735
- purpose, and optional fallback.
1012
+ ### Custom properties
736
1013
 
737
- A custom component can apply the public trait matching the treatment it needs
738
- and optionally consume the same variable hierarchy:
1014
+ `base.css` derives full primary, secondary, and neutral ramps from each
1015
+ palette's `500` anchor plus light/dark mix endpoints. A color file therefore
1016
+ sets anchors and endpoints, while a theme maps or overrides semantic families
1017
+ such as `--button-*`, `--input-*`, `--panel-*`, and `--selection-*`.
1018
+ `frayThemeVariableCatalog` exposes the supported vocabulary.
739
1019
 
740
- ```css
741
- acme-grid.datacomponentlike > header {
742
- color: var(--table-header-color, var(--header-color));
743
- background: var(--table-header-background, var(--header-background));
744
- }
745
- ```
1020
+ `--application-background` owns the canvas behind an axis-filling application
1021
+ root and its islands. Its base value is the white palette endpoint, which
1022
+ Shiny and Minimal both retain.
746
1023
 
747
- Complete traits use the `like` suffix. A component whose outer and content
748
- regions are distinct uses `shell` and `inner`, for example `buttonshell` with
749
- `buttoninner`, or `datacomponentshell` with `datacomponentinner`. Other public
750
- families include `inputlike`, `headerlike`, `coloredlike`, `panellike`, and
751
- `toolbarlike`; the panel and toolbar families also expose
752
- `panelshell`/`panelinner` and `toolbarshell`/`toolbarinner`. Wrappers are not
753
- introduced solely to carry a split trait.
754
-
755
- Themes directly target native elements, public traits, native pseudo-parts,
756
- and native/ARIA state. They never target `data-fray-component` or `data-part`.
757
- Every theme seeds inherited variables with a zero-specificity
758
- `:root`/`[data-theme]` boundary rule. Presentation selectors use `@scope` with
759
- nested theme roots and `[data-theme-exclude]` limits, a named cascade layer,
760
- and low-specificity `:where()` selectors. Shiny's highlights and
761
- select/progress decoration follow that contract and yield to native
762
- representation under forced colors.
763
-
764
- `coloredlike` defaults to the primary palette. Components can provide
765
- `--colored-base`, `--colored-light`, `--colored-dark`, and
766
- `--colored-contrast`; the active theme decides whether those inputs become a
767
- flat color, gradient, other polish, or no special treatment.
1024
+ The explicit `island` modifier consumes `--island-margin`,
1025
+ `--island-padding`, `--island-background`, `--island-border`,
1026
+ `--island-radius`, and `--island-shadow`. Base and Minimal keep its layout and
1027
+ elevation neutral; Shiny uses it for Bank2-style separated surfaces. Themes do
1028
+ not infer island boundaries from component type or nesting, and islands cannot
1029
+ contain other islands.
768
1030
 
769
1031
  ### Runtime selection
770
1032
 
771
1033
  `replaceFrayStylesheet` maintains one
772
1034
  `link[data-fray-stylesheet="theme"]` and one
773
1035
  `link[data-fray-stylesheet="colors"]`. Replacing either link also sets the
774
- corresponding `data-theme` or `data-color` root attribute. The
775
- `ThemePicker` and `ColorPicker` controls expose the same operation through the
776
- normal Fray value-control contract.
777
-
778
- The default option catalogs resolve URLs against Fray's published package
779
- layout for direct ESM/CDN use. A bundled application should ask its bundler to
780
- emit each selectable CSS file as an asset and supply those resulting URLs:
781
-
782
- ```tsx
783
- import {ColorPicker, Component, ThemePicker} from '@sylwellsoftware/fray'
784
- import iceblueHref from '@sylwellsoftware/fray/colors/iceblue/colors.css?url'
785
- import purpleHref from '@sylwellsoftware/fray/colors/purple/colors.css?url'
786
- import minimalHref from '@sylwellsoftware/fray/themes/minimal/theme.css?url'
787
- import shinyHref from '@sylwellsoftware/fray/themes/shiny/theme.css?url'
788
-
789
- const themes = [
790
- {value: 'shiny', label: 'Shiny', href: shinyHref},
791
- {value: 'minimal', label: 'Minimal', href: minimalHref},
792
- ]
793
- const colors = [
794
- {value: 'iceblue', label: 'Ice blue', href: iceblueHref},
795
- {value: 'purple', label: 'Purple', href: purpleHref},
796
- ]
797
-
798
- class AppearanceControls extends Component {
799
- render() {
800
- return <aside aria-label="Appearance">
801
- <ThemePicker label="Theme" options={themes} defaultValue="shiny" />
802
- <ColorPicker label="Colors" options={colors} defaultValue="iceblue" />
803
- </aside>
804
- }
805
-
806
- static dependencies = [ColorPicker, ThemePicker]
807
- }
808
- ```
809
-
810
- The `?url` syntax above is supported by Vite; use the equivalent emitted-asset
811
- mechanism for another bundler. The structural stylesheet is not replaced.
812
-
813
- See [`themes/README.md`](themes/README.md) for the complete architecture and
814
- variable families, and [`colors/README.md`](colors/README.md) for the palette
815
- contract.
1036
+ corresponding root data attribute. `ThemePicker` and `ColorPicker` expose the
1037
+ same operation through the normal value-control contract. The base file and
1038
+ runtime-injected structural stylesheet stay loaded while those two links are
1039
+ replaced.
816
1040
 
817
- Stable examples are tested with axe in Chromium, Firefox, and WebKit and have
818
- no serious or critical automated violations. Browser tests also cover keyboard
819
- operation, labelled roles, reduced motion, 200% configured text sizing, and
820
- forced-colors focus visibility. A formal manual screen-reader pass is still a
821
- release-candidate requirement; automated checks are not a substitute for it.
1041
+ See [the theme contract](themes/README.md) and
1042
+ [the palette contract](colors/README.md) for authoring details.
822
1043
 
823
1044
  ## Data workflows
824
1045
 
@@ -882,8 +1103,8 @@ For a tree derived from domain state, a callback updates that real source; the
882
1103
  tree derivation then rebuilds and `deriveTreeNode` resolves the fresh node.
883
1104
  `updateTreeNode` is the equivalent pure path-copy operation.
884
1105
 
885
- Generic `FilterState` keeps `neutral`, `prefer`, `require`, and `deny` semantic
886
- values separate from glyphs and transport. `filterByState`,
1106
+ Generic `FilterState` keeps `neutral`, `prefer`, `require`, and `deny`
1107
+ semantic values separate from glyphs and transport. `filterByState`,
887
1108
  `deriveFilterPredicate`, and `deriveFilteredItems` consume caller-supplied
888
1109
  dimension matchers. `serializeFilterState`/`parseFilterState` round-trip
889
1110
  validated version-1 plain data without owning URL or storage access. Unknown
@@ -902,5 +1123,5 @@ React/Vue adapters, a broad design system, a virtualized production data grid,
902
1123
  legacy-browser support, or a stable `1.0` API. See the [workspace
903
1124
  overview](../../README.md), [API surface](../../docs/API_SURFACE.md),
904
1125
  [architecture overview](../../docs/architecture.md),
905
- [changelog](../../CHANGELOG.md), [contribution guide](../../CONTRIBUTING.md),
1126
+ [changelog](CHANGELOG.md), [contribution guide](../../CONTRIBUTING.md),
906
1127
  and [security policy](../../SECURITY.md).