foldkit 0.16.0 → 0.18.0

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 (113) hide show
  1. package/README.md +3 -8
  2. package/dist/{fieldValidation.d.ts → fieldValidation/index.d.ts} +9 -9
  3. package/dist/fieldValidation/index.d.ts.map +1 -0
  4. package/dist/{fieldValidation.js → fieldValidation/index.js} +5 -4
  5. package/dist/fieldValidation/public.d.ts +3 -0
  6. package/dist/fieldValidation/public.d.ts.map +1 -0
  7. package/dist/fieldValidation/public.js +1 -0
  8. package/dist/{html.d.ts → html/index.d.ts} +40 -4
  9. package/dist/html/index.d.ts.map +1 -0
  10. package/dist/{html.js → html/index.js} +31 -4
  11. package/dist/html/public.d.ts +3 -0
  12. package/dist/html/public.d.ts.map +1 -0
  13. package/dist/html/public.js +1 -0
  14. package/dist/index.d.ts +9 -8
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +9 -8
  17. package/dist/{navigation.d.ts → navigation/index.d.ts} +1 -1
  18. package/dist/navigation/index.d.ts.map +1 -0
  19. package/dist/navigation/public.d.ts +2 -0
  20. package/dist/navigation/public.d.ts.map +1 -0
  21. package/dist/navigation/public.js +1 -0
  22. package/dist/route/index.d.ts +2 -0
  23. package/dist/route/index.d.ts.map +1 -0
  24. package/dist/{parser.d.ts → route/parser.d.ts} +1 -1
  25. package/dist/route/parser.d.ts.map +1 -0
  26. package/dist/route/public.d.ts +3 -0
  27. package/dist/route/public.d.ts.map +1 -0
  28. package/dist/route/public.js +1 -0
  29. package/dist/runtime/browserListeners.d.ts.map +1 -1
  30. package/dist/runtime/browserListeners.js +2 -2
  31. package/dist/runtime/public.d.ts +5 -0
  32. package/dist/runtime/public.d.ts.map +1 -0
  33. package/dist/runtime/public.js +2 -0
  34. package/dist/runtime/runtime.d.ts +1 -1
  35. package/dist/runtime/runtime.d.ts.map +1 -1
  36. package/dist/runtime/urlRequest.d.ts +4 -4
  37. package/dist/runtime/urlRequest.d.ts.map +1 -1
  38. package/dist/runtime/urlRequest.js +3 -2
  39. package/dist/schema/index.d.ts +26 -0
  40. package/dist/schema/index.d.ts.map +1 -0
  41. package/dist/schema/index.js +14 -0
  42. package/dist/schema/public.d.ts +3 -0
  43. package/dist/schema/public.d.ts.map +1 -0
  44. package/dist/schema/public.js +1 -0
  45. package/dist/{struct.d.ts → struct/index.d.ts} +2 -1
  46. package/dist/struct/index.d.ts.map +1 -0
  47. package/dist/struct/index.js +3 -0
  48. package/dist/struct/public.d.ts +2 -0
  49. package/dist/struct/public.d.ts.map +1 -0
  50. package/dist/struct/public.js +1 -0
  51. package/dist/task/index.d.ts +109 -0
  52. package/dist/task/index.d.ts.map +1 -0
  53. package/dist/task/index.js +168 -0
  54. package/dist/task/public.d.ts +2 -0
  55. package/dist/task/public.d.ts.map +1 -0
  56. package/dist/task/public.js +1 -0
  57. package/dist/ui/dialog/index.d.ts +46 -0
  58. package/dist/ui/dialog/index.d.ts.map +1 -0
  59. package/dist/ui/dialog/index.js +67 -0
  60. package/dist/ui/dialog/public.d.ts +3 -0
  61. package/dist/ui/dialog/public.d.ts.map +1 -0
  62. package/dist/ui/dialog/public.js +1 -0
  63. package/dist/ui/disclosure/index.d.ts +47 -0
  64. package/dist/ui/disclosure/index.d.ts.map +1 -0
  65. package/dist/ui/disclosure/index.js +90 -0
  66. package/dist/ui/disclosure/public.d.ts +3 -0
  67. package/dist/ui/disclosure/public.d.ts.map +1 -0
  68. package/dist/ui/disclosure/public.js +1 -0
  69. package/dist/ui/index.d.ts +4 -1
  70. package/dist/ui/index.d.ts.map +1 -1
  71. package/dist/ui/index.js +4 -1
  72. package/dist/ui/keyboard.d.ts +4 -0
  73. package/dist/ui/keyboard.d.ts.map +1 -0
  74. package/dist/ui/keyboard.js +7 -0
  75. package/dist/ui/menu/index.d.ts +136 -0
  76. package/dist/ui/menu/index.d.ts.map +1 -0
  77. package/dist/ui/menu/index.js +297 -0
  78. package/dist/ui/menu/public.d.ts +3 -0
  79. package/dist/ui/menu/public.d.ts.map +1 -0
  80. package/dist/ui/menu/public.js +1 -0
  81. package/dist/ui/tabs/index.d.ts +76 -0
  82. package/dist/ui/tabs/index.d.ts.map +1 -0
  83. package/dist/ui/{tabs.js → tabs/index.js} +25 -25
  84. package/dist/ui/tabs/public.d.ts +3 -0
  85. package/dist/ui/tabs/public.d.ts.map +1 -0
  86. package/dist/ui/tabs/public.js +1 -0
  87. package/dist/{url.d.ts → url/index.d.ts} +2 -2
  88. package/dist/url/index.d.ts.map +1 -0
  89. package/dist/{url.js → url/index.js} +1 -1
  90. package/dist/url/public.d.ts +2 -0
  91. package/dist/url/public.d.ts.map +1 -0
  92. package/dist/url/public.js +1 -0
  93. package/package.json +32 -19
  94. package/dist/fieldValidation.d.ts.map +0 -1
  95. package/dist/html.d.ts.map +0 -1
  96. package/dist/navigation.d.ts.map +0 -1
  97. package/dist/parser.d.ts.map +0 -1
  98. package/dist/route.d.ts +0 -2
  99. package/dist/route.d.ts.map +0 -1
  100. package/dist/schema.d.ts +0 -20
  101. package/dist/schema.d.ts.map +0 -1
  102. package/dist/schema.js +0 -16
  103. package/dist/struct.d.ts.map +0 -1
  104. package/dist/struct.js +0 -2
  105. package/dist/task.d.ts +0 -64
  106. package/dist/task.d.ts.map +0 -1
  107. package/dist/task.js +0 -87
  108. package/dist/ui/tabs.d.ts +0 -65
  109. package/dist/ui/tabs.d.ts.map +0 -1
  110. package/dist/url.d.ts.map +0 -1
  111. /package/dist/{navigation.js → navigation/index.js} +0 -0
  112. /package/dist/{route.js → route/index.js} +0 -0
  113. /package/dist/{parser.js → route/parser.js} +0 -0
@@ -0,0 +1,90 @@
1
+ import { Match as M, Option, Schema as S } from 'effect';
2
+ import { html } from '../../html';
3
+ import { ts } from '../../schema';
4
+ import { evo } from '../../struct';
5
+ import * as Task from '../../task';
6
+ // MODEL
7
+ /** Schema for the disclosure component's state, tracking its unique ID and open/closed status. */
8
+ export const Model = S.Struct({
9
+ id: S.String,
10
+ isOpen: S.Boolean,
11
+ });
12
+ // MESSAGE
13
+ /** Sent when the disclosure button is clicked. Toggles the open/closed state. */
14
+ export const Toggled = ts('Toggled');
15
+ /** Sent to explicitly close the disclosure, regardless of its current state. */
16
+ export const Closed = ts('Closed');
17
+ /** Placeholder message used when no action is needed, such as after a focus command completes. */
18
+ export const NoOp = ts('NoOp');
19
+ /** Union of all messages the disclosure component can produce. */
20
+ export const Message = S.Union(Toggled, Closed, NoOp);
21
+ /** Creates an initial disclosure model from a config. Defaults to closed. */
22
+ export const init = (config) => ({
23
+ id: config.id,
24
+ isOpen: config.isOpen ?? false,
25
+ });
26
+ // UPDATE
27
+ const buttonId = (id) => `${id}-button`;
28
+ const panelId = (id) => `${id}-panel`;
29
+ /** Processes a disclosure message and returns the next model and commands. */
30
+ export const update = (model, message) => M.value(message).pipe(M.withReturnType(), M.tagsExhaustive({
31
+ Toggled: () => {
32
+ const maybeFocusCommand = Option.liftPredicate(Task.focus(`#${buttonId(model.id)}`, () => NoOp()), () => model.isOpen);
33
+ return [
34
+ evo(model, { isOpen: () => !model.isOpen }),
35
+ Option.toArray(maybeFocusCommand),
36
+ ];
37
+ },
38
+ Closed: () => {
39
+ const maybeFocusCommand = Option.liftPredicate(Task.focus(`#${buttonId(model.id)}`, () => NoOp()), () => model.isOpen);
40
+ return [
41
+ evo(model, { isOpen: () => false }),
42
+ Option.toArray(maybeFocusCommand),
43
+ ];
44
+ },
45
+ NoOp: () => [model, []],
46
+ }));
47
+ /** Renders a headless disclosure component with accessible ARIA attributes and keyboard support. */
48
+ export const view = (config) => {
49
+ const { div, empty, AriaControls, AriaDisabled, AriaExpanded, Class, DataAttribute, Disabled, Hidden, Id, OnClick, OnKeyDown, Tabindex, Type, keyed, } = html();
50
+ const { model: { id, isOpen }, toMessage, buttonClassName, buttonContent, panelClassName, panelContent, isDisabled, persistPanel, buttonElement = 'button', panelElement = 'div', className, } = config;
51
+ const isNativeButton = buttonElement === 'button';
52
+ const handleKeyDown = (key) =>
53
+ /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
54
+ M.value(key).pipe(M.whenOr('Enter', ' ', () => toMessage(Toggled())), M.orElse(() => toMessage(NoOp())));
55
+ const disabledAttributes = [
56
+ Disabled(true),
57
+ AriaDisabled(true),
58
+ DataAttribute('disabled', ''),
59
+ ];
60
+ const interactionAttributes = isDisabled
61
+ ? disabledAttributes
62
+ : [
63
+ OnClick(toMessage(Toggled())),
64
+ ...(!isNativeButton ? [OnKeyDown(handleKeyDown)] : []),
65
+ ];
66
+ const buttonAttributes = [
67
+ Class(buttonClassName),
68
+ Id(buttonId(id)),
69
+ AriaExpanded(isOpen),
70
+ AriaControls(panelId(id)),
71
+ ...(isNativeButton ? [Type('button')] : [Tabindex(0)]),
72
+ ...(isOpen ? [DataAttribute('open', '')] : []),
73
+ ...interactionAttributes,
74
+ ];
75
+ const panelAttributes = [
76
+ Class(panelClassName),
77
+ Id(panelId(id)),
78
+ ...(isOpen ? [DataAttribute('open', '')] : []),
79
+ ];
80
+ const persistedPanel = keyed(panelElement)(panelId(id), [...panelAttributes, Hidden(!isOpen)], [panelContent]);
81
+ const activePanel = isOpen
82
+ ? keyed(panelElement)(panelId(id), panelAttributes, [panelContent])
83
+ : empty;
84
+ const panel = persistPanel ? persistedPanel : activePanel;
85
+ const wrapperAttributes = className ? [Class(className)] : [];
86
+ return div(wrapperAttributes, [
87
+ keyed(buttonElement)(buttonId(id), buttonAttributes, [buttonContent]),
88
+ panel,
89
+ ]);
90
+ };
@@ -0,0 +1,3 @@
1
+ export { init, update, view, Model, Message } from './index';
2
+ export type { Toggled, Closed, NoOp, InitConfig, ViewConfig } from './index';
3
+ //# sourceMappingURL=public.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../../src/ui/disclosure/public.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AAE5D,YAAY,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA"}
@@ -0,0 +1 @@
1
+ export { init, update, view, Model, Message } from './index';
@@ -1,2 +1,5 @@
1
- export * as Tabs from './tabs';
1
+ export * as Dialog from './dialog/public';
2
+ export * as Disclosure from './disclosure/public';
3
+ export * as Menu from './menu/public';
4
+ export * as Tabs from './tabs/public';
2
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,QAAQ,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,iBAAiB,CAAA;AACzC,OAAO,KAAK,UAAU,MAAM,qBAAqB,CAAA;AACjD,OAAO,KAAK,IAAI,MAAM,eAAe,CAAA;AACrC,OAAO,KAAK,IAAI,MAAM,eAAe,CAAA"}
package/dist/ui/index.js CHANGED
@@ -1 +1,4 @@
1
- export * as Tabs from './tabs';
1
+ export * as Dialog from './dialog/public';
2
+ export * as Disclosure from './disclosure/public';
3
+ export * as Menu from './menu/public';
4
+ export * as Tabs from './tabs/public';
@@ -0,0 +1,4 @@
1
+ export declare const wrapIndex: (index: number, length: number) => number;
2
+ export declare const findFirstEnabledIndex: (itemCount: number, focusedIndex: number, isDisabled: (index: number) => boolean) => (startIndex: number, direction: 1 | -1) => number;
3
+ export declare const keyToIndex: (nextKey: string, previousKey: string, itemCount: number, focusedIndex: number, isDisabled: (index: number) => boolean) => ((key: string) => number);
4
+ //# sourceMappingURL=keyboard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keyboard.d.ts","sourceRoot":"","sources":["../../src/ui/keyboard.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,SAAS,GAAI,OAAO,MAAM,EAAE,QAAQ,MAAM,KAAG,MACpB,CAAA;AAEtC,eAAO,MAAM,qBAAqB,GAE9B,WAAW,MAAM,EACjB,cAAc,MAAM,EACpB,YAAY,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,MAEvC,YAAY,MAAM,EAAE,WAAW,CAAC,GAAG,CAAC,CAAC,KAAG,MAQtC,CAAA;AAEL,eAAO,MAAM,UAAU,GACrB,SAAS,MAAM,EACf,aAAa,MAAM,EACnB,WAAW,MAAM,EACjB,cAAc,MAAM,EACpB,YAAY,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,KACrC,CAAC,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAW1B,CAAA"}
@@ -0,0 +1,7 @@
1
+ import { Array, Match as M, Option, Predicate, pipe } from 'effect';
2
+ export const wrapIndex = (index, length) => ((index % length) + length) % length;
3
+ export const findFirstEnabledIndex = (itemCount, focusedIndex, isDisabled) => (startIndex, direction) => pipe(itemCount, Array.makeBy((step) => wrapIndex(startIndex + step * direction, itemCount)), Array.findFirst(Predicate.not(isDisabled)), Option.getOrElse(() => focusedIndex));
4
+ export const keyToIndex = (nextKey, previousKey, itemCount, focusedIndex, isDisabled) => {
5
+ const find = findFirstEnabledIndex(itemCount, focusedIndex, isDisabled);
6
+ return (key) => M.value(key).pipe(M.when(nextKey, () => find(focusedIndex + 1, 1)), M.when(previousKey, () => find(focusedIndex - 1, -1)), M.whenOr('Home', 'PageUp', () => find(0, 1)), M.whenOr('End', 'PageDown', () => find(itemCount - 1, -1)), M.orElse(() => focusedIndex));
7
+ };
@@ -0,0 +1,136 @@
1
+ import { Option, Schema as S } from 'effect';
2
+ import type { Html } from '../../html';
3
+ import type { Command } from '../../runtime/runtime';
4
+ /** Schema for the activation trigger — whether the user interacted via mouse or keyboard. */
5
+ export declare const ActivationTrigger: S.Literal<["Pointer", "Keyboard"]>;
6
+ export type ActivationTrigger = typeof ActivationTrigger.Type;
7
+ /** Schema for the menu component's state, tracking open/closed status, active item, activation trigger, and typeahead search. */
8
+ export declare const Model: S.Struct<{
9
+ id: typeof S.String;
10
+ isOpen: typeof S.Boolean;
11
+ maybeActiveItemIndex: S.OptionFromSelf<typeof S.Number>;
12
+ activationTrigger: S.Literal<["Pointer", "Keyboard"]>;
13
+ searchQuery: typeof S.String;
14
+ searchVersion: typeof S.Number;
15
+ maybeLastPointerPosition: S.OptionFromSelf<S.Struct<{
16
+ screenX: typeof S.Number;
17
+ screenY: typeof S.Number;
18
+ }>>;
19
+ }>;
20
+ export type Model = typeof Model.Type;
21
+ /** Sent when the menu opens via button click or keyboard. Contains an optional initial active item index — None for pointer, Some for keyboard. */
22
+ export declare const Opened: import("../../schema").CallableTaggedStruct<"Opened", {
23
+ maybeActiveItemIndex: S.OptionFromSelf<typeof S.Number>;
24
+ }>;
25
+ /** Sent when the menu closes via Escape key or backdrop click. */
26
+ export declare const Closed: import("../../schema").CallableTaggedStruct<"Closed", {}>;
27
+ /** Sent when focus leaves the menu items container via Tab key. */
28
+ export declare const ClosedByTab: import("../../schema").CallableTaggedStruct<"ClosedByTab", {}>;
29
+ /** Sent when an item is highlighted via arrow keys or mouse hover. Includes activation trigger. */
30
+ export declare const ItemActivated: import("../../schema").CallableTaggedStruct<"ItemActivated", {
31
+ index: typeof S.Number;
32
+ activationTrigger: S.Literal<["Pointer", "Keyboard"]>;
33
+ }>;
34
+ /** Sent when the mouse leaves an enabled item. */
35
+ export declare const ItemDeactivated: import("../../schema").CallableTaggedStruct<"ItemDeactivated", {}>;
36
+ /** Sent when an item is selected via Enter, Space, or click. */
37
+ export declare const ItemSelected: import("../../schema").CallableTaggedStruct<"ItemSelected", {
38
+ index: typeof S.Number;
39
+ }>;
40
+ /** Sent when a printable character is typed for typeahead search. */
41
+ export declare const Searched: import("../../schema").CallableTaggedStruct<"Searched", {
42
+ key: typeof S.String;
43
+ maybeTargetIndex: S.OptionFromSelf<typeof S.Number>;
44
+ }>;
45
+ /** Sent after the search debounce period to clear the accumulated query. */
46
+ export declare const SearchCleared: import("../../schema").CallableTaggedStruct<"SearchCleared", {
47
+ version: typeof S.Number;
48
+ }>;
49
+ /** Sent when the pointer moves over a menu item, carrying screen coordinates for tracked-pointer comparison. */
50
+ export declare const PointerMovedOverItem: import("../../schema").CallableTaggedStruct<"PointerMovedOverItem", {
51
+ index: typeof S.Number;
52
+ screenX: typeof S.Number;
53
+ screenY: typeof S.Number;
54
+ }>;
55
+ /** Placeholder message used when no action is needed. */
56
+ export declare const NoOp: import("../../schema").CallableTaggedStruct<"NoOp", {}>;
57
+ /** Union of all messages the menu component can produce. */
58
+ export declare const Message: S.Union<[import("../../schema").CallableTaggedStruct<"Opened", {
59
+ maybeActiveItemIndex: S.OptionFromSelf<typeof S.Number>;
60
+ }>, import("../../schema").CallableTaggedStruct<"Closed", {}>, import("../../schema").CallableTaggedStruct<"ClosedByTab", {}>, import("../../schema").CallableTaggedStruct<"ItemActivated", {
61
+ index: typeof S.Number;
62
+ activationTrigger: S.Literal<["Pointer", "Keyboard"]>;
63
+ }>, import("../../schema").CallableTaggedStruct<"ItemDeactivated", {}>, import("../../schema").CallableTaggedStruct<"ItemSelected", {
64
+ index: typeof S.Number;
65
+ }>, import("../../schema").CallableTaggedStruct<"PointerMovedOverItem", {
66
+ index: typeof S.Number;
67
+ screenX: typeof S.Number;
68
+ screenY: typeof S.Number;
69
+ }>, import("../../schema").CallableTaggedStruct<"Searched", {
70
+ key: typeof S.String;
71
+ maybeTargetIndex: S.OptionFromSelf<typeof S.Number>;
72
+ }>, import("../../schema").CallableTaggedStruct<"SearchCleared", {
73
+ version: typeof S.Number;
74
+ }>, import("../../schema").CallableTaggedStruct<"NoOp", {}>]>;
75
+ export type Opened = typeof Opened.Type;
76
+ export type Closed = typeof Closed.Type;
77
+ export type ClosedByTab = typeof ClosedByTab.Type;
78
+ export type ItemActivated = typeof ItemActivated.Type;
79
+ export type ItemDeactivated = typeof ItemDeactivated.Type;
80
+ export type ItemSelected = typeof ItemSelected.Type;
81
+ export type PointerMovedOverItem = typeof PointerMovedOverItem.Type;
82
+ export type Searched = typeof Searched.Type;
83
+ export type SearchCleared = typeof SearchCleared.Type;
84
+ export type NoOp = typeof NoOp.Type;
85
+ export type Message = typeof Message.Type;
86
+ /** Configuration for creating a menu model with `init`. */
87
+ export type InitConfig = Readonly<{
88
+ id: string;
89
+ }>;
90
+ /** Creates an initial menu model from a config. Defaults to closed with no active item. */
91
+ export declare const init: (config: InitConfig) => Model;
92
+ /** Processes a menu message and returns the next model and commands. */
93
+ export declare const update: (model: Model, message: Message) => [Model, ReadonlyArray<Command<Message>>];
94
+ /** Configuration for an individual menu item's appearance. */
95
+ export type ItemConfig = Readonly<{
96
+ className: string;
97
+ content: Html;
98
+ }>;
99
+ /** Configuration for a group heading rendered above a group of items. */
100
+ export type GroupHeading = Readonly<{
101
+ content: Html;
102
+ className: string;
103
+ }>;
104
+ /** Configuration for rendering a menu with `view`. */
105
+ export type ViewConfig<Message, Item extends string> = Readonly<{
106
+ model: Model;
107
+ toMessage: (message: Opened | Closed | ClosedByTab | ItemActivated | ItemDeactivated | ItemSelected | PointerMovedOverItem | Searched) => Message;
108
+ items: ReadonlyArray<Item>;
109
+ itemToConfig: (item: Item, context: Readonly<{
110
+ isActive: boolean;
111
+ isDisabled: boolean;
112
+ }>) => ItemConfig;
113
+ isItemDisabled?: (item: Item, index: number) => boolean;
114
+ itemToSearchText?: (item: Item, index: number) => string;
115
+ isButtonDisabled?: boolean;
116
+ buttonContent: Html;
117
+ buttonClassName: string;
118
+ itemsClassName: string;
119
+ backdropClassName: string;
120
+ className?: string;
121
+ itemGroupKey?: (item: Item, index: number) => string;
122
+ groupToHeading?: (groupKey: string) => GroupHeading | undefined;
123
+ groupClassName?: string;
124
+ separatorClassName?: string;
125
+ }>;
126
+ type Segment<A> = Readonly<{
127
+ key: string;
128
+ items: ReadonlyArray<A>;
129
+ }>;
130
+ export declare const groupContiguous: <A>(items: ReadonlyArray<A>, toKey: (item: A, index: number) => string) => ReadonlyArray<Segment<A>>;
131
+ /** Finds the first enabled item whose search text starts with the query, searching forward from the active item and wrapping around. On a fresh search, starts after the active item; on a refinement, includes the active item. */
132
+ export declare const resolveTypeaheadMatch: <Item extends string>(items: ReadonlyArray<Item>, query: string, maybeActiveItemIndex: Option.Option<number>, isDisabled: (index: number) => boolean, itemToSearchText: (item: Item, index: number) => string, isRefinement: boolean) => Option.Option<number>;
133
+ /** Renders a headless menu with typeahead search, keyboard navigation, and aria-activedescendant focus management. */
134
+ export declare const view: <Message, Item extends string>(config: ViewConfig<Message, Item>) => Html;
135
+ export {};
136
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ui/menu/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,297 @@
1
+ import { Array, Match as M, Option, Schema as S, String as Str, pipe, } from 'effect';
2
+ import { html } from '../../html';
3
+ import { ts } from '../../schema';
4
+ import { evo } from '../../struct';
5
+ import * as Task from '../../task';
6
+ import { findFirstEnabledIndex, keyToIndex, wrapIndex } from '../keyboard';
7
+ // MODEL
8
+ /** Schema for the activation trigger — whether the user interacted via mouse or keyboard. */
9
+ export const ActivationTrigger = S.Literal('Pointer', 'Keyboard');
10
+ /** Schema for the menu component's state, tracking open/closed status, active item, activation trigger, and typeahead search. */
11
+ export const Model = S.Struct({
12
+ id: S.String,
13
+ isOpen: S.Boolean,
14
+ maybeActiveItemIndex: S.OptionFromSelf(S.Number),
15
+ activationTrigger: ActivationTrigger,
16
+ searchQuery: S.String,
17
+ searchVersion: S.Number,
18
+ maybeLastPointerPosition: S.OptionFromSelf(S.Struct({ screenX: S.Number, screenY: S.Number })),
19
+ });
20
+ // MESSAGE
21
+ /** Sent when the menu opens via button click or keyboard. Contains an optional initial active item index — None for pointer, Some for keyboard. */
22
+ export const Opened = ts('Opened', {
23
+ maybeActiveItemIndex: S.OptionFromSelf(S.Number),
24
+ });
25
+ /** Sent when the menu closes via Escape key or backdrop click. */
26
+ export const Closed = ts('Closed');
27
+ /** Sent when focus leaves the menu items container via Tab key. */
28
+ export const ClosedByTab = ts('ClosedByTab');
29
+ /** Sent when an item is highlighted via arrow keys or mouse hover. Includes activation trigger. */
30
+ export const ItemActivated = ts('ItemActivated', {
31
+ index: S.Number,
32
+ activationTrigger: ActivationTrigger,
33
+ });
34
+ /** Sent when the mouse leaves an enabled item. */
35
+ export const ItemDeactivated = ts('ItemDeactivated');
36
+ /** Sent when an item is selected via Enter, Space, or click. */
37
+ export const ItemSelected = ts('ItemSelected', { index: S.Number });
38
+ /** Sent when a printable character is typed for typeahead search. */
39
+ export const Searched = ts('Searched', {
40
+ key: S.String,
41
+ maybeTargetIndex: S.OptionFromSelf(S.Number),
42
+ });
43
+ /** Sent after the search debounce period to clear the accumulated query. */
44
+ export const SearchCleared = ts('SearchCleared', { version: S.Number });
45
+ /** Sent when the pointer moves over a menu item, carrying screen coordinates for tracked-pointer comparison. */
46
+ export const PointerMovedOverItem = ts('PointerMovedOverItem', {
47
+ index: S.Number,
48
+ screenX: S.Number,
49
+ screenY: S.Number,
50
+ });
51
+ /** Placeholder message used when no action is needed. */
52
+ export const NoOp = ts('NoOp');
53
+ /** Union of all messages the menu component can produce. */
54
+ export const Message = S.Union(Opened, Closed, ClosedByTab, ItemActivated, ItemDeactivated, ItemSelected, PointerMovedOverItem, Searched, SearchCleared, NoOp);
55
+ // INIT
56
+ const SEARCH_DEBOUNCE_MILLISECONDS = 350;
57
+ /** Creates an initial menu model from a config. Defaults to closed with no active item. */
58
+ export const init = (config) => ({
59
+ id: config.id,
60
+ isOpen: false,
61
+ maybeActiveItemIndex: Option.none(),
62
+ activationTrigger: 'Keyboard',
63
+ searchQuery: '',
64
+ searchVersion: 0,
65
+ maybeLastPointerPosition: Option.none(),
66
+ });
67
+ // UPDATE
68
+ const closedModel = (model) => evo(model, {
69
+ isOpen: () => false,
70
+ maybeActiveItemIndex: () => Option.none(),
71
+ activationTrigger: () => 'Keyboard',
72
+ searchQuery: () => '',
73
+ searchVersion: () => 0,
74
+ maybeLastPointerPosition: () => Option.none(),
75
+ });
76
+ const buttonSelector = (id) => `#${id}-button`;
77
+ const itemsSelector = (id) => `#${id}-items`;
78
+ const itemSelector = (id, index) => `#${id}-item-${index}`;
79
+ /** Processes a menu message and returns the next model and commands. */
80
+ export const update = (model, message) => M.value(message).pipe(M.withReturnType(), M.tagsExhaustive({
81
+ Opened: ({ maybeActiveItemIndex }) => [
82
+ evo(model, {
83
+ isOpen: () => true,
84
+ maybeActiveItemIndex: () => maybeActiveItemIndex,
85
+ activationTrigger: () => Option.match(maybeActiveItemIndex, {
86
+ onNone: () => 'Pointer',
87
+ onSome: () => 'Keyboard',
88
+ }),
89
+ searchQuery: () => '',
90
+ searchVersion: () => 0,
91
+ maybeLastPointerPosition: () => Option.none(),
92
+ }),
93
+ [Task.focus(itemsSelector(model.id), () => NoOp())],
94
+ ],
95
+ Closed: () => [
96
+ closedModel(model),
97
+ [Task.focus(buttonSelector(model.id), () => NoOp())],
98
+ ],
99
+ ClosedByTab: () => [closedModel(model), []],
100
+ ItemActivated: ({ index, activationTrigger }) => [
101
+ evo(model, {
102
+ maybeActiveItemIndex: () => Option.some(index),
103
+ activationTrigger: () => activationTrigger,
104
+ }),
105
+ activationTrigger === 'Keyboard'
106
+ ? [Task.scrollIntoView(itemSelector(model.id, index), () => NoOp())]
107
+ : [],
108
+ ],
109
+ PointerMovedOverItem: ({ index, screenX, screenY }) => {
110
+ const isSamePosition = Option.exists(model.maybeLastPointerPosition, (position) => position.screenX === screenX && position.screenY === screenY);
111
+ if (isSamePosition) {
112
+ return [model, []];
113
+ }
114
+ return [
115
+ evo(model, {
116
+ maybeActiveItemIndex: () => Option.some(index),
117
+ activationTrigger: () => 'Pointer',
118
+ maybeLastPointerPosition: () => Option.some({ screenX, screenY }),
119
+ }),
120
+ [],
121
+ ];
122
+ },
123
+ ItemDeactivated: () => model.activationTrigger === 'Pointer'
124
+ ? [evo(model, { maybeActiveItemIndex: () => Option.none() }), []]
125
+ : [model, []],
126
+ ItemSelected: () => [
127
+ closedModel(model),
128
+ [Task.focus(buttonSelector(model.id), () => NoOp())],
129
+ ],
130
+ Searched: ({ key, maybeTargetIndex }) => {
131
+ const nextSearchQuery = model.searchQuery + key;
132
+ const nextSearchVersion = model.searchVersion + 1;
133
+ return [
134
+ evo(model, {
135
+ searchQuery: () => nextSearchQuery,
136
+ searchVersion: () => nextSearchVersion,
137
+ maybeActiveItemIndex: () => Option.orElse(maybeTargetIndex, () => model.maybeActiveItemIndex),
138
+ }),
139
+ [
140
+ Task.delay(SEARCH_DEBOUNCE_MILLISECONDS, () => SearchCleared({ version: nextSearchVersion })),
141
+ ],
142
+ ];
143
+ },
144
+ SearchCleared: ({ version }) => {
145
+ if (version !== model.searchVersion) {
146
+ return [model, []];
147
+ }
148
+ return [evo(model, { searchQuery: () => '' }), []];
149
+ },
150
+ NoOp: () => [model, []],
151
+ }));
152
+ export const groupContiguous = (items, toKey) => {
153
+ const tagged = Array.map(items, (item, index) => ({
154
+ key: toKey(item, index),
155
+ item,
156
+ }));
157
+ return Array.chop(tagged, (nonEmpty) => {
158
+ const key = Array.headNonEmpty(nonEmpty).key;
159
+ const [matching, rest] = Array.span(nonEmpty, (tagged) => tagged.key === key);
160
+ return [{ key, items: Array.map(matching, ({ item }) => item) }, rest];
161
+ });
162
+ };
163
+ const itemId = (id, index) => `${id}-item-${index}`;
164
+ /** Finds the first enabled item whose search text starts with the query, searching forward from the active item and wrapping around. On a fresh search, starts after the active item; on a refinement, includes the active item. */
165
+ export const resolveTypeaheadMatch = (items, query, maybeActiveItemIndex, isDisabled, itemToSearchText, isRefinement) => {
166
+ const lowerQuery = Str.toLowerCase(query);
167
+ const offset = isRefinement ? 0 : 1;
168
+ const startIndex = Option.match(maybeActiveItemIndex, {
169
+ onNone: () => 0,
170
+ onSome: (index) => index + offset,
171
+ });
172
+ const isEnabledMatch = (index) => !isDisabled(index) &&
173
+ pipe(items, Array.get(index), Option.exists((item) => pipe(itemToSearchText(item, index), Str.toLowerCase, Str.startsWith(lowerQuery))));
174
+ return pipe(items.length, Array.makeBy((step) => wrapIndex(startIndex + step, items.length)), Array.findFirst(isEnabledMatch));
175
+ };
176
+ /** Renders a headless menu with typeahead search, keyboard navigation, and aria-activedescendant focus management. */
177
+ export const view = (config) => {
178
+ const { div, AriaActiveDescendant, AriaControls, AriaDisabled, AriaExpanded, AriaHasPopup, AriaLabelledBy, Class, DataAttribute, Id, OnBlur, OnClick, OnKeyDownPreventDefault, OnMouseLeave, OnPointerMove, Role, Tabindex, Type, keyed, } = html();
179
+ const { model: { id, isOpen, maybeActiveItemIndex, searchQuery }, toMessage, items, itemToConfig, isItemDisabled, itemToSearchText = (item) => item, isButtonDisabled, buttonContent, buttonClassName, itemsClassName, backdropClassName, className, itemGroupKey, groupToHeading, groupClassName, separatorClassName, } = config;
180
+ const isDisabled = (index) => !!isItemDisabled &&
181
+ pipe(items, Array.get(index), Option.exists((item) => isItemDisabled(item, index)));
182
+ const firstEnabledIndex = findFirstEnabledIndex(items.length, 0, isDisabled)(0, 1);
183
+ const lastEnabledIndex = findFirstEnabledIndex(items.length, 0, isDisabled)(items.length - 1, -1);
184
+ const handleButtonKeyDown = (key) => M.value(key).pipe(M.whenOr('Enter', ' ', 'ArrowDown', () => Option.some(toMessage(Opened({
185
+ maybeActiveItemIndex: Option.some(firstEnabledIndex),
186
+ })))), M.when('ArrowUp', () => Option.some(toMessage(Opened({
187
+ maybeActiveItemIndex: Option.some(lastEnabledIndex),
188
+ })))), M.orElse(() => Option.none()));
189
+ const handleButtonClick = () => isOpen
190
+ ? toMessage(Closed())
191
+ : toMessage(Opened({ maybeActiveItemIndex: Option.none() }));
192
+ const resolveActiveIndex = keyToIndex('ArrowDown', 'ArrowUp', items.length, Option.getOrElse(maybeActiveItemIndex, () => 0), isDisabled);
193
+ const handleItemsKeyDown = (key) => M.value(key).pipe(M.when('Escape', () => Option.some(toMessage(Closed()))), M.whenOr('Enter', ' ', () => Option.map(maybeActiveItemIndex, (index) => toMessage(ItemSelected({ index })))), M.whenOr('ArrowDown', 'ArrowUp', 'Home', 'End', 'PageUp', 'PageDown', () => Option.some(toMessage(ItemActivated({
194
+ index: resolveActiveIndex(key),
195
+ activationTrigger: 'Keyboard',
196
+ })))), M.when((key) => key.length === 1, () => {
197
+ const nextQuery = searchQuery + key;
198
+ const maybeTargetIndex = resolveTypeaheadMatch(items, nextQuery, maybeActiveItemIndex, isDisabled, itemToSearchText, Str.isNonEmpty(searchQuery));
199
+ return Option.some(toMessage(Searched({ key, maybeTargetIndex })));
200
+ }), M.orElse(() => Option.none()));
201
+ const buttonAttributes = [
202
+ Id(`${id}-button`),
203
+ Type('button'),
204
+ Class(buttonClassName),
205
+ AriaHasPopup('menu'),
206
+ AriaExpanded(isOpen),
207
+ AriaControls(`${id}-items`),
208
+ ...(isButtonDisabled
209
+ ? [AriaDisabled(true), DataAttribute('disabled', '')]
210
+ : [
211
+ OnKeyDownPreventDefault(handleButtonKeyDown),
212
+ OnClick(handleButtonClick()),
213
+ ]),
214
+ ...(isOpen ? [DataAttribute('open', '')] : []),
215
+ ];
216
+ const maybeActiveDescendant = Option.match(maybeActiveItemIndex, {
217
+ onNone: () => [],
218
+ onSome: (index) => [AriaActiveDescendant(itemId(id, index))],
219
+ });
220
+ const itemsContainerAttributes = [
221
+ Id(`${id}-items`),
222
+ Role('menu'),
223
+ AriaLabelledBy(`${id}-button`),
224
+ ...maybeActiveDescendant,
225
+ Tabindex(0),
226
+ Class(itemsClassName),
227
+ OnKeyDownPreventDefault(handleItemsKeyDown),
228
+ OnBlur(toMessage(ClosedByTab())),
229
+ ];
230
+ const menuItems = Array.map(items, (item, index) => {
231
+ const isActiveItem = Option.exists(maybeActiveItemIndex, (activeIndex) => activeIndex === index);
232
+ const isDisabledItem = isDisabled(index);
233
+ const itemConfig = itemToConfig(item, {
234
+ isActive: isActiveItem,
235
+ isDisabled: isDisabledItem,
236
+ });
237
+ return keyed('div')(itemId(id, index), [
238
+ Id(itemId(id, index)),
239
+ Role('menuitem'),
240
+ Tabindex(-1),
241
+ Class(itemConfig.className),
242
+ ...(isActiveItem ? [DataAttribute('active', '')] : []),
243
+ ...(isDisabledItem
244
+ ? [AriaDisabled(true), DataAttribute('disabled', '')]
245
+ : [
246
+ OnClick(toMessage(ItemSelected({ index }))),
247
+ OnPointerMove((screenX, screenY) => toMessage(PointerMovedOverItem({ index, screenX, screenY }))),
248
+ OnMouseLeave(toMessage(ItemDeactivated())),
249
+ ]),
250
+ ], [itemConfig.content]);
251
+ });
252
+ const renderGroupedItems = () => {
253
+ if (!itemGroupKey) {
254
+ return menuItems;
255
+ }
256
+ const segments = groupContiguous(menuItems, (_, index) => Array.get(items, index).pipe(Option.match({
257
+ onNone: () => '',
258
+ onSome: (item) => itemGroupKey(item, index),
259
+ })));
260
+ return Array.flatMap(segments, (segment, segmentIndex) => {
261
+ const maybeHeading = Option.fromNullable(groupToHeading && groupToHeading(segment.key));
262
+ const headingId = `${id}-heading-${segment.key}`;
263
+ const headingElement = Option.match(maybeHeading, {
264
+ onNone: () => [],
265
+ onSome: (heading) => [
266
+ keyed('div')(headingId, [Id(headingId), Role('presentation'), Class(heading.className)], [heading.content]),
267
+ ],
268
+ });
269
+ const groupContent = [...headingElement, ...segment.items];
270
+ const groupElement = keyed('div')(`${id}-group-${segment.key}`, [
271
+ Role('group'),
272
+ ...(Option.isSome(maybeHeading) ? [AriaLabelledBy(headingId)] : []),
273
+ ...(groupClassName ? [Class(groupClassName)] : []),
274
+ ], groupContent);
275
+ const separator = segmentIndex > 0 && separatorClassName
276
+ ? [
277
+ keyed('div')(`${id}-separator-${segmentIndex}`, [Role('separator'), Class(separatorClassName)], []),
278
+ ]
279
+ : [];
280
+ return [...separator, groupElement];
281
+ });
282
+ };
283
+ const backdrop = keyed('div')(`${id}-backdrop`, [Class(backdropClassName), OnClick(toMessage(Closed()))], []);
284
+ const renderedItems = renderGroupedItems();
285
+ const openContent = [
286
+ backdrop,
287
+ keyed('div')(`${id}-items-container`, itemsContainerAttributes, renderedItems),
288
+ ];
289
+ const wrapperAttributes = [
290
+ ...(className ? [Class(className)] : []),
291
+ ...(isOpen ? [DataAttribute('open', '')] : []),
292
+ ];
293
+ return div(wrapperAttributes, [
294
+ keyed('button')(`${id}-button`, buttonAttributes, [buttonContent]),
295
+ ...(isOpen ? openContent : []),
296
+ ]);
297
+ };
@@ -0,0 +1,3 @@
1
+ export { init, update, view, Model, Message } from './index';
2
+ export type { ActivationTrigger, Opened, Closed, ClosedByTab, ItemActivated, ItemDeactivated, ItemSelected, PointerMovedOverItem, Searched, SearchCleared, NoOp, InitConfig, ViewConfig, ItemConfig, GroupHeading, } from './index';
3
+ //# sourceMappingURL=public.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../../src/ui/menu/public.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AAE5D,YAAY,EACV,iBAAiB,EACjB,MAAM,EACN,MAAM,EACN,WAAW,EACX,aAAa,EACb,eAAe,EACf,YAAY,EACZ,oBAAoB,EACpB,QAAQ,EACR,aAAa,EACb,IAAI,EACJ,UAAU,EACV,UAAU,EACV,UAAU,EACV,YAAY,GACb,MAAM,SAAS,CAAA"}
@@ -0,0 +1 @@
1
+ export { init, update, view, Model, Message } from './index';