@typed/ui 1.0.0-beta.1 → 1.0.0-beta.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +54 -10
  3. package/dist/Alert.d.ts +72 -0
  4. package/dist/Alert.d.ts.map +1 -0
  5. package/dist/Alert.js +40 -0
  6. package/dist/Button.d.ts +100 -0
  7. package/dist/Button.d.ts.map +1 -0
  8. package/dist/Button.js +42 -0
  9. package/dist/Carousel.d.ts +346 -0
  10. package/dist/Carousel.d.ts.map +1 -0
  11. package/dist/Carousel.js +264 -0
  12. package/dist/Checkbox.d.ts +168 -0
  13. package/dist/Checkbox.d.ts.map +1 -0
  14. package/dist/Checkbox.js +146 -0
  15. package/dist/Collection.d.ts +254 -0
  16. package/dist/Collection.d.ts.map +1 -0
  17. package/dist/Collection.js +218 -0
  18. package/dist/Combobox.d.ts +367 -0
  19. package/dist/Combobox.d.ts.map +1 -0
  20. package/dist/Combobox.js +300 -0
  21. package/dist/Composite.d.ts +823 -0
  22. package/dist/Composite.d.ts.map +1 -0
  23. package/dist/Composite.js +615 -0
  24. package/dist/Dialog.d.ts +544 -0
  25. package/dist/Dialog.d.ts.map +1 -0
  26. package/dist/Dialog.js +357 -0
  27. package/dist/Disclosure.d.ts +219 -0
  28. package/dist/Disclosure.d.ts.map +1 -0
  29. package/dist/Disclosure.js +128 -0
  30. package/dist/Dom/Events.d.ts +122 -0
  31. package/dist/Dom/Events.d.ts.map +1 -0
  32. package/dist/Dom/Events.js +192 -0
  33. package/dist/Dom/Props.d.ts +161 -0
  34. package/dist/Dom/Props.d.ts.map +1 -0
  35. package/dist/Dom/Props.js +110 -0
  36. package/dist/Dom/Refs.d.ts +58 -0
  37. package/dist/Dom/Refs.d.ts.map +1 -0
  38. package/dist/Dom/Refs.js +61 -0
  39. package/dist/Dom/Render.d.ts +59 -0
  40. package/dist/Dom/Render.d.ts.map +1 -0
  41. package/dist/Dom/Render.js +71 -0
  42. package/dist/Dom/Types.d.ts +570 -0
  43. package/dist/Dom/Types.d.ts.map +1 -0
  44. package/dist/Dom/Types.js +1 -0
  45. package/dist/Dom/index.d.ts +20 -0
  46. package/dist/Dom/index.d.ts.map +1 -0
  47. package/dist/Dom/index.js +8 -0
  48. package/dist/Dom.d.ts +14 -0
  49. package/dist/Dom.d.ts.map +1 -0
  50. package/dist/Dom.js +13 -0
  51. package/dist/Focusable.d.ts +85 -0
  52. package/dist/Focusable.d.ts.map +1 -0
  53. package/dist/Focusable.js +35 -0
  54. package/dist/Form.d.ts +1727 -0
  55. package/dist/Form.d.ts.map +1 -0
  56. package/dist/Form.js +1142 -0
  57. package/dist/Grid.d.ts +388 -0
  58. package/dist/Grid.d.ts.map +1 -0
  59. package/dist/Grid.js +284 -0
  60. package/dist/Group.d.ts +128 -0
  61. package/dist/Group.d.ts.map +1 -0
  62. package/dist/Group.js +71 -0
  63. package/dist/Heading.d.ts +87 -0
  64. package/dist/Heading.d.ts.map +1 -0
  65. package/dist/Heading.js +58 -0
  66. package/dist/Hovercard.d.ts +297 -0
  67. package/dist/Hovercard.d.ts.map +1 -0
  68. package/dist/Hovercard.js +188 -0
  69. package/dist/HttpRouter.d.ts +129 -6
  70. package/dist/HttpRouter.d.ts.map +1 -1
  71. package/dist/HttpRouter.js +198 -55
  72. package/dist/Link.d.ts +63 -28
  73. package/dist/Link.d.ts.map +1 -1
  74. package/dist/Link.js +84 -37
  75. package/dist/Listbox.d.ts +305 -0
  76. package/dist/Listbox.d.ts.map +1 -0
  77. package/dist/Listbox.js +245 -0
  78. package/dist/Menu.d.ts +663 -0
  79. package/dist/Menu.d.ts.map +1 -0
  80. package/dist/Menu.js +569 -0
  81. package/dist/Menubar.d.ts +249 -0
  82. package/dist/Menubar.d.ts.map +1 -0
  83. package/dist/Menubar.js +207 -0
  84. package/dist/Meter.d.ts +157 -0
  85. package/dist/Meter.d.ts.map +1 -0
  86. package/dist/Meter.js +87 -0
  87. package/dist/NativeDetails.d.ts +41 -0
  88. package/dist/NativeDetails.d.ts.map +1 -0
  89. package/dist/NativeDetails.js +40 -0
  90. package/dist/NativeDialog.d.ts +66 -0
  91. package/dist/NativeDialog.d.ts.map +1 -0
  92. package/dist/NativeDialog.js +88 -0
  93. package/dist/NativePopover.d.ts +43 -0
  94. package/dist/NativePopover.d.ts.map +1 -0
  95. package/dist/NativePopover.js +84 -0
  96. package/dist/Popover.d.ts +240 -0
  97. package/dist/Popover.d.ts.map +1 -0
  98. package/dist/Popover.js +140 -0
  99. package/dist/RadioGroup.d.ts +330 -0
  100. package/dist/RadioGroup.d.ts.map +1 -0
  101. package/dist/RadioGroup.js +241 -0
  102. package/dist/Role.d.ts +64 -0
  103. package/dist/Role.d.ts.map +1 -0
  104. package/dist/Role.js +27 -0
  105. package/dist/Select.d.ts +418 -0
  106. package/dist/Select.d.ts.map +1 -0
  107. package/dist/Select.js +357 -0
  108. package/dist/Separator.d.ts +58 -0
  109. package/dist/Separator.d.ts.map +1 -0
  110. package/dist/Separator.js +32 -0
  111. package/dist/Slider.d.ts +141 -0
  112. package/dist/Slider.d.ts.map +1 -0
  113. package/dist/Slider.js +101 -0
  114. package/dist/SpinButton.d.ts +141 -0
  115. package/dist/SpinButton.d.ts.map +1 -0
  116. package/dist/SpinButton.js +101 -0
  117. package/dist/Storybook.d.ts +76 -0
  118. package/dist/Storybook.d.ts.map +1 -0
  119. package/dist/Storybook.js +102 -0
  120. package/dist/Switch.d.ts +148 -0
  121. package/dist/Switch.d.ts.map +1 -0
  122. package/dist/Switch.js +110 -0
  123. package/dist/Tab.d.ts +26 -0
  124. package/dist/Tab.d.ts.map +1 -0
  125. package/dist/Tab.js +25 -0
  126. package/dist/Tabs.d.ts +411 -0
  127. package/dist/Tabs.d.ts.map +1 -0
  128. package/dist/Tabs.js +262 -0
  129. package/dist/Toolbar.d.ts +248 -0
  130. package/dist/Toolbar.d.ts.map +1 -0
  131. package/dist/Toolbar.js +187 -0
  132. package/dist/Tooltip.d.ts +296 -0
  133. package/dist/Tooltip.d.ts.map +1 -0
  134. package/dist/Tooltip.js +172 -0
  135. package/dist/Tree.d.ts +405 -0
  136. package/dist/Tree.d.ts.map +1 -0
  137. package/dist/Tree.js +333 -0
  138. package/dist/TreeGrid.d.ts +426 -0
  139. package/dist/TreeGrid.d.ts.map +1 -0
  140. package/dist/TreeGrid.js +308 -0
  141. package/dist/VisuallyHidden.d.ts +68 -0
  142. package/dist/VisuallyHidden.d.ts.map +1 -0
  143. package/dist/VisuallyHidden.js +44 -0
  144. package/dist/WindowSplitter.d.ts +336 -0
  145. package/dist/WindowSplitter.d.ts.map +1 -0
  146. package/dist/WindowSplitter.js +305 -0
  147. package/dist/index.d.ts +48 -0
  148. package/dist/index.d.ts.map +1 -1
  149. package/dist/index.js +48 -0
  150. package/package.json +48 -20
  151. package/src/HttpRouter.test.ts +0 -294
  152. package/src/HttpRouter.ts +0 -168
  153. package/src/Link.test.ts +0 -84
  154. package/src/Link.ts +0 -107
  155. package/src/index.ts +0 -2
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Toolbar is a roving-focus composite with toolbar and button roles. Orientation and RTL control
3
+ * Arrow-key order; Home and End select the document-order endpoints.
4
+ *
5
+ * @remarks
6
+ * The module keeps policy, state transitions, and DOM rendering separable so applications can use
7
+ * the state and pure operations without mounting UI, or supply custom hosts without replacing native
8
+ * events and browser-owned focus.
9
+ *
10
+ * Learn the interaction in the [Toolbar guide](/explore/ui-toolbar).
11
+ *
12
+ * @since 1.0.0
13
+ * @category modules
14
+ * @packageDocumentation
15
+ */
16
+ import * as Effect from "effect/Effect";
17
+ import { RefSubject } from "@typed/fx";
18
+ import { EventHandler, html, } from "@typed/template";
19
+ import * as Collection from "./Collection.js";
20
+ import * as Composite from "./Composite.js";
21
+ import * as Dom from "./Dom.js";
22
+ /**
23
+ * Effect Schema used by makeState to encode, decode, and hydrate Toolbar state.
24
+ *
25
+ * @remarks
26
+ * @example
27
+ * ```ts
28
+ * import * as Schema from "effect/Schema";
29
+ * import * as Toolbar from "@typed/ui/Toolbar";
30
+ *
31
+ * const decodeState = Schema.decodeUnknownEffect(Toolbar.StateSchema);
32
+ * ```
33
+ * @since 1.0.0
34
+ * @category Command focus
35
+ */
36
+ export const StateSchema = Composite.StateSchema;
37
+ /**
38
+ * Creates hydrated Toolbar state. Uses Composite defaults unless overridden.
39
+ *
40
+ * @remarks
41
+ * The returned Effect creates the RefSubject when run. That state is renderer-independent;
42
+ * collection registrations belong to the separate Scope that runs register or ref, not to state
43
+ * creation.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * import * as Effect from "effect/Effect";
48
+ * import * as Toolbar from "@typed/ui/Toolbar";
49
+ *
50
+ * const program = Effect.scoped(
51
+ * Effect.gen(function* () {
52
+ * const state = yield* Toolbar.makeState({});
53
+ * const collection = yield* Toolbar.makeCollection();
54
+ * return { state: yield* state, collection: yield* collection };
55
+ * }),
56
+ * );
57
+ * ```
58
+ * @since 1.0.0
59
+ * @category Command focus
60
+ */
61
+ export function makeState(initial = {}) {
62
+ return Composite.makeState(initial);
63
+ }
64
+ /**
65
+ * Creates a scoped Collection for Toolbar items.
66
+ *
67
+ * @remarks
68
+ * The returned Effect allocates the RefSubject in the caller's Scope. Each later registration is
69
+ * owned by the Scope that runs register, independently of this construction Effect.
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * import * as Effect from "effect/Effect";
74
+ * import * as Toolbar from "@typed/ui/Toolbar";
75
+ *
76
+ * const program = Effect.scoped(
77
+ * Effect.gen(function* () {
78
+ * const collection = yield* Toolbar.makeCollection();
79
+ * return yield* collection;
80
+ * }),
81
+ * );
82
+ * ```
83
+ * @since 1.0.0
84
+ * @category Command registration
85
+ */
86
+ export const makeCollection = Collection.makeState;
87
+ function rootProps(options) {
88
+ const onfocus = options.collection === undefined
89
+ ? undefined
90
+ : Effect.gen(function* () {
91
+ if ((yield* options.state).activeId !== null)
92
+ return;
93
+ yield* Composite.moveAndFocus({ state: options.state, collection: options.collection }, "first");
94
+ });
95
+ const onkeydown = options.collection === undefined
96
+ ? undefined
97
+ : EventHandler.make(Effect.fn(function* (event) {
98
+ if (event.key === "Enter" || event.key === " ") {
99
+ const activeId = (yield* options.state).activeId;
100
+ const item = activeId === null
101
+ ? undefined
102
+ : (yield* options.collection).find((candidate) => candidate.id === activeId);
103
+ const click = item?.disabled === true ? undefined : Reflect.get(item?.element ?? {}, "click");
104
+ if (typeof click === "function") {
105
+ event.preventDefault();
106
+ yield* Effect.sync(() => click.call(item.element));
107
+ }
108
+ return;
109
+ }
110
+ const direction = Composite.keyMove(event, yield* options.state);
111
+ if (direction === undefined)
112
+ return;
113
+ event.preventDefault();
114
+ yield* Composite.moveAndFocus({ state: options.state, collection: options.collection }, direction);
115
+ }));
116
+ return ({ property }) => ({
117
+ role: "toolbar",
118
+ "aria-label": property("label", undefined),
119
+ "aria-orientation": RefSubject.map(options.state, (state) => state.orientation),
120
+ tabindex: Composite.rootTabIndex(options.state),
121
+ onfocus,
122
+ onkeydown,
123
+ ref: options.state,
124
+ });
125
+ }
126
+ /**
127
+ * Renders the toolbar root and moves roving focus through registered items in DOM order.
128
+ *
129
+ * @remarks
130
+ * The returned Fx installs DOM refs, native listeners, state subscriptions, and optional
131
+ * collection registrations only when rendered. The rendering Scope removes those resources;
132
+ * unrelated nodes and attributes remain caller-owned.
133
+ *
134
+ * @since 1.0.0
135
+ * @category Toolbar surface
136
+ */
137
+ export function Root(options, host) {
138
+ return Dom.renderHost()(options, host, rootProps(options), options.content, (props, content) => {
139
+ return html `<div ...${props}>${content}</div>`;
140
+ });
141
+ }
142
+ /**
143
+ * Consumer-facing alias of the canonical Toolbar component with identical behavior and lifetime.
144
+ *
145
+ * @remarks
146
+ * The alias acquires nothing. Rendering it has exactly the canonical component's Scope and DOM
147
+ * ownership contract.
148
+ *
149
+ * @since 1.0.0
150
+ * @category Toolbar surface
151
+ */
152
+ export const Toolbar = Root;
153
+ function itemProps(options) {
154
+ const activate = options.disabled === true
155
+ ? Effect.void
156
+ : RefSubject.update(options.state, (state) => ({ ...state, activeId: options.id }));
157
+ const register = options.collection === undefined
158
+ ? undefined
159
+ : Collection.ref(options.collection, {
160
+ id: options.id,
161
+ value: options.id,
162
+ textValue: options.textValue ?? options.id,
163
+ disabled: options.disabled,
164
+ });
165
+ return () => ({
166
+ id: options.id,
167
+ role: "button",
168
+ "aria-disabled": options.disabled ?? false,
169
+ tabindex: Composite.tabIndex(options.state, options.id),
170
+ onfocus: activate,
171
+ ref: Dom.composeRefs(register, options.ref),
172
+ });
173
+ }
174
+ /**
175
+ * Renders and optionally registers a toolbar button; focus activates it unless disabled.
176
+ *
177
+ * @remarks
178
+ * The returned Fx installs DOM refs, native listeners, state subscriptions, and optional
179
+ * collection registrations only when rendered. The rendering Scope removes those resources;
180
+ * unrelated nodes and attributes remain caller-owned.
181
+ *
182
+ * @since 1.0.0
183
+ * @category Toolbar commands
184
+ */
185
+ export function Item(options, host) {
186
+ return Dom.renderHost()(options, host, itemProps(options), options.content, (props, content) => html `<div ...${props}>${content}</div>`);
187
+ }
@@ -0,0 +1,296 @@
1
+ import * as Effect from "effect/Effect";
2
+ import type * as Scope from "effect/Scope";
3
+ import * as Schema from "effect/Schema";
4
+ import type { Fx } from "@typed/fx/Fx";
5
+ import { RefSubject } from "@typed/fx";
6
+ import { EventHandler, type Renderable, type RenderEvent, type RenderTemplate } from "@typed/template";
7
+ import * as Dom from "./Dom.js";
8
+ import type { HostResult } from "./Dom/Types.js";
9
+ /** Current tooltip identity and visibility.
10
+ * @remarks
11
+ * ## Why
12
+ * A stable id connects the anchor's description relationship to native popover
13
+ * content while open state stays renderer-independent.
14
+ * ## Ownership and lifetime
15
+ * Plain state retains no resources; RefSubject observation is Scope-owned.
16
+ * @since 1.0.0
17
+ * @category Open state
18
+ */
19
+ export interface State {
20
+ /** Stable id used by `aria-describedby` and tooltip content.
21
+ * @remarks
22
+ * ## Why
23
+ * Server and client must agree on the relationship target during hydration.
24
+ * ## Ownership and lifetime
25
+ * Plain data acquires no resources.
26
+ * @since 1.0.0
27
+ * @category Identity and relationships
28
+ */
29
+ readonly id: string;
30
+ /** Whether tooltip content is open.
31
+ * @remarks
32
+ * ## Why
33
+ * The value coordinates delayed anchor interactions and native popover state.
34
+ * ## Ownership and lifetime
35
+ * Plain data acquires no resources.
36
+ * @since 1.0.0
37
+ * @category Open state
38
+ */
39
+ readonly open: boolean;
40
+ }
41
+ /** Initial tooltip identity and visibility.
42
+ * @remarks
43
+ * ## Why
44
+ * Requiring an id makes the accessibility relationship explicit.
45
+ * ## Ownership and lifetime
46
+ * Configuration is inert.
47
+ * @since 1.0.0
48
+ * @category Open state
49
+ */
50
+ export interface InitialState {
51
+ /** Stable tooltip id.
52
+ * @remarks
53
+ * ## Why
54
+ * Deterministic ids prevent SSR/client relationship drift.
55
+ * ## Ownership and lifetime
56
+ * Plain data retains no resources.
57
+ * @since 1.0.0
58
+ * @category Identity and relationships
59
+ */
60
+ readonly id: string;
61
+ /** Initial visibility, defaulting to false.
62
+ * @remarks
63
+ * ## Why
64
+ * Closed is the deterministic default before user focus or hover.
65
+ * ## Ownership and lifetime
66
+ * Plain data retains no resources.
67
+ * @since 1.0.0
68
+ * @category Open state
69
+ */
70
+ readonly open?: boolean;
71
+ }
72
+ /** Schema for tooltip hydration state.
73
+ * @remarks
74
+ * ## Why
75
+ * Shared identity/open encoding keeps SSR and browser relationships compatible.
76
+ * ## Ownership and lifetime
77
+ * The immutable schema acquires no resources.
78
+ * @since 1.0.0
79
+ * @category Open state
80
+ */
81
+ export declare const StateSchema: Schema.Struct<{
82
+ readonly id: Schema.String;
83
+ readonly open: Schema.Boolean;
84
+ }>;
85
+ /** Creates hydrated tooltip state.
86
+ * @remarks
87
+ * ## Why
88
+ * Visibility logic can be tested without mounting an anchor or popover.
89
+ * ## Ownership and lifetime
90
+ * The calling Effect Scope owns the returned RefSubject.
91
+ * @example
92
+ * ```ts
93
+ * import { Effect } from "effect"
94
+ * import * as Tooltip from "@typed/ui/Tooltip"
95
+ *
96
+ * const program = Effect.gen(function* () {
97
+ * return yield* Tooltip.makeState({ id: "save-help" })
98
+ * })
99
+ * ```
100
+ * @since 1.0.0
101
+ * @category Open state
102
+ */
103
+ export declare function makeState(initial: InitialState): Effect.Effect<RefSubject.HydratedRefSubject<{
104
+ readonly id: string;
105
+ readonly open: boolean;
106
+ }, Schema.SchemaError, never, never>, never, Scope.Scope>;
107
+ /** Sets tooltip visibility.
108
+ * @remarks
109
+ * ## Why
110
+ * Delayed focus/hover logic converges on one explicit state transition.
111
+ * ## Ownership and lifetime
112
+ * The Effect uses the existing state Scope and acquires no resource.
113
+ * @example
114
+ * ```ts
115
+ * import { Effect } from "effect"
116
+ * import * as Tooltip from "@typed/ui/Tooltip"
117
+ *
118
+ * const program = Effect.gen(function* () {
119
+ * const state = yield* Tooltip.makeState({ id: "save-help" })
120
+ * yield* Tooltip.setOpen(state, true)
121
+ * })
122
+ * ```
123
+ * @since 1.0.0
124
+ * @category Open state
125
+ */
126
+ export declare function setOpen<E, R>(state: RefSubject.RefSubject<State, E, R>, open: boolean): Effect.Effect<State, E, R>;
127
+ /** Options for a tooltip anchor host.
128
+ * @remarks
129
+ * ## Why
130
+ * Pointer entry works with the default span. Keyboard focus behavior requires
131
+ * the host itself to be focusable, for example `props: { tabindex: 0 }` or a
132
+ * focusable custom host; focus and blur on descendants do not bubble to it.
133
+ * ## Ownership and lifetime
134
+ * Options are inert; rendering owns listeners/timers by Scope.
135
+ * @since 1.0.0
136
+ * @category Pointer and focus anchors
137
+ */
138
+ export interface AnchorOptions extends Dom.HostOptions<HTMLSpanElement> {
139
+ /** Hydrated state shared with tooltip content.
140
+ * @remarks
141
+ * ## Why
142
+ * Its id drives `aria-describedby` and its open value drives the popover.
143
+ * ## Ownership and lifetime
144
+ * The anchor borrows state; its original Scope owns it.
145
+ * @since 1.0.0
146
+ * @category State connection
147
+ */
148
+ readonly state: RefSubject.HydratedRefSubject<State, Schema.SchemaError>;
149
+ /** Anchor content.
150
+ * @remarks
151
+ * ## Why
152
+ * The anchor wraps descriptive content. A focusable descendant does not make
153
+ * the default span receive its non-bubbling focus and blur handlers.
154
+ * ## Ownership and lifetime
155
+ * Dynamic content follows the anchor Scope.
156
+ * @since 1.0.0
157
+ * @category Rendered content
158
+ */
159
+ readonly content: Renderable.Any;
160
+ /** Delay in milliseconds before opening.
161
+ * @remarks
162
+ * ## Why
163
+ * A delay avoids accidental tooltip activation during pointer transit.
164
+ * ## Ownership and lifetime
165
+ * Delayed Effects run inside the component interaction lifetime; newer
166
+ * schedules invalidate older versions.
167
+ * @since 1.0.0
168
+ * @category Interaction delays
169
+ */
170
+ readonly showDelay?: number;
171
+ /** Delay in milliseconds before closing.
172
+ * @remarks
173
+ * ## Why
174
+ * A delay prevents flicker during brief pointer movement.
175
+ * ## Ownership and lifetime
176
+ * Newer schedules invalidate older delayed updates.
177
+ * @since 1.0.0
178
+ * @category Interaction delays
179
+ */
180
+ readonly hideDelay?: number;
181
+ }
182
+ declare function anchorInternalProps<const Options extends AnchorOptions>(options: Options): () => {
183
+ readonly "aria-describedby": RefSubject.Computed<string, Schema.SchemaError, never>;
184
+ readonly onfocus: Effect.Effect<void, Schema.SchemaError, never>;
185
+ readonly onblur: Effect.Effect<void, Schema.SchemaError, never>;
186
+ readonly onkeydown: EventHandler.EventHandler<KeyboardEvent, Schema.SchemaError, never>;
187
+ readonly onmouseenter: Effect.Effect<void, Schema.SchemaError, never>;
188
+ readonly onmouseleave: Effect.Effect<void, Schema.SchemaError, never>;
189
+ };
190
+ type AnchorInternalProps<Options extends AnchorOptions> = ReturnType<ReturnType<typeof anchorInternalProps<Options>>>;
191
+ /** Renders a tooltip anchor with pointer and optional host-focus behavior.
192
+ * @remarks
193
+ * ## Why
194
+ * The default host is a non-focusable `<span>`, so it provides pointer behavior
195
+ * only. Give that host `tabindex`, or supply a natively focusable custom host,
196
+ * to activate its direct focus, blur, and keydown handlers. Those events are
197
+ * attached to the host itself and do not observe focus on nested content.
198
+ * Tooltip content remains descriptive rather than an interactive dialog.
199
+ * ## Ownership and lifetime
200
+ * Running the Fx owns event listeners and dynamic relationship state in its
201
+ * Scope. Custom hosts must preserve `aria-describedby` and all handlers.
202
+ * @example
203
+ * ```ts
204
+ * import { Effect } from "effect"
205
+ * import * as Tooltip from "@typed/ui/Tooltip"
206
+ *
207
+ * const program = Effect.gen(function* () {
208
+ * const state = yield* Tooltip.makeState({ id: "save-help" })
209
+ * return Tooltip.Anchor({
210
+ * state,
211
+ * content: "Save",
212
+ * props: { tabindex: 0 }
213
+ * })
214
+ * })
215
+ * ```
216
+ * @since 1.0.0
217
+ * @category Pointer and focus anchors
218
+ */
219
+ export declare function Anchor<const Options extends AnchorOptions, const Host extends HostResult = never>(options: Options, host?: Dom.HostOverride<Dom.RenderHostProps<Options, AnchorInternalProps<Options>>, Options["content"], Host>): Fx<RenderEvent, Renderable.Error<Options | Host>, Renderable.Services<Options | Host> | Scope.Scope | RenderTemplate>;
220
+ /** Options for native tooltip popover content.
221
+ * @remarks
222
+ * ## Why
223
+ * The role and native top-layer lifecycle remain explicit.
224
+ * ## Ownership and lifetime
225
+ * Options are inert; rendering owns listeners/ref observation by Scope.
226
+ * @since 1.0.0
227
+ * @category Native content host
228
+ */
229
+ export interface ContentOptions extends Dom.HostOptions<HTMLDivElement> {
230
+ /** Hydrated state supplying content id and visibility.
231
+ * @remarks
232
+ * ## Why
233
+ * One source keeps ARIA relationship and native popover state aligned.
234
+ * ## Ownership and lifetime
235
+ * Content borrows state; its original Scope owns it.
236
+ * @since 1.0.0
237
+ * @category State connection
238
+ */
239
+ readonly state: RefSubject.HydratedRefSubject<State, Schema.SchemaError>;
240
+ /** Non-interactive explanatory tooltip content.
241
+ * @remarks
242
+ * ## Why
243
+ * Tooltip content describes its anchor and must not contain required controls.
244
+ * ## Ownership and lifetime
245
+ * Dynamic content follows the content Scope.
246
+ * @since 1.0.0
247
+ * @category Rendered content
248
+ */
249
+ readonly content: Renderable.Any;
250
+ }
251
+ declare function contentInternalProps<const Options extends ContentOptions>(options: Options): () => {
252
+ readonly id: RefSubject.Computed<string, Schema.SchemaError, never>;
253
+ readonly role: "tooltip";
254
+ readonly popover: "manual";
255
+ readonly onmouseenter: Effect.Effect<void, Schema.SchemaError, never>;
256
+ readonly onmouseleave: EventHandler.EventHandler<MouseEvent, Schema.SchemaError, never>;
257
+ readonly onkeydown: EventHandler.EventHandler<KeyboardEvent, Schema.SchemaError, never>;
258
+ readonly ontoggle: EventHandler.EventHandler<Event, Schema.SchemaError, never>;
259
+ readonly ref: import("./Dom/Refs.js").ComposedRef<RefSubject.HydratedRefSubject<State, Schema.SchemaError, never, never>, (element: HTMLElement) => Effect.Effect<void, Schema.SchemaError, Scope.Scope>> | undefined;
260
+ };
261
+ type ContentInternalProps<Options extends ContentOptions> = ReturnType<ReturnType<typeof contentInternalProps<Options>>>;
262
+ /** Renders `role="tooltip"` content through the native Popover API.
263
+ * @remarks
264
+ * ## Why
265
+ * Native top-layer output avoids reparenting into a private portal while real
266
+ * toggle/mouse/keyboard events synchronize renderer-independent state.
267
+ * ## Ownership and lifetime
268
+ * Running the Fx owns listeners and NativePopover observation in its Scope. A
269
+ * custom host must preserve id, role, manual popover, handlers, and one
270
+ * hydration ref owner.
271
+ * @example
272
+ * ```ts
273
+ * import { Effect } from "effect"
274
+ * import * as Tooltip from "@typed/ui/Tooltip"
275
+ *
276
+ * const program = Effect.gen(function* () {
277
+ * const state = yield* Tooltip.makeState({ id: "save-help" })
278
+ * return Tooltip.Content({ state, content: "Stores changes" })
279
+ * })
280
+ * ```
281
+ * @since 1.0.0
282
+ * @category Native content host
283
+ */
284
+ export declare function Content<const Options extends ContentOptions, const Host extends HostResult = never>(options: Options, host?: Dom.HostOverride<Dom.RenderHostProps<Options, ContentInternalProps<Options>>, Options["content"], Host>): Fx<RenderEvent, Renderable.Error<Options | Host>, Renderable.Services<Options | Host> | Scope.Scope | RenderTemplate>;
285
+ /** Canonical widget alias for `Content`.
286
+ * @remarks
287
+ * ## Why
288
+ * The alias provides the widget name while `Content` names compound use.
289
+ * ## Ownership and lifetime
290
+ * It has exactly the same Scope and popover ownership as `Content`.
291
+ * @since 1.0.0
292
+ * @category Native content host
293
+ */
294
+ export declare const Tooltip: typeof Content;
295
+ export {};
296
+ //# sourceMappingURL=Tooltip.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Tooltip.d.ts","sourceRoot":"","sources":["../src/Tooltip.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,172 @@
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
+ import * as NativePopover from "./NativePopover.js";
7
+ /** Schema for tooltip hydration state.
8
+ * @remarks
9
+ * ## Why
10
+ * Shared identity/open encoding keeps SSR and browser relationships compatible.
11
+ * ## Ownership and lifetime
12
+ * The immutable schema acquires no resources.
13
+ * @since 1.0.0
14
+ * @category Open state
15
+ */
16
+ export const StateSchema = Schema.Struct({ id: Schema.String, open: Schema.Boolean });
17
+ /** Creates hydrated tooltip state.
18
+ * @remarks
19
+ * ## Why
20
+ * Visibility logic can be tested without mounting an anchor or popover.
21
+ * ## Ownership and lifetime
22
+ * The calling Effect Scope owns the returned RefSubject.
23
+ * @example
24
+ * ```ts
25
+ * import { Effect } from "effect"
26
+ * import * as Tooltip from "@typed/ui/Tooltip"
27
+ *
28
+ * const program = Effect.gen(function* () {
29
+ * return yield* Tooltip.makeState({ id: "save-help" })
30
+ * })
31
+ * ```
32
+ * @since 1.0.0
33
+ * @category Open state
34
+ */
35
+ export function makeState(initial) {
36
+ return RefSubject.hydrate(StateSchema, { id: initial.id, open: initial.open ?? false });
37
+ }
38
+ /** Sets tooltip visibility.
39
+ * @remarks
40
+ * ## Why
41
+ * Delayed focus/hover logic converges on one explicit state transition.
42
+ * ## Ownership and lifetime
43
+ * The Effect uses the existing state Scope and acquires no resource.
44
+ * @example
45
+ * ```ts
46
+ * import { Effect } from "effect"
47
+ * import * as Tooltip from "@typed/ui/Tooltip"
48
+ *
49
+ * const program = Effect.gen(function* () {
50
+ * const state = yield* Tooltip.makeState({ id: "save-help" })
51
+ * yield* Tooltip.setOpen(state, true)
52
+ * })
53
+ * ```
54
+ * @since 1.0.0
55
+ * @category Open state
56
+ */
57
+ export function setOpen(state, open) {
58
+ return RefSubject.update(state, (current) => ({ ...current, open }));
59
+ }
60
+ const scheduleVersions = new WeakMap();
61
+ const scheduleOpen = Effect.fn(function* (state, open, delay) {
62
+ const version = (scheduleVersions.get(state) ?? 0) + 1;
63
+ scheduleVersions.set(state, version);
64
+ if (delay > 0)
65
+ yield* Effect.sleep(delay);
66
+ if (scheduleVersions.get(state) === version)
67
+ yield* setOpen(state, open);
68
+ });
69
+ function anchorInternalProps(options) {
70
+ const id = RefSubject.map(options.state, (state) => state.id);
71
+ return () => ({
72
+ "aria-describedby": id,
73
+ onfocus: scheduleOpen(options.state, true, options.showDelay ?? 0),
74
+ onblur: scheduleOpen(options.state, false, options.hideDelay ?? 0),
75
+ onkeydown: EventHandler.make(Effect.fn(function* (event) {
76
+ if (event.key === "Escape")
77
+ yield* scheduleOpen(options.state, false, 0);
78
+ })),
79
+ onmouseenter: scheduleOpen(options.state, true, options.showDelay ?? 0),
80
+ onmouseleave: scheduleOpen(options.state, false, options.hideDelay ?? 0),
81
+ });
82
+ }
83
+ /** Renders a tooltip anchor with pointer and optional host-focus behavior.
84
+ * @remarks
85
+ * ## Why
86
+ * The default host is a non-focusable `<span>`, so it provides pointer behavior
87
+ * only. Give that host `tabindex`, or supply a natively focusable custom host,
88
+ * to activate its direct focus, blur, and keydown handlers. Those events are
89
+ * attached to the host itself and do not observe focus on nested content.
90
+ * Tooltip content remains descriptive rather than an interactive dialog.
91
+ * ## Ownership and lifetime
92
+ * Running the Fx owns event listeners and dynamic relationship state in its
93
+ * Scope. Custom hosts must preserve `aria-describedby` and all handlers.
94
+ * @example
95
+ * ```ts
96
+ * import { Effect } from "effect"
97
+ * import * as Tooltip from "@typed/ui/Tooltip"
98
+ *
99
+ * const program = Effect.gen(function* () {
100
+ * const state = yield* Tooltip.makeState({ id: "save-help" })
101
+ * return Tooltip.Anchor({
102
+ * state,
103
+ * content: "Save",
104
+ * props: { tabindex: 0 }
105
+ * })
106
+ * })
107
+ * ```
108
+ * @since 1.0.0
109
+ * @category Pointer and focus anchors
110
+ */
111
+ export function Anchor(options, host) {
112
+ return Dom.renderHost()(options, host, anchorInternalProps(options), options.content, (props, content) => html `<span ...${props}>${content}</span>`);
113
+ }
114
+ function contentInternalProps(options) {
115
+ const id = RefSubject.map(options.state, (state) => state.id);
116
+ return () => ({
117
+ id,
118
+ role: "tooltip",
119
+ popover: "manual",
120
+ onmouseenter: scheduleOpen(options.state, true, 0),
121
+ onmouseleave: EventHandler.make(Effect.fn(function* (event) {
122
+ const contentId = (yield* options.state).id;
123
+ if (event.relatedTarget instanceof Element &&
124
+ event.relatedTarget.getAttribute("aria-describedby") === contentId)
125
+ return;
126
+ yield* scheduleOpen(options.state, false, 0);
127
+ })),
128
+ onkeydown: EventHandler.make(Effect.fn(function* (event) {
129
+ if (event.key === "Escape")
130
+ yield* scheduleOpen(options.state, false, 0);
131
+ })),
132
+ ontoggle: EventHandler.make(Effect.fn((event) => setOpen(options.state, Dom.toggleState(event) === "open"))),
133
+ ref: Dom.composeRefs(options.state, NativePopover.ref(options.state)),
134
+ });
135
+ }
136
+ /** Renders `role="tooltip"` content through the native Popover API.
137
+ * @remarks
138
+ * ## Why
139
+ * Native top-layer output avoids reparenting into a private portal while real
140
+ * toggle/mouse/keyboard events synchronize renderer-independent state.
141
+ * ## Ownership and lifetime
142
+ * Running the Fx owns listeners and NativePopover observation in its Scope. A
143
+ * custom host must preserve id, role, manual popover, handlers, and one
144
+ * hydration ref owner.
145
+ * @example
146
+ * ```ts
147
+ * import { Effect } from "effect"
148
+ * import * as Tooltip from "@typed/ui/Tooltip"
149
+ *
150
+ * const program = Effect.gen(function* () {
151
+ * const state = yield* Tooltip.makeState({ id: "save-help" })
152
+ * return Tooltip.Content({ state, content: "Stores changes" })
153
+ * })
154
+ * ```
155
+ * @since 1.0.0
156
+ * @category Native content host
157
+ */
158
+ export function Content(options, host) {
159
+ return Dom.renderHost()(options, host, contentInternalProps(options), options.content, (props, content) => {
160
+ return html `<div ...${props}>${content}</div>`;
161
+ });
162
+ }
163
+ /** Canonical widget alias for `Content`.
164
+ * @remarks
165
+ * ## Why
166
+ * The alias provides the widget name while `Content` names compound use.
167
+ * ## Ownership and lifetime
168
+ * It has exactly the same Scope and popover ownership as `Content`.
169
+ * @since 1.0.0
170
+ * @category Native content host
171
+ */
172
+ export const Tooltip = Content;