@sylwellsoftware/fray 1.1.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 -977
- package/colors/README.md +42 -13
- package/package.json +3 -3
- package/themes/README.md +46 -22
package/README.md
CHANGED
|
@@ -1,471 +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. Either modifier also
|
|
150
|
-
establishes the theme's `--font-family`, `--font-size`, and `--line-height` on
|
|
151
|
-
the application root so native content and Fray controls inherit the published
|
|
152
|
-
typography without application CSS. A root with neither modifier keeps its
|
|
153
|
-
ordinary embedded/content-sized behavior, including its host page's typography.
|
|
154
|
-
|
|
155
|
-
The prebuilt structural file targets Fray's default `fray-` hosts. Applications
|
|
156
|
-
with custom components or configured host names may instead register their root
|
|
157
|
-
dependencies and call `runtime.injectStyles(document)`; collection remains
|
|
158
|
-
idempotent and produces one application-scoped structural style element.
|
|
159
|
-
|
|
160
|
-
For automatic JSX, configure TypeScript with `"jsx": "react-jsx"` and
|
|
161
|
-
`"jsxImportSource": "@sylwellsoftware/fray"`. Classic JSX uses `h` as `jsxFactory` and
|
|
162
|
-
`Fragment` as `jsxFragmentFactory`. JSX and `h()` produce the same vnodes.
|
|
163
|
-
|
|
164
|
-
The same root can be written with automatic JSX:
|
|
165
|
-
|
|
166
|
-
```tsx
|
|
167
|
-
class App extends Component {
|
|
168
|
-
render() {
|
|
169
|
-
return <Panel header="Profile">
|
|
170
|
-
<Textbox label="Name" valueEmitter={name} />
|
|
171
|
-
<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} />
|
|
172
93
|
</Panel>
|
|
173
94
|
}
|
|
174
95
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
## Application services without prop-drilling
|
|
96
|
+
onDestroy() {
|
|
97
|
+
this.name.dispose()
|
|
98
|
+
}
|
|
180
99
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
100
|
+
private save() {
|
|
101
|
+
console.log(this.name.get())
|
|
102
|
+
}
|
|
184
103
|
|
|
185
|
-
|
|
186
|
-
class ProjectService {
|
|
187
|
-
readonly label = 'Projects'
|
|
188
|
-
readonly projects = new RestEndpoint<
|
|
189
|
-
{search: string},
|
|
190
|
-
readonly Project[]
|
|
191
|
-
>({url: '/api/projects', parseResult: parseProjects})
|
|
104
|
+
static dependencies = [Button, Panel, Textbox, Toolbar]
|
|
192
105
|
}
|
|
193
106
|
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
])
|
|
198
|
-
const runtime = createFrayRuntime({services})
|
|
107
|
+
const runtime = createFrayRuntime()
|
|
108
|
+
runtime.registerStyles(ProfileApp).injectStyles(document)
|
|
109
|
+
runtime.mount(runtime.create(ProfileApp), document.querySelector('#app')!)
|
|
199
110
|
```
|
|
200
111
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
class ProjectList extends Component {
|
|
206
|
-
static requiredServices = [projectService]
|
|
207
|
-
private service!: ProjectService
|
|
208
|
-
|
|
209
|
-
initialize() {
|
|
210
|
-
this.service = this.requireService(projectService)
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
render() {
|
|
214
|
-
return h('output', null, this.service.label)
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
```
|
|
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.
|
|
218
116
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
initialization, and an undeclared lookup also fails clearly.
|
|
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.
|
|
223
120
|
|
|
224
|
-
|
|
225
|
-
per scope, can explicitly require another registered service, and are checked
|
|
226
|
-
for circular resolution. `scope.dispose()` disposes initialized services in
|
|
227
|
-
reverse creation order. Components dispose the queries/results they open, not
|
|
228
|
-
the shared service. Create one scope per browser application or test. Because
|
|
229
|
-
the scope is explicit rather than global, a future non-browser adapter can
|
|
230
|
-
preserve request/session isolation without changing service definitions.
|
|
121
|
+
## Components and lifecycle
|
|
231
122
|
|
|
232
|
-
|
|
233
|
-
query state comes from caller-owned endpoint results. Function components stay
|
|
234
|
-
presentation-oriented and receive rendered values or emitters from a nearby
|
|
235
|
-
lifecycle-owning class component.
|
|
123
|
+
A class component has explicit phases:
|
|
236
124
|
|
|
237
|
-
|
|
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.
|
|
238
134
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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.
|
|
243
140
|
|
|
244
141
|
```tsx
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
RouteLink,
|
|
249
|
-
RouteQuery,
|
|
250
|
-
RouteUnavailableError,
|
|
251
|
-
RouteValue,
|
|
252
|
-
Tab,
|
|
253
|
-
TabPanel,
|
|
254
|
-
createBrowserRouter,
|
|
255
|
-
createHashNavigation,
|
|
256
|
-
createFrayRuntime,
|
|
257
|
-
defineRoute,
|
|
258
|
-
defineRouteParameter,
|
|
259
|
-
routeTarget,
|
|
260
|
-
stringRouteCodec,
|
|
261
|
-
waitForRouteValue,
|
|
262
|
-
} from '@sylwellsoftware/fray'
|
|
142
|
+
class Counter extends Component {
|
|
143
|
+
readonly count = new Emitter(0)
|
|
144
|
+
readonly label = this.count.map((value) => `Count: ${value}`)
|
|
263
145
|
|
|
264
|
-
const routes = {
|
|
265
|
-
changes: defineRoute('changes'),
|
|
266
|
-
security: defineRoute('security'),
|
|
267
|
-
overview: defineRoute('security-overview', 'overview'),
|
|
268
|
-
projects: defineRoute('security-projects', 'projects'),
|
|
269
|
-
}
|
|
270
|
-
const activeApplication = new Emitter('changes')
|
|
271
|
-
const activeSecurityView = new Emitter('overview')
|
|
272
|
-
|
|
273
|
-
class SecurityApplication extends Component {
|
|
274
146
|
render() {
|
|
275
|
-
return <
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
147
|
+
return <Button
|
|
148
|
+
label={this.label}
|
|
149
|
+
onClick={() => this.count.set(this.count.get() + 1)}
|
|
150
|
+
/>
|
|
279
151
|
}
|
|
280
152
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
class App extends Component {
|
|
285
|
-
render() {
|
|
286
|
-
return <>
|
|
287
|
-
<nav aria-label="Applications">
|
|
288
|
-
<RouteLink to={routeTarget(routes.security, routes.projects)}>
|
|
289
|
-
Security projects
|
|
290
|
-
</RouteLink>
|
|
291
|
-
</nav>
|
|
292
|
-
<TabPanel valueEmitter={activeApplication} label="Applications">
|
|
293
|
-
<Tab id="changes" label="Changes" route={routes.changes}>Changes</Tab>
|
|
294
|
-
<Tab id="security" label="Security" route={routes.security}>
|
|
295
|
-
<SecurityApplication />
|
|
296
|
-
</Tab>
|
|
297
|
-
</TabPanel>
|
|
298
|
-
</>
|
|
153
|
+
onDestroy() {
|
|
154
|
+
this.label.dispose()
|
|
155
|
+
this.count.dispose()
|
|
299
156
|
}
|
|
300
157
|
|
|
301
|
-
static dependencies = [
|
|
158
|
+
static dependencies = [Button]
|
|
302
159
|
}
|
|
303
|
-
|
|
304
|
-
const router = createBrowserRouter({adapter: createHashNavigation(window)})
|
|
305
|
-
const runtime = createFrayRuntime({router})
|
|
306
|
-
const app = runtime.mount(runtime.create(App), document.querySelector('#app')!)
|
|
307
|
-
|
|
308
|
-
addEventListener('pagehide', () => {
|
|
309
|
-
app.destroy()
|
|
310
|
-
router.dispose()
|
|
311
|
-
}, {once: true})
|
|
312
160
|
```
|
|
313
161
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
from one whose parent has not mounted yet. Duplicate IDs or literal paths and
|
|
319
|
-
multiple parameter routes in one scope fail at completion. Literal routes take
|
|
320
|
-
precedence over the optional parameter route.
|
|
321
|
-
|
|
322
|
-
`RouteLink` is a native anchor. A descriptor resolves against the current
|
|
323
|
-
lineage, which is convenient for sibling destinations. A link outside the
|
|
324
|
-
destination's mounted lineage uses `routeTarget(...)` with the full chain from
|
|
325
|
-
the root. Modified clicks, downloads, and non-`_self` targets retain native
|
|
326
|
-
browser behavior. `router.navigate(target)` pushes by default;
|
|
327
|
-
`router.redirect(target)` and `navigate(target, {history: 'replace'})` replace.
|
|
328
|
-
`router.href(target)`, `router.resolve(descriptor, context)`, and
|
|
329
|
-
`router.isActive(target, exact)` support advanced composition.
|
|
330
|
-
|
|
331
|
-
Dynamic path values use a typed codec and an application-owned nullable
|
|
332
|
-
emitter:
|
|
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.
|
|
333
166
|
|
|
334
|
-
|
|
335
|
-
const projectRoute = defineRouteParameter('project', stringRouteCodec, 'project-id')
|
|
336
|
-
const selectedProjectId = new Emitter<string | null>(null)
|
|
337
|
-
|
|
338
|
-
<RouteValue
|
|
339
|
-
route={projectRoute}
|
|
340
|
-
valueEmitter={selectedProjectId}
|
|
341
|
-
resolve={async (projectId, _context, signal) => {
|
|
342
|
-
const projects = await waitForRouteValue(
|
|
343
|
-
projectResults,
|
|
344
|
-
(items) => items.length > 0,
|
|
345
|
-
signal,
|
|
346
|
-
)
|
|
347
|
-
if (!projects.some(({id}) => id === projectId)) {
|
|
348
|
-
throw new RouteUnavailableError(`Unknown project "${projectId}"`)
|
|
349
|
-
}
|
|
350
|
-
return projectId
|
|
351
|
-
}}
|
|
352
|
-
>
|
|
353
|
-
<ProjectView />
|
|
354
|
-
</RouteValue>
|
|
355
|
-
```
|
|
356
|
-
|
|
357
|
-
Resolvers run in path order, may normalize the decoded value, and receive the
|
|
358
|
-
settled ancestor values plus an `AbortSignal`. A newer navigation or router
|
|
359
|
-
disposal aborts outstanding work. `waitForRouteValue` is a convenience for a
|
|
360
|
-
Glue readable; fetching, authorization, retries, and domain validation remain
|
|
361
|
-
application-owned. Set `scopeChildren` on `RouteValue` only when further route
|
|
362
|
-
levels live beneath the dynamic value. Leaving it off avoids remounting an
|
|
363
|
-
otherwise stable leaf view when selection changes.
|
|
364
|
-
|
|
365
|
-
Explicit query bindings make selected shareable view state routable without
|
|
366
|
-
coupling the originating control to the router:
|
|
367
|
-
|
|
368
|
-
```tsx
|
|
369
|
-
<RouteQuery
|
|
370
|
-
name="range"
|
|
371
|
-
valueEmitter={historyRange}
|
|
372
|
-
codec={historyRangeCodec}
|
|
373
|
-
defaultValue="12m"
|
|
374
|
-
>
|
|
375
|
-
<HistoryView />
|
|
376
|
-
</RouteQuery>
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
Defaults are omitted, owned names sort deterministically, foreign query keys
|
|
380
|
-
are preserved, and passive emitter changes replace the current URL. Invalid
|
|
381
|
-
owned values reset to the default and produce a structured `router.issue`.
|
|
382
|
-
Path failures similarly fall back to the deepest resolved parent and replace
|
|
383
|
-
the failed entry. Applications observe `router.transition` for pending/idle
|
|
384
|
-
state and render `router.issue` as localized, accessible feedback. An explicit
|
|
385
|
-
navigation clears the issue.
|
|
386
|
-
|
|
387
|
-
Choose `createHashNavigation(window)` for static hosting; it reserves the URL
|
|
388
|
-
fragment. Choose `createHistoryNavigation(window, {basePath: '/app/'})` for
|
|
389
|
-
ordinary paths; the deployment must serve the application entry point for
|
|
390
|
-
direct requests below that base. `MemoryNavigationAdapter` supplies
|
|
391
|
-
deterministic tests and does not imply server rendering. The application owns
|
|
392
|
-
and disposes the router; destroying a runtime root removes mounted route
|
|
393
|
-
registrations but does not dispose the caller-owned router.
|
|
394
|
-
|
|
395
|
-
## Component host elements
|
|
396
|
-
|
|
397
|
-
Fray components with a wrapper render a standards-valid custom host element,
|
|
398
|
-
not a framework identity class. The default application runtime therefore
|
|
399
|
-
produces DOM such as:
|
|
400
|
-
|
|
401
|
-
```html
|
|
402
|
-
<fray-panel data-fray class="island" data-fray-component="panel">
|
|
403
|
-
<fray-textbox data-fray data-fray-component="textbox">
|
|
404
|
-
<input data-fray type="text">
|
|
405
|
-
</fray-textbox>
|
|
406
|
-
<button data-fray data-fray-component="button">Save</button>
|
|
407
|
-
</fray-panel>
|
|
408
|
-
```
|
|
409
|
-
|
|
410
|
-
Native semantics remain native: `Button` renders `button`, `Toggle` renders
|
|
411
|
-
`fieldset`, `Sidebar` renders `aside`, and table header components render
|
|
412
|
-
`thead`/`th`. `Tab` is a declarative child consumed by `TabPanel` and has no
|
|
413
|
-
independent root. The
|
|
414
|
-
`data-fray-component` keeps diagnostics unambiguous. It is not a structural or
|
|
415
|
-
theme selector. Fray may merge public presentation traits such as `island`
|
|
416
|
-
and `colored` with an application-supplied `class`/`className`; those traits
|
|
417
|
-
describe a reusable capability, not component identity.
|
|
418
|
-
|
|
419
|
-
Pass `island` to a fixed-host component when that surface should be visually
|
|
420
|
-
separated from the page. Fray adds the reusable `island` class to its host;
|
|
421
|
-
applications may use the same class on deliberate native surface boundaries.
|
|
422
|
-
The component-owned rule consumes `--island-*` variables, so an island theme
|
|
423
|
-
can add gutters, an edge, and elevation while a flat theme can leave the
|
|
424
|
-
modifier visually neutral. Islands are one surface layer: nesting an `island`
|
|
425
|
-
component below another island is rejected. Application-authored native island
|
|
426
|
-
classes must follow the same no-nesting invariant. A surface island never
|
|
427
|
-
creates a nested theme or palette scope.
|
|
428
|
-
|
|
429
|
-
Apply `colored` only to an element that supplies an ordered dark/base/light
|
|
430
|
-
triplet through `--c1`, `--c2`, and `--c3`. The shared component rule paints
|
|
431
|
-
that triplet as a gradient; named themes can add depth through
|
|
432
|
-
`--colored-shadow`. Shiny supplies its glossy shadow while the base and other
|
|
433
|
-
themes remain flat.
|
|
434
|
-
|
|
435
|
-
Built-in host names are fixed public DOM: Fray adds its one required custom
|
|
436
|
-
element hyphen and removes internal word separators from the component stem.
|
|
437
|
-
For example, `Panel`, `ListView`, and `ThemePicker` render as `<fray-panel>`,
|
|
438
|
-
`<fray-listview>`, and `<fray-themepicker>`. They cannot be prefixed, renamed,
|
|
439
|
-
or made prefix-free at runtime. This lets distributed stylesheets target hosts
|
|
440
|
-
directly and predictably.
|
|
441
|
-
|
|
442
|
-
Every element created by Fray's renderer also receives the boolean `data-fray`
|
|
443
|
-
attribute. Themes can therefore target native Fray output without affecting
|
|
444
|
-
other UI libraries: `button[data-fray]`, `input[data-fray]`, and
|
|
445
|
-
`dialog[data-fray]`. The marker is renderer-owned and cannot be removed through
|
|
446
|
-
component props. `data-fray-component` remains diagnostic metadata, not an
|
|
447
|
-
ordinary styling selector.
|
|
448
|
-
|
|
449
|
-
Create the runtime once at application startup, then create and mount the root
|
|
450
|
-
through that runtime. Styles live in the document's global cascade because the
|
|
451
|
-
hosts are deliberately unregistered light-DOM elements, not Web Components or
|
|
452
|
-
Shadow DOM boundaries.
|
|
453
|
-
|
|
454
|
-
## Reactive templates
|
|
167
|
+
### Custom component hosts
|
|
455
168
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
contract without changing Glue binding semantics.
|
|
460
|
-
|
|
461
|
-
Fray's built-ins are authored in TSX. Direct
|
|
462
|
-
`h()` remains supported for consumers that do not use JSX and as the renderer's
|
|
463
|
-
canonical vnode operation, but it is not the built-in component authoring
|
|
464
|
-
format. The workspace lint gate rejects new `h()` templates under
|
|
465
|
-
`packages/fray/src/Components`.
|
|
466
|
-
|
|
467
|
-
Custom hosts are available inside a TSX component through its protected `Host`
|
|
468
|
-
template component:
|
|
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-`:
|
|
469
172
|
|
|
470
173
|
```tsx
|
|
471
174
|
interface BadgeProps extends ComponentProps {
|
|
@@ -481,105 +184,37 @@ class Badge extends Component<BadgeProps> {
|
|
|
481
184
|
}
|
|
482
185
|
|
|
483
186
|
static override hostName = 'badge'
|
|
187
|
+
static override css = css`
|
|
188
|
+
& { display: inline-flex; }
|
|
189
|
+
&[data-tone="positive"] { color: var(--palette-green); }
|
|
190
|
+
`
|
|
484
191
|
}
|
|
485
192
|
```
|
|
486
193
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
Glue values have explicit behavior at each template boundary:
|
|
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.
|
|
493
199
|
|
|
494
|
-
|
|
495
|
-
| --- | --- |
|
|
496
|
-
| `{emitter}` | Render the current value and patch only that child range on emission. |
|
|
497
|
-
| `<Child source={emitter} />` | Pass the emitter object unchanged; the child owns how it consumes it. |
|
|
498
|
-
| `prop={live(emitter)}` | Subscribe a DOM property or a component-declared live prop one way to the emitter's current value. |
|
|
499
|
-
| `<input bind:value={emitter} />` | Bind a writable string emitter and native `value` two ways. |
|
|
500
|
-
| `<input bind:checked={emitter} />` | Bind a writable boolean emitter and native `checked` two ways. |
|
|
501
|
-
| `this.read(emitter)` | Read during `render()` and rerender the component while that dependency is used. |
|
|
502
|
-
| `this.snapshot(emitter)` | Track and read `{value, fetchState, error}` for stateful rendering. |
|
|
503
|
-
|
|
504
|
-
Direct child and `live()` subscriptions are renderer-owned and are released
|
|
505
|
-
when their nodes disappear. Render-time `read()`/`snapshot()` dependencies are
|
|
506
|
-
reconciled after every render, so conditional dependencies are also released.
|
|
507
|
-
The component still owns and disposes emitters it creates. Direct rendering
|
|
508
|
-
uses only an emitter's value; use `snapshot()` when loading and error state
|
|
509
|
-
must affect the markup.
|
|
510
|
-
|
|
511
|
-
Component props do not implicitly unwrap emitters. A raw emitter prop passes
|
|
512
|
-
the emitter object to the component, while `live(emitter)` passes its current
|
|
513
|
-
value and subscribes at the renderer boundary. Every class component has an
|
|
514
|
-
explicit live-prop contract and rejects `live()` outside its allowlist in both
|
|
515
|
-
typed templates and at runtime. `live()` is reserved for small render-time
|
|
516
|
-
state such as availability, validation, and busy/pressed state. Identity,
|
|
517
|
-
callbacks, initial values, structural collections, and emitter ownership are
|
|
518
|
-
ordinary props. A data prop documented as accepting a readable emitter is a
|
|
519
|
-
separate input-source contract, not a `live()` prop.
|
|
520
|
-
|
|
521
|
-
This complete example uses mutable and derived emitters, direct emitter
|
|
522
|
-
children, raw emitter props, native two-way bindings, a one-way live property,
|
|
523
|
-
conditional tracked state, and a `LiveQuery` passed to a child:
|
|
524
|
-
|
|
525
|
-
```tsx
|
|
526
|
-
import {
|
|
527
|
-
DerivedEmitter,
|
|
528
|
-
Emitter,
|
|
529
|
-
FetchState,
|
|
530
|
-
LiveQuery,
|
|
531
|
-
QueryHandler,
|
|
532
|
-
} from '@sylwellsoftware/glue'
|
|
533
|
-
import type {ReadableEmitter} from '@sylwellsoftware/glue'
|
|
534
|
-
import {
|
|
535
|
-
Button,
|
|
536
|
-
Component,
|
|
537
|
-
Panel,
|
|
538
|
-
Textbox,
|
|
539
|
-
createFrayRuntime,
|
|
540
|
-
live,
|
|
541
|
-
} from '@sylwellsoftware/fray'
|
|
542
|
-
import type {ComponentProps, WritableEmitter} from '@sylwellsoftware/fray'
|
|
200
|
+
## Reactive templates
|
|
543
201
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
title: string
|
|
547
|
-
}
|
|
202
|
+
Fray exposes four distinct reactive forms. Choose the form that matches the
|
|
203
|
+
ownership boundary.
|
|
548
204
|
|
|
549
|
-
|
|
550
|
-
{id: 101, title: 'Add release attestations'},
|
|
551
|
-
{id: 102, title: 'Migrate the demo to TypeScript'},
|
|
552
|
-
]
|
|
553
|
-
|
|
554
|
-
class ChangeQueryHandler extends QueryHandler<{filter: string}, readonly Change[]> {
|
|
555
|
-
override fetch({filter}: {filter: string}): readonly Change[] {
|
|
556
|
-
const needle = filter.trim().toLocaleLowerCase()
|
|
557
|
-
return needle === ''
|
|
558
|
-
? fixtures
|
|
559
|
-
: fixtures.filter(({title}) =>
|
|
560
|
-
title.toLocaleLowerCase().includes(needle))
|
|
561
|
-
}
|
|
562
|
-
}
|
|
205
|
+
### Tracked reads
|
|
563
206
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
approved: WritableEmitter<boolean>
|
|
567
|
-
}
|
|
207
|
+
Use `read()` when control flow or an ordinary value depends on an emitter. Use
|
|
208
|
+
`snapshot()` when loading and error state matter:
|
|
568
209
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
<label>
|
|
574
|
-
<input type="checkbox" bind:checked={this.props.approved} />
|
|
575
|
-
Approved
|
|
576
|
-
</label>
|
|
577
|
-
</section>
|
|
578
|
-
}
|
|
210
|
+
```tsx
|
|
211
|
+
interface Item {
|
|
212
|
+
id: string
|
|
213
|
+
label: string
|
|
579
214
|
}
|
|
580
215
|
|
|
581
216
|
interface ResultsProps extends ComponentProps {
|
|
582
|
-
results: ReadableEmitter<readonly
|
|
217
|
+
results: ReadableEmitter<readonly Item[] | undefined>
|
|
583
218
|
}
|
|
584
219
|
|
|
585
220
|
class Results extends Component<ResultsProps> {
|
|
@@ -589,271 +224,98 @@ class Results extends Component<ResultsProps> {
|
|
|
589
224
|
return <p role="alert">{String(error)}</p>
|
|
590
225
|
}
|
|
591
226
|
return <ul aria-busy={fetchState === FetchState.Loading}>
|
|
592
|
-
{(value ?? []).map((
|
|
227
|
+
{(value ?? []).map((item) => <li key={item.id}>{item.label}</li>)}
|
|
593
228
|
</ul>
|
|
594
229
|
}
|
|
595
230
|
}
|
|
596
|
-
|
|
597
|
-
class ChangeApp extends Component {
|
|
598
|
-
readonly title = new Emitter('Add native Glue template bindings')
|
|
599
|
-
readonly approved = new Emitter(false)
|
|
600
|
-
readonly filter = new Emitter('')
|
|
601
|
-
readonly showPreview = new Emitter(true)
|
|
602
|
-
readonly heading = new DerivedEmitter(
|
|
603
|
-
[this.title, this.approved] as const,
|
|
604
|
-
([title, approved]) => `${title} — ${approved ? 'approved' : 'draft'}`,
|
|
605
|
-
)
|
|
606
|
-
readonly results = new LiveQuery({
|
|
607
|
-
handler: new ChangeQueryHandler(),
|
|
608
|
-
args: {filter: this.filter},
|
|
609
|
-
})
|
|
610
|
-
|
|
611
|
-
render() {
|
|
612
|
-
return <Panel header={this.heading}>
|
|
613
|
-
<Textbox label="Title" valueEmitter={this.title} />
|
|
614
|
-
<label>Search <input bind:value={this.filter} /></label>
|
|
615
|
-
|
|
616
|
-
<output title={live(this.heading)}>Current title: {this.title}</output>
|
|
617
|
-
|
|
618
|
-
<label>
|
|
619
|
-
<input type="checkbox" bind:checked={this.showPreview} />
|
|
620
|
-
Show preview
|
|
621
|
-
</label>
|
|
622
|
-
{this.read(this.showPreview)
|
|
623
|
-
? <Preview heading={this.heading} approved={this.approved} />
|
|
624
|
-
: null}
|
|
625
|
-
|
|
626
|
-
<Results results={this.results} />
|
|
627
|
-
<Button
|
|
628
|
-
label="Refresh results"
|
|
629
|
-
onClick={() => void this.results.refresh()}
|
|
630
|
-
/>
|
|
631
|
-
</Panel>
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
onDestroy(): void {
|
|
635
|
-
this.results.dispose()
|
|
636
|
-
this.heading.dispose()
|
|
637
|
-
this.showPreview.dispose()
|
|
638
|
-
this.filter.dispose()
|
|
639
|
-
this.approved.dispose()
|
|
640
|
-
this.title.dispose()
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
static dependencies = [Button, Panel, Preview, Results, Textbox]
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
const runtime = createFrayRuntime()
|
|
647
|
-
runtime.registerStyles(ChangeApp).injectStyles(document)
|
|
648
|
-
runtime.mount(runtime.create(ChangeApp), document.querySelector('#app')!)
|
|
649
231
|
```
|
|
650
232
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
components such as `Preview` to receive a stable emitter. Use `live()` only
|
|
654
|
-
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.
|
|
655
235
|
|
|
656
|
-
|
|
236
|
+
### Fine-grained emitter children
|
|
657
237
|
|
|
658
|
-
|
|
238
|
+
A readable emitter in child position updates only its owned DOM range:
|
|
659
239
|
|
|
660
|
-
```
|
|
661
|
-
|
|
662
|
-
writable: readable contract + set(next)
|
|
240
|
+
```tsx
|
|
241
|
+
<output>Current name: {name}</output>
|
|
663
242
|
```
|
|
664
243
|
|
|
665
|
-
|
|
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.
|
|
666
246
|
|
|
667
|
-
-
|
|
668
|
-
DOM/component updates;
|
|
669
|
-
- use `this.read(emitter)` when the component's structure depends on its value;
|
|
670
|
-
- use `this.snapshot(emitter)` when loading/error state affects the structure;
|
|
671
|
-
- pass the emitter object as a normal prop when a child control owns the
|
|
672
|
-
interaction;
|
|
673
|
-
- use callbacks for one-way commands and emitters for values that must be read,
|
|
674
|
-
composed, or observed elsewhere.
|
|
247
|
+
### One-way live properties
|
|
675
248
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
owns any semantic `DerivedEmitter`; a data owner constructs or receives the
|
|
679
|
-
`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:
|
|
680
251
|
|
|
681
|
-
```
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
└──────────────┴────────────────┘
|
|
685
|
-
Fray view
|
|
252
|
+
```tsx
|
|
253
|
+
<Button label="Submit" disabled={live(submitting)} />
|
|
254
|
+
<output title={live(summary)}>{summary}</output>
|
|
686
255
|
```
|
|
687
256
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
component-owned resources. State created by a component should be disposed in
|
|
692
|
-
`onDestroy()` as shown above. State supplied through props remains owned by the
|
|
693
|
-
caller unless an API explicitly says otherwise.
|
|
694
|
-
|
|
695
|
-
For asynchronous views, keep the widget's semantic structure present whenever
|
|
696
|
-
practical and render initial loading, refresh-with-previous-data, empty, error,
|
|
697
|
-
and ready states explicitly. Glue owns the query state transition; Fray owns
|
|
698
|
-
how that state is presented.
|
|
699
|
-
|
|
700
|
-
## State convention
|
|
701
|
-
|
|
702
|
-
Every stateful control follows one ownership rule:
|
|
703
|
-
|
|
704
|
-
- pass a writable `valueEmitter` to share and externally update state;
|
|
705
|
-
- otherwise pass `defaultValue` to initialize the control's owned emitter;
|
|
706
|
-
- read the active emitter from the component instance's `valueEmitter`
|
|
707
|
-
property;
|
|
708
|
-
- use `onInput` or `onChange` to observe user actions. The callback runs after
|
|
709
|
-
the emitter changes and does not replace the emitter contract.
|
|
710
|
-
|
|
711
|
-
`value` remains an initial-value alias for imported `0.x` call-site
|
|
712
|
-
compatibility; it is not a prop-driven controlled mode. Prefer `valueEmitter`
|
|
713
|
-
or `defaultValue` in new code.
|
|
714
|
-
|
|
715
|
-
## Stable component reference
|
|
716
|
-
|
|
717
|
-
All components also accept `children`, `className` (`class` is an alias), an
|
|
718
|
-
`island` surface modifier, and a sibling-local `key` through the common
|
|
719
|
-
component props. The modifier is static presentation input, not a `live()`
|
|
720
|
-
property.
|
|
721
|
-
|
|
722
|
-
| Component | Important props | User callback | State/emitter behavior |
|
|
723
|
-
| --- | --- | --- | --- |
|
|
724
|
-
| `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. |
|
|
725
|
-
| `Toolbar` | `label`, `orientation`, `id`, `children` | None | Stateless named toolbar; orientation is horizontal or vertical. |
|
|
726
|
-
| `Textbox` | `label` or `ariaLabel`, value props, `disabled`, `required`, `readOnly`, `error`, native text constraints, `inputRef` | `onInput(value, event)`, `onChange(value, event)` | String `valueEmitter`; external emitter changes patch the native input without replacing it. |
|
|
727
|
-
| `Dropdown<T>` | `options`, `label` or `ariaLabel`, value props, `disabled`, `required`, `error`, `placeholder`, `name` | `onChange(value, event)` | Typed string/number `valueEmitter`; `options` may be an array or readable emitter. |
|
|
728
|
-
| `RadioButton` | `label`, `name`, `value`, `checked`, `disabled`, `required`, `error` | `onChange(checked, event)` | Native radio input with a labelled fixed shell; `checked`, `disabled`, `required`, and `error` support `live()`. |
|
|
729
|
-
| `RadioGroup<T>` | Plain-array `options`, `label` or `ariaLabel`, value props, `name`, `disabled`, `required`, `error` | `onChange(value, event)` | Native radio inputs with one selected `valueEmitter`; `disabled`, `required`, and `error` support `live()`. |
|
|
730
|
-
| `Toggle<T>` | `options`, `label` or `ariaLabel`, value props, `disabled`, `required`, `error` | `onChange(value, event)` | One selected value; `disabled`, `required`, and `error` support `live()`. |
|
|
731
|
-
| `Checkbox<T>` | `symbols`, `label`, value props, `disabled`, `required`, `error`, `name` | `onChange(value, event)` | Two-state semantic value by default; `disabled`, `required`, and `error` support `live()`. |
|
|
732
|
-
| `TriCheckbox` | Checkbox props except `symbols` | `onChange(value, event)` | Cycles deny → neutral → prefer using `FilterMode`. |
|
|
733
|
-
| `QuadCheckbox` | Checkbox props except `symbols` | `onChange(value, event)` | Cycles deny → neutral → prefer → require using `FilterMode`. |
|
|
734
|
-
| `Header` | `id`, `headingId`, `level`, `children` | None | Styled heading surface using a native `h1`–`h6`; level defaults to `2`. |
|
|
735
|
-
| `GroupPanel` | `header`, `id`, `children` | None | Labelled bordered control group with a full-height vertical Header. |
|
|
736
|
-
| `Panel` | `header`, `toolbar`, `orientation`, `disabled`, `id`, `children` | None | Stateless labelled section that composes Header when header content exists; `disabled` describes the region but does not mutate descendant controls. |
|
|
737
|
-
| `Sidebar` | `header`, `toolbar`, `ariaLabel`, `id`, `children` | None | Fixed `fray-sidebar` host containing a native complementary region with fixed header/toolbar parts and independently scrolling content. |
|
|
738
|
-
| `SplitView` | `primary`, `secondary`, `direction`, `primarySize`, pane labels | None | Stateless, non-resizable two-pane flex layout with explicit overflow ownership and keyboard-focusable panes; `primarySize` is a flex-basis value. |
|
|
739
|
-
| `DescriptionList` / `DescriptionItem` | list `label`; item `term`, `value` or children | None | Native `dl`/`dt`/`dd` record summary with responsive term/value wrapping. |
|
|
740
|
-
| `ProgressBar` | `label`, `value` or `valueEmitter`, `max`, `valueText` | None | Native progress semantics with a clipped-label visual surface; a null value is indeterminate. |
|
|
741
|
-
| `ThemePicker` | `label` or `ariaLabel`, theme `options`, value props, `targetDocument`, `disabled` | `onChange(value, option, event)` | String `valueEmitter`; replaces only the theme stylesheet link. |
|
|
742
|
-
| `ColorPicker` | `label` or `ariaLabel`, color `options`, value props, `targetDocument`, `disabled` | `onChange(value, option, event)` | String `valueEmitter`; replaces only the color stylesheet link. |
|
|
743
|
-
| `Tab` | `id`, `label`, `disabled`, optional literal `route`, `children` | None | Declarative content marker consumed by `TabPanel`; a route annotation binds tab activation to the current route scope. |
|
|
744
|
-
| `TabLine` | `tabs`, `label`, `baseId`, value props | `onChange(id, event)` | Active-tab `valueEmitter`; arrow keys skip disabled tabs, with Home/End support. |
|
|
745
|
-
| `TabPanel` | `tabs` or `Tab` children, `label`, `id`, value props | `onChange(id, event)` | Owns or consumes the active-tab emitter, wires the selected tabpanel, and contextually registers annotated tabs when a router is present. |
|
|
746
|
-
|
|
747
|
-
### Live binding and data-source contracts
|
|
748
|
-
|
|
749
|
-
Use `live()` only for the props listed here. All other component props reject
|
|
750
|
-
`live()` in TSX, `h()`, and at runtime. Form-control errors render an alert,
|
|
751
|
-
set invalid state, and associate the control or group with that message.
|
|
752
|
-
|
|
753
|
-
| Component | `live()` props | Dedicated reactive input/state props |
|
|
754
|
-
| --- | --- | --- |
|
|
755
|
-
| `Button` | `disabled`, `pressed`, `busy` | None |
|
|
756
|
-
| `Textbox` | `disabled`, `required`, `readOnly`, `error` | `valueEmitter` |
|
|
757
|
-
| `Dropdown` | `disabled`, `required`, `error` | `options` may be an array or readable emitter; `valueEmitter` |
|
|
758
|
-
| `RadioButton` | `checked`, `disabled`, `required`, `error` | None |
|
|
759
|
-
| `RadioGroup` | `disabled`, `required`, `error` | `valueEmitter`; `options` is always an ordinary array |
|
|
760
|
-
| `Toggle` | `disabled`, `required`, `error` | `valueEmitter`; `options` is always an ordinary array |
|
|
761
|
-
| `Checkbox`, `TriCheckbox`, `QuadCheckbox` | `disabled`, `required`, `error` | `valueEmitter` |
|
|
762
|
-
| `ThemePicker`, `ColorPicker` | `disabled` | `valueEmitter` |
|
|
763
|
-
| `Dialog` | `showCloseButton` | `valueEmitter` controls open state |
|
|
764
|
-
| `Panel` | `disabled` | None |
|
|
765
|
-
| `ProgressBar` | None | `valueEmitter` |
|
|
766
|
-
| `ListView` | None | `items` may be an array or readable emitter; selection emitters are outputs |
|
|
767
|
-
| `TreeView` | None | `nodes` may be an array or readable emitter; selected/expanded emitters are outputs; `itemLabelClassName` and `itemLabelStyle` decorate each block label beside its expander |
|
|
768
|
-
| `DataTable` | None | `data` may be an array or readable emitter; `dataSource`/`rest` and selection emitters are explicit source/state contracts |
|
|
769
|
-
| `FilterPanel` | None | `options` may be an array or readable emitter |
|
|
770
|
-
| Layout, tab, description, toolbar, placeholder, and routing components | None | Their ordinary structural/configuration props require an owner rerender when changed |
|
|
771
|
-
|
|
772
|
-
Readable data sources carry their own fetch state and error through Glue's
|
|
773
|
-
`getFetchState()` and `getError()` APIs. `ListView`, `DataTable`, `FilterPanel`,
|
|
774
|
-
and `TreeView` surface those source failures as data-loading errors. This is
|
|
775
|
-
separate from a control's `error` prop, which represents validation or other
|
|
776
|
-
application-level input feedback.
|
|
777
|
-
|
|
778
|
-
Invalid option arrays, duplicate tab IDs, unsupported orientations, malformed
|
|
779
|
-
emitters, and non-function callbacks fail with descriptive errors.
|
|
780
|
-
|
|
781
|
-
## Component examples
|
|
782
|
-
|
|
783
|
-
### Actions and toolbar
|
|
784
|
-
|
|
785
|
-
```ts
|
|
786
|
-
h(Toolbar, {label: 'Editor actions'},
|
|
787
|
-
h(Button, {label: 'Save', onClick: save}),
|
|
788
|
-
h(Button, {label: 'Delete', disabled: true}))
|
|
789
|
-
```
|
|
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()`.
|
|
790
260
|
|
|
791
|
-
###
|
|
261
|
+
### Two-way native bindings
|
|
792
262
|
|
|
793
|
-
|
|
794
|
-
|
|
263
|
+
`bind:value` accepts a writable string emitter and `bind:checked` accepts a
|
|
264
|
+
writable boolean emitter:
|
|
795
265
|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
required: true,
|
|
800
|
-
error: query.get() === '' ? 'Enter a search term' : null,
|
|
801
|
-
onInput: (value) => console.log('search changed', value),
|
|
802
|
-
})
|
|
266
|
+
```tsx
|
|
267
|
+
<input aria-label="Search" bind:value={search} />
|
|
268
|
+
<input type="checkbox" bind:checked={showArchived} />
|
|
803
269
|
```
|
|
804
270
|
|
|
805
|
-
|
|
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.
|
|
806
274
|
|
|
807
|
-
|
|
808
|
-
const role = new Emitter<'author' | 'reviewer'>('author')
|
|
275
|
+
## Value-control convention
|
|
809
276
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
{value: 'reviewer', label: 'Reviewer'},
|
|
816
|
-
],
|
|
817
|
-
onChange: (value) => console.log(value),
|
|
818
|
-
})
|
|
819
|
-
```
|
|
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.
|
|
820
282
|
|
|
821
|
-
|
|
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.
|
|
822
286
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
onChange: (value) => console.log(value),
|
|
829
|
-
})
|
|
830
|
-
```
|
|
287
|
+
## Component reference
|
|
288
|
+
|
|
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.
|
|
831
292
|
|
|
832
|
-
|
|
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.
|
|
833
296
|
|
|
834
|
-
|
|
835
|
-
`options` are ordinary structural input, its `valueEmitter` is a stable raw
|
|
836
|
-
writable state channel, and only `disabled`, `required`, and `error` accept one-way
|
|
837
|
-
`live()` bindings.
|
|
297
|
+
### Actions, inputs, and choices
|
|
838
298
|
|
|
839
|
-
|
|
|
299
|
+
| Component | Purpose | Key props and state |
|
|
840
300
|
| --- | --- | --- |
|
|
841
|
-
| `
|
|
842
|
-
| `
|
|
843
|
-
| `
|
|
844
|
-
| `
|
|
845
|
-
| `valueEmitter`
|
|
846
|
-
| `value`, `
|
|
847
|
-
| `
|
|
848
|
-
| `
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
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.
|
|
852
316
|
|
|
853
317
|
```tsx
|
|
854
|
-
const
|
|
855
|
-
const unavailable = new Emitter(false)
|
|
856
|
-
const mustChoose = new Emitter(true)
|
|
318
|
+
const view = new Emitter<'list' | 'grid'>('list')
|
|
857
319
|
|
|
858
320
|
<RadioGroup
|
|
859
321
|
label="View"
|
|
@@ -861,282 +323,267 @@ const mustChoose = new Emitter(true)
|
|
|
861
323
|
['list', 'List'],
|
|
862
324
|
['grid', 'Grid'],
|
|
863
325
|
]}
|
|
864
|
-
valueEmitter={
|
|
865
|
-
disabled={live(unavailable)}
|
|
866
|
-
required={live(mustChoose)}
|
|
326
|
+
valueEmitter={view}
|
|
867
327
|
/>
|
|
868
328
|
```
|
|
869
329
|
|
|
870
|
-
|
|
871
|
-
application genuinely owns a changing option vocabulary, its owning class
|
|
872
|
-
component must make that structural rerender explicit:
|
|
873
|
-
|
|
874
|
-
```tsx
|
|
875
|
-
interface ViewChooserProps extends ComponentProps {
|
|
876
|
-
options: ReadableEmitter<readonly RadioOption[]>
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
class ViewChooser extends Component<ViewChooserProps> {
|
|
880
|
-
render() {
|
|
881
|
-
return <RadioGroup
|
|
882
|
-
label="View"
|
|
883
|
-
options={this.read(this.props.options)}
|
|
884
|
-
/>
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
```
|
|
888
|
-
|
|
889
|
-
Calling `optionsEmitter.get()` directly in `render()` only reads a snapshot and
|
|
890
|
-
does not subscribe. `this.read(optionsEmitter)` rerenders the owner when the
|
|
891
|
-
array changes; normal vnode reconciliation then supplies the new ordinary
|
|
892
|
-
array prop to `RadioGroup`. The owner remains responsible for deciding what an
|
|
893
|
-
option removal means for its selected-value emitter. Fray does not silently
|
|
894
|
-
select, clear, or otherwise rewrite that state.
|
|
895
|
-
|
|
896
|
-
`label` is a `FrayChild`, so it may still contain an emitter that is rendered
|
|
897
|
-
and subscribed as child content. That fine-grained child behavior is distinct
|
|
898
|
-
from making the `label` property itself a `live()` binding. The same is true of
|
|
899
|
-
an individual `RadioOption` label: its rendered content may be reactive without
|
|
900
|
-
making the option array or its selectable values live.
|
|
901
|
-
|
|
902
|
-
Because `live()` subscriptions belong to a parent render record, pass live
|
|
903
|
-
props through JSX or `h()`. Direct `new RadioGroup(...)` construction accepts
|
|
904
|
-
resolved booleans and ordinary arrays only.
|
|
905
|
-
|
|
906
|
-
### Checkbox variants
|
|
907
|
-
|
|
908
|
-
```ts
|
|
909
|
-
h(Checkbox, {label: 'Include archived'})
|
|
910
|
-
h(TriCheckbox, {label: 'Match policy', defaultValue: FilterMode.Neutral})
|
|
911
|
-
h(QuadCheckbox, {label: 'Required tags', defaultValue: FilterMode.Require})
|
|
912
|
-
```
|
|
330
|
+
### Layout and navigation
|
|
913
331
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
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.
|
|
918
346
|
|
|
919
347
|
```tsx
|
|
920
|
-
<
|
|
921
|
-
|
|
922
|
-
</
|
|
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>
|
|
923
352
|
```
|
|
924
353
|
|
|
925
|
-
`
|
|
926
|
-
|
|
927
|
-
the nested native heading owns document semantics.
|
|
354
|
+
`SplitView` is a fixed two-pane composition primitive. It does not impose
|
|
355
|
+
application resizing policy or persist pane sizes.
|
|
928
356
|
|
|
929
|
-
###
|
|
357
|
+
### Data and record views
|
|
930
358
|
|
|
931
|
-
|
|
932
|
-
h(Panel, {
|
|
933
|
-
header: 'Account',
|
|
934
|
-
orientation: 'vertical',
|
|
935
|
-
toolbar: h(Toolbar, {label: 'Account actions'},
|
|
936
|
-
h(Button, {label: 'Edit'})),
|
|
937
|
-
}, h('p', null, 'Account details'))
|
|
938
|
-
```
|
|
939
|
-
|
|
940
|
-
### Sidebar
|
|
941
|
-
|
|
942
|
-
```ts
|
|
943
|
-
h(Sidebar, {
|
|
944
|
-
id: 'project-navigation',
|
|
945
|
-
header: 'Projects',
|
|
946
|
-
toolbar: h(Toolbar, {label: 'Project filters'},
|
|
947
|
-
h(Textbox, {label: 'Search projects'})),
|
|
948
|
-
}, h('ul', null, h('li', null, 'Release automation')))
|
|
949
|
-
```
|
|
950
|
-
|
|
951
|
-
The surrounding grid or flex layout must bound the Sidebar's height. Its native
|
|
952
|
-
complementary region is inside the fixed Sidebar host; Header and toolbar remain
|
|
953
|
-
fixed while the dedicated content part owns vertical scrolling and is keyboard-
|
|
954
|
-
focusable. Supply `ariaLabel` when there is no visible `header`.
|
|
955
|
-
|
|
956
|
-
### Declarative tabs
|
|
957
|
-
|
|
958
|
-
```ts
|
|
959
|
-
h(TabPanel, {id: 'profile', label: 'Profile sections'},
|
|
960
|
-
h(Tab, {id: 'summary', label: 'Summary'}, 'Summary content'),
|
|
961
|
-
h(Tab, {id: 'details', label: 'Details'}, 'Details content'))
|
|
962
|
-
```
|
|
963
|
-
|
|
964
|
-
### Standalone tab line
|
|
965
|
-
|
|
966
|
-
```ts
|
|
967
|
-
h(TabLine, {
|
|
968
|
-
baseId: 'settings',
|
|
969
|
-
label: 'Settings sections',
|
|
970
|
-
defaultValue: 'general',
|
|
971
|
-
tabs: [
|
|
972
|
-
{id: 'general', label: 'General'},
|
|
973
|
-
{id: 'advanced', label: 'Advanced'},
|
|
974
|
-
],
|
|
975
|
-
onChange: (id) => console.log('active tab', id),
|
|
976
|
-
})
|
|
977
|
-
```
|
|
978
|
-
|
|
979
|
-
Use `TabPanel` when Fray should render content and ARIA relationships. Use a
|
|
980
|
-
standalone `TabLine` only when the consumer owns the corresponding tabpanel
|
|
981
|
-
content and IDs.
|
|
982
|
-
|
|
983
|
-
## Styling and accessibility
|
|
984
|
-
|
|
985
|
-
A Fray application loads four ordered styling inputs:
|
|
986
|
-
|
|
987
|
-
| Order | Input | Responsibility |
|
|
359
|
+
| Component | Purpose | Key props and state |
|
|
988
360
|
| --- | --- | --- |
|
|
989
|
-
|
|
|
990
|
-
|
|
|
991
|
-
|
|
|
992
|
-
|
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
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` |
|
|
1023
414
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
such as `--button-*`, `--input-*`, `--panel-*`, and `--selection-*`.
|
|
1028
|
-
`frayThemeVariableCatalog` exposes the supported vocabulary.
|
|
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.
|
|
1029
418
|
|
|
1030
|
-
|
|
1031
|
-
root and its islands. Its base value is the white palette endpoint, which
|
|
1032
|
-
Shiny and Minimal both retain.
|
|
419
|
+
## Semantic filter state
|
|
1033
420
|
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
elevation neutral; Shiny uses it for Bank2-style separated surfaces. Themes do
|
|
1038
|
-
not infer island boundaries from component type or nesting, and islands cannot
|
|
1039
|
-
contain other islands.
|
|
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.
|
|
1040
424
|
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
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.
|
|
1045
429
|
|
|
1046
|
-
|
|
430
|
+
Use `matchesFilterState` or `filterByState` for pure evaluation;
|
|
431
|
+
`deriveFilterPredicate` and `deriveFilteredItems` for reactive results; and
|
|
432
|
+
`serializeFilterState`/`parseFilterState` for deterministic versioned data.
|
|
1047
433
|
|
|
1048
|
-
|
|
1049
|
-
`link[data-fray-stylesheet="theme"]` and one
|
|
1050
|
-
`link[data-fray-stylesheet="colors"]`. Replacing either link also sets the
|
|
1051
|
-
corresponding root data attribute. `ThemePicker` and `ColorPicker` expose the
|
|
1052
|
-
same operation through the normal value-control contract. The base file and
|
|
1053
|
-
runtime-injected structural stylesheet stay loaded while those two links are
|
|
1054
|
-
replaced.
|
|
434
|
+
## Application services
|
|
1055
435
|
|
|
1056
|
-
|
|
1057
|
-
|
|
436
|
+
Service implementations remain ordinary application TypeScript. Fray provides
|
|
437
|
+
typed keys and a fixed application scope, not dependency discovery:
|
|
1058
438
|
|
|
1059
|
-
|
|
439
|
+
```tsx
|
|
440
|
+
class ProjectService {
|
|
441
|
+
readonly label = 'Projects'
|
|
442
|
+
}
|
|
1060
443
|
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
444
|
+
const projectService = defineService<ProjectService>('projects')
|
|
445
|
+
const services = createServiceScope([
|
|
446
|
+
provideService(projectService, () => new ProjectService()),
|
|
447
|
+
])
|
|
1064
448
|
|
|
1065
|
-
|
|
1066
|
-
|
|
449
|
+
class ProjectTitle extends Component {
|
|
450
|
+
static requiredServices = [projectService]
|
|
451
|
+
private service!: ProjectService
|
|
1067
452
|
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
<ListView items={projects} selectedItemEmitter={selected} itemKey="id" />
|
|
1073
|
-
<DataTable
|
|
1074
|
-
data={projects}
|
|
1075
|
-
columns={columns}
|
|
1076
|
-
multiSelect
|
|
1077
|
-
selectedItemsEmitter={selectedRows}
|
|
1078
|
-
/>
|
|
1079
|
-
```
|
|
453
|
+
initialize() {
|
|
454
|
+
this.service = this.requireService(projectService)
|
|
455
|
+
}
|
|
1080
456
|
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
457
|
+
render() {
|
|
458
|
+
return <output>{this.service.label}</output>
|
|
459
|
+
}
|
|
460
|
+
}
|
|
1084
461
|
|
|
1085
|
-
|
|
462
|
+
const runtime = createFrayRuntime({services})
|
|
463
|
+
```
|
|
1086
464
|
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
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.
|
|
1090
470
|
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
columns={columns}
|
|
1095
|
-
/>
|
|
471
|
+
`FrayRuntime` carries one `ServiceScope`, optional router, and isolated
|
|
472
|
+
`StyleRegistry`. `createFrayRuntime()` is the normal construction entry point;
|
|
473
|
+
`defaultFrayRuntime` supports direct compatibility mounting.
|
|
1096
474
|
|
|
1097
|
-
|
|
1098
|
-
const source = createQueryTableDataSource({query, sortEmitter, filtersEmitter})
|
|
1099
|
-
<DataTable dataSource={source} columns={columns} />
|
|
1100
|
-
```
|
|
475
|
+
## Browser routing
|
|
1101
476
|
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
disposal. The default REST serializer retains the compact existing endpoint
|
|
1106
|
-
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.
|
|
1107
480
|
|
|
1108
|
-
|
|
481
|
+
```tsx
|
|
482
|
+
const portfolioRoute = defineRoute('portfolio')
|
|
483
|
+
const projectRoute = defineRouteParameter('project', stringRouteCodec)
|
|
484
|
+
const selectedProject = new Emitter<string | null>(null)
|
|
1109
485
|
|
|
1110
|
-
|
|
1111
|
-
const
|
|
486
|
+
const router = createBrowserRouter({adapter: createHashNavigation()})
|
|
487
|
+
const runtime = createFrayRuntime({router})
|
|
1112
488
|
|
|
1113
|
-
|
|
1114
|
-
|
|
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>
|
|
1115
500
|
```
|
|
1116
501
|
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
`
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
`
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
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)
|