@typed/ui 1.0.0-beta.4 → 1.0.0-beta.6
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 +53 -9
- package/dist/Alert.d.ts +78 -0
- package/dist/Alert.d.ts.map +1 -0
- package/dist/Alert.js +37 -0
- package/dist/Button.d.ts +144 -0
- package/dist/Button.d.ts.map +1 -0
- package/dist/Button.js +38 -0
- package/dist/Carousel.d.ts +547 -0
- package/dist/Carousel.d.ts.map +1 -0
- package/dist/Carousel.js +387 -0
- package/dist/Checkbox.d.ts +203 -0
- package/dist/Checkbox.d.ts.map +1 -0
- package/dist/Checkbox.js +140 -0
- package/dist/Collection.d.ts +254 -0
- package/dist/Collection.d.ts.map +1 -0
- package/dist/Collection.js +218 -0
- package/dist/Combobox.d.ts +516 -0
- package/dist/Combobox.d.ts.map +1 -0
- package/dist/Combobox.js +371 -0
- package/dist/Component.d.ts +127 -0
- package/dist/Component.d.ts.map +1 -0
- package/dist/Component.js +60 -0
- package/dist/Composite.d.ts +823 -0
- package/dist/Composite.d.ts.map +1 -0
- package/dist/Composite.js +615 -0
- package/dist/Dialog.d.ts +544 -0
- package/dist/Dialog.d.ts.map +1 -0
- package/dist/Dialog.js +355 -0
- package/dist/Disclosure.d.ts +219 -0
- package/dist/Disclosure.d.ts.map +1 -0
- package/dist/Disclosure.js +128 -0
- package/dist/Dom/Events.d.ts +122 -0
- package/dist/Dom/Events.d.ts.map +1 -0
- package/dist/Dom/Events.js +192 -0
- package/dist/Dom/Props.d.ts +161 -0
- package/dist/Dom/Props.d.ts.map +1 -0
- package/dist/Dom/Props.js +110 -0
- package/dist/Dom/Refs.d.ts +58 -0
- package/dist/Dom/Refs.d.ts.map +1 -0
- package/dist/Dom/Refs.js +61 -0
- package/dist/Dom/Render.d.ts +59 -0
- package/dist/Dom/Render.d.ts.map +1 -0
- package/dist/Dom/Render.js +71 -0
- package/dist/Dom/Types.d.ts +570 -0
- package/dist/Dom/Types.d.ts.map +1 -0
- package/dist/Dom/Types.js +1 -0
- package/dist/Dom/index.d.ts +20 -0
- package/dist/Dom/index.d.ts.map +1 -0
- package/dist/Dom/index.js +8 -0
- package/dist/Dom.d.ts +14 -0
- package/dist/Dom.d.ts.map +1 -0
- package/dist/Dom.js +13 -0
- package/dist/Focusable.d.ts +85 -0
- package/dist/Focusable.d.ts.map +1 -0
- package/dist/Focusable.js +35 -0
- package/dist/Form.d.ts +1695 -0
- package/dist/Form.d.ts.map +1 -0
- package/dist/Form.js +987 -0
- package/dist/Grid.d.ts +569 -0
- package/dist/Grid.d.ts.map +1 -0
- package/dist/Grid.js +379 -0
- package/dist/Group.d.ts +147 -0
- package/dist/Group.d.ts.map +1 -0
- package/dist/Group.js +63 -0
- package/dist/Heading.d.ts +86 -0
- package/dist/Heading.d.ts.map +1 -0
- package/dist/Heading.js +48 -0
- package/dist/Hovercard.d.ts +297 -0
- package/dist/Hovercard.d.ts.map +1 -0
- package/dist/Hovercard.js +188 -0
- package/dist/HttpRouter.d.ts +129 -6
- package/dist/HttpRouter.d.ts.map +1 -1
- package/dist/HttpRouter.js +196 -53
- package/dist/Link.d.ts +67 -28
- package/dist/Link.d.ts.map +1 -1
- package/dist/Link.js +91 -37
- package/dist/Listbox.d.ts +437 -0
- package/dist/Listbox.d.ts.map +1 -0
- package/dist/Listbox.js +316 -0
- package/dist/Menu.d.ts +972 -0
- package/dist/Menu.d.ts.map +1 -0
- package/dist/Menu.js +731 -0
- package/dist/Menubar.d.ts +367 -0
- package/dist/Menubar.d.ts.map +1 -0
- package/dist/Menubar.js +265 -0
- package/dist/Meter.d.ts +217 -0
- package/dist/Meter.d.ts.map +1 -0
- package/dist/Meter.js +94 -0
- package/dist/NativeDetails.d.ts +41 -0
- package/dist/NativeDetails.d.ts.map +1 -0
- package/dist/NativeDetails.js +40 -0
- package/dist/NativeDialog.d.ts +64 -0
- package/dist/NativeDialog.d.ts.map +1 -0
- package/dist/NativeDialog.js +50 -0
- package/dist/NativePopover.d.ts +41 -0
- package/dist/NativePopover.d.ts.map +1 -0
- package/dist/NativePopover.js +46 -0
- package/dist/Popover.d.ts +241 -0
- package/dist/Popover.d.ts.map +1 -0
- package/dist/Popover.js +141 -0
- package/dist/RadioGroup.d.ts +432 -0
- package/dist/RadioGroup.d.ts.map +1 -0
- package/dist/RadioGroup.js +291 -0
- package/dist/Role.d.ts +64 -0
- package/dist/Role.d.ts.map +1 -0
- package/dist/Role.js +27 -0
- package/dist/Select.d.ts +529 -0
- package/dist/Select.d.ts.map +1 -0
- package/dist/Select.js +407 -0
- package/dist/Separator.d.ts +54 -0
- package/dist/Separator.d.ts.map +1 -0
- package/dist/Separator.js +26 -0
- package/dist/Slider.d.ts +188 -0
- package/dist/Slider.d.ts.map +1 -0
- package/dist/Slider.js +96 -0
- package/dist/SpinButton.d.ts +188 -0
- package/dist/SpinButton.d.ts.map +1 -0
- package/dist/SpinButton.js +96 -0
- package/dist/Storybook.d.ts +76 -0
- package/dist/Storybook.d.ts.map +1 -0
- package/dist/Storybook.js +102 -0
- package/dist/Switch.d.ts +187 -0
- package/dist/Switch.d.ts.map +1 -0
- package/dist/Switch.js +116 -0
- package/dist/Tab.d.ts +26 -0
- package/dist/Tab.d.ts.map +1 -0
- package/dist/Tab.js +25 -0
- package/dist/Tabs.d.ts +591 -0
- package/dist/Tabs.d.ts.map +1 -0
- package/dist/Tabs.js +346 -0
- package/dist/Toolbar.d.ts +366 -0
- package/dist/Toolbar.d.ts.map +1 -0
- package/dist/Toolbar.js +245 -0
- package/dist/Tooltip.d.ts +296 -0
- package/dist/Tooltip.d.ts.map +1 -0
- package/dist/Tooltip.js +172 -0
- package/dist/Tree.d.ts +591 -0
- package/dist/Tree.d.ts.map +1 -0
- package/dist/Tree.js +434 -0
- package/dist/TreeGrid.d.ts +645 -0
- package/dist/TreeGrid.d.ts.map +1 -0
- package/dist/TreeGrid.js +420 -0
- package/dist/VisuallyHidden.d.ts +54 -0
- package/dist/VisuallyHidden.d.ts.map +1 -0
- package/dist/VisuallyHidden.js +27 -0
- package/dist/WindowSplitter.d.ts +376 -0
- package/dist/WindowSplitter.d.ts.map +1 -0
- package/dist/WindowSplitter.js +222 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +49 -0
- package/package.json +45 -18
- package/src/HttpRouter.test.ts +0 -256
- package/src/HttpRouter.ts +0 -165
- package/src/Link.test.ts +0 -84
- package/src/Link.ts +0 -107
- package/src/index.ts +0 -2
package/dist/Checkbox.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
import { RefSubject } from "@typed/fx";
|
|
4
|
+
import { EventHandler, html } from "@typed/template";
|
|
5
|
+
import * as Dom from "./Dom.js";
|
|
6
|
+
/** Schema used to encode and hydrate checkbox state.
|
|
7
|
+
* @remarks
|
|
8
|
+
* ## Why
|
|
9
|
+
* A shared schema keeps server and client state shape identical.
|
|
10
|
+
* ## Ownership and lifetime
|
|
11
|
+
* The schema is immutable and acquires no resources.
|
|
12
|
+
* @since 1.0.0
|
|
13
|
+
* @category schemas
|
|
14
|
+
*/
|
|
15
|
+
export const StateSchema = Schema.Struct({
|
|
16
|
+
checked: Schema.Literals([true, false, "mixed"]),
|
|
17
|
+
});
|
|
18
|
+
/** Creates hydrated, renderer-independent checkbox state.
|
|
19
|
+
* @remarks
|
|
20
|
+
* ## Why
|
|
21
|
+
* State transitions remain testable without rendering and can be consumed by
|
|
22
|
+
* any UI producer.
|
|
23
|
+
* ## Ownership and lifetime
|
|
24
|
+
* The caller's Effect Scope owns the hydrated RefSubject and its subscriptions.
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* import { Effect } from "effect"
|
|
28
|
+
* import * as Checkbox from "@typed/ui/Checkbox"
|
|
29
|
+
*
|
|
30
|
+
* const program = Effect.gen(function* () {
|
|
31
|
+
* return yield* Checkbox.makeState({ checked: "mixed" })
|
|
32
|
+
* })
|
|
33
|
+
* ```
|
|
34
|
+
* @since 1.0.0
|
|
35
|
+
* @category constructors
|
|
36
|
+
*/
|
|
37
|
+
export function makeState(initial = {}) {
|
|
38
|
+
return RefSubject.hydrate(StateSchema, { checked: initial.checked ?? false });
|
|
39
|
+
}
|
|
40
|
+
/** Sets the checkbox state to a boolean or mixed value.
|
|
41
|
+
* @remarks
|
|
42
|
+
* ## Why
|
|
43
|
+
* A single atomic RefSubject update keeps DOM properties and ARIA state aligned.
|
|
44
|
+
* ## Ownership and lifetime
|
|
45
|
+
* The Effect uses the existing subject lifetime and acquires no new resource.
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* import { Effect } from "effect"
|
|
49
|
+
* import * as Checkbox from "@typed/ui/Checkbox"
|
|
50
|
+
*
|
|
51
|
+
* const program = Effect.gen(function* () {
|
|
52
|
+
* const state = yield* Checkbox.makeState()
|
|
53
|
+
* yield* Checkbox.setChecked(state, true)
|
|
54
|
+
* })
|
|
55
|
+
* ```
|
|
56
|
+
* @since 1.0.0
|
|
57
|
+
* @category state
|
|
58
|
+
*/
|
|
59
|
+
export function setChecked(state, checked) {
|
|
60
|
+
return RefSubject.update(state, (current) => ({ ...current, checked }));
|
|
61
|
+
}
|
|
62
|
+
/** Toggles checked to false and false or mixed to true.
|
|
63
|
+
* @remarks
|
|
64
|
+
* ## Why
|
|
65
|
+
* User activation of an indeterminate checkbox resolves to a concrete checked
|
|
66
|
+
* state, matching native input behavior.
|
|
67
|
+
* ## Ownership and lifetime
|
|
68
|
+
* The Effect updates the existing RefSubject and acquires no resource.
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* import { Effect } from "effect"
|
|
72
|
+
* import * as Checkbox from "@typed/ui/Checkbox"
|
|
73
|
+
*
|
|
74
|
+
* const program = Effect.gen(function* () {
|
|
75
|
+
* const state = yield* Checkbox.makeState({ checked: "mixed" })
|
|
76
|
+
* yield* Checkbox.toggle(state)
|
|
77
|
+
* })
|
|
78
|
+
* ```
|
|
79
|
+
* @since 1.0.0
|
|
80
|
+
* @category state
|
|
81
|
+
*/
|
|
82
|
+
export function toggle(state) {
|
|
83
|
+
return RefSubject.update(state, (current) => ({
|
|
84
|
+
...current,
|
|
85
|
+
checked: current.checked === true ? false : true,
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
function internalProps(options) {
|
|
89
|
+
const checked = RefSubject.map(options.state, (state) => state.checked === true);
|
|
90
|
+
const indeterminate = RefSubject.map(options.state, (state) => state.checked === "mixed");
|
|
91
|
+
const onChange = EventHandler.make(Effect.fn((event) => setChecked(options.state, Dom.currentTarget(event).checked)));
|
|
92
|
+
return ({ property }) => ({
|
|
93
|
+
type: "checkbox",
|
|
94
|
+
"aria-checked": RefSubject.map(options.state, (state) => state.checked),
|
|
95
|
+
"?checked": checked,
|
|
96
|
+
".checked": checked,
|
|
97
|
+
"?disabled": property("disabled", false),
|
|
98
|
+
"?required": property("required", false),
|
|
99
|
+
".indeterminate": indeterminate,
|
|
100
|
+
onchange: onChange,
|
|
101
|
+
ref: options.state,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/** Renders a native checkbox input synchronized with hydrated state.
|
|
105
|
+
* @remarks
|
|
106
|
+
* ## Why
|
|
107
|
+
* Native keyboard, form, disabled, required, and accessibility behavior remain
|
|
108
|
+
* available while state is exposed through RefSubject.
|
|
109
|
+
* ## Ownership and lifetime
|
|
110
|
+
* Running the Fx installs native listeners and state subscriptions in its
|
|
111
|
+
* Scope. A custom host must apply `type`, checked, indeterminate, ARIA, and ref
|
|
112
|
+
* props; only one hydration owner may be composed for the element.
|
|
113
|
+
* @example
|
|
114
|
+
* ```ts
|
|
115
|
+
* import { Effect } from "effect"
|
|
116
|
+
* import * as Checkbox from "@typed/ui/Checkbox"
|
|
117
|
+
*
|
|
118
|
+
* const program = Effect.gen(function* () {
|
|
119
|
+
* const state = yield* Checkbox.makeState()
|
|
120
|
+
* return Checkbox.Input({ state })
|
|
121
|
+
* })
|
|
122
|
+
* ```
|
|
123
|
+
* @since 1.0.0
|
|
124
|
+
* @category components
|
|
125
|
+
*/
|
|
126
|
+
export function Input(options, host) {
|
|
127
|
+
return Dom.renderHost()(options, host, internalProps(options), "", (i) => {
|
|
128
|
+
return html `<input ...${i} />`;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/** Canonical component alias for `Input`.
|
|
132
|
+
* @remarks
|
|
133
|
+
* ## Why
|
|
134
|
+
* The alias provides the widget name while retaining the explicit input API.
|
|
135
|
+
* ## Ownership and lifetime
|
|
136
|
+
* It has exactly the same Scope and native-element ownership as `Input`.
|
|
137
|
+
* @since 1.0.0
|
|
138
|
+
* @category aliases
|
|
139
|
+
*/
|
|
140
|
+
export const Checkbox = Input;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collection is the scoped registry shared by composite widgets. Registration order is not
|
|
3
|
+
* navigation order: byDomOrder reads the mounted nodes with compareDocumentPosition, and each
|
|
4
|
+
* register Effect removes its own item when that Effect's Scope closes.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* The module keeps policy, state transitions, and DOM rendering separable so applications can use
|
|
8
|
+
* the state and pure operations without mounting UI, or supply custom hosts without replacing native
|
|
9
|
+
* events and browser-owned focus.
|
|
10
|
+
*
|
|
11
|
+
* @since 1.0.0
|
|
12
|
+
* @category modules
|
|
13
|
+
* @packageDocumentation
|
|
14
|
+
*/
|
|
15
|
+
import * as Effect from "effect/Effect";
|
|
16
|
+
import * as Scope from "effect/Scope";
|
|
17
|
+
import { RefSubject } from "@typed/fx";
|
|
18
|
+
/**
|
|
19
|
+
* Public Collection.Item behavioral model.
|
|
20
|
+
*
|
|
21
|
+
* @remarks
|
|
22
|
+
* ## Why
|
|
23
|
+
*
|
|
24
|
+
* The public model lets custom composites reuse Collection's deterministic policy without copying
|
|
25
|
+
* an internal shape.
|
|
26
|
+
*
|
|
27
|
+
* ## Ownership and lifetime
|
|
28
|
+
*
|
|
29
|
+
* This declaration is data or schema metadata and acquires no resources.
|
|
30
|
+
*
|
|
31
|
+
* ## Example
|
|
32
|
+
*
|
|
33
|
+
* Import with `import type { Item } from "@typed/ui/Collection";` Extend the [Collection.makeState
|
|
34
|
+
* runnable setup](/reference/%40typed%2Fui%2FCollection%23makeState). A registered value can retain
|
|
35
|
+
* its real DOM identity:
|
|
36
|
+
* `const item: Item<string> = { id: "save", value: "save", element: document.createElement("button") }`.
|
|
37
|
+
* @since 1.0.0
|
|
38
|
+
* @category models
|
|
39
|
+
*/
|
|
40
|
+
export interface Item<Value = unknown, Element extends object = globalThis.Element> {
|
|
41
|
+
/**
|
|
42
|
+
* Stable id used for collection identity and ARIA relationships.
|
|
43
|
+
* @since 1.0.0
|
|
44
|
+
* @category models
|
|
45
|
+
*/
|
|
46
|
+
readonly id: string;
|
|
47
|
+
/**
|
|
48
|
+
* Mounted element handle used for DOM ordering, focus, and scrolling.
|
|
49
|
+
* @since 1.0.0
|
|
50
|
+
* @category models
|
|
51
|
+
*/
|
|
52
|
+
readonly element?: Element;
|
|
53
|
+
/**
|
|
54
|
+
* Flag used by collection movement and widget handlers to skip activation by default.
|
|
55
|
+
* @since 1.0.0
|
|
56
|
+
* @category models
|
|
57
|
+
*/
|
|
58
|
+
readonly disabled?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Whether the item participates as a submenu entry.
|
|
61
|
+
* @since 1.0.0
|
|
62
|
+
* @category models
|
|
63
|
+
*/
|
|
64
|
+
readonly submenu?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Search text used by typeahead independently of rendered markup.
|
|
67
|
+
* @since 1.0.0
|
|
68
|
+
* @category models
|
|
69
|
+
*/
|
|
70
|
+
readonly textValue?: string;
|
|
71
|
+
/**
|
|
72
|
+
* Current semantic value selected or edited by the widget.
|
|
73
|
+
* @since 1.0.0
|
|
74
|
+
* @category models
|
|
75
|
+
*/
|
|
76
|
+
readonly value?: Value;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Complete renderer-independent state for Collection.
|
|
80
|
+
*
|
|
81
|
+
* @remarks
|
|
82
|
+
* ## Why
|
|
83
|
+
*
|
|
84
|
+
* Applications can inspect, update, and test Collection behavior without mounting or coupling the
|
|
85
|
+
* state to a renderer.
|
|
86
|
+
*
|
|
87
|
+
* ## Ownership and lifetime
|
|
88
|
+
*
|
|
89
|
+
* This declaration is data or schema metadata and acquires no resources.
|
|
90
|
+
*
|
|
91
|
+
* ## Example
|
|
92
|
+
*
|
|
93
|
+
* Import with `import type { State } from "@typed/ui/Collection";` Extend the [Collection.makeState
|
|
94
|
+
* runnable setup](/reference/%40typed%2Fui%2FCollection%23makeState). Collection state is an
|
|
95
|
+
* immutable sequence: `const snapshot: State<string> = [{ id: "save", value: "save" }]`.
|
|
96
|
+
* @since 1.0.0
|
|
97
|
+
* @category models
|
|
98
|
+
*/
|
|
99
|
+
export type State<Value = unknown, Element extends object = globalThis.Element> = readonly Item<Value, Element>[];
|
|
100
|
+
/**
|
|
101
|
+
* Creates hydrated Collection state. The collection starts with the supplied immutable item array,
|
|
102
|
+
* or empty.
|
|
103
|
+
*
|
|
104
|
+
* @remarks
|
|
105
|
+
* ## Why
|
|
106
|
+
*
|
|
107
|
+
* State and collection ownership can be composed and tested independently from any renderer.
|
|
108
|
+
*
|
|
109
|
+
* ## Ownership and lifetime
|
|
110
|
+
*
|
|
111
|
+
* The returned Effect creates the RefSubject when run. That state is renderer-independent;
|
|
112
|
+
* collection registrations belong to the separate Scope that runs register or ref, not to state
|
|
113
|
+
* creation.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```ts
|
|
117
|
+
* import * as Effect from "effect/Effect";
|
|
118
|
+
* import * as Collection from "@typed/ui/Collection";
|
|
119
|
+
*
|
|
120
|
+
* const program = Effect.scoped(
|
|
121
|
+
* Effect.gen(function* () {
|
|
122
|
+
* const collection = yield* Collection.makeState<string>();
|
|
123
|
+
* return yield* collection;
|
|
124
|
+
* }),
|
|
125
|
+
* );
|
|
126
|
+
* ```
|
|
127
|
+
* @since 1.0.0
|
|
128
|
+
* @category constructors
|
|
129
|
+
*/
|
|
130
|
+
export declare function makeState<Value = unknown, Element extends object = globalThis.Element>(initial?: State<Value, Element>): Effect.Effect<RefSubject.RefSubject<State<Value, Element>>, never, Scope.Scope>;
|
|
131
|
+
/**
|
|
132
|
+
* Upserts an item immediately and installs a finalizer in the Scope that runs this Effect; the
|
|
133
|
+
* finalizer removes only that exact registration.
|
|
134
|
+
*
|
|
135
|
+
* @remarks
|
|
136
|
+
* ## Why
|
|
137
|
+
*
|
|
138
|
+
* The operation exposes Collection's transition directly so callers can compose it in Effect
|
|
139
|
+
* programs and native event handlers.
|
|
140
|
+
*
|
|
141
|
+
* ## Ownership and lifetime
|
|
142
|
+
*
|
|
143
|
+
* Registration is owned by the Scope in which the registration Effect runs. Closing that Scope
|
|
144
|
+
* removes the exact registered item; creating the collection or state does not own this cleanup.
|
|
145
|
+
*
|
|
146
|
+
* ## Example
|
|
147
|
+
*
|
|
148
|
+
* Import with `import { register } from "@typed/ui/Collection";` Extend the [Collection.makeState
|
|
149
|
+
* runnable setup](/reference/%40typed%2Fui%2FCollection%23makeState). Inside the linked Effect
|
|
150
|
+
* program invoke `yield* register(collection, { id: "save", value: "save" })`, then read the
|
|
151
|
+
* collection to observe the inserted item.
|
|
152
|
+
* @since 1.0.0
|
|
153
|
+
* @category combinators
|
|
154
|
+
*/
|
|
155
|
+
export declare function register<Value, Element extends object, E, R>(collection: RefSubject.RefSubject<State<Value, Element>, E, R>, item: Item<Value, Element>): Effect.Effect<void, E, R | Scope.Scope>;
|
|
156
|
+
/**
|
|
157
|
+
* Registers a mounted element and removes it automatically with the Scope in which the returned
|
|
158
|
+
* Effect is run.
|
|
159
|
+
*
|
|
160
|
+
* @remarks
|
|
161
|
+
* ## Why
|
|
162
|
+
*
|
|
163
|
+
* The operation exposes Collection's transition directly so callers can compose it in Effect
|
|
164
|
+
* programs and native event handlers.
|
|
165
|
+
*
|
|
166
|
+
* ## Ownership and lifetime
|
|
167
|
+
*
|
|
168
|
+
* Registration is owned by the Scope in which the registration Effect runs. Closing that Scope
|
|
169
|
+
* removes the exact registered item; creating the collection or state does not own this cleanup.
|
|
170
|
+
*
|
|
171
|
+
* ## Example
|
|
172
|
+
*
|
|
173
|
+
* Import with `import { ref } from "@typed/ui/Collection";` Extend the [Collection.makeState
|
|
174
|
+
* runnable setup](/reference/%40typed%2Fui%2FCollection%23makeState). Inside the linked Effect
|
|
175
|
+
* program create `const attach = ref(collection, { id: "save", value: "save" })`, then run `yield*
|
|
176
|
+
* attach(document.createElement("button"))` and read the collection before Scope close.
|
|
177
|
+
* @since 1.0.0
|
|
178
|
+
* @category combinators
|
|
179
|
+
*/
|
|
180
|
+
export declare function ref<Value, Element extends object, E, R>(collection: RefSubject.RefSubject<State<Value, Element>, E, R>, item: Omit<Item<Value, Element>, "element">): (element: Element) => Effect.Effect<void, E, R | Scope.Scope>;
|
|
181
|
+
/**
|
|
182
|
+
* Removes every registered item with the supplied id and retains the remaining order.
|
|
183
|
+
*
|
|
184
|
+
* @remarks
|
|
185
|
+
* ## Why
|
|
186
|
+
*
|
|
187
|
+
* The operation exposes Collection's transition directly so callers can compose it in Effect
|
|
188
|
+
* programs and native event handlers.
|
|
189
|
+
*
|
|
190
|
+
* ## Ownership and lifetime
|
|
191
|
+
*
|
|
192
|
+
* The returned Effect performs the update or DOM side effect only when run, preserves the declared
|
|
193
|
+
* error and service channels, and retains no resources after completion.
|
|
194
|
+
*
|
|
195
|
+
* ## Example
|
|
196
|
+
*
|
|
197
|
+
* Import with `import { unregister } from "@typed/ui/Collection";` Extend the [Collection.makeState
|
|
198
|
+
* runnable setup](/reference/%40typed%2Fui%2FCollection%23makeState). Inside the linked Effect
|
|
199
|
+
* program first register `"save"`, run `yield* unregister(collection, "save")`, then read the
|
|
200
|
+
* collection to observe that only the matching id was removed.
|
|
201
|
+
* @since 1.0.0
|
|
202
|
+
* @category combinators
|
|
203
|
+
*/
|
|
204
|
+
export declare function unregister<Value, Element extends object, E, R>(collection: RefSubject.RefSubject<State<Value, Element>, E, R>, id: string): Effect.Effect<void, E, R>;
|
|
205
|
+
/**
|
|
206
|
+
* Returns a new array excluding only items whose disabled flag is exactly true.
|
|
207
|
+
*
|
|
208
|
+
* @remarks
|
|
209
|
+
* ## Why
|
|
210
|
+
*
|
|
211
|
+
* Separating this deterministic policy from event wiring lets applications test it directly and
|
|
212
|
+
* reuse it in custom composites.
|
|
213
|
+
*
|
|
214
|
+
* ## Ownership and lifetime
|
|
215
|
+
*
|
|
216
|
+
* This is a synchronous calculation. It acquires no resources and does not mutate the input array,
|
|
217
|
+
* state, event, or DOM.
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* ```ts
|
|
221
|
+
* import * as Collection from "@typed/ui/Collection";
|
|
222
|
+
*
|
|
223
|
+
* const enabled = Collection.enabledItems([{ id: "a" }, { id: "b", disabled: true }]);
|
|
224
|
+
* ```
|
|
225
|
+
* @since 1.0.0
|
|
226
|
+
* @category combinators
|
|
227
|
+
*/
|
|
228
|
+
export declare function enabledItems<ItemType extends Item<unknown, object>>(items: readonly ItemType[]): readonly ItemType[];
|
|
229
|
+
/**
|
|
230
|
+
* Returns a sorted copy using compareDocumentPosition; items without comparable mounted elements
|
|
231
|
+
* retain an equivalent ordering position.
|
|
232
|
+
*
|
|
233
|
+
* @remarks
|
|
234
|
+
* ## Why
|
|
235
|
+
*
|
|
236
|
+
* Separating this deterministic policy from event wiring lets applications test it directly and
|
|
237
|
+
* reuse it in custom composites.
|
|
238
|
+
*
|
|
239
|
+
* ## Ownership and lifetime
|
|
240
|
+
*
|
|
241
|
+
* This is a synchronous calculation. It acquires no resources and does not mutate the input array,
|
|
242
|
+
* state, event, or DOM.
|
|
243
|
+
*
|
|
244
|
+
* @example
|
|
245
|
+
* ```ts
|
|
246
|
+
* import * as Collection from "@typed/ui/Collection";
|
|
247
|
+
*
|
|
248
|
+
* const ordered = Collection.byDomOrder([{ id: "body", element: document.body }]);
|
|
249
|
+
* ```
|
|
250
|
+
* @since 1.0.0
|
|
251
|
+
* @category combinators
|
|
252
|
+
*/
|
|
253
|
+
export declare function byDomOrder<ItemType extends Item<unknown, object>>(items: readonly ItemType[]): readonly ItemType[];
|
|
254
|
+
//# sourceMappingURL=Collection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Collection.d.ts","sourceRoot":"","sources":["../src/Collection.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collection is the scoped registry shared by composite widgets. Registration order is not
|
|
3
|
+
* navigation order: byDomOrder reads the mounted nodes with compareDocumentPosition, and each
|
|
4
|
+
* register Effect removes its own item when that Effect's Scope closes.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* The module keeps policy, state transitions, and DOM rendering separable so applications can use
|
|
8
|
+
* the state and pure operations without mounting UI, or supply custom hosts without replacing native
|
|
9
|
+
* events and browser-owned focus.
|
|
10
|
+
*
|
|
11
|
+
* @since 1.0.0
|
|
12
|
+
* @category modules
|
|
13
|
+
* @packageDocumentation
|
|
14
|
+
*/
|
|
15
|
+
import * as Effect from "effect/Effect";
|
|
16
|
+
import * as Equal from "effect/Equal";
|
|
17
|
+
import * as Scope from "effect/Scope";
|
|
18
|
+
import { RefSubject } from "@typed/fx";
|
|
19
|
+
import * as Equivalence from "effect/Equivalence";
|
|
20
|
+
/**
|
|
21
|
+
* Creates hydrated Collection state. The collection starts with the supplied immutable item array,
|
|
22
|
+
* or empty.
|
|
23
|
+
*
|
|
24
|
+
* @remarks
|
|
25
|
+
* ## Why
|
|
26
|
+
*
|
|
27
|
+
* State and collection ownership can be composed and tested independently from any renderer.
|
|
28
|
+
*
|
|
29
|
+
* ## Ownership and lifetime
|
|
30
|
+
*
|
|
31
|
+
* The returned Effect creates the RefSubject when run. That state is renderer-independent;
|
|
32
|
+
* collection registrations belong to the separate Scope that runs register or ref, not to state
|
|
33
|
+
* creation.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* import * as Effect from "effect/Effect";
|
|
38
|
+
* import * as Collection from "@typed/ui/Collection";
|
|
39
|
+
*
|
|
40
|
+
* const program = Effect.scoped(
|
|
41
|
+
* Effect.gen(function* () {
|
|
42
|
+
* const collection = yield* Collection.makeState<string>();
|
|
43
|
+
* return yield* collection;
|
|
44
|
+
* }),
|
|
45
|
+
* );
|
|
46
|
+
* ```
|
|
47
|
+
* @since 1.0.0
|
|
48
|
+
* @category constructors
|
|
49
|
+
*/
|
|
50
|
+
export function makeState(initial = []) {
|
|
51
|
+
// DOM elements are runtime handles; structural equality would traverse browser internals.
|
|
52
|
+
return RefSubject.make(initial, { eq: Equivalence.Array(itemEquivalence()) });
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Upserts an item immediately and installs a finalizer in the Scope that runs this Effect; the
|
|
56
|
+
* finalizer removes only that exact registration.
|
|
57
|
+
*
|
|
58
|
+
* @remarks
|
|
59
|
+
* ## Why
|
|
60
|
+
*
|
|
61
|
+
* The operation exposes Collection's transition directly so callers can compose it in Effect
|
|
62
|
+
* programs and native event handlers.
|
|
63
|
+
*
|
|
64
|
+
* ## Ownership and lifetime
|
|
65
|
+
*
|
|
66
|
+
* Registration is owned by the Scope in which the registration Effect runs. Closing that Scope
|
|
67
|
+
* removes the exact registered item; creating the collection or state does not own this cleanup.
|
|
68
|
+
*
|
|
69
|
+
* ## Example
|
|
70
|
+
*
|
|
71
|
+
* Import with `import { register } from "@typed/ui/Collection";` Extend the [Collection.makeState
|
|
72
|
+
* runnable setup](/reference/%40typed%2Fui%2FCollection%23makeState). Inside the linked Effect
|
|
73
|
+
* program invoke `yield* register(collection, { id: "save", value: "save" })`, then read the
|
|
74
|
+
* collection to observe the inserted item.
|
|
75
|
+
* @since 1.0.0
|
|
76
|
+
* @category combinators
|
|
77
|
+
*/
|
|
78
|
+
export function register(collection, item) {
|
|
79
|
+
return Effect.gen(function* () {
|
|
80
|
+
yield* RefSubject.update(collection, (items) => upsert(items, item)).pipe(Effect.asVoid);
|
|
81
|
+
const services = yield* Effect.context();
|
|
82
|
+
const scope = yield* Effect.scope;
|
|
83
|
+
yield* Scope.addFinalizer(scope, unregisterRegistered(collection, item).pipe(Effect.provide(services), Effect.ignore({ log: true })));
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Registers a mounted element and removes it automatically with the Scope in which the returned
|
|
88
|
+
* Effect is run.
|
|
89
|
+
*
|
|
90
|
+
* @remarks
|
|
91
|
+
* ## Why
|
|
92
|
+
*
|
|
93
|
+
* The operation exposes Collection's transition directly so callers can compose it in Effect
|
|
94
|
+
* programs and native event handlers.
|
|
95
|
+
*
|
|
96
|
+
* ## Ownership and lifetime
|
|
97
|
+
*
|
|
98
|
+
* Registration is owned by the Scope in which the registration Effect runs. Closing that Scope
|
|
99
|
+
* removes the exact registered item; creating the collection or state does not own this cleanup.
|
|
100
|
+
*
|
|
101
|
+
* ## Example
|
|
102
|
+
*
|
|
103
|
+
* Import with `import { ref } from "@typed/ui/Collection";` Extend the [Collection.makeState
|
|
104
|
+
* runnable setup](/reference/%40typed%2Fui%2FCollection%23makeState). Inside the linked Effect
|
|
105
|
+
* program create `const attach = ref(collection, { id: "save", value: "save" })`, then run `yield*
|
|
106
|
+
* attach(document.createElement("button"))` and read the collection before Scope close.
|
|
107
|
+
* @since 1.0.0
|
|
108
|
+
* @category combinators
|
|
109
|
+
*/
|
|
110
|
+
export function ref(collection, item) {
|
|
111
|
+
return Effect.fn((element) => register(collection, { ...item, element }));
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Removes every registered item with the supplied id and retains the remaining order.
|
|
115
|
+
*
|
|
116
|
+
* @remarks
|
|
117
|
+
* ## Why
|
|
118
|
+
*
|
|
119
|
+
* The operation exposes Collection's transition directly so callers can compose it in Effect
|
|
120
|
+
* programs and native event handlers.
|
|
121
|
+
*
|
|
122
|
+
* ## Ownership and lifetime
|
|
123
|
+
*
|
|
124
|
+
* The returned Effect performs the update or DOM side effect only when run, preserves the declared
|
|
125
|
+
* error and service channels, and retains no resources after completion.
|
|
126
|
+
*
|
|
127
|
+
* ## Example
|
|
128
|
+
*
|
|
129
|
+
* Import with `import { unregister } from "@typed/ui/Collection";` Extend the [Collection.makeState
|
|
130
|
+
* runnable setup](/reference/%40typed%2Fui%2FCollection%23makeState). Inside the linked Effect
|
|
131
|
+
* program first register `"save"`, run `yield* unregister(collection, "save")`, then read the
|
|
132
|
+
* collection to observe that only the matching id was removed.
|
|
133
|
+
* @since 1.0.0
|
|
134
|
+
* @category combinators
|
|
135
|
+
*/
|
|
136
|
+
export function unregister(collection, id) {
|
|
137
|
+
return RefSubject.update(collection, (items) => items.filter((item) => item.id !== id)).pipe(Effect.asVoid);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Returns a new array excluding only items whose disabled flag is exactly true.
|
|
141
|
+
*
|
|
142
|
+
* @remarks
|
|
143
|
+
* ## Why
|
|
144
|
+
*
|
|
145
|
+
* Separating this deterministic policy from event wiring lets applications test it directly and
|
|
146
|
+
* reuse it in custom composites.
|
|
147
|
+
*
|
|
148
|
+
* ## Ownership and lifetime
|
|
149
|
+
*
|
|
150
|
+
* This is a synchronous calculation. It acquires no resources and does not mutate the input array,
|
|
151
|
+
* state, event, or DOM.
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```ts
|
|
155
|
+
* import * as Collection from "@typed/ui/Collection";
|
|
156
|
+
*
|
|
157
|
+
* const enabled = Collection.enabledItems([{ id: "a" }, { id: "b", disabled: true }]);
|
|
158
|
+
* ```
|
|
159
|
+
* @since 1.0.0
|
|
160
|
+
* @category combinators
|
|
161
|
+
*/
|
|
162
|
+
export function enabledItems(items) {
|
|
163
|
+
return items.filter((item) => item.disabled !== true);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Returns a sorted copy using compareDocumentPosition; items without comparable mounted elements
|
|
167
|
+
* retain an equivalent ordering position.
|
|
168
|
+
*
|
|
169
|
+
* @remarks
|
|
170
|
+
* ## Why
|
|
171
|
+
*
|
|
172
|
+
* Separating this deterministic policy from event wiring lets applications test it directly and
|
|
173
|
+
* reuse it in custom composites.
|
|
174
|
+
*
|
|
175
|
+
* ## Ownership and lifetime
|
|
176
|
+
*
|
|
177
|
+
* This is a synchronous calculation. It acquires no resources and does not mutate the input array,
|
|
178
|
+
* state, event, or DOM.
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* ```ts
|
|
182
|
+
* import * as Collection from "@typed/ui/Collection";
|
|
183
|
+
*
|
|
184
|
+
* const ordered = Collection.byDomOrder([{ id: "body", element: document.body }]);
|
|
185
|
+
* ```
|
|
186
|
+
* @since 1.0.0
|
|
187
|
+
* @category combinators
|
|
188
|
+
*/
|
|
189
|
+
export function byDomOrder(items) {
|
|
190
|
+
return items.toSorted((left, right) => {
|
|
191
|
+
if (left.element === undefined || right.element === undefined)
|
|
192
|
+
return 0;
|
|
193
|
+
const compareDocumentPosition = Reflect.get(left.element, "compareDocumentPosition");
|
|
194
|
+
if (typeof compareDocumentPosition !== "function")
|
|
195
|
+
return 0;
|
|
196
|
+
const position = compareDocumentPosition.call(left.element, right.element);
|
|
197
|
+
if (position & 2)
|
|
198
|
+
return 1;
|
|
199
|
+
if (position & 4)
|
|
200
|
+
return -1;
|
|
201
|
+
return 0;
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
function unregisterRegistered(collection, item) {
|
|
205
|
+
return RefSubject.update(collection, (items) => items.filter((current) => current.id !== item.id || current !== item)).pipe(Effect.asVoid);
|
|
206
|
+
}
|
|
207
|
+
function upsert(items, item) {
|
|
208
|
+
const index = items.findIndex((current) => current.id === item.id);
|
|
209
|
+
return index === -1 ? [...items, item] : items.toSpliced(index, 1, item);
|
|
210
|
+
}
|
|
211
|
+
function itemEquivalence() {
|
|
212
|
+
return Equivalence.make((left, right) => left.id === right.id &&
|
|
213
|
+
left.element === right.element &&
|
|
214
|
+
left.disabled === right.disabled &&
|
|
215
|
+
left.submenu === right.submenu &&
|
|
216
|
+
left.textValue === right.textValue &&
|
|
217
|
+
Equal.equals(left.value, right.value));
|
|
218
|
+
}
|