@typed/ui 1.0.0-beta.0 → 1.0.0-beta.10

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 (156) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +78 -26
  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 +199 -56
  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 +47 -14
  151. package/src/HttpRouter.test.ts +0 -294
  152. package/src/HttpRouter.ts +0 -168
  153. package/src/Link.test.ts +0 -85
  154. package/src/Link.ts +0 -107
  155. package/src/index.ts +0 -2
  156. package/tsconfig.json +0 -6
package/dist/Meter.js ADDED
@@ -0,0 +1,87 @@
1
+ import * as Schema from "effect/Schema";
2
+ import { RefSubject } from "@typed/fx";
3
+ import { html } from "@typed/template";
4
+ import * as Dom from "./Dom.js";
5
+ /**
6
+ */
7
+ export const StateSchema = Schema.Struct({ value: Schema.Finite });
8
+ /**
9
+ * Creates hydrated meter state.
10
+ * @remarks
11
+ * The state remains testable and reusable without rendering a UI component.
12
+ * The caller's Effect Scope owns the hydrated RefSubject.
13
+ * @example
14
+ * ```ts
15
+ * import { Effect } from "effect"
16
+ * import * as Meter from "@typed/ui/Meter"
17
+ *
18
+ * const program = Effect.gen(function* () {
19
+ * return yield* Meter.makeState({ value: 0.72 })
20
+ * })
21
+ * ```
22
+ * @since 1.0.0
23
+ * @category State construction
24
+ */
25
+ export function makeState(initial) {
26
+ return RefSubject.hydrate(StateSchema, initial);
27
+ }
28
+ /**
29
+ * Assigns the displayed measurement without clamping it to the meter range.
30
+ *
31
+ * @remarks
32
+ * Keep measurement units consistent with min/max and threshold options. This operation does not
33
+ * execute threshold actions or validate the preferred range.
34
+ * @since 1.0.0
35
+ * @category State transitions
36
+ */
37
+ export function setValue(state, value) {
38
+ return RefSubject.update(state, (current) => ({ ...current, value }));
39
+ }
40
+ function internalProps(options) {
41
+ return ({ property }) => ({
42
+ value: RefSubject.map(options.state, (state) => state.value),
43
+ min: property("min", undefined),
44
+ max: property("max", undefined),
45
+ low: property("low", undefined),
46
+ high: property("high", undefined),
47
+ optimum: property("optimum", undefined),
48
+ ref: options.state,
49
+ });
50
+ }
51
+ /**
52
+ * Renders a native meter for a measurement within a known range.
53
+ *
54
+ * @remarks
55
+ * Supply a label and coherent min/max/low/high/optimum values. The browser presents thresholds;
56
+ * the component does not trigger alerts when they are crossed. Use a slider for editing or a
57
+ * progress element for operation completion. The meter has no keyboard action.
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * import { RefSubject } from "@typed/fx";
62
+ * import { html } from "@typed/template";
63
+ * import { component } from "@typed/template";
64
+ * import * as Meter from "@typed/ui/Meter";
65
+ *
66
+ * export const StorageUsage = component(function* () {
67
+ * const state = yield* Meter.makeState({ value: 64 });
68
+ * const description = RefSubject.map(state, ({ value }) => `${value} GB of 100 GB used`);
69
+ * return html`<section>
70
+ * <label for="storage-meter">Storage used</label>
71
+ * ${Meter.Meter({
72
+ * state, min: 0, max: 100, low: 60, high: 85, optimum: 0,
73
+ * content: description,
74
+ * props: { id: "storage-meter", "aria-valuetext": description },
75
+ * })}
76
+ * <p>${description}</p>
77
+ * </section>`;
78
+ * });
79
+ * ```
80
+ * @since 1.0.0
81
+ * @category Controls
82
+ */
83
+ export function Meter(options, host) {
84
+ return Dom.renderHost()(options, host, internalProps(options), options.content ?? "", (props, content) => {
85
+ return html `<meter ...${props}>${content}</meter>`;
86
+ });
87
+ }
@@ -0,0 +1,41 @@
1
+ import * as Effect from "effect/Effect";
2
+ import type * as Scope from "effect/Scope";
3
+ import { RefSubject } from "@typed/fx";
4
+ interface State {
5
+ readonly open: boolean;
6
+ }
7
+ /**
8
+ * Creates a scoped ref that synchronizes state to a native `<details>` element.
9
+ *
10
+ * @remarks
11
+ * ## Why
12
+ *
13
+ * `<details>` already owns disclosure rendering and keyboard behavior. This ref
14
+ * only applies pushed `open` state to that real element; the public Disclosure
15
+ * component listens to native `toggle` events for the reverse direction.
16
+ *
17
+ * ## Ownership and lifetime
18
+ *
19
+ * Applying the ref forks one observer in the current Effect Scope. Scope
20
+ * finalization interrupts it; the state remains owned by its original Scope and
21
+ * the DOM element remains owned by its renderer. Use only one hydration ref
22
+ * owner for an element.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * import { Effect } from "effect"
27
+ * import * as Disclosure from "@typed/ui/Disclosure"
28
+ * import * as NativeDetails from "@typed/ui/NativeDetails"
29
+ *
30
+ * const program = Effect.gen(function* () {
31
+ * const state = yield* Disclosure.makeState()
32
+ * return NativeDetails.ref(state)
33
+ * })
34
+ * ```
35
+ *
36
+ * @since 1.0.0
37
+ * @category Native synchronization
38
+ */
39
+ export declare function ref<S extends State, E, R>(state: RefSubject.RefSubject<S, E, R>): (element: HTMLDetailsElement) => Effect.Effect<void, E, R | Scope.Scope>;
40
+ export {};
41
+ //# sourceMappingURL=NativeDetails.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NativeDetails.d.ts","sourceRoot":"","sources":["../src/NativeDetails.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,KAAK,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAM,UAAU,EAAE,MAAM,WAAW,CAAC;AAE3C,UAAU,KAAK;IACb,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,GAAG,CAAC,CAAC,SAAS,KAAK,EAAE,CAAC,EAAE,CAAC,EACvC,KAAK,EAAE,UAAU,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GACpC,CAAC,OAAO,EAAE,kBAAkB,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAe1E"}
@@ -0,0 +1,40 @@
1
+ import * as Effect from "effect/Effect";
2
+ import { Fx } from "@typed/fx";
3
+ /**
4
+ * Creates a scoped ref that synchronizes state to a native `<details>` element.
5
+ *
6
+ * @remarks
7
+ * ## Why
8
+ *
9
+ * `<details>` already owns disclosure rendering and keyboard behavior. This ref
10
+ * only applies pushed `open` state to that real element; the public Disclosure
11
+ * component listens to native `toggle` events for the reverse direction.
12
+ *
13
+ * ## Ownership and lifetime
14
+ *
15
+ * Applying the ref forks one observer in the current Effect Scope. Scope
16
+ * finalization interrupts it; the state remains owned by its original Scope and
17
+ * the DOM element remains owned by its renderer. Use only one hydration ref
18
+ * owner for an element.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * import { Effect } from "effect"
23
+ * import * as Disclosure from "@typed/ui/Disclosure"
24
+ * import * as NativeDetails from "@typed/ui/NativeDetails"
25
+ *
26
+ * const program = Effect.gen(function* () {
27
+ * const state = yield* Disclosure.makeState()
28
+ * return NativeDetails.ref(state)
29
+ * })
30
+ * ```
31
+ *
32
+ * @since 1.0.0
33
+ * @category Native synchronization
34
+ */
35
+ export function ref(state) {
36
+ return Effect.fn((element) => Effect.asVoid(Effect.forkScoped(Fx.observe(state, Effect.fn((value) => Effect.sync(() => {
37
+ if (element.open !== value.open)
38
+ element.open = value.open;
39
+ }))))));
40
+ }
@@ -0,0 +1,66 @@
1
+ import * as Effect from "effect/Effect";
2
+ import type * as Scope from "effect/Scope";
3
+ import { RefSubject } from "@typed/fx";
4
+ interface State {
5
+ readonly open: boolean;
6
+ }
7
+ /** Native dialog synchronization mode.
8
+ * @remarks
9
+ * ## Why
10
+ * The choice maps directly to `showModal()` or `show()` rather than simulating
11
+ * modality in application code.
12
+ * ## Ownership and lifetime
13
+ * Options are inert and retain no resources.
14
+ * @since 1.0.0
15
+ * @category Native display options
16
+ */
17
+ export interface Options {
18
+ /** Whether opening uses the modal top layer; defaults to true.
19
+ * @remarks
20
+ * ## Why
21
+ * Modal and non-modal dialogs have different focus, inertness, and dismissal
22
+ * behavior owned by the browser.
23
+ * ## Ownership and lifetime
24
+ * The flag is read during synchronization and retains no resources.
25
+ * @since 1.0.0
26
+ * @category Native display options
27
+ */
28
+ readonly modal?: boolean;
29
+ }
30
+ /**
31
+ * Creates a scoped ref that drives a real `HTMLDialogElement` from state.
32
+ *
33
+ * @remarks
34
+ * ## Why
35
+ *
36
+ * The ref delegates open/close, top-layer placement, focus handling, and modal
37
+ * inertness to `showModal()`, `show()`, and `close()`. It does not reproduce a
38
+ * dialog with ordinary divs or synthetic events.
39
+ *
40
+ * ## Ownership and lifetime
41
+ *
42
+ * Applying the ref forks an observer in the current Effect Scope. Modal opening
43
+ * waits for a detached host to connect; a newer state or Scope finalization cancels
44
+ * that pending connection check. Hidden documents may defer the check until animation
45
+ * frames resume. Finalization does not close state owned elsewhere or remove
46
+ * the element. `Dialog.Content` handles native `cancel`, `close`, and `toggle`
47
+ * events and composes exactly one hydration owner.
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * import { Effect } from "effect"
52
+ * import * as Dialog from "@typed/ui/Dialog"
53
+ * import * as NativeDialog from "@typed/ui/NativeDialog"
54
+ *
55
+ * const program = Effect.gen(function* () {
56
+ * const state = yield* Dialog.makeState()
57
+ * return NativeDialog.ref(state, { modal: true })
58
+ * })
59
+ * ```
60
+ *
61
+ * @since 1.0.0
62
+ * @category Native synchronization
63
+ */
64
+ export declare function ref<S extends State, E, R>(state: RefSubject.RefSubject<S, E, R>, options?: Options): (element: HTMLDialogElement) => Effect.Effect<void, E, R | Scope.Scope>;
65
+ export {};
66
+ //# sourceMappingURL=NativeDialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NativeDialog.d.ts","sourceRoot":"","sources":["../src/NativeDialog.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,KAAK,KAAK,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAM,UAAU,EAAE,MAAM,WAAW,CAAC;AAE3C,UAAU,KAAK;IACb,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,OAAO;IACtB;;;;;;;;;OASG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,GAAG,CAAC,CAAC,SAAS,KAAK,EAAE,CAAC,EAAE,CAAC,EACvC,KAAK,EAAE,UAAU,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EACrC,OAAO,GAAE,OAAY,GACpB,CAAC,OAAO,EAAE,iBAAiB,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAqBzE"}
@@ -0,0 +1,88 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Fiber from "effect/Fiber";
3
+ import { Fx } from "@typed/fx";
4
+ /**
5
+ * Creates a scoped ref that drives a real `HTMLDialogElement` from state.
6
+ *
7
+ * @remarks
8
+ * ## Why
9
+ *
10
+ * The ref delegates open/close, top-layer placement, focus handling, and modal
11
+ * inertness to `showModal()`, `show()`, and `close()`. It does not reproduce a
12
+ * dialog with ordinary divs or synthetic events.
13
+ *
14
+ * ## Ownership and lifetime
15
+ *
16
+ * Applying the ref forks an observer in the current Effect Scope. Modal opening
17
+ * waits for a detached host to connect; a newer state or Scope finalization cancels
18
+ * that pending connection check. Hidden documents may defer the check until animation
19
+ * frames resume. Finalization does not close state owned elsewhere or remove
20
+ * the element. `Dialog.Content` handles native `cancel`, `close`, and `toggle`
21
+ * events and composes exactly one hydration owner.
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * import { Effect } from "effect"
26
+ * import * as Dialog from "@typed/ui/Dialog"
27
+ * import * as NativeDialog from "@typed/ui/NativeDialog"
28
+ *
29
+ * const program = Effect.gen(function* () {
30
+ * const state = yield* Dialog.makeState()
31
+ * return NativeDialog.ref(state, { modal: true })
32
+ * })
33
+ * ```
34
+ *
35
+ * @since 1.0.0
36
+ * @category Native synchronization
37
+ */
38
+ export function ref(state, options = {}) {
39
+ return Effect.fn(function* (element) {
40
+ let pending;
41
+ yield* Effect.forkScoped(Fx.observe(state, Effect.fn(function* (value) {
42
+ if (pending !== undefined) {
43
+ const previous = pending;
44
+ pending = undefined;
45
+ yield* Fiber.interrupt(previous);
46
+ }
47
+ const update = Effect.sync(() => synchronize(element, value.open, options));
48
+ if (value.open && options.modal !== false && !element.isConnected) {
49
+ pending = yield* Effect.forkScoped(Effect.andThen(whenConnected(element), update));
50
+ }
51
+ else {
52
+ // Complete connected transitions before queued native lifecycle events
53
+ // can reflect an older open state back into the application.
54
+ yield* update;
55
+ }
56
+ })));
57
+ });
58
+ }
59
+ // Template refs run before their host is inserted. Native showModal requires a connected element.
60
+ function whenConnected(element) {
61
+ return Effect.callback((resume) => {
62
+ if (element.isConnected) {
63
+ resume(Effect.void);
64
+ return;
65
+ }
66
+ const check = () => {
67
+ if (element.isConnected)
68
+ resume(Effect.void);
69
+ else
70
+ frame = requestAnimationFrame(check);
71
+ };
72
+ let frame = requestAnimationFrame(check);
73
+ return Effect.sync(() => cancelAnimationFrame(frame));
74
+ });
75
+ }
76
+ function synchronize(element, open, options) {
77
+ if (open) {
78
+ if (element.open)
79
+ return;
80
+ if (options.modal === false)
81
+ element.show();
82
+ else
83
+ element.showModal();
84
+ }
85
+ else if (element.open) {
86
+ element.close();
87
+ }
88
+ }
@@ -0,0 +1,43 @@
1
+ import * as Effect from "effect/Effect";
2
+ import type * as Scope from "effect/Scope";
3
+ import { RefSubject } from "@typed/fx";
4
+ interface State {
5
+ readonly open: boolean;
6
+ }
7
+ /**
8
+ * Creates a scoped ref that synchronizes state with the native Popover API.
9
+ *
10
+ * @remarks
11
+ * ## Why
12
+ *
13
+ * The browser owns top-layer placement and popover lifecycle. The ref checks
14
+ * `:popover-open` before calling `showPopover()` or `hidePopover()`, avoiding
15
+ * invalid duplicate transitions while preserving native toggle events.
16
+ *
17
+ * ## Ownership and lifetime
18
+ *
19
+ * Applying the ref forks one observer in the current Effect Scope. Opening
20
+ * waits for a detached host to connect. A newer state or Scope finalization
21
+ * cancels that pending connection check; hidden documents may defer it until
22
+ * animation frames resume. Closing the Scope interrupts observation without
23
+ * removing the host or closing caller-owned state. The host must support the Popover API and retain its `popover`
24
+ * attribute. Only one hydration owner may be composed for the element.
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * import { Effect } from "effect"
29
+ * import * as NativePopover from "@typed/ui/NativePopover"
30
+ * import * as Popover from "@typed/ui/Popover"
31
+ *
32
+ * const program = Effect.gen(function* () {
33
+ * const state = yield* Popover.makeState()
34
+ * return NativePopover.ref(state)
35
+ * })
36
+ * ```
37
+ *
38
+ * @since 1.0.0
39
+ * @category Native synchronization
40
+ */
41
+ export declare function ref<S extends State, E, R>(state: RefSubject.RefSubject<S, E, R>): (element: HTMLElement) => Effect.Effect<void, E, R | Scope.Scope>;
42
+ export {};
43
+ //# sourceMappingURL=NativePopover.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NativePopover.d.ts","sourceRoot":"","sources":["../src/NativePopover.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,KAAK,KAAK,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAM,UAAU,EAAE,MAAM,WAAW,CAAC;AAE3C,UAAU,KAAK;IACb,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,GAAG,CAAC,CAAC,SAAS,KAAK,EAAE,CAAC,EAAE,CAAC,EACvC,KAAK,EAAE,UAAU,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GACpC,CAAC,OAAO,EAAE,WAAW,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAqBnE"}
@@ -0,0 +1,84 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Fiber from "effect/Fiber";
3
+ import { Fx } from "@typed/fx";
4
+ /**
5
+ * Creates a scoped ref that synchronizes state with the native Popover API.
6
+ *
7
+ * @remarks
8
+ * ## Why
9
+ *
10
+ * The browser owns top-layer placement and popover lifecycle. The ref checks
11
+ * `:popover-open` before calling `showPopover()` or `hidePopover()`, avoiding
12
+ * invalid duplicate transitions while preserving native toggle events.
13
+ *
14
+ * ## Ownership and lifetime
15
+ *
16
+ * Applying the ref forks one observer in the current Effect Scope. Opening
17
+ * waits for a detached host to connect. A newer state or Scope finalization
18
+ * cancels that pending connection check; hidden documents may defer it until
19
+ * animation frames resume. Closing the Scope interrupts observation without
20
+ * removing the host or closing caller-owned state. The host must support the Popover API and retain its `popover`
21
+ * attribute. Only one hydration owner may be composed for the element.
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * import { Effect } from "effect"
26
+ * import * as NativePopover from "@typed/ui/NativePopover"
27
+ * import * as Popover from "@typed/ui/Popover"
28
+ *
29
+ * const program = Effect.gen(function* () {
30
+ * const state = yield* Popover.makeState()
31
+ * return NativePopover.ref(state)
32
+ * })
33
+ * ```
34
+ *
35
+ * @since 1.0.0
36
+ * @category Native synchronization
37
+ */
38
+ export function ref(state) {
39
+ return Effect.fn(function* (element) {
40
+ let pending;
41
+ yield* Effect.forkScoped(Fx.observe(state, Effect.fn(function* (value) {
42
+ if (pending !== undefined) {
43
+ const previous = pending;
44
+ pending = undefined;
45
+ yield* Fiber.interrupt(previous);
46
+ }
47
+ const update = Effect.sync(() => synchronize(element, value.open));
48
+ if (value.open && !element.isConnected) {
49
+ pending = yield* Effect.forkScoped(Effect.andThen(whenConnected(element), update));
50
+ }
51
+ else {
52
+ // Connected transitions stay in the observer: Menu/Select can focus
53
+ // their now-visible items immediately after updating open state.
54
+ yield* update;
55
+ }
56
+ })));
57
+ });
58
+ }
59
+ // Template refs run before insertion; native showPopover requires a connected host.
60
+ function whenConnected(element) {
61
+ return Effect.callback((resume) => {
62
+ if (element.isConnected) {
63
+ resume(Effect.void);
64
+ return;
65
+ }
66
+ const check = () => {
67
+ if (element.isConnected)
68
+ resume(Effect.void);
69
+ else
70
+ frame = requestAnimationFrame(check);
71
+ };
72
+ let frame = requestAnimationFrame(check);
73
+ return Effect.sync(() => cancelAnimationFrame(frame));
74
+ });
75
+ }
76
+ function synchronize(element, open) {
77
+ if (open) {
78
+ if (!element.matches(":popover-open"))
79
+ element.showPopover();
80
+ }
81
+ else if (element.matches(":popover-open")) {
82
+ element.hidePopover();
83
+ }
84
+ }