@sylwellsoftware/fray 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +424 -962
- package/colors/README.md +42 -12
- package/dist/Components/component.d.ts.map +1 -1
- package/dist/Components/data/treeview/treeview.d.ts +3 -0
- package/dist/Components/data/treeview/treeview.d.ts.map +1 -1
- package/dist/Components/layout/groupPanel.d.ts +21 -0
- package/dist/Components/layout/groupPanel.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +745 -657
- package/dist/index.js.map +1 -1
- package/dist/{jsx-dev-runtime-CRDpvXOH.js → jsx-dev-runtime-8s_xIag_.js} +42 -34
- package/dist/jsx-dev-runtime-8s_xIag_.js.map +1 -0
- package/dist/jsx-dev-runtime.js +1 -1
- package/dist/jsx-runtime.js +1 -1
- package/dist/styling/theme.d.ts.map +1 -1
- package/package.json +3 -3
- package/styles/structural.css +74 -20
- package/themes/README.md +46 -22
- package/themes/base.css +4 -2
- package/themes/shiny/theme.css +3 -9
- package/dist/jsx-dev-runtime-CRDpvXOH.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,462 +1,174 @@
|
|
|
1
1
|
# Fray
|
|
2
2
|
|
|
3
|
-
Fray is a browser-only component
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Fray is a browser-only TypeScript component runtime built around Glue
|
|
4
|
+
emitters. It provides TSX rendering, explicit component lifecycle, accessible
|
|
5
|
+
controls and data views, scoped services and routing, and dependency-collected
|
|
6
|
+
structural CSS.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
Fray 1.x is ESM-only and targets current evergreen browsers. Install it with
|
|
9
|
+
its Glue peer:
|
|
8
10
|
|
|
9
11
|
```bash
|
|
10
12
|
pnpm add @sylwellsoftware/glue @sylwellsoftware/fray
|
|
11
13
|
```
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
Chromium, Firefox, and Safari at candidate time. Its reproducible test matrix
|
|
15
|
-
uses Playwright's pinned Chromium, Firefox, and WebKit builds. Repository
|
|
16
|
-
tooling requires Node 22+ and pnpm 10; Fray's runtime itself is browser-only.
|
|
15
|
+
## Design and ownership
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
emitter that a derivation or query can observe, and a component renders the
|
|
23
|
-
downstream value it actually needs. State remains owned and explicit, while
|
|
24
|
-
reactive propagation and rendering mechanics stay library concerns.
|
|
25
|
-
|
|
26
|
-
Presentation should be equally direct. Native HTML already defines buttons,
|
|
27
|
-
inputs, tables, lists, progress, dialogs, and landmarks, so Fray uses those
|
|
28
|
-
elements when their semantics match. Components that need another boundary use
|
|
29
|
-
readable light-DOM host names rather than framework identity classes.
|
|
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.
|
|
36
|
-
|
|
37
|
-
## Design model
|
|
38
|
-
|
|
39
|
-
Fray is the presentation half of a deliberately two-layer architecture:
|
|
17
|
+
Fray presents application values without moving them into a second UI-specific
|
|
18
|
+
state system. Controls write ordinary Glue emitters, components read the
|
|
19
|
+
downstream values they need, and applications retain ownership of domain
|
|
20
|
+
policy and asynchronous work.
|
|
40
21
|
|
|
41
22
|
```text
|
|
42
|
-
|
|
43
|
-
domain policy, composition, endpoints,
|
|
44
|
-
|
|
45
|
-
|
|
23
|
+
application
|
|
24
|
+
domain policy, composition, services, endpoints, routes, theme selection
|
|
25
|
+
│
|
|
26
|
+
▼
|
|
46
27
|
Fray
|
|
47
|
-
TSX
|
|
48
|
-
|
|
49
|
-
|
|
28
|
+
TSX, DOM, events, lifecycle, accessibility, structural presentation
|
|
29
|
+
│ get / subscribe / set
|
|
30
|
+
▼
|
|
50
31
|
Glue
|
|
51
|
-
mutable
|
|
32
|
+
mutable values, derived values, live queries, commands, diagnostics
|
|
52
33
|
```
|
|
53
34
|
|
|
54
|
-
The
|
|
55
|
-
remains usable without a UI; Fray does not introduce hooks, a hidden component
|
|
56
|
-
state store, a query language, or transport policy to compete with Glue.
|
|
57
|
-
|
|
58
|
-
Fray follows these design rules:
|
|
59
|
-
|
|
60
|
-
- **Declarative structure, ordinary TypeScript logic.** TSX or `h()` describes
|
|
61
|
-
the current DOM. Normal methods and event handlers express algorithms and
|
|
62
|
-
commands.
|
|
63
|
-
- **Small components compose into larger widgets.** A table, for example, is
|
|
64
|
-
assembled from headers, cells, filtering, selection, loading, and error
|
|
65
|
-
pieces instead of becoming one opaque primitive.
|
|
66
|
-
- **Use the browser.** Native elements and semantics are preferred for inputs,
|
|
67
|
-
buttons, labels, tables, progress, dialogs, and landmarks. Fray's custom host
|
|
68
|
-
names are light-DOM ownership/styling hooks, not registered Web Components.
|
|
69
|
-
- **One reactive model.** Shared or composable state lives in Glue emitters;
|
|
70
|
-
derived values replace manually mirrored state; live data lives in
|
|
71
|
-
`LiveQuery`. Short-lived presentation details may remain explicit component
|
|
72
|
-
fields when no other object must observe them.
|
|
73
|
-
- **Explicit ownership and cleanup.** Components own the child components,
|
|
74
|
-
subscriptions, listeners, emitters, and queries they create, and release
|
|
75
|
-
them with their lifecycle.
|
|
76
|
-
- **Stable browser state during updates.** The synchronous keyed patcher
|
|
77
|
-
preserves compatible DOM nodes, focus, selection, input state, and event
|
|
78
|
-
listener cardinality while reconciling a component's new vnode tree.
|
|
79
|
-
- **Progressive tooling.** JSX and generated structural CSS are build-time
|
|
80
|
-
conveniences over the same small runtime contracts; they are not separate
|
|
81
|
-
execution models.
|
|
82
|
-
|
|
83
|
-
Responsibility stays at the narrowest layer that understands it:
|
|
35
|
+
The boundaries are deliberate:
|
|
84
36
|
|
|
85
37
|
| Concern | Owner |
|
|
86
38
|
| --- | --- |
|
|
87
|
-
| Domain state,
|
|
88
|
-
| DOM structure, native events,
|
|
89
|
-
| Mutable
|
|
90
|
-
|
|
|
91
|
-
|
|
|
92
|
-
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
```
|
|
39
|
+
| Domain state, validation policy, endpoint configuration, service providers, routes, page composition | Application |
|
|
40
|
+
| DOM structure, native events, accessible semantics, component lifetime, visual async states | Fray |
|
|
41
|
+
| Mutable and computed values, query execution and status, command lifecycle, optional causality | Glue |
|
|
42
|
+
| Retrieval, wire serialization, persistence | Application-supplied handlers and adapters |
|
|
43
|
+
| Structural selectors and component layout | Fray component CSS |
|
|
44
|
+
| Theme treatment, palette, application layout | Separately loaded CSS and application CSS |
|
|
45
|
+
|
|
46
|
+
Fray prefers native HTML when it expresses the contract. Custom `fray-*`
|
|
47
|
+
hosts are readable light-DOM ownership and styling boundaries; they are not
|
|
48
|
+
registered custom elements and do not use Shadow DOM.
|
|
49
|
+
|
|
50
|
+
## Set up TSX
|
|
51
|
+
|
|
52
|
+
Use Fray's automatic JSX runtime:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"compilerOptions": {
|
|
57
|
+
"jsx": "react-jsx",
|
|
58
|
+
"jsxImportSource": "@sylwellsoftware/fray"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Load the variable base, one color palette, and one theme. Register the root
|
|
64
|
+
component before mounting so Fray can collect its structural CSS dependencies:
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
103
67
|
import {Emitter} from '@sylwellsoftware/glue'
|
|
104
68
|
import {
|
|
105
69
|
Button,
|
|
106
70
|
Component,
|
|
107
|
-
Header,
|
|
108
71
|
Panel,
|
|
109
|
-
Sidebar,
|
|
110
72
|
Textbox,
|
|
111
73
|
Toolbar,
|
|
112
74
|
createFrayRuntime,
|
|
113
|
-
h,
|
|
114
75
|
} from '@sylwellsoftware/fray'
|
|
76
|
+
|
|
115
77
|
import '@sylwellsoftware/fray/themes/base.css'
|
|
116
78
|
import '@sylwellsoftware/fray/colors/iceblue/colors.css'
|
|
117
79
|
import '@sylwellsoftware/fray/themes/minimal/theme.css'
|
|
118
80
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
class App extends Component {
|
|
122
|
-
static dependencies = [Button, Panel, Textbox, Toolbar]
|
|
81
|
+
class ProfileApp extends Component {
|
|
82
|
+
readonly name = new Emitter('Ada')
|
|
123
83
|
|
|
124
84
|
render() {
|
|
125
|
-
return
|
|
126
|
-
className
|
|
127
|
-
header
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const runtime = createFrayRuntime()
|
|
136
|
-
runtime.registerStyles(App).injectStyles(document)
|
|
137
|
-
runtime.mount(runtime.create(App), document.querySelector('#app')!)
|
|
138
|
-
|
|
139
|
-
function save(value: string) {
|
|
140
|
-
console.log(value)
|
|
141
|
-
}
|
|
142
|
-
```
|
|
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
|
-
|
|
152
|
-
The prebuilt structural file targets Fray's default `fray-` hosts. Applications
|
|
153
|
-
with custom components or configured host names may instead register their root
|
|
154
|
-
dependencies and call `runtime.injectStyles(document)`; collection remains
|
|
155
|
-
idempotent and produces one application-scoped structural style element.
|
|
156
|
-
|
|
157
|
-
For automatic JSX, configure TypeScript with `"jsx": "react-jsx"` and
|
|
158
|
-
`"jsxImportSource": "@sylwellsoftware/fray"`. Classic JSX uses `h` as `jsxFactory` and
|
|
159
|
-
`Fragment` as `jsxFragmentFactory`. JSX and `h()` produce the same vnodes.
|
|
160
|
-
|
|
161
|
-
The same root can be written with automatic JSX:
|
|
162
|
-
|
|
163
|
-
```tsx
|
|
164
|
-
class App extends Component {
|
|
165
|
-
render() {
|
|
166
|
-
return <Panel header="Profile">
|
|
167
|
-
<Textbox label="Name" valueEmitter={name} />
|
|
168
|
-
<Button label="Save" onClick={() => save(name.get())} />
|
|
85
|
+
return <Panel
|
|
86
|
+
className="fray-fill-horizontal fray-fill-vertical"
|
|
87
|
+
header="Profile"
|
|
88
|
+
toolbar={<Toolbar label="Profile actions">
|
|
89
|
+
<Button label="Save" onClick={() => this.save()} />
|
|
90
|
+
</Toolbar>}
|
|
91
|
+
>
|
|
92
|
+
<Textbox label="Name" valueEmitter={this.name} />
|
|
169
93
|
</Panel>
|
|
170
94
|
}
|
|
171
95
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
## Application services without prop-drilling
|
|
96
|
+
onDestroy() {
|
|
97
|
+
this.name.dispose()
|
|
98
|
+
}
|
|
177
99
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
100
|
+
private save() {
|
|
101
|
+
console.log(this.name.get())
|
|
102
|
+
}
|
|
181
103
|
|
|
182
|
-
|
|
183
|
-
class ProjectService {
|
|
184
|
-
readonly label = 'Projects'
|
|
185
|
-
readonly projects = new RestEndpoint<
|
|
186
|
-
{search: string},
|
|
187
|
-
readonly Project[]
|
|
188
|
-
>({url: '/api/projects', parseResult: parseProjects})
|
|
104
|
+
static dependencies = [Button, Panel, Textbox, Toolbar]
|
|
189
105
|
}
|
|
190
106
|
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
])
|
|
195
|
-
const runtime = createFrayRuntime({services})
|
|
107
|
+
const runtime = createFrayRuntime()
|
|
108
|
+
runtime.registerStyles(ProfileApp).injectStyles(document)
|
|
109
|
+
runtime.mount(runtime.create(ProfileApp), document.querySelector('#app')!)
|
|
196
110
|
```
|
|
197
111
|
|
|
198
|
-
|
|
199
|
-
|
|
112
|
+
`static dependencies` is transitive and idempotent. It declares the Fray and
|
|
113
|
+
application components whose structural CSS the root can render. Applications
|
|
114
|
+
that prefer a complete static asset may import
|
|
115
|
+
`@sylwellsoftware/fray/styles/structural.css` instead of collecting styles.
|
|
200
116
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
private service!: ProjectService
|
|
117
|
+
The low-level `h()` vnode factory remains exported for non-JSX integrations,
|
|
118
|
+
but TSX is the documented authoring model for applications and Fray
|
|
119
|
+
components.
|
|
205
120
|
|
|
206
|
-
|
|
207
|
-
this.service = this.requireService(projectService)
|
|
208
|
-
}
|
|
121
|
+
## Components and lifecycle
|
|
209
122
|
|
|
210
|
-
|
|
211
|
-
return h('output', null, this.service.label)
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
Dependencies must be declared in `static requiredServices`. Resolution is
|
|
217
|
-
available during `initialize()` and later, after Fray has assigned the runtime;
|
|
218
|
-
constructors cannot resolve services. A missing service fails before component
|
|
219
|
-
initialization, and an undeclared lookup also fails clearly.
|
|
220
|
-
|
|
221
|
-
Providers are fixed when `ServiceScope` is created. Factories run lazily once
|
|
222
|
-
per scope, can explicitly require another registered service, and are checked
|
|
223
|
-
for circular resolution. `scope.dispose()` disposes initialized services in
|
|
224
|
-
reverse creation order. Components dispose the queries/results they open, not
|
|
225
|
-
the shared service. Create one scope per browser application or test. Because
|
|
226
|
-
the scope is explicit rather than global, a future non-browser adapter can
|
|
227
|
-
preserve request/session isolation without changing service definitions.
|
|
123
|
+
A class component has explicit phases:
|
|
228
124
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
125
|
+
1. The constructor stores props and creates local objects, without subscribing
|
|
126
|
+
or rendering.
|
|
127
|
+
2. `initialize()` runs once after Fray assigns the runtime. Create subscriptions
|
|
128
|
+
or resolve declared services here.
|
|
129
|
+
3. `render()` returns TSX, a primitive, an emitter child, a component, or an
|
|
130
|
+
array of children.
|
|
131
|
+
4. `afterMount()` runs after the first DOM commit; `afterUpdate()` runs after
|
|
132
|
+
later commits.
|
|
133
|
+
5. `onDestroy()` releases resources owned by the component.
|
|
233
134
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
annotations without requiring a duplicate central route tree:
|
|
135
|
+
`watch()` schedules a component update when an observable changes.
|
|
136
|
+
`read(emitter)` returns its value and tracks it only for the current render.
|
|
137
|
+
`snapshot(emitter)` tracks and returns `{value, fetchState, error}`.
|
|
138
|
+
`onCleanup()` registers listeners or other cleanup functions that Fray invokes
|
|
139
|
+
on destruction.
|
|
240
140
|
|
|
241
141
|
```tsx
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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')
|
|
142
|
+
class Counter extends Component {
|
|
143
|
+
readonly count = new Emitter(0)
|
|
144
|
+
readonly label = this.count.map((value) => `Count: ${value}`)
|
|
269
145
|
|
|
270
|
-
class SecurityApplication extends Component {
|
|
271
146
|
render() {
|
|
272
|
-
return <
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
147
|
+
return <Button
|
|
148
|
+
label={this.label}
|
|
149
|
+
onClick={() => this.count.set(this.count.get() + 1)}
|
|
150
|
+
/>
|
|
276
151
|
}
|
|
277
152
|
|
|
278
|
-
|
|
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
|
-
</>
|
|
153
|
+
onDestroy() {
|
|
154
|
+
this.label.dispose()
|
|
155
|
+
this.count.dispose()
|
|
296
156
|
}
|
|
297
157
|
|
|
298
|
-
static dependencies = [
|
|
158
|
+
static dependencies = [Button]
|
|
299
159
|
}
|
|
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
|
-
|
|
392
|
-
## Component host elements
|
|
393
|
-
|
|
394
|
-
Fray components with a wrapper render a standards-valid custom host element,
|
|
395
|
-
not a framework identity class. The default application runtime therefore
|
|
396
|
-
produces DOM such as:
|
|
397
|
-
|
|
398
|
-
```html
|
|
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">
|
|
402
|
-
</fray-textbox>
|
|
403
|
-
<button data-fray data-fray-component="button">Save</button>
|
|
404
|
-
</fray-panel>
|
|
405
160
|
```
|
|
406
161
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
`data-fray-component` keeps diagnostics unambiguous. It is not a structural or
|
|
412
|
-
theme selector. Fray may merge public presentation traits such as `island`
|
|
413
|
-
with an application-supplied `class`/`className`; those traits describe a
|
|
414
|
-
reusable capability, not component identity.
|
|
415
|
-
|
|
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.
|
|
439
|
-
|
|
440
|
-
Create the runtime once at application startup, then create and mount the root
|
|
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.
|
|
162
|
+
Fray's synchronous keyed reconciler preserves compatible DOM and component
|
|
163
|
+
identity, focus, cursor and native input state, and event-listener cardinality.
|
|
164
|
+
Use stable `key` values for reordered siblings. Never reuse one component
|
|
165
|
+
instance under two owners.
|
|
444
166
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
TypeScript TSX is Fray's supported template syntax. Classic JSX and direct
|
|
448
|
-
`h()` calls are equivalent frontends: both lower to the same vnode tree and
|
|
449
|
-
use the same renderer. A future template-file syntax can target this vnode
|
|
450
|
-
contract without changing Glue binding semantics.
|
|
451
|
-
|
|
452
|
-
Fray's built-ins are authored in TSX. Direct
|
|
453
|
-
`h()` remains supported for consumers that do not use JSX and as the renderer's
|
|
454
|
-
canonical vnode operation, but it is not the built-in component authoring
|
|
455
|
-
format. The workspace lint gate rejects new `h()` templates under
|
|
456
|
-
`packages/fray/src/Components`.
|
|
167
|
+
### Custom component hosts
|
|
457
168
|
|
|
458
|
-
|
|
459
|
-
|
|
169
|
+
Wrapped components declare a host stem and render `this.Host`. The runtime maps
|
|
170
|
+
the stem to one fixed, standards-valid name by removing internal hyphens and
|
|
171
|
+
prefixing `fray-`:
|
|
460
172
|
|
|
461
173
|
```tsx
|
|
462
174
|
interface BadgeProps extends ComponentProps {
|
|
@@ -472,105 +184,37 @@ class Badge extends Component<BadgeProps> {
|
|
|
472
184
|
}
|
|
473
185
|
|
|
474
186
|
static override hostName = 'badge'
|
|
187
|
+
static override css = css`
|
|
188
|
+
& { display: inline-flex; }
|
|
189
|
+
&[data-tone="positive"] { color: var(--palette-green); }
|
|
190
|
+
`
|
|
475
191
|
}
|
|
476
192
|
```
|
|
477
193
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
Glue values have explicit behavior at each template boundary:
|
|
484
|
-
|
|
485
|
-
| Template form | Meaning |
|
|
486
|
-
| --- | --- |
|
|
487
|
-
| `{emitter}` | Render the current value and patch only that child range on emission. |
|
|
488
|
-
| `<Child source={emitter} />` | Pass the emitter object unchanged; the child owns how it consumes it. |
|
|
489
|
-
| `prop={live(emitter)}` | Subscribe a DOM property or a component-declared live prop one way to the emitter's current value. |
|
|
490
|
-
| `<input bind:value={emitter} />` | Bind a writable string emitter and native `value` two ways. |
|
|
491
|
-
| `<input bind:checked={emitter} />` | Bind a writable boolean emitter and native `checked` two ways. |
|
|
492
|
-
| `this.read(emitter)` | Read during `render()` and rerender the component while that dependency is used. |
|
|
493
|
-
| `this.snapshot(emitter)` | Track and read `{value, fetchState, error}` for stateful rendering. |
|
|
494
|
-
|
|
495
|
-
Direct child and `live()` subscriptions are renderer-owned and are released
|
|
496
|
-
when their nodes disappear. Render-time `read()`/`snapshot()` dependencies are
|
|
497
|
-
reconciled after every render, so conditional dependencies are also released.
|
|
498
|
-
The component still owns and disposes emitters it creates. Direct rendering
|
|
499
|
-
uses only an emitter's value; use `snapshot()` when loading and error state
|
|
500
|
-
must affect the markup.
|
|
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
|
-
|
|
512
|
-
This complete example uses mutable and derived emitters, direct emitter
|
|
513
|
-
children, raw emitter props, native two-way bindings, a one-way live property,
|
|
514
|
-
conditional tracked state, and a `LiveQuery` passed to a child:
|
|
194
|
+
The `&` selector resolves against the concrete host during style collection.
|
|
195
|
+
Native-root components render their native element directly. Fray-created DOM
|
|
196
|
+
has `data-fray` for diagnostics, but component styling uses the owning host,
|
|
197
|
+
native/ARIA state, fixed part elements, and meaningful traits rather than data
|
|
198
|
+
attributes as routine CSS hooks.
|
|
515
199
|
|
|
516
|
-
|
|
517
|
-
import {
|
|
518
|
-
DerivedEmitter,
|
|
519
|
-
Emitter,
|
|
520
|
-
FetchState,
|
|
521
|
-
LiveQuery,
|
|
522
|
-
QueryHandler,
|
|
523
|
-
} from '@sylwellsoftware/glue'
|
|
524
|
-
import type {ReadableEmitter} from '@sylwellsoftware/glue'
|
|
525
|
-
import {
|
|
526
|
-
Button,
|
|
527
|
-
Component,
|
|
528
|
-
Panel,
|
|
529
|
-
Textbox,
|
|
530
|
-
createFrayRuntime,
|
|
531
|
-
live,
|
|
532
|
-
} from '@sylwellsoftware/fray'
|
|
533
|
-
import type {ComponentProps, WritableEmitter} from '@sylwellsoftware/fray'
|
|
200
|
+
## Reactive templates
|
|
534
201
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
title: string
|
|
538
|
-
}
|
|
202
|
+
Fray exposes four distinct reactive forms. Choose the form that matches the
|
|
203
|
+
ownership boundary.
|
|
539
204
|
|
|
540
|
-
|
|
541
|
-
{id: 101, title: 'Add release attestations'},
|
|
542
|
-
{id: 102, title: 'Migrate the demo to TypeScript'},
|
|
543
|
-
]
|
|
544
|
-
|
|
545
|
-
class ChangeQueryHandler extends QueryHandler<{filter: string}, readonly Change[]> {
|
|
546
|
-
override fetch({filter}: {filter: string}): readonly Change[] {
|
|
547
|
-
const needle = filter.trim().toLocaleLowerCase()
|
|
548
|
-
return needle === ''
|
|
549
|
-
? fixtures
|
|
550
|
-
: fixtures.filter(({title}) =>
|
|
551
|
-
title.toLocaleLowerCase().includes(needle))
|
|
552
|
-
}
|
|
553
|
-
}
|
|
205
|
+
### Tracked reads
|
|
554
206
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
approved: WritableEmitter<boolean>
|
|
558
|
-
}
|
|
207
|
+
Use `read()` when control flow or an ordinary value depends on an emitter. Use
|
|
208
|
+
`snapshot()` when loading and error state matter:
|
|
559
209
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
<label>
|
|
565
|
-
<input type="checkbox" bind:checked={this.props.approved} />
|
|
566
|
-
Approved
|
|
567
|
-
</label>
|
|
568
|
-
</section>
|
|
569
|
-
}
|
|
210
|
+
```tsx
|
|
211
|
+
interface Item {
|
|
212
|
+
id: string
|
|
213
|
+
label: string
|
|
570
214
|
}
|
|
571
215
|
|
|
572
216
|
interface ResultsProps extends ComponentProps {
|
|
573
|
-
results: ReadableEmitter<readonly
|
|
217
|
+
results: ReadableEmitter<readonly Item[] | undefined>
|
|
574
218
|
}
|
|
575
219
|
|
|
576
220
|
class Results extends Component<ResultsProps> {
|
|
@@ -580,270 +224,98 @@ class Results extends Component<ResultsProps> {
|
|
|
580
224
|
return <p role="alert">{String(error)}</p>
|
|
581
225
|
}
|
|
582
226
|
return <ul aria-busy={fetchState === FetchState.Loading}>
|
|
583
|
-
{(value ?? []).map((
|
|
227
|
+
{(value ?? []).map((item) => <li key={item.id}>{item.label}</li>)}
|
|
584
228
|
</ul>
|
|
585
229
|
}
|
|
586
230
|
}
|
|
587
|
-
|
|
588
|
-
class ChangeApp extends Component {
|
|
589
|
-
readonly title = new Emitter('Add native Glue template bindings')
|
|
590
|
-
readonly approved = new Emitter(false)
|
|
591
|
-
readonly filter = new Emitter('')
|
|
592
|
-
readonly showPreview = new Emitter(true)
|
|
593
|
-
readonly heading = new DerivedEmitter(
|
|
594
|
-
[this.title, this.approved] as const,
|
|
595
|
-
([title, approved]) => `${title} — ${approved ? 'approved' : 'draft'}`,
|
|
596
|
-
)
|
|
597
|
-
readonly results = new LiveQuery({
|
|
598
|
-
handler: new ChangeQueryHandler(),
|
|
599
|
-
args: {filter: this.filter},
|
|
600
|
-
})
|
|
601
|
-
|
|
602
|
-
render() {
|
|
603
|
-
return <Panel header={this.heading}>
|
|
604
|
-
<Textbox label="Title" valueEmitter={this.title} />
|
|
605
|
-
<label>Search <input bind:value={this.filter} /></label>
|
|
606
|
-
|
|
607
|
-
<output title={live(this.heading)}>Current title: {this.title}</output>
|
|
608
|
-
|
|
609
|
-
<label>
|
|
610
|
-
<input type="checkbox" bind:checked={this.showPreview} />
|
|
611
|
-
Show preview
|
|
612
|
-
</label>
|
|
613
|
-
{this.read(this.showPreview)
|
|
614
|
-
? <Preview heading={this.heading} approved={this.approved} />
|
|
615
|
-
: null}
|
|
616
|
-
|
|
617
|
-
<Results results={this.results} />
|
|
618
|
-
<Button
|
|
619
|
-
label="Refresh results"
|
|
620
|
-
onClick={() => void this.results.refresh()}
|
|
621
|
-
/>
|
|
622
|
-
</Panel>
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
onDestroy(): void {
|
|
626
|
-
this.results.dispose()
|
|
627
|
-
this.heading.dispose()
|
|
628
|
-
this.showPreview.dispose()
|
|
629
|
-
this.filter.dispose()
|
|
630
|
-
this.approved.dispose()
|
|
631
|
-
this.title.dispose()
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
static dependencies = [Button, Panel, Preview, Results, Textbox]
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
const runtime = createFrayRuntime()
|
|
638
|
-
runtime.registerStyles(ChangeApp).injectStyles(document)
|
|
639
|
-
runtime.mount(runtime.create(ChangeApp), document.querySelector('#app')!)
|
|
640
231
|
```
|
|
641
232
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
components such as `Preview` to receive a stable emitter. Use `live()` only
|
|
645
|
-
when the receiver expects a scalar prop and one-way updates are desired.
|
|
233
|
+
The surrounding component rerenders when a tracked source changes, and Fray
|
|
234
|
+
reconciles the tracked source set after every render.
|
|
646
235
|
|
|
647
|
-
|
|
236
|
+
### Fine-grained emitter children
|
|
648
237
|
|
|
649
|
-
|
|
238
|
+
A readable emitter in child position updates only its owned DOM range:
|
|
650
239
|
|
|
651
|
-
```
|
|
652
|
-
|
|
653
|
-
writable: readable contract + set(next)
|
|
240
|
+
```tsx
|
|
241
|
+
<output>Current name: {name}</output>
|
|
654
242
|
```
|
|
655
243
|
|
|
656
|
-
|
|
244
|
+
An emitter passed as a normal component prop remains the same object. Fray does
|
|
245
|
+
not inspect arbitrary prop values or discover dependencies implicitly.
|
|
657
246
|
|
|
658
|
-
-
|
|
659
|
-
DOM/component updates;
|
|
660
|
-
- use `this.read(emitter)` when the component's structure depends on its value;
|
|
661
|
-
- use `this.snapshot(emitter)` when loading/error state affects the structure;
|
|
662
|
-
- pass the emitter object as a normal prop when a child control owns the
|
|
663
|
-
interaction;
|
|
664
|
-
- use callbacks for one-way commands and emitters for values that must be read,
|
|
665
|
-
composed, or observed elsewhere.
|
|
247
|
+
### One-way live properties
|
|
666
248
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
owns any semantic `DerivedEmitter`; a data owner constructs or receives the
|
|
670
|
-
`LiveQuery`; the injected handler alone owns transport serialization.
|
|
249
|
+
`live()` updates a DOM property or a component-declared live prop without
|
|
250
|
+
rerendering its parent:
|
|
671
251
|
|
|
672
|
-
```
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
└──────────────┴────────────────┘
|
|
676
|
-
Fray view
|
|
252
|
+
```tsx
|
|
253
|
+
<Button label="Submit" disabled={live(submitting)} />
|
|
254
|
+
<output title={live(summary)}>{summary}</output>
|
|
677
255
|
```
|
|
678
256
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
component-owned resources. State created by a component should be disposed in
|
|
683
|
-
`onDestroy()` as shown above. State supplied through props remains owned by the
|
|
684
|
-
caller unless an API explicitly says otherwise.
|
|
685
|
-
|
|
686
|
-
For asynchronous views, keep the widget's semantic structure present whenever
|
|
687
|
-
practical and render initial loading, refresh-with-previous-data, empty, error,
|
|
688
|
-
and ready states explicitly. Glue owns the query state transition; Fray owns
|
|
689
|
-
how that state is presented.
|
|
690
|
-
|
|
691
|
-
## State convention
|
|
692
|
-
|
|
693
|
-
Every stateful control follows one ownership rule:
|
|
694
|
-
|
|
695
|
-
- pass a writable `valueEmitter` to share and externally update state;
|
|
696
|
-
- otherwise pass `defaultValue` to initialize the control's owned emitter;
|
|
697
|
-
- read the active emitter from the component instance's `valueEmitter`
|
|
698
|
-
property;
|
|
699
|
-
- use `onInput` or `onChange` to observe user actions. The callback runs after
|
|
700
|
-
the emitter changes and does not replace the emitter contract.
|
|
701
|
-
|
|
702
|
-
`value` remains an initial-value alias for imported `0.x` call-site
|
|
703
|
-
compatibility; it is not a prop-driven controlled mode. Prefer `valueEmitter`
|
|
704
|
-
or `defaultValue` in new code.
|
|
705
|
-
|
|
706
|
-
## Stable component reference
|
|
707
|
-
|
|
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.
|
|
712
|
-
|
|
713
|
-
| Component | Important props | User callback | State/emitter behavior |
|
|
714
|
-
| --- | --- | --- | --- |
|
|
715
|
-
| `Button` | `label`, `type`, `disabled`, `pressed`, `busy`, `busyLabel`, `ariaLabel`, native `id`/`name`/`value`/`title` | `onClick(event)` | Stateless native button; busy state disables activation and is presentation only. |
|
|
716
|
-
| `Toolbar` | `label`, `orientation`, `id`, `children` | None | Stateless named toolbar; orientation is horizontal or vertical. |
|
|
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. |
|
|
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. |
|
|
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()`. |
|
|
723
|
-
| `TriCheckbox` | Checkbox props except `symbols` | `onChange(value, event)` | Cycles deny → neutral → prefer using `FilterMode`. |
|
|
724
|
-
| `QuadCheckbox` | Checkbox props except `symbols` | `onChange(value, event)` | Cycles deny → neutral → prefer → require using `FilterMode`. |
|
|
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. |
|
|
729
|
-
| `DescriptionList` / `DescriptionItem` | list `label`; item `term`, `value` or children | None | Native `dl`/`dt`/`dd` record summary with responsive term/value wrapping. |
|
|
730
|
-
| `ProgressBar` | `label`, `value` or `valueEmitter`, `max`, `valueText` | None | Native progress semantics with a clipped-label visual surface; a null value is indeterminate. |
|
|
731
|
-
| `ThemePicker` | `label` or `ariaLabel`, theme `options`, value props, `targetDocument`, `disabled` | `onChange(value, option, event)` | String `valueEmitter`; replaces only the theme stylesheet link. |
|
|
732
|
-
| `ColorPicker` | `label` or `ariaLabel`, color `options`, value props, `targetDocument`, `disabled` | `onChange(value, option, event)` | String `valueEmitter`; replaces only the color stylesheet link. |
|
|
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. |
|
|
734
|
-
| `TabLine` | `tabs`, `label`, `baseId`, value props | `onChange(id, event)` | Active-tab `valueEmitter`; arrow keys skip disabled tabs, with Home/End support. |
|
|
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.
|
|
767
|
-
|
|
768
|
-
Invalid option arrays, duplicate tab IDs, unsupported orientations, malformed
|
|
769
|
-
emitters, and non-function callbacks fail with descriptive errors.
|
|
770
|
-
|
|
771
|
-
## Component examples
|
|
772
|
-
|
|
773
|
-
### Actions and toolbar
|
|
774
|
-
|
|
775
|
-
```ts
|
|
776
|
-
h(Toolbar, {label: 'Editor actions'},
|
|
777
|
-
h(Button, {label: 'Save', onClick: save}),
|
|
778
|
-
h(Button, {label: 'Delete', disabled: true}))
|
|
779
|
-
```
|
|
257
|
+
Built-in components allowlist their live props. TypeScript and runtime checks
|
|
258
|
+
reject a binding on an undeclared prop. Value/data emitters such as
|
|
259
|
+
`valueEmitter`, `items`, and `nodes` are raw contracts and do not use `live()`.
|
|
780
260
|
|
|
781
|
-
###
|
|
261
|
+
### Two-way native bindings
|
|
782
262
|
|
|
783
|
-
|
|
784
|
-
|
|
263
|
+
`bind:value` accepts a writable string emitter and `bind:checked` accepts a
|
|
264
|
+
writable boolean emitter:
|
|
785
265
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
required: true,
|
|
790
|
-
error: query.get() === '' ? 'Enter a search term' : null,
|
|
791
|
-
onInput: (value) => console.log('search changed', value),
|
|
792
|
-
})
|
|
266
|
+
```tsx
|
|
267
|
+
<input aria-label="Search" bind:value={search} />
|
|
268
|
+
<input type="checkbox" bind:checked={showArchived} />
|
|
793
269
|
```
|
|
794
270
|
|
|
795
|
-
|
|
271
|
+
Fray keeps the property synchronized in both directions and owns the renderer
|
|
272
|
+
subscription. Higher-level value controls use the same explicit
|
|
273
|
+
`valueEmitter` convention.
|
|
796
274
|
|
|
797
|
-
|
|
798
|
-
const role = new Emitter<'author' | 'reviewer'>('author')
|
|
275
|
+
## Value-control convention
|
|
799
276
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
{value: 'reviewer', label: 'Reviewer'},
|
|
806
|
-
],
|
|
807
|
-
onChange: (value) => console.log(value),
|
|
808
|
-
})
|
|
809
|
-
```
|
|
277
|
+
Stateful controls expose a public writable `valueEmitter`. Callers can supply
|
|
278
|
+
one with `valueEmitter`, supply an initial uncontrolled value with
|
|
279
|
+
`defaultValue`, or let the control create its documented fallback. `value` is
|
|
280
|
+
retained as an initial-value compatibility alias; it is not a continuously
|
|
281
|
+
controlled prop. `onChange` reports user-driven changes.
|
|
810
282
|
|
|
811
|
-
|
|
283
|
+
Availability and validation can be ordinary values or supported `live()`
|
|
284
|
+
bindings. Labels should be visible whenever possible; `ariaLabel` is the
|
|
285
|
+
fallback for controls without visible label content.
|
|
812
286
|
|
|
813
|
-
|
|
814
|
-
h(Toggle, {
|
|
815
|
-
label: 'View',
|
|
816
|
-
defaultValue: 'list',
|
|
817
|
-
options: [['list', 'List'], ['grid', 'Grid']],
|
|
818
|
-
onChange: (value) => console.log(value),
|
|
819
|
-
})
|
|
820
|
-
```
|
|
287
|
+
## Component reference
|
|
821
288
|
|
|
822
|
-
|
|
289
|
+
Every public component is listed below. Generic `className`, `class`, `island`,
|
|
290
|
+
`key`, and `children` come from `ComponentProps` and are omitted from the key
|
|
291
|
+
props column.
|
|
823
292
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
`live()` bindings.
|
|
293
|
+
The generic controls `Dropdown`, `RadioGroup`, and `Toggle` preserve their
|
|
294
|
+
option value type through `valueEmitter` and `onChange`; the `<T>` notation in
|
|
295
|
+
the tables below denotes that TypeScript type parameter.
|
|
828
296
|
|
|
829
|
-
|
|
297
|
+
### Actions, inputs, and choices
|
|
298
|
+
|
|
299
|
+
| Component | Purpose | Key props and state |
|
|
830
300
|
| --- | --- | --- |
|
|
831
|
-
| `
|
|
832
|
-
| `
|
|
833
|
-
| `
|
|
834
|
-
| `
|
|
835
|
-
| `valueEmitter`
|
|
836
|
-
| `value`, `
|
|
837
|
-
| `
|
|
838
|
-
| `
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
301
|
+
| `Button` | Native button with optional pressed and busy state | `label`, `type`, `disabled`, `pressed`, `busy`, `busyLabel`, `onClick`; live: `disabled`, `pressed`, `busy` |
|
|
302
|
+
| `Toolbar` | Named action group | `label`, `orientation` |
|
|
303
|
+
| `Label` | Native label for rich or live text | `text`, `htmlFor`; live: `text` |
|
|
304
|
+
| `Textbox` | Labelled native text input with validation | `label`, `valueEmitter`, `defaultValue`, `type`, `name`, `placeholder`, `disabled`, `required`, `readOnly`, `error`, native text constraints, `inputRef`, `onInput`, `onChange`; live: availability and `error` |
|
|
305
|
+
| `Dropdown<T>` | Labelled native select | `options`, `label`, `valueEmitter`, `defaultValue`, `placeholder`, `disabled`, `required`, `error`, `onChange`; `options` may be static or a readable emitter |
|
|
306
|
+
| `RadioButton` | Standalone native radio and label | `label`, `name`, `value`, `checked`, `disabled`, `required`, `error`, `onChange`; live: state, availability, `error` |
|
|
307
|
+
| `RadioGroup<T>` | Named native-radio fieldset owning one value | `options` as `[value, label]` tuples, `label`, `valueEmitter`, `defaultValue`, `disabled`, `required`, `error`, `onChange`; options are ordinary render data |
|
|
308
|
+
| `Toggle<T>` | ARIA radio group rendered as toggle buttons | `options` as `[value, label]` tuples, `label`, `valueEmitter`, `defaultValue`, `disabled`, `required`, `error`, `onChange` |
|
|
309
|
+
| `Checkbox<T>` | Configurable keyboard-operable semantic state cycle | `symbols` as `[content, value]` tuples, `label`, `valueEmitter`, `defaultValue`, `disabled`, `required`, `error`, `onChange` |
|
|
310
|
+
| `TriCheckbox` | Neutral/prefer/deny `FilterMode` cycle | Same public props as `Checkbox`, except fixed symbols |
|
|
311
|
+
| `QuadCheckbox` | Neutral/prefer/require/deny `FilterMode` cycle | Same public props as `Checkbox`, except fixed symbols |
|
|
312
|
+
|
|
313
|
+
`FilterMode` exports `neutral`, `prefer`, `require`, and `deny` semantic values.
|
|
314
|
+
Arrow keys move backward or forward through a multi-state checkbox; Space uses
|
|
315
|
+
the native forward cycle.
|
|
842
316
|
|
|
843
317
|
```tsx
|
|
844
|
-
const
|
|
845
|
-
const unavailable = new Emitter(false)
|
|
846
|
-
const mustChoose = new Emitter(true)
|
|
318
|
+
const view = new Emitter<'list' | 'grid'>('list')
|
|
847
319
|
|
|
848
320
|
<RadioGroup
|
|
849
321
|
label="View"
|
|
@@ -851,277 +323,267 @@ const mustChoose = new Emitter(true)
|
|
|
851
323
|
['list', 'List'],
|
|
852
324
|
['grid', 'Grid'],
|
|
853
325
|
]}
|
|
854
|
-
valueEmitter={
|
|
855
|
-
disabled={live(unavailable)}
|
|
856
|
-
required={live(mustChoose)}
|
|
326
|
+
valueEmitter={view}
|
|
857
327
|
/>
|
|
858
328
|
```
|
|
859
329
|
|
|
860
|
-
|
|
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
|
-
|
|
896
|
-
### Checkbox variants
|
|
897
|
-
|
|
898
|
-
```ts
|
|
899
|
-
h(Checkbox, {label: 'Include archived'})
|
|
900
|
-
h(TriCheckbox, {label: 'Match policy', defaultValue: FilterMode.Neutral})
|
|
901
|
-
h(QuadCheckbox, {label: 'Required tags', defaultValue: FilterMode.Require})
|
|
902
|
-
```
|
|
903
|
-
|
|
904
|
-
`FilterMode` values are `Deny`, `Neutral`, `Prefer`, and `Require`. The basic
|
|
905
|
-
checkbox uses neutral/prefer, while the variants expose the additional states.
|
|
330
|
+
### Layout and navigation
|
|
906
331
|
|
|
907
|
-
|
|
332
|
+
| Component | Purpose | Key props and state |
|
|
333
|
+
| --- | --- | --- |
|
|
334
|
+
| `Header` | Styled native heading surface | `level` (1–6), `headingId`, content |
|
|
335
|
+
| `GroupPanel` | Labelled bordered group with a vertical header | required `header`, content |
|
|
336
|
+
| `Panel` | Optional labelled region with toolbar and content flow | `header`, `toolbar`, `orientation`, `disabled`; live: `disabled` |
|
|
337
|
+
| `Sidebar` | Labelled `aside` with fixed header/toolbar and scrolling content | `header`, `toolbar`, `ariaLabel`, content |
|
|
338
|
+
| `SplitView` | Two-pane layout | `primary`, `secondary`, `direction`, `primarySize`, region labels |
|
|
339
|
+
| `Tab` | Declarative tab definition consumed by `TabPanel` | `id`, `label`, `disabled`, optional literal `route`, content |
|
|
340
|
+
| `TabLine` | Standalone keyboard-operable tab list | `tabs`, `valueEmitter`/`activeTabEmitter`, initial value, `label`, `onChange` |
|
|
341
|
+
| `TabPanel` | Tab list plus owned tabpanel sections | declarative `Tab` children or `tabs` definitions; value props, `label`, `onChange` |
|
|
342
|
+
|
|
343
|
+
`TabLine` supports Home, End, and orientation-appropriate arrow navigation and
|
|
344
|
+
skips disabled tabs. `TabPanel` can register routed tabs when it is mounted in
|
|
345
|
+
a router-backed route scope.
|
|
908
346
|
|
|
909
347
|
```tsx
|
|
910
|
-
<
|
|
911
|
-
|
|
912
|
-
</
|
|
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
|
-
|
|
919
|
-
### Panel
|
|
920
|
-
|
|
921
|
-
```ts
|
|
922
|
-
h(Panel, {
|
|
923
|
-
header: 'Account',
|
|
924
|
-
orientation: 'vertical',
|
|
925
|
-
toolbar: h(Toolbar, {label: 'Account actions'},
|
|
926
|
-
h(Button, {label: 'Edit'})),
|
|
927
|
-
}, h('p', null, 'Account details'))
|
|
928
|
-
```
|
|
929
|
-
|
|
930
|
-
### Sidebar
|
|
931
|
-
|
|
932
|
-
```ts
|
|
933
|
-
h(Sidebar, {
|
|
934
|
-
id: 'project-navigation',
|
|
935
|
-
header: 'Projects',
|
|
936
|
-
toolbar: h(Toolbar, {label: 'Project filters'},
|
|
937
|
-
h(Textbox, {label: 'Search projects'})),
|
|
938
|
-
}, h('ul', null, h('li', null, 'Release automation')))
|
|
939
|
-
```
|
|
940
|
-
|
|
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`.
|
|
945
|
-
|
|
946
|
-
### Declarative tabs
|
|
947
|
-
|
|
948
|
-
```ts
|
|
949
|
-
h(TabPanel, {id: 'profile', label: 'Profile sections'},
|
|
950
|
-
h(Tab, {id: 'summary', label: 'Summary'}, 'Summary content'),
|
|
951
|
-
h(Tab, {id: 'details', label: 'Details'}, 'Details content'))
|
|
952
|
-
```
|
|
953
|
-
|
|
954
|
-
### Standalone tab line
|
|
955
|
-
|
|
956
|
-
```ts
|
|
957
|
-
h(TabLine, {
|
|
958
|
-
baseId: 'settings',
|
|
959
|
-
label: 'Settings sections',
|
|
960
|
-
defaultValue: 'general',
|
|
961
|
-
tabs: [
|
|
962
|
-
{id: 'general', label: 'General'},
|
|
963
|
-
{id: 'advanced', label: 'Advanced'},
|
|
964
|
-
],
|
|
965
|
-
onChange: (id) => console.log('active tab', id),
|
|
966
|
-
})
|
|
348
|
+
<TabPanel id="profile" label="Profile sections">
|
|
349
|
+
<Tab id="summary" label="Summary">Summary content</Tab>
|
|
350
|
+
<Tab id="details" label="Details">Details content</Tab>
|
|
351
|
+
</TabPanel>
|
|
967
352
|
```
|
|
968
353
|
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
content and IDs.
|
|
354
|
+
`SplitView` is a fixed two-pane composition primitive. It does not impose
|
|
355
|
+
application resizing policy or persist pane sizes.
|
|
972
356
|
|
|
973
|
-
|
|
357
|
+
### Data and record views
|
|
974
358
|
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
| Order | Input | Responsibility |
|
|
359
|
+
| Component | Purpose | Key props and state |
|
|
978
360
|
| --- | --- | --- |
|
|
979
|
-
|
|
|
980
|
-
|
|
|
981
|
-
|
|
|
982
|
-
|
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
361
|
+
| `DescriptionList` | Native `dl` record summary | `label`, `DescriptionItem` children |
|
|
362
|
+
| `DescriptionItem` | Native `dt`/`dd` pair | required `term`, `value` or content |
|
|
363
|
+
| `Placeholder` | Decorative loading placeholder | numeric `width`, clamped to 10–100 percent |
|
|
364
|
+
| `ListView<T>` | Keyed single- or multi-select ARIA listbox | `items`, `itemKey`, `label`, `renderItem`, `multiSelect`, selected emitter |
|
|
365
|
+
| `TreeItem<T>` | Declarative tree-node marker | `id`, `label`, `textValue`, `value`, nested `TreeItem` children |
|
|
366
|
+
| `TreeView<T>` | Keyed single-select ARIA tree | `nodes` or declarative items, `label`, selected/expanded emitters, `renderItem`, per-label class/style callbacks, `onSelect` |
|
|
367
|
+
| `FilterPanel` | Semantic filter-control fieldset | `options`, `filters`, `filterModes`, `defaultSemanticState`, `label`, `onChange` |
|
|
368
|
+
| `TableHeaderCell` | Sort/filter header-cell control | column key/label plus sort/filter state callbacks |
|
|
369
|
+
| `TableHeader` | Header row over public column definitions | `columns`, sort/filter emitters and callbacks |
|
|
370
|
+
| `DataTable<T>` | Accessible local, caller-query, or REST-backed table | `columns`, one data input, `rowKey`, caption/messages, semantic filter options, single/multi selection |
|
|
371
|
+
|
|
372
|
+
`ListView`, `TreeView`, and `DataTable` reconcile selection by stable keys when
|
|
373
|
+
fresh item objects arrive. Supply an explicit key for application data; index
|
|
374
|
+
fallbacks are only safe for immutable ordering. `ListView.items` and
|
|
375
|
+
`TreeView.nodes` accept static arrays or readable emitters and present loading,
|
|
376
|
+
empty, and error states from the emitter snapshot.
|
|
377
|
+
|
|
378
|
+
Advanced compositions may use `BaseSelectionHandler`,
|
|
379
|
+
`SingleSelectionHandler`, `MultiSelectionHandler`, and
|
|
380
|
+
`createSelectionHandler` directly. Ordinary applications should prefer the
|
|
381
|
+
selection behavior already owned by `ListView` and `DataTable`.
|
|
382
|
+
|
|
383
|
+
`TreeView` owns keyboard navigation, expansion, typeahead, and selection. Use
|
|
384
|
+
`itemLabelClassName` and `itemLabelStyle` when only the label beside the
|
|
385
|
+
expander needs a reusable presentation trait such as `colored`.
|
|
386
|
+
|
|
387
|
+
#### DataTable inputs and ownership
|
|
388
|
+
|
|
389
|
+
`DataTable` requires exactly one data mode:
|
|
390
|
+
|
|
391
|
+
- `data`: a static array or readable emitter; the table owns the local derived
|
|
392
|
+
data source it creates.
|
|
393
|
+
- `dataSource`: a caller-owned `TableDataSource`; the caller disposes it.
|
|
394
|
+
- `rest`: convenience options for a table-owned REST-backed source.
|
|
395
|
+
|
|
396
|
+
For reusable sources, use `createLocalTableDataSource`,
|
|
397
|
+
`createQueryTableDataSource`, `createHandlerTableDataSource`, or
|
|
398
|
+
`createRestTableDataSource`. Sources expose `query`, `sortEmitter`,
|
|
399
|
+
`filtersEmitter`, optional `retry`, and `dispose()`.
|
|
400
|
+
|
|
401
|
+
`TableColumn` definitions own display and local comparison/filter functions.
|
|
402
|
+
The pure `applyLocalTableState`, `serializeTableQuery`, and related table-query
|
|
403
|
+
helpers keep local behavior and remote encoding explicit. Pagination,
|
|
404
|
+
virtualization, and server-specific wire policy remain application concerns.
|
|
405
|
+
|
|
406
|
+
### Dialog, status, and presentation selection
|
|
407
|
+
|
|
408
|
+
| Component | Purpose | Key props and state |
|
|
409
|
+
| --- | --- | --- |
|
|
410
|
+
| `Dialog` | Controlled native modal with focus containment and restoration | `title`, `description`, `actions`, `valueEmitter`/`defaultValue`, `closeLabel`, `showCloseButton`, `initialFocusRef`, `onClose` |
|
|
411
|
+
| `ProgressBar` | Labelled native progress with visual track | required `label`, `value` or `valueEmitter`, `max`, `valueText`; `null` is indeterminate |
|
|
412
|
+
| `ThemePicker` | Select and replace a Fray theme link | value props, `options`, `label`/`ariaLabel`, `disabled`, `targetDocument`, `onChange` |
|
|
413
|
+
| `ColorPicker` | Select and replace a Fray color link | same contract as `ThemePicker` |
|
|
1011
414
|
|
|
1012
|
-
|
|
415
|
+
The pickers use `frayThemeOptions` and `frayColorOptions` by default. An
|
|
416
|
+
application still owns whether runtime selection is offered, which options are
|
|
417
|
+
available, and whether the selected identifier is persisted.
|
|
1013
418
|
|
|
1014
|
-
|
|
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.
|
|
419
|
+
## Semantic filter state
|
|
1019
420
|
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
421
|
+
Fray's filter helpers keep presentation symbols separate from matching policy.
|
|
422
|
+
A `FilterState` is plain, versionable data keyed by dimension and option. A
|
|
423
|
+
`FilterDimensionDefinition` supplies the application-owned matchers.
|
|
1023
424
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
not infer island boundaries from component type or nesting, and islands cannot
|
|
1029
|
-
contain other islands.
|
|
425
|
+
Dimensions combine with AND. Within a dimension, deny wins, every required
|
|
426
|
+
option must match, and at least one preferred option must match when any are
|
|
427
|
+
active. Unknown persisted keys survive serialization without constraining
|
|
428
|
+
current matching.
|
|
1030
429
|
|
|
1031
|
-
|
|
430
|
+
Use `matchesFilterState` or `filterByState` for pure evaluation;
|
|
431
|
+
`deriveFilterPredicate` and `deriveFilteredItems` for reactive results; and
|
|
432
|
+
`serializeFilterState`/`parseFilterState` for deterministic versioned data.
|
|
1032
433
|
|
|
1033
|
-
|
|
1034
|
-
`link[data-fray-stylesheet="theme"]` and one
|
|
1035
|
-
`link[data-fray-stylesheet="colors"]`. Replacing either link also sets the
|
|
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.
|
|
434
|
+
## Application services
|
|
1040
435
|
|
|
1041
|
-
|
|
1042
|
-
|
|
436
|
+
Service implementations remain ordinary application TypeScript. Fray provides
|
|
437
|
+
typed keys and a fixed application scope, not dependency discovery:
|
|
1043
438
|
|
|
1044
|
-
|
|
439
|
+
```tsx
|
|
440
|
+
class ProjectService {
|
|
441
|
+
readonly label = 'Projects'
|
|
442
|
+
}
|
|
1045
443
|
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
444
|
+
const projectService = defineService<ProjectService>('projects')
|
|
445
|
+
const services = createServiceScope([
|
|
446
|
+
provideService(projectService, () => new ProjectService()),
|
|
447
|
+
])
|
|
1049
448
|
|
|
1050
|
-
|
|
1051
|
-
|
|
449
|
+
class ProjectTitle extends Component {
|
|
450
|
+
static requiredServices = [projectService]
|
|
451
|
+
private service!: ProjectService
|
|
1052
452
|
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
<ListView items={projects} selectedItemEmitter={selected} itemKey="id" />
|
|
1058
|
-
<DataTable
|
|
1059
|
-
data={projects}
|
|
1060
|
-
columns={columns}
|
|
1061
|
-
multiSelect
|
|
1062
|
-
selectedItemsEmitter={selectedRows}
|
|
1063
|
-
/>
|
|
1064
|
-
```
|
|
453
|
+
initialize() {
|
|
454
|
+
this.service = this.requireService(projectService)
|
|
455
|
+
}
|
|
1065
456
|
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
457
|
+
render() {
|
|
458
|
+
return <output>{this.service.label}</output>
|
|
459
|
+
}
|
|
460
|
+
}
|
|
1069
461
|
|
|
1070
|
-
|
|
462
|
+
const runtime = createFrayRuntime({services})
|
|
463
|
+
```
|
|
1071
464
|
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
465
|
+
Providers are immutable, lazy, and scope-shared. Factories can explicitly
|
|
466
|
+
resolve declared dependencies through their `ServiceResolver`; cycles and
|
|
467
|
+
missing providers fail clearly. `ServiceScope.dispose()` disposes initialized
|
|
468
|
+
services in reverse creation order. Components own the queries/results they
|
|
469
|
+
open; they do not dispose scope-shared services.
|
|
1075
470
|
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
columns={columns}
|
|
1080
|
-
/>
|
|
471
|
+
`FrayRuntime` carries one `ServiceScope`, optional router, and isolated
|
|
472
|
+
`StyleRegistry`. `createFrayRuntime()` is the normal construction entry point;
|
|
473
|
+
`defaultFrayRuntime` supports direct compatibility mounting.
|
|
1081
474
|
|
|
1082
|
-
|
|
1083
|
-
const source = createQueryTableDataSource({query, sortEmitter, filtersEmitter})
|
|
1084
|
-
<DataTable dataSource={source} columns={columns} />
|
|
1085
|
-
```
|
|
475
|
+
## Browser routing
|
|
1086
476
|
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
disposal. The default REST serializer retains the compact existing endpoint
|
|
1091
|
-
tokens; inject `serializeQuery` when an endpoint uses another wire contract.
|
|
477
|
+
Fray routing binds explicit route vocabulary to ordinary writable emitters.
|
|
478
|
+
The application owns descriptors, codecs, data-dependent resolvers, and the
|
|
479
|
+
navigation adapter.
|
|
1092
480
|
|
|
1093
|
-
|
|
481
|
+
```tsx
|
|
482
|
+
const portfolioRoute = defineRoute('portfolio')
|
|
483
|
+
const projectRoute = defineRouteParameter('project', stringRouteCodec)
|
|
484
|
+
const selectedProject = new Emitter<string | null>(null)
|
|
1094
485
|
|
|
1095
|
-
|
|
1096
|
-
const
|
|
486
|
+
const router = createBrowserRouter({adapter: createHashNavigation()})
|
|
487
|
+
const runtime = createFrayRuntime({router})
|
|
1097
488
|
|
|
1098
|
-
|
|
1099
|
-
|
|
489
|
+
<TabPanel label="Application sections">
|
|
490
|
+
<Tab id="portfolio" label="Portfolio" route={portfolioRoute}>
|
|
491
|
+
<RouteValue
|
|
492
|
+
route={projectRoute}
|
|
493
|
+
valueEmitter={selectedProject}
|
|
494
|
+
scopeChildren={true}
|
|
495
|
+
>
|
|
496
|
+
<ProjectScreen selectedProject={selectedProject} />
|
|
497
|
+
</RouteValue>
|
|
498
|
+
</Tab>
|
|
499
|
+
</TabPanel>
|
|
1100
500
|
```
|
|
1101
501
|
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
`
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
`
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
and
|
|
502
|
+
Core routing exports:
|
|
503
|
+
|
|
504
|
+
- `defineRoute`, `defineRouteParameter`, `routeParameter`, `routeTarget`, and
|
|
505
|
+
`withRouteQuery` create immutable descriptors and targets.
|
|
506
|
+
- `BrowserRouter`/`createBrowserRouter` progressively restore mounted scopes,
|
|
507
|
+
normalize locations, and expose structured issue state.
|
|
508
|
+
- `createHistoryNavigation`, `createHashNavigation`, and
|
|
509
|
+
`MemoryNavigationAdapter` decide where locations live.
|
|
510
|
+
- `RouteScope` establishes lineage; `RouteValue` binds dynamic path values;
|
|
511
|
+
`RouteQuery` binds one named query value; `RouteLink` renders a real anchor.
|
|
512
|
+
- `waitForRouteValue` lets a resolver await a readable application
|
|
513
|
+
prerequisite with cancellation.
|
|
514
|
+
|
|
515
|
+
Resolvers may return `RouteRedirect` through `redirectTo()`, or throw
|
|
516
|
+
`RouteUnavailableError` when the requested value cannot be represented in the
|
|
517
|
+
mounted application state.
|
|
518
|
+
|
|
519
|
+
Explicit navigation pushes by default. Restoration never pushes; redirects,
|
|
520
|
+
fallback, canonicalization, and passive bound-state changes replace. A
|
|
521
|
+
superseding transition aborts pending resolvers. Invalid locations settle at
|
|
522
|
+
the deepest valid parent and leave accessible issue presentation to the
|
|
523
|
+
application.
|
|
524
|
+
|
|
525
|
+
The history adapter needs server fallback for direct deep requests. The hash
|
|
526
|
+
adapter reserves the fragment. The memory adapter is intended for deterministic
|
|
527
|
+
tests. The caller owns and disposes the router.
|
|
528
|
+
|
|
529
|
+
## Styling contract
|
|
530
|
+
|
|
531
|
+
Load presentation in this order:
|
|
532
|
+
|
|
533
|
+
1. `@sylwellsoftware/fray/themes/base.css`
|
|
534
|
+
2. Collected CSS or `@sylwellsoftware/fray/styles/structural.css`
|
|
535
|
+
3. One `@sylwellsoftware/fray/colors/<name>/colors.css`
|
|
536
|
+
4. One `@sylwellsoftware/fray/themes/<name>/theme.css`
|
|
537
|
+
|
|
538
|
+
`base.css` declares variables and derives palette roles but contains no
|
|
539
|
+
component selectors. Color files provide anchors and endpoints. Theme files
|
|
540
|
+
provide intentional variable overrides. Component `static css` owns selectors,
|
|
541
|
+
layout, pseudo-elements, native states, and interaction mechanics.
|
|
542
|
+
|
|
543
|
+
`frayThemeVariableCatalog` describes the supported palette and semantic
|
|
544
|
+
variable hierarchy. `findFrayStylesheetOption`, `replaceFrayStylesheet`,
|
|
545
|
+
`setFrayAppearance`, and `getFrayAppearance` support application-controlled
|
|
546
|
+
runtime selection.
|
|
547
|
+
|
|
548
|
+
### Root sizing and typography
|
|
549
|
+
|
|
550
|
+
Fray does not force a mount root to fill its container. Put
|
|
551
|
+
`fray-fill-horizontal`, `fray-fill-vertical`, or both on the rendered
|
|
552
|
+
application root to claim `100vw`, `100vh`, or the full viewport. Each modifier
|
|
553
|
+
also applies `--font-family`, `--font-size`, and `--line-height` so native and
|
|
554
|
+
Fray descendants inherit the theme typography. An embedded root without a fill
|
|
555
|
+
class keeps the host page's typography and content sizing.
|
|
556
|
+
|
|
557
|
+
### Reusable traits
|
|
558
|
+
|
|
559
|
+
`island` marks one deliberate themeable surface boundary. Pass
|
|
560
|
+
`island={true}` to a wrapped component or use the class on application-owned
|
|
561
|
+
native markup. Fray rejects nested component islands; application markup must
|
|
562
|
+
preserve the same one-layer invariant.
|
|
563
|
+
|
|
564
|
+
`colored` consumes an explicit `--c1`, `--c2`, `--c3` triplet for the shared
|
|
565
|
+
gradient and `--colored-shadow` treatment. It does not choose semantic colors
|
|
566
|
+
for the application.
|
|
567
|
+
|
|
568
|
+
## Accessibility and browser support
|
|
569
|
+
|
|
570
|
+
Fray components use native controls and landmarks where possible, expose
|
|
571
|
+
accessible names, preserve focus during keyed updates, and render loading,
|
|
572
|
+
empty, and error messages outside collection semantics. The browser matrix
|
|
573
|
+
covers pinned Chromium, Firefox, and WebKit builds, including keyboard flows,
|
|
574
|
+
200% text, forced colors, and automated accessibility checks.
|
|
575
|
+
|
|
576
|
+
Applications remain responsible for meaningful labels, heading hierarchy,
|
|
577
|
+
domain validation messages, color contrast introduced by application CSS,
|
|
578
|
+
focus order across composed screens, and manual assistive-technology testing.
|
|
579
|
+
|
|
580
|
+
Fray does not support SSR, hydration, Shadow DOM, registered Web Components,
|
|
581
|
+
legacy browsers, or a concurrent rendering scheduler.
|
|
582
|
+
|
|
583
|
+
## Further reference
|
|
584
|
+
|
|
585
|
+
- [Public API surface](../../docs/API_SURFACE.md)
|
|
586
|
+
- [Architecture](../../docs/architecture.md)
|
|
587
|
+
- [Theme contract](themes/README.md)
|
|
588
|
+
- [Color palette contract](colors/README.md)
|
|
589
|
+
- [Release history](CHANGELOG.md)
|