foldkit 0.101.0 → 0.102.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. package/README.md +2 -1
  2. package/dist/canvas/view.d.ts +1 -1
  3. package/dist/canvas/view.d.ts.map +1 -1
  4. package/dist/canvas/view.js +5 -5
  5. package/dist/command/index.d.ts +71 -0
  6. package/dist/command/index.d.ts.map +1 -1
  7. package/dist/command/index.js +34 -1
  8. package/dist/command/public.d.ts +1 -1
  9. package/dist/command/public.d.ts.map +1 -1
  10. package/dist/command/public.js +1 -1
  11. package/dist/devTools/overlay.d.ts.map +1 -1
  12. package/dist/devTools/overlay.js +137 -110
  13. package/dist/dom/dom.d.ts +8 -11
  14. package/dist/dom/dom.d.ts.map +1 -1
  15. package/dist/dom/dom.js +8 -11
  16. package/dist/dom/elementMovement.d.ts +1 -3
  17. package/dist/dom/elementMovement.d.ts.map +1 -1
  18. package/dist/dom/elementMovement.js +1 -3
  19. package/dist/dom/inert.d.ts +2 -4
  20. package/dist/dom/inert.d.ts.map +1 -1
  21. package/dist/dom/inert.js +2 -4
  22. package/dist/dom/scrollLock.d.ts +2 -2
  23. package/dist/dom/scrollLock.js +2 -2
  24. package/dist/dom/waitForAnimation.d.ts +1 -1
  25. package/dist/dom/waitForAnimation.js +1 -1
  26. package/dist/html/boundary.d.ts +98 -0
  27. package/dist/html/boundary.d.ts.map +1 -0
  28. package/dist/html/boundary.js +176 -0
  29. package/dist/html/childAttribute.d.ts +44 -0
  30. package/dist/html/childAttribute.d.ts.map +1 -0
  31. package/dist/html/childAttribute.js +34 -0
  32. package/dist/html/index.d.ts +70 -23
  33. package/dist/html/index.d.ts.map +1 -1
  34. package/dist/html/index.js +639 -575
  35. package/dist/html/lazy.d.ts +12 -7
  36. package/dist/html/lazy.d.ts.map +1 -1
  37. package/dist/html/lazy.js +30 -11
  38. package/dist/html/public.d.ts +2 -2
  39. package/dist/html/public.d.ts.map +1 -1
  40. package/dist/html/public.js +1 -1
  41. package/dist/html/runtimeSingleton.d.ts +72 -0
  42. package/dist/html/runtimeSingleton.d.ts.map +1 -0
  43. package/dist/html/runtimeSingleton.js +112 -0
  44. package/dist/html/submodel.d.ts +98 -0
  45. package/dist/html/submodel.d.ts.map +1 -0
  46. package/dist/html/submodel.js +190 -0
  47. package/dist/index.d.ts +1 -0
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +1 -0
  50. package/dist/render/render.d.ts +1 -1
  51. package/dist/render/render.js +1 -1
  52. package/dist/runtime/messagePriority.d.ts +5 -1
  53. package/dist/runtime/messagePriority.d.ts.map +1 -1
  54. package/dist/runtime/messagePriority.js +25 -4
  55. package/dist/runtime/runtime.d.ts +3 -1
  56. package/dist/runtime/runtime.d.ts.map +1 -1
  57. package/dist/runtime/runtime.js +123 -67
  58. package/dist/submodel/public.d.ts +4 -0
  59. package/dist/submodel/public.d.ts.map +1 -0
  60. package/dist/submodel/public.js +1 -0
  61. package/dist/submodel/submodel.d.ts +32 -0
  62. package/dist/submodel/submodel.d.ts.map +1 -0
  63. package/dist/submodel/submodel.js +1 -0
  64. package/dist/test/apps/disabledButton.d.ts +4 -5
  65. package/dist/test/apps/disabledButton.d.ts.map +1 -1
  66. package/dist/test/apps/disabledButton.js +16 -16
  67. package/dist/test/scene.d.ts +8 -8
  68. package/dist/test/scene.d.ts.map +1 -1
  69. package/dist/test/scene.js +25 -13
  70. package/dist/test/story.d.ts +15 -8
  71. package/dist/test/story.d.ts.map +1 -1
  72. package/dist/test/story.js +21 -9
  73. package/dist/ui/animation/index.d.ts +30 -14
  74. package/dist/ui/animation/index.d.ts.map +1 -1
  75. package/dist/ui/animation/index.js +9 -19
  76. package/dist/ui/animation/public.d.ts +2 -2
  77. package/dist/ui/animation/public.d.ts.map +1 -1
  78. package/dist/ui/animation/public.js +1 -1
  79. package/dist/ui/calendar/index.d.ts +199 -84
  80. package/dist/ui/calendar/index.d.ts.map +1 -1
  81. package/dist/ui/calendar/index.js +129 -140
  82. package/dist/ui/calendar/public.d.ts +2 -2
  83. package/dist/ui/calendar/public.d.ts.map +1 -1
  84. package/dist/ui/calendar/public.js +1 -1
  85. package/dist/ui/checkbox/index.d.ts +93 -21
  86. package/dist/ui/checkbox/index.d.ts.map +1 -1
  87. package/dist/ui/checkbox/index.js +62 -33
  88. package/dist/ui/checkbox/public.d.ts +2 -2
  89. package/dist/ui/checkbox/public.d.ts.map +1 -1
  90. package/dist/ui/checkbox/public.js +1 -1
  91. package/dist/ui/combobox/multi.d.ts +35 -91
  92. package/dist/ui/combobox/multi.d.ts.map +1 -1
  93. package/dist/ui/combobox/multi.js +34 -17
  94. package/dist/ui/combobox/multiPublic.d.ts +2 -2
  95. package/dist/ui/combobox/multiPublic.d.ts.map +1 -1
  96. package/dist/ui/combobox/multiPublic.js +1 -1
  97. package/dist/ui/combobox/public.d.ts +3 -3
  98. package/dist/ui/combobox/public.d.ts.map +1 -1
  99. package/dist/ui/combobox/public.js +2 -2
  100. package/dist/ui/combobox/shared.d.ts +56 -31
  101. package/dist/ui/combobox/shared.d.ts.map +1 -1
  102. package/dist/ui/combobox/shared.js +333 -322
  103. package/dist/ui/combobox/single.d.ts +46 -93
  104. package/dist/ui/combobox/single.d.ts.map +1 -1
  105. package/dist/ui/combobox/single.js +44 -17
  106. package/dist/ui/datePicker/index.d.ts +256 -48
  107. package/dist/ui/datePicker/index.d.ts.map +1 -1
  108. package/dist/ui/datePicker/index.js +149 -104
  109. package/dist/ui/datePicker/public.d.ts +2 -2
  110. package/dist/ui/datePicker/public.d.ts.map +1 -1
  111. package/dist/ui/datePicker/public.js +1 -1
  112. package/dist/ui/dialog/index.d.ts +95 -39
  113. package/dist/ui/dialog/index.d.ts.map +1 -1
  114. package/dist/ui/dialog/index.js +71 -62
  115. package/dist/ui/dialog/public.d.ts +2 -2
  116. package/dist/ui/dialog/public.d.ts.map +1 -1
  117. package/dist/ui/dialog/public.js +1 -1
  118. package/dist/ui/disclosure/index.d.ts +71 -31
  119. package/dist/ui/disclosure/index.d.ts.map +1 -1
  120. package/dist/ui/disclosure/index.js +57 -62
  121. package/dist/ui/disclosure/public.d.ts +2 -2
  122. package/dist/ui/disclosure/public.d.ts.map +1 -1
  123. package/dist/ui/disclosure/public.js +1 -1
  124. package/dist/ui/dragAndDrop/index.d.ts +6 -6
  125. package/dist/ui/dragAndDrop/index.d.ts.map +1 -1
  126. package/dist/ui/dragAndDrop/index.js +7 -7
  127. package/dist/ui/dragAndDrop/public.d.ts +1 -1
  128. package/dist/ui/dragAndDrop/public.d.ts.map +1 -1
  129. package/dist/ui/dragAndDrop/public.js +1 -1
  130. package/dist/ui/fileDrop/index.d.ts +42 -46
  131. package/dist/ui/fileDrop/index.d.ts.map +1 -1
  132. package/dist/ui/fileDrop/index.js +30 -46
  133. package/dist/ui/fileDrop/public.d.ts +2 -2
  134. package/dist/ui/fileDrop/public.d.ts.map +1 -1
  135. package/dist/ui/fileDrop/public.js +1 -1
  136. package/dist/ui/listbox/multi.d.ts +39 -84
  137. package/dist/ui/listbox/multi.d.ts.map +1 -1
  138. package/dist/ui/listbox/multi.js +38 -20
  139. package/dist/ui/listbox/multiPublic.d.ts +2 -2
  140. package/dist/ui/listbox/multiPublic.d.ts.map +1 -1
  141. package/dist/ui/listbox/multiPublic.js +1 -1
  142. package/dist/ui/listbox/public.d.ts +3 -3
  143. package/dist/ui/listbox/public.d.ts.map +1 -1
  144. package/dist/ui/listbox/public.js +2 -2
  145. package/dist/ui/listbox/shared.d.ts +71 -30
  146. package/dist/ui/listbox/shared.d.ts.map +1 -1
  147. package/dist/ui/listbox/shared.js +319 -296
  148. package/dist/ui/listbox/single.d.ts +57 -85
  149. package/dist/ui/listbox/single.d.ts.map +1 -1
  150. package/dist/ui/listbox/single.js +48 -24
  151. package/dist/ui/menu/index.d.ts +80 -36
  152. package/dist/ui/menu/index.d.ts.map +1 -1
  153. package/dist/ui/menu/index.js +117 -86
  154. package/dist/ui/menu/public.d.ts +2 -2
  155. package/dist/ui/menu/public.d.ts.map +1 -1
  156. package/dist/ui/menu/public.js +1 -1
  157. package/dist/ui/popover/index.d.ts +117 -44
  158. package/dist/ui/popover/index.d.ts.map +1 -1
  159. package/dist/ui/popover/index.js +88 -101
  160. package/dist/ui/popover/public.d.ts +2 -2
  161. package/dist/ui/popover/public.d.ts.map +1 -1
  162. package/dist/ui/popover/public.js +1 -1
  163. package/dist/ui/radioGroup/index.d.ts +122 -45
  164. package/dist/ui/radioGroup/index.d.ts.map +1 -1
  165. package/dist/ui/radioGroup/index.js +111 -72
  166. package/dist/ui/radioGroup/public.d.ts +2 -2
  167. package/dist/ui/radioGroup/public.d.ts.map +1 -1
  168. package/dist/ui/radioGroup/public.js +1 -1
  169. package/dist/ui/slider/index.d.ts +72 -34
  170. package/dist/ui/slider/index.d.ts.map +1 -1
  171. package/dist/ui/slider/index.js +40 -49
  172. package/dist/ui/slider/public.d.ts +2 -2
  173. package/dist/ui/slider/public.d.ts.map +1 -1
  174. package/dist/ui/slider/public.js +1 -1
  175. package/dist/ui/switch/index.d.ts +74 -21
  176. package/dist/ui/switch/index.d.ts.map +1 -1
  177. package/dist/ui/switch/index.js +62 -33
  178. package/dist/ui/switch/public.d.ts +2 -2
  179. package/dist/ui/switch/public.d.ts.map +1 -1
  180. package/dist/ui/switch/public.js +1 -1
  181. package/dist/ui/tabs/index.d.ts +107 -45
  182. package/dist/ui/tabs/index.d.ts.map +1 -1
  183. package/dist/ui/tabs/index.js +99 -81
  184. package/dist/ui/tabs/public.d.ts +2 -2
  185. package/dist/ui/tabs/public.d.ts.map +1 -1
  186. package/dist/ui/tabs/public.js +1 -1
  187. package/dist/ui/toast/index.d.ts +93 -109
  188. package/dist/ui/toast/index.d.ts.map +1 -1
  189. package/dist/ui/toast/index.js +16 -29
  190. package/dist/ui/toast/schema.d.ts +15 -4
  191. package/dist/ui/toast/schema.d.ts.map +1 -1
  192. package/dist/ui/toast/schema.js +11 -4
  193. package/dist/ui/toast/update.d.ts +36 -18
  194. package/dist/ui/toast/update.d.ts.map +1 -1
  195. package/dist/ui/toast/update.js +33 -14
  196. package/dist/ui/tooltip/index.d.ts +94 -42
  197. package/dist/ui/tooltip/index.d.ts.map +1 -1
  198. package/dist/ui/tooltip/index.js +64 -73
  199. package/dist/ui/tooltip/public.d.ts +2 -2
  200. package/dist/ui/tooltip/public.d.ts.map +1 -1
  201. package/dist/ui/tooltip/public.js +1 -1
  202. package/dist/ui/virtualList/index.d.ts +18 -41
  203. package/dist/ui/virtualList/index.d.ts.map +1 -1
  204. package/dist/ui/virtualList/index.js +17 -37
  205. package/dist/ui/virtualList/public.d.ts +2 -2
  206. package/dist/ui/virtualList/public.d.ts.map +1 -1
  207. package/dist/ui/virtualList/public.js +1 -1
  208. package/dist/vdom.d.ts +3 -2
  209. package/dist/vdom.d.ts.map +1 -1
  210. package/dist/vdom.js +44 -0
  211. package/package.json +1 -1
@@ -1,13 +1,13 @@
1
- import { Duration, Effect, Equal, Match as M, Number, Option, Schema as S, } from 'effect';
1
+ import { Duration, Effect, Equal, Function, Match as M, Number, Option, Schema as S, } from 'effect';
2
2
  import * as Command from '../../command/index.js';
3
3
  import { OptionExt } from '../../effectExtensions/index.js';
4
- import { createLazy, html, } from '../../html/index.js';
4
+ import { childAttributes, defineView, html, } from '../../html/index.js';
5
5
  import { m } from '../../message/index.js';
6
6
  import * as Mount from '../../mount/index.js';
7
7
  import { evo } from '../../struct/index.js';
8
8
  import { AnchorConfig, anchorSetup } from '../anchor.js';
9
9
  // MODEL
10
- /** Schema for the tooltip component's state. `isOpen` is visibility; `isHovered` tracks pointer on trigger; `isFocused` tracks tooltip-affirming focus on the trigger (focus arriving without a preceding mouse press keyboard, touch, or pen; mouse-click-induced focus is excluded since it doesn't affirm the user wants the tooltip visible); `isDismissed` suppresses re-opening after the user dismissed the tooltip (via Escape or left-click) until they disengage (leave or blur). `showDelay` is the hover-to-show duration. `maybeLastPointerType` records the most recent pointer type that pressed the trigger, so a mouse-click-induced focus can be distinguished from other focus. */
10
+ /** Schema for the tooltip component's state. `isOpen` is visibility; `isHovered` tracks pointer on trigger; `isFocused` tracks tooltip-affirming focus on the trigger (focus arriving without a preceding mouse press, like keyboard, touch, or pen; mouse-click-induced focus is excluded since it doesn't affirm the user wants the tooltip visible); `isDismissed` suppresses re-opening after the user dismissed the tooltip (via Escape or left-click) until they disengage (leave or blur). `showDelay` is the hover-to-show duration. `maybeLastPointerType` records the most recent pointer type that pressed the trigger, so a mouse-click-induced focus can be distinguished from other focus. */
11
11
  export const Model = S.Struct({
12
12
  id: S.String,
13
13
  isOpen: S.Boolean,
@@ -19,30 +19,26 @@ export const Model = S.Struct({
19
19
  maybeLastPointerType: S.Option(S.String),
20
20
  });
21
21
  // MESSAGE
22
- /** Sent when the pointer enters the tooltip trigger. Starts the show-delay timer. */
22
+ /** Sent when the pointer enters the tooltip trigger. */
23
23
  export const EnteredTrigger = m('EnteredTrigger');
24
- /** Sent when the pointer leaves the tooltip trigger. Cancels any pending show-delay and hides the tooltip unless focus is active. */
24
+ /** Sent when the pointer leaves the tooltip trigger. */
25
25
  export const LeftTrigger = m('LeftTrigger');
26
- /** Sent when focus enters the trigger. Shows the tooltip immediately unless the focus was caused by a mouse press, in which case the hover-delay path handles it instead. */
26
+ /** Sent when focus enters the trigger. */
27
27
  export const FocusedTrigger = m('FocusedTrigger');
28
- /** Sent when focus leaves the trigger. Hides the tooltip unless hover is active. */
28
+ /** Sent when focus leaves the trigger. */
29
29
  export const BlurredTrigger = m('BlurredTrigger');
30
- /** Sent when Escape is pressed while the tooltip is visible. Hides the tooltip and flags `isDismissed` so hover and focus do not re-open it until the user disengages (leaves or blurs the trigger). */
30
+ /** Sent when Escape is pressed while the tooltip is visible. */
31
31
  export const PressedEscape = m('PressedEscape');
32
- /** Sent when a pointer presses the trigger. Records the pointer type so a following focus event from the same mouse click can be suppressed, and a left-click on an open tooltip dismisses it (the user is clicking the button for its action, not to keep the tooltip visible). */
32
+ /** Sent when a pointer presses the trigger. */
33
33
  export const PressedPointerOnTrigger = m('PressedPointerOnTrigger', {
34
34
  pointerType: S.String,
35
35
  button: S.Number,
36
36
  });
37
- /** Sent when the show-delay timer fires. Carries a generation number that is compared against the current pending version to discard stale timers. */
37
+ /** Sent when the show-delay timer fires. */
38
38
  export const ElapsedShowDelay = m('ElapsedShowDelay', {
39
39
  version: S.Number,
40
40
  });
41
- /** Signals that the show-delay has changed (e.g. in response to a user preference, input-method change, or reduced-motion setting). Does not affect the current open/closed state; the new delay applies to the next hover. Typically dispatched via the `setShowDelay` helper. */
42
- export const ChangedShowDelay = m('ChangedShowDelay', {
43
- showDelay: S.DurationFromMillis,
44
- });
45
- /** Sent when the tooltip panel mounts and Floating UI has positioned it. Update no-ops; the side effect is the act of positioning, surfaced for DevTools observability. */
41
+ /** Sent when the tooltip panel mounts and Floating UI has positioned it. */
46
42
  export const CompletedAnchorTooltip = m('CompletedAnchorTooltip');
47
43
  /** Union of all messages the tooltip component can produce. */
48
44
  export const Message = S.Union([
@@ -53,9 +49,17 @@ export const Message = S.Union([
53
49
  PressedEscape,
54
50
  PressedPointerOnTrigger,
55
51
  ElapsedShowDelay,
56
- ChangedShowDelay,
57
52
  CompletedAnchorTooltip,
58
53
  ]);
54
+ // OUT MESSAGE
55
+ /** Emitted once the tooltip transitions to visible (`isOpen` becomes true).
56
+ * Consumers typically use this for analytics, instrumentation, or to
57
+ * coordinate with other transient UI. */
58
+ export const Shown = m('Shown');
59
+ /** Emitted once the tooltip transitions to hidden (`isOpen` becomes false). */
60
+ export const Hidden = m('Hidden');
61
+ /** Union of out-messages the tooltip component can produce. */
62
+ export const OutMessage = S.Union([Shown, Hidden]);
59
63
  // INIT
60
64
  const DEFAULT_SHOW_DELAY = Duration.millis(500);
61
65
  const LEFT_MOUSE_BUTTON = 0;
@@ -73,11 +77,9 @@ export const init = (config) => ({
73
77
  maybeLastPointerType: Option.none(),
74
78
  });
75
79
  const withUpdateReturn = M.withReturnType();
76
- /** Waits for the tooltip's show delay before emitting `ElapsedShowDelay`. The version is echoed back so a stale timer is ignored when the user leaves before the delay fires. */
80
+ /** Waits for the tooltip's show delay before emitting `ElapsedShowDelay`. */
77
81
  export const ShowAfterDelay = Command.define('ShowAfterDelay', { delay: S.DurationFromMillis, version: S.Number }, ElapsedShowDelay)(({ delay, version }) => Effect.sleep(delay).pipe(Effect.as(ElapsedShowDelay({ version }))));
78
- /** The anchor-positioning Mount this Tooltip renders on its panel. Exposed so
79
- * Scene tests can call `Scene.Mount.resolve(AnchorTooltip, CompletedAnchorTooltip())`
80
- * to acknowledge the mount produced by the rendered panel. */
82
+ /** The anchor-positioning Mount this Tooltip renders on its panel. */
81
83
  export const AnchorTooltip = Mount.define('AnchorTooltip', { buttonId: S.String, anchor: AnchorConfig }, CompletedAnchorTooltip)(({ buttonId, anchor }) => element => Effect.gen(function* () {
82
84
  yield* Effect.acquireRelease(Effect.sync(() => anchorSetup({
83
85
  buttonId,
@@ -86,8 +88,7 @@ export const AnchorTooltip = Mount.define('AnchorTooltip', { buttonId: S.String,
86
88
  })(element)), cleanup => Effect.sync(cleanup));
87
89
  return CompletedAnchorTooltip();
88
90
  }));
89
- /** Processes a tooltip message and returns the next model and commands. */
90
- export const update = (model, message) => M.value(message).pipe(withUpdateReturn, M.tagsExhaustive({
91
+ const computeUpdate = (model, message) => M.value(message).pipe(withUpdateReturn, M.tagsExhaustive({
91
92
  EnteredTrigger: () => {
92
93
  if (model.isOpen || model.isDismissed) {
93
94
  return [evo(model, { isHovered: () => true }), []];
@@ -186,21 +187,36 @@ export const update = (model, message) => M.value(message).pipe(withUpdateReturn
186
187
  }
187
188
  return [evo(model, { isOpen: () => true }), []];
188
189
  },
189
- ChangedShowDelay: ({ showDelay }) => [
190
- evo(model, { showDelay: () => showDelay }),
191
- [],
192
- ],
193
190
  CompletedAnchorTooltip: () => [model, []],
194
191
  }));
195
- /** Programmatically updates the tooltip's hover show-delay. Use this in response to user preference changes, input-method switches, or reduced-motion settings. The new delay applies to the next hover; any pending timer is unaffected (its stale version will discard harmlessly when it fires). */
196
- export const setShowDelay = (model, showDelay) => update(model, ChangedShowDelay({ showDelay: Duration.fromInputUnsafe(showDelay) }));
197
- /** Renders a headless tooltip with an anchored non-interactive panel. Shows on hover (after delay) or focus (from keyboard, touch, or pen; mouse-click focus is excluded); hides on leave, blur, Escape, or left-click of the trigger. Uses `role="tooltip"` and links the trigger via `aria-describedby`. */
198
- export const view = (config) => {
192
+ /** Processes a tooltip message and returns the next model, commands, and
193
+ * an optional OutMessage. `Shown`/`Hidden` fire only on `isOpen`
194
+ * transitions, so consumers don't get spurious events for messages that
195
+ * only update hover/focus/delay state without changing visibility. */
196
+ export const update = (model, message) => {
197
+ const [nextModel, commands] = computeUpdate(model, message);
198
+ const maybeOutMessage = !model.isOpen && nextModel.isOpen
199
+ ? Option.some(Shown())
200
+ : model.isOpen && !nextModel.isOpen
201
+ ? Option.some(Hidden())
202
+ : Option.none();
203
+ return [nextModel, commands, maybeOutMessage];
204
+ };
205
+ /** Reflects an externally-sourced hover show-delay onto the model without
206
+ * emitting an OutMessage. Use to mirror an external config value (a user
207
+ * preference, a restored setting) onto the tooltip. */
208
+ export const reflectShowDelay = Function.dual(2, (model, showDelay) => evo(model, { showDelay: () => Duration.fromInputUnsafe(showDelay) }));
209
+ /** Renders a headless tooltip with an anchored non-interactive panel.
210
+ * Shows on hover (after delay) or focus (from keyboard, touch, or pen;
211
+ * mouse-click focus is excluded); hides on leave, blur, Escape, or
212
+ * left-click of the trigger. */
213
+ export const view = defineView((model, viewInputs) => {
199
214
  const h = html();
200
- const { model: { id, isOpen }, toParentMessage, anchor, triggerContent, triggerClassName, triggerAttributes = [], content, panelClassName, panelAttributes = [], isDisabled, className, attributes = [], } = config;
201
- const handleTriggerKeyDown = (key) => M.value(key).pipe(M.when('Escape', () => OptionExt.when(isOpen, toParentMessage(PressedEscape()))), M.orElse(() => Option.none()));
202
- const handleTriggerPointerDown = (pointerType, button) => Option.some(toParentMessage(PressedPointerOnTrigger({ pointerType, button })));
203
- const resolvedTriggerAttributes = [
215
+ const { id, isOpen } = model;
216
+ const { anchor, toView, isDisabled } = viewInputs;
217
+ const handleTriggerKeyDown = (key) => M.value(key).pipe(M.when('Escape', () => OptionExt.when(isOpen, PressedEscape())), M.orElse(() => Option.none()));
218
+ const handleTriggerPointerDown = (pointerType, button) => Option.some(PressedPointerOnTrigger({ pointerType, button }));
219
+ const triggerAttributes = [
204
220
  h.Id(`${id}-trigger`),
205
221
  h.Type('button'),
206
222
  h.AriaDescribedBy(`${id}-panel`),
@@ -208,54 +224,29 @@ export const view = (config) => {
208
224
  ...(isDisabled
209
225
  ? [h.AriaDisabled(true), h.DataAttribute('disabled', '')]
210
226
  : [
211
- h.OnMouseEnter(toParentMessage(EnteredTrigger())),
212
- h.OnMouseLeave(toParentMessage(LeftTrigger())),
213
- h.OnFocus(toParentMessage(FocusedTrigger())),
214
- h.OnBlur(toParentMessage(BlurredTrigger())),
227
+ h.OnMouseEnter(EnteredTrigger()),
228
+ h.OnMouseLeave(LeftTrigger()),
229
+ h.OnFocus(FocusedTrigger()),
230
+ h.OnBlur(BlurredTrigger()),
215
231
  h.OnKeyDownPreventDefault(handleTriggerKeyDown),
216
232
  h.OnPointerDown(handleTriggerPointerDown),
217
233
  ]),
218
- ...(triggerClassName ? [h.Class(triggerClassName)] : []),
219
- ...triggerAttributes,
220
234
  ];
221
- const anchorTooltip = Mount.mapMessage(AnchorTooltip({ buttonId: `${id}-trigger`, anchor }), toParentMessage);
222
- const anchorAttributes = [
235
+ const panelAttributes = [
236
+ h.Id(`${id}-panel`),
237
+ h.Role('tooltip'),
223
238
  h.Style({
224
239
  position: 'absolute',
225
240
  margin: '0',
226
241
  visibility: 'hidden',
227
242
  pointerEvents: 'none',
228
243
  }),
229
- h.OnMount(anchorTooltip),
230
- ];
231
- const resolvedPanelAttributes = [
232
- h.Id(`${id}-panel`),
233
- h.Role('tooltip'),
234
- ...anchorAttributes,
244
+ h.OnMount(AnchorTooltip({ buttonId: `${id}-trigger`, anchor })),
235
245
  ...(isOpen ? [h.DataAttribute('open', '')] : []),
236
- ...(panelClassName ? [h.Class(panelClassName)] : []),
237
- ...panelAttributes,
238
- ];
239
- const wrapperAttributes = [
240
- ...(className ? [h.Class(className)] : []),
241
- ...attributes,
242
246
  ];
243
- return h.div(wrapperAttributes, [
244
- h.keyed('button')(`${id}-trigger`, resolvedTriggerAttributes, [
245
- triggerContent,
246
- ]),
247
- ...(isOpen
248
- ? [h.keyed('div')(`${id}-panel`, resolvedPanelAttributes, [content])]
249
- : []),
250
- ]);
251
- };
252
- /** Creates a memoized tooltip view. Static config is captured in a closure;
253
- * only `model` and `toParentMessage` are compared per render via `createLazy`. */
254
- export const lazy = (staticConfig) => {
255
- const lazyView = createLazy();
256
- return (model, toParentMessage) => lazyView((currentModel, currentToParentMessage) => view({
257
- ...staticConfig,
258
- model: currentModel,
259
- toParentMessage: currentToParentMessage,
260
- }), [model, toParentMessage]);
261
- };
247
+ return toView({
248
+ trigger: childAttributes(triggerAttributes),
249
+ panel: childAttributes(panelAttributes),
250
+ isVisible: isOpen,
251
+ });
252
+ });
@@ -1,4 +1,4 @@
1
- export { init, update, view, lazy, setShowDelay, Model, Message, EnteredTrigger, LeftTrigger, FocusedTrigger, BlurredTrigger, PressedEscape, PressedPointerOnTrigger, ElapsedShowDelay, ChangedShowDelay, ShowAfterDelay, CompletedAnchorTooltip, AnchorTooltip, } from './index.js';
2
- export type { InitConfig, ViewConfig } from './index.js';
1
+ export { init, update, view, reflectShowDelay, Model, Message, OutMessage, Shown, Hidden, EnteredTrigger, LeftTrigger, FocusedTrigger, BlurredTrigger, PressedEscape, PressedPointerOnTrigger, ElapsedShowDelay, ShowAfterDelay, CompletedAnchorTooltip, AnchorTooltip, } from './index.js';
2
+ export type { InitConfig, ViewInputs, RenderInfo } from './index.js';
3
3
  export type { AnchorConfig } from '../anchor.js';
4
4
  //# sourceMappingURL=public.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../../src/ui/tooltip/public.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,IAAI,EACJ,MAAM,EACN,IAAI,EACJ,IAAI,EACJ,YAAY,EACZ,KAAK,EACL,OAAO,EACP,cAAc,EACd,WAAW,EACX,cAAc,EACd,cAAc,EACd,aAAa,EACb,uBAAuB,EACvB,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,EACd,sBAAsB,EACtB,aAAa,GACd,MAAM,YAAY,CAAA;AAEnB,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAExD,YAAY,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA"}
1
+ {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../../src/ui/tooltip/public.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,IAAI,EACJ,MAAM,EACN,IAAI,EACJ,gBAAgB,EAChB,KAAK,EACL,OAAO,EACP,UAAU,EACV,KAAK,EACL,MAAM,EACN,cAAc,EACd,WAAW,EACX,cAAc,EACd,cAAc,EACd,aAAa,EACb,uBAAuB,EACvB,gBAAgB,EAChB,cAAc,EACd,sBAAsB,EACtB,aAAa,GACd,MAAM,YAAY,CAAA;AAEnB,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAEpE,YAAY,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA"}
@@ -1 +1 @@
1
- export { init, update, view, lazy, setShowDelay, Model, Message, EnteredTrigger, LeftTrigger, FocusedTrigger, BlurredTrigger, PressedEscape, PressedPointerOnTrigger, ElapsedShowDelay, ChangedShowDelay, ShowAfterDelay, CompletedAnchorTooltip, AnchorTooltip, } from './index.js';
1
+ export { init, update, view, reflectShowDelay, Model, Message, OutMessage, Shown, Hidden, EnteredTrigger, LeftTrigger, FocusedTrigger, BlurredTrigger, PressedEscape, PressedPointerOnTrigger, ElapsedShowDelay, ShowAfterDelay, CompletedAnchorTooltip, AnchorTooltip, } from './index.js';
@@ -1,6 +1,6 @@
1
1
  import { Effect, Option, Schema as S, Stream } from 'effect';
2
2
  import * as Command from '../../command/index.js';
3
- import { type Attribute, type Html, type TagName } from '../../html/index.js';
3
+ import { type ChildAttribute, type Html, type SubmodelView, type TagName } from '../../html/index.js';
4
4
  /** Schema for the virtual list's state. Tracks scroll position, container
5
5
  * measurement, and any in-flight programmatic scroll. */
6
6
  export declare const Model: S.Struct<{
@@ -181,55 +181,32 @@ export declare const subscriptions: {
181
181
  readonly __subscription: never;
182
182
  };
183
183
  };
184
- /** Configuration for rendering a virtual list with `view`.
184
+ /** Per-render view inputs passed to `view` via `h.submodel`'s `viewInputs` field.
185
185
  *
186
- * VirtualList does not take a `toParentMessage` callback. All input
186
+ * VirtualList does not surface event handlers in the view. All input
187
187
  * (scroll events and resize observations) flows through the
188
- * `containerEvents` Subscription, not through view-bound handlers.
189
- * Consumers wrap the subscription's stream into their parent Message in
190
- * their own `subscriptions` definition. */
191
- export type ViewConfig<ParentMessage, Item> = Readonly<{
192
- model: Model;
188
+ * `containerEvents` Subscription. The consumer wraps that
189
+ * Subscription's stream into their parent Message in their own
190
+ * `subscriptions` definition. */
191
+ export type ViewInputs<Item> = Readonly<{
193
192
  items: ReadonlyArray<Item>;
194
193
  itemToKey: (item: Item, index: number) => string;
195
194
  itemToView: (item: Item, index: number) => Html;
196
- /** Optional per-item row height in pixels. When provided, the list renders
197
- * with variable-height rows: each row's wrapper takes the height returned
198
- * by this callback, and scroll math walks the items to compute the visible
199
- * slice and spacers. When absent, all rows use `model.rowHeightPx`. Use
200
- * this for tables with wrapping cells, taller detail rows, or any list
201
- * where rows differ. Prefer the uniform `rowHeightPx` path when row
202
- * heights are stable: it avoids the per-render walk over `items`. */
203
195
  itemToRowHeightPx?: (item: Item, index: number) => number;
204
- /** Number of rows rendered above and below the visible viewport. Higher
205
- * values smooth out fast scroll at the cost of mounting more DOM. Default
206
- * is 5; react-window uses 1 and react-virtualized uses 3. Pick a value
207
- * that suits the row's mount cost. */
208
196
  overscan?: number;
209
197
  rowElement?: TagName;
210
- className?: string;
211
- attributes?: ReadonlyArray<Attribute<ParentMessage>>;
198
+ containerClassName?: string;
199
+ containerAttributes?: ReadonlyArray<ChildAttribute>;
212
200
  }>;
213
201
  /** Renders a virtualized list. Only items inside the viewport (plus an
214
- * overscan buffer) are mounted; spacer elements above and below the slice
215
- * keep the scrollbar's apparent total height correct.
202
+ * overscan buffer) are mounted; spacer elements above and below the
203
+ * slice keep the scrollbar's apparent total height correct.
216
204
  *
217
- * Items must be keyed via `itemToKey` so the VDOM matches `row 150` to
218
- * `row 150` after the slice shifts during scroll, rather than matching by
219
- * position and producing stale DOM.
220
- *
221
- * Each row wrapper is rendered with `display: grid` so the consumer's
222
- * `itemToView` content fills the configured `rowHeightPx` and the full row
223
- * width. Use flex/grid with `align-items: center` inside `itemToView` to
224
- * vertically center content within the row.
225
- *
226
- * Each row carries `aria-setsize` (total item count) and `aria-posinset`
227
- * (1-based logical row index) so screen readers announce the full list
228
- * size and each row's position within it, rather than the much smaller
229
- * count of currently mounted rows. */
230
- export declare const view: <ParentMessage, Item>(config: ViewConfig<ParentMessage, Item>) => Html;
231
- /** Creates a memoized virtual list view. Static config is captured in a
232
- * closure; only `model` and `items` are compared per render via
233
- * `createLazy`. */
234
- export declare const lazy: <ParentMessage, Item>(staticConfig: Omit<ViewConfig<ParentMessage, Item>, "model" | "items">) => ((model: Model, items: ReadonlyArray<Item>) => Html);
205
+ * Generic over `Item`: call as `Ui.VirtualList.view<MyItem>()` at the
206
+ * embed site to get a `SubmodelView` typed for your item type. The
207
+ * underlying view implementation is shared; the call only narrows the
208
+ * type. */
209
+ type ViewForItem<Item> = SubmodelView<Model, Message, ViewInputs<Item>>;
210
+ export declare const view: <Item>() => ViewForItem<Item>;
211
+ export {};
235
212
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ui/virtualList/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ui/virtualList/index.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { Array, Effect, Match as M, Number, Option, Queue, Schema as S, Stream, pipe, } from 'effect';
2
2
  import * as Command from '../../command/index.js';
3
- import { createLazy, html, } from '../../html/index.js';
3
+ import { childAttributes, defineView, html, } from '../../html/index.js';
4
4
  import { m } from '../../message/index.js';
5
5
  import * as Subscription from '../../runtime/subscription.js';
6
6
  import { ts } from '../../schema/index.js';
@@ -332,27 +332,13 @@ export const subscriptions = Subscription.make()(entry => ({
332
332
  }));
333
333
  // VIEW
334
334
  const DEFAULT_OVERSCAN = 5;
335
- /** Renders a virtualized list. Only items inside the viewport (plus an
336
- * overscan buffer) are mounted; spacer elements above and below the slice
337
- * keep the scrollbar's apparent total height correct.
338
- *
339
- * Items must be keyed via `itemToKey` so the VDOM matches `row 150` to
340
- * `row 150` after the slice shifts during scroll, rather than matching by
341
- * position and producing stale DOM.
342
- *
343
- * Each row wrapper is rendered with `display: grid` so the consumer's
344
- * `itemToView` content fills the configured `rowHeightPx` and the full row
345
- * width. Use flex/grid with `align-items: center` inside `itemToView` to
346
- * vertically center content within the row.
347
- *
348
- * Each row carries `aria-setsize` (total item count) and `aria-posinset`
349
- * (1-based logical row index) so screen readers announce the full list
350
- * size and each row's position within it, rather than the much smaller
351
- * count of currently mounted rows. */
352
- export const view = (config) => {
335
+ export const view = () =>
336
+ /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
337
+ viewImpl;
338
+ const viewImpl = defineView((model, viewInputs) => {
353
339
  const h = html();
354
- const { model, items, itemToKey, itemToView, itemToRowHeightPx, overscan = DEFAULT_OVERSCAN, rowElement = 'li', className, attributes = [], } = config;
355
- const containerAttributes = [
340
+ const { items, itemToKey, itemToView, itemToRowHeightPx, overscan = DEFAULT_OVERSCAN, rowElement = 'li', containerClassName, containerAttributes = [], } = viewInputs;
341
+ const baseContainerAttributes = [
356
342
  h.Id(model.id),
357
343
  h.Role('list'),
358
344
  h.DataAttribute('virtual-list-id', model.id),
@@ -362,10 +348,15 @@ export const view = (config) => {
362
348
  margin: '0',
363
349
  padding: '0',
364
350
  }),
365
- ...(className !== undefined ? [h.Class(className)] : []),
366
- ...attributes,
351
+ ...(containerClassName !== undefined
352
+ ? [h.Class(containerClassName)]
353
+ : []),
354
+ ];
355
+ const allContainerAttributes = [
356
+ ...childAttributes(baseContainerAttributes),
357
+ ...containerAttributes,
367
358
  ];
368
- const renderContainer = (children) => h.keyed('ul')(model.id, containerAttributes, children);
359
+ const renderContainer = (children) => h.keyed('ul')(model.id, allContainerAttributes, children);
369
360
  const maybeWindow = itemToRowHeightPx !== undefined
370
361
  ? visibleWindowVariable(model, items, itemToRowHeightPx, overscan)
371
362
  : visibleWindow(model, items.length, overscan);
@@ -374,7 +365,7 @@ export const view = (config) => {
374
365
  : model.rowHeightPx;
375
366
  return Option.match(maybeWindow, {
376
367
  onNone: () => renderContainer([]),
377
- onSome: ({ startIndex, endIndex, topSpacerHeight, bottomSpacerHeight }) => {
368
+ onSome: ({ startIndex, endIndex, topSpacerHeight, bottomSpacerHeight, }) => {
378
369
  const visibleItems = items.slice(startIndex, endIndex);
379
370
  const topSpacer = h.keyed('li')(`${model.id}-top-spacer`, [h.Role('presentation'), h.Style({ height: `${topSpacerHeight}px` })], []);
380
371
  const bottomSpacer = h.keyed('li')(`${model.id}-bottom-spacer`, [
@@ -397,15 +388,4 @@ export const view = (config) => {
397
388
  return renderContainer([topSpacer, ...renderedRows, bottomSpacer]);
398
389
  },
399
390
  });
400
- };
401
- /** Creates a memoized virtual list view. Static config is captured in a
402
- * closure; only `model` and `items` are compared per render via
403
- * `createLazy`. */
404
- export const lazy = (staticConfig) => {
405
- const lazyView = createLazy();
406
- return (model, items) => lazyView((currentModel, currentItems) => view({
407
- ...staticConfig,
408
- model: currentModel,
409
- items: currentItems,
410
- }), [model, items]);
411
- };
391
+ });
@@ -1,3 +1,3 @@
1
- export { init, update, scrollToIndex, scrollToIndexVariable, view, lazy, subscriptions, visibleWindow, visibleWindowVariable, Model, Message, ScrolledContainer, MeasuredContainer, CompletedApplyScroll, } from './index.js';
2
- export type { InitConfig, ViewConfig, VisibleWindow } from './index.js';
1
+ export { init, update, scrollToIndex, scrollToIndexVariable, view, subscriptions, visibleWindow, visibleWindowVariable, Model, Message, ScrolledContainer, MeasuredContainer, CompletedApplyScroll, } from './index.js';
2
+ export type { InitConfig, ViewInputs, VisibleWindow } from './index.js';
3
3
  //# sourceMappingURL=public.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../../src/ui/virtualList/public.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,IAAI,EACJ,MAAM,EACN,aAAa,EACb,qBAAqB,EACrB,IAAI,EACJ,IAAI,EACJ,aAAa,EACb,aAAa,EACb,qBAAqB,EACrB,KAAK,EACL,OAAO,EACP,iBAAiB,EACjB,iBAAiB,EACjB,oBAAoB,GACrB,MAAM,YAAY,CAAA;AAEnB,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../../src/ui/virtualList/public.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,IAAI,EACJ,MAAM,EACN,aAAa,EACb,qBAAqB,EACrB,IAAI,EACJ,aAAa,EACb,aAAa,EACb,qBAAqB,EACrB,KAAK,EACL,OAAO,EACP,iBAAiB,EACjB,iBAAiB,EACjB,oBAAoB,GACrB,MAAM,YAAY,CAAA;AAEnB,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA"}
@@ -1 +1 @@
1
- export { init, update, scrollToIndex, scrollToIndexVariable, view, lazy, subscriptions, visibleWindow, visibleWindowVariable, Model, Message, ScrolledContainer, MeasuredContainer, CompletedApplyScroll, } from './index.js';
1
+ export { init, update, scrollToIndex, scrollToIndexVariable, view, subscriptions, visibleWindow, visibleWindowVariable, Model, Message, ScrolledContainer, MeasuredContainer, CompletedApplyScroll, } from './index.js';
package/dist/vdom.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { toVNode } from 'snabbdom';
1
+ import { type VNode, toVNode } from 'snabbdom';
2
2
  export type { VNode } from 'snabbdom';
3
3
  export { toVNode };
4
- export declare const patch: (oldVnode: import("snabbdom").VNode | Element | DocumentFragment, vnode: import("snabbdom").VNode) => import("snabbdom").VNode;
4
+ export declare const patch: (oldVnode: VNode | Element | DocumentFragment, vnode: VNode) => VNode;
5
+ export declare const dedupeSharedVNodes: (root: VNode) => VNode;
5
6
  //# sourceMappingURL=vdom.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"vdom.d.ts","sourceRoot":"","sources":["../src/vdom.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,OAAO,EACR,MAAM,UAAU,CAAA;AAIjB,YAAY,EAAE,KAAK,EAAE,MAAM,UAAU,CAAA;AACrC,OAAO,EAAE,OAAO,EAAE,CAAA;AAElB,eAAO,MAAM,KAAK,gIAOhB,CAAA"}
1
+ {"version":3,"file":"vdom.d.ts","sourceRoot":"","sources":["../src/vdom.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,KAAK,EAOV,OAAO,EACR,MAAM,UAAU,CAAA;AAIjB,YAAY,EAAE,KAAK,EAAE,MAAM,UAAU,CAAA;AACrC,OAAO,EAAE,OAAO,EAAE,CAAA;AAElB,eAAO,MAAM,KAAK,uEAOhB,CAAA;AAeF,eAAO,MAAM,kBAAkB,GAAI,MAAM,KAAK,KAAG,KA8BhD,CAAA"}
package/dist/vdom.js CHANGED
@@ -9,3 +9,47 @@ export const patch = init([
9
9
  propsModule,
10
10
  styleModule,
11
11
  ]);
12
+ // NOTE: snabbdom records each element's live DOM node on `vnode.elm` by
13
+ // mutating the vnode object during patch. A vnode object placed in more than
14
+ // one tree position would share a single `.elm`, so removals and text updates
15
+ // land on the wrong DOM node. View code legitimately reuses vnode values, e.g.
16
+ // `const checkmark = h.span(...)` dropped into several slots, so before each
17
+ // patch we clone any vnode object reached a second time, giving every position
18
+ // its own object.
19
+ // Detection is keyed off a per-patch Set, so a vnode reused across renders (a
20
+ // memoized `createLazy` subtree, the identical object each render) is reached
21
+ // only once per patch and passes through untouched, leaving snabbdom's
22
+ // same-object subtree short-circuit intact. Allocation happens only along
23
+ // paths where a duplicate is actually found; a tree with no reuse returns
24
+ // unchanged.
25
+ export const dedupeSharedVNodes = (root) => {
26
+ const seen = new Set();
27
+ const visit = (node) => {
28
+ const base = seen.has(node) ? { ...node, elm: undefined } : node;
29
+ seen.add(base);
30
+ const children = base.children;
31
+ if (children === undefined) {
32
+ return base;
33
+ }
34
+ let nextChildren;
35
+ for (let index = 0; index < children.length; index++) {
36
+ const child = children[index];
37
+ const deduped = typeof child === 'string' ? child : visit(child);
38
+ if (deduped !== child) {
39
+ if (nextChildren === undefined) {
40
+ nextChildren = children.slice();
41
+ }
42
+ nextChildren[index] = deduped;
43
+ }
44
+ }
45
+ if (nextChildren === undefined) {
46
+ return base;
47
+ }
48
+ if (base === node) {
49
+ return { ...node, children: nextChildren };
50
+ }
51
+ base.children = nextChildren;
52
+ return base;
53
+ };
54
+ return visit(root);
55
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foldkit",
3
- "version": "0.101.0",
3
+ "version": "0.102.1",
4
4
  "description": "A TypeScript frontend framework, built on Effect and architected like Elm",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",