@fgv/ts-app-shell 5.1.0-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 (180) hide show
  1. package/README.md +26 -0
  2. package/dist/index.browser.js +3 -0
  3. package/dist/index.js +43 -0
  4. package/dist/packlets/ai-assist/index.js +6 -0
  5. package/dist/packlets/ai-assist/useAiAssist.js +219 -0
  6. package/dist/packlets/cascade/CascadeContainer.js +83 -0
  7. package/dist/packlets/cascade/ComparisonView.js +48 -0
  8. package/dist/packlets/cascade/EntityTabLayout.js +104 -0
  9. package/dist/packlets/cascade/MobileCascadeStack.js +63 -0
  10. package/dist/packlets/cascade/index.js +37 -0
  11. package/dist/packlets/cascade/model.js +30 -0
  12. package/dist/packlets/cascade/useCascadeOps.js +206 -0
  13. package/dist/packlets/cascade/useCascadeTransitions.js +58 -0
  14. package/dist/packlets/detail/DetailHelpers.js +103 -0
  15. package/dist/packlets/detail/index.js +6 -0
  16. package/dist/packlets/drop-zone/JsonDropZone.js +112 -0
  17. package/dist/packlets/drop-zone/index.js +6 -0
  18. package/dist/packlets/editing/EditFieldHelpers.js +130 -0
  19. package/dist/packlets/editing/MultiActionButton.js +73 -0
  20. package/dist/packlets/editing/NumericInput.js +119 -0
  21. package/dist/packlets/editing/TypeaheadInput.js +207 -0
  22. package/dist/packlets/editing/index.js +10 -0
  23. package/dist/packlets/editing/useTypeaheadMatch.js +102 -0
  24. package/dist/packlets/keyboard/index.js +7 -0
  25. package/dist/packlets/keyboard/registry.js +133 -0
  26. package/dist/packlets/keyboard/useKeyboardShortcuts.js +117 -0
  27. package/dist/packlets/messages/MessagesContext.js +76 -0
  28. package/dist/packlets/messages/MessagesLogger.js +103 -0
  29. package/dist/packlets/messages/StatusBar.js +154 -0
  30. package/dist/packlets/messages/Toast.js +68 -0
  31. package/dist/packlets/messages/index.js +11 -0
  32. package/dist/packlets/messages/model.js +56 -0
  33. package/dist/packlets/messages/useLogReporter.js +66 -0
  34. package/dist/packlets/modal/ConfirmDialog.js +78 -0
  35. package/dist/packlets/modal/Modal.js +55 -0
  36. package/dist/packlets/modal/index.js +7 -0
  37. package/dist/packlets/print/PrintEnclosure.js +60 -0
  38. package/dist/packlets/print/index.js +7 -0
  39. package/dist/packlets/print/openPrintWindow.js +112 -0
  40. package/dist/packlets/responsive/ResponsiveProvider.js +56 -0
  41. package/dist/packlets/responsive/index.js +7 -0
  42. package/dist/packlets/responsive/useResponsiveLayout.js +118 -0
  43. package/dist/packlets/selectors/EntityRow.js +276 -0
  44. package/dist/packlets/selectors/PreferredSelector.js +251 -0
  45. package/dist/packlets/selectors/index.js +24 -0
  46. package/dist/packlets/sidebar/CollectionSection.js +107 -0
  47. package/dist/packlets/sidebar/EntityList.js +164 -0
  48. package/dist/packlets/sidebar/FilterBar.js +42 -0
  49. package/dist/packlets/sidebar/FilterRow.js +182 -0
  50. package/dist/packlets/sidebar/GroupedEntityList.js +183 -0
  51. package/dist/packlets/sidebar/SearchBar.js +34 -0
  52. package/dist/packlets/sidebar/SidebarLayout.js +62 -0
  53. package/dist/packlets/sidebar/index.js +12 -0
  54. package/dist/packlets/theme/ThemeProvider.js +141 -0
  55. package/dist/packlets/theme/index.js +6 -0
  56. package/dist/packlets/top-bar/ModeSelector.js +46 -0
  57. package/dist/packlets/top-bar/TabBar.js +37 -0
  58. package/dist/packlets/top-bar/index.js +7 -0
  59. package/dist/packlets/url-sync/index.js +6 -0
  60. package/dist/packlets/url-sync/useUrlSync.js +157 -0
  61. package/eslint.config.js +22 -0
  62. package/lib/index.browser.d.ts +2 -0
  63. package/lib/index.browser.js +19 -0
  64. package/lib/index.d.ts +28 -0
  65. package/lib/index.js +59 -0
  66. package/lib/packlets/ai-assist/index.d.ts +6 -0
  67. package/lib/packlets/ai-assist/index.js +11 -0
  68. package/lib/packlets/ai-assist/useAiAssist.d.ts +77 -0
  69. package/lib/packlets/ai-assist/useAiAssist.js +223 -0
  70. package/lib/packlets/cascade/CascadeContainer.d.ts +44 -0
  71. package/lib/packlets/cascade/CascadeContainer.js +119 -0
  72. package/lib/packlets/cascade/ComparisonView.d.ts +35 -0
  73. package/lib/packlets/cascade/ComparisonView.js +54 -0
  74. package/lib/packlets/cascade/EntityTabLayout.d.ts +47 -0
  75. package/lib/packlets/cascade/EntityTabLayout.js +110 -0
  76. package/lib/packlets/cascade/MobileCascadeStack.d.ts +20 -0
  77. package/lib/packlets/cascade/MobileCascadeStack.js +99 -0
  78. package/lib/packlets/cascade/index.d.ts +12 -0
  79. package/lib/packlets/cascade/index.js +48 -0
  80. package/lib/packlets/cascade/model.d.ts +57 -0
  81. package/lib/packlets/cascade/model.js +33 -0
  82. package/lib/packlets/cascade/useCascadeOps.d.ts +111 -0
  83. package/lib/packlets/cascade/useCascadeOps.js +209 -0
  84. package/lib/packlets/cascade/useCascadeTransitions.d.ts +19 -0
  85. package/lib/packlets/cascade/useCascadeTransitions.js +62 -0
  86. package/lib/packlets/detail/DetailHelpers.d.ts +83 -0
  87. package/lib/packlets/detail/DetailHelpers.js +113 -0
  88. package/lib/packlets/detail/index.d.ts +6 -0
  89. package/lib/packlets/detail/index.js +14 -0
  90. package/lib/packlets/drop-zone/JsonDropZone.d.ts +40 -0
  91. package/lib/packlets/drop-zone/JsonDropZone.js +149 -0
  92. package/lib/packlets/drop-zone/index.d.ts +6 -0
  93. package/lib/packlets/drop-zone/index.js +10 -0
  94. package/lib/packlets/editing/EditFieldHelpers.d.ts +171 -0
  95. package/lib/packlets/editing/EditFieldHelpers.js +144 -0
  96. package/lib/packlets/editing/MultiActionButton.d.ts +45 -0
  97. package/lib/packlets/editing/MultiActionButton.js +109 -0
  98. package/lib/packlets/editing/NumericInput.d.ts +47 -0
  99. package/lib/packlets/editing/NumericInput.js +155 -0
  100. package/lib/packlets/editing/TypeaheadInput.d.ts +46 -0
  101. package/lib/packlets/editing/TypeaheadInput.js +243 -0
  102. package/lib/packlets/editing/index.d.ts +10 -0
  103. package/lib/packlets/editing/index.js +26 -0
  104. package/lib/packlets/editing/useTypeaheadMatch.d.ts +42 -0
  105. package/lib/packlets/editing/useTypeaheadMatch.js +105 -0
  106. package/lib/packlets/keyboard/index.d.ts +7 -0
  107. package/lib/packlets/keyboard/index.js +15 -0
  108. package/lib/packlets/keyboard/registry.d.ts +92 -0
  109. package/lib/packlets/keyboard/registry.js +138 -0
  110. package/lib/packlets/keyboard/useKeyboardShortcuts.d.ts +50 -0
  111. package/lib/packlets/keyboard/useKeyboardShortcuts.js +155 -0
  112. package/lib/packlets/messages/MessagesContext.d.ts +40 -0
  113. package/lib/packlets/messages/MessagesContext.js +113 -0
  114. package/lib/packlets/messages/MessagesLogger.d.ts +50 -0
  115. package/lib/packlets/messages/MessagesLogger.js +107 -0
  116. package/lib/packlets/messages/StatusBar.d.ts +22 -0
  117. package/lib/packlets/messages/StatusBar.js +190 -0
  118. package/lib/packlets/messages/Toast.d.ts +31 -0
  119. package/lib/packlets/messages/Toast.js +105 -0
  120. package/lib/packlets/messages/index.d.ts +11 -0
  121. package/lib/packlets/messages/index.js +24 -0
  122. package/lib/packlets/messages/model.d.ts +59 -0
  123. package/lib/packlets/messages/model.js +61 -0
  124. package/lib/packlets/messages/useLogReporter.d.ts +22 -0
  125. package/lib/packlets/messages/useLogReporter.js +69 -0
  126. package/lib/packlets/modal/ConfirmDialog.d.ts +39 -0
  127. package/lib/packlets/modal/ConfirmDialog.js +114 -0
  128. package/lib/packlets/modal/Modal.d.ts +22 -0
  129. package/lib/packlets/modal/Modal.js +91 -0
  130. package/lib/packlets/modal/index.d.ts +7 -0
  131. package/lib/packlets/modal/index.js +12 -0
  132. package/lib/packlets/print/PrintEnclosure.d.ts +33 -0
  133. package/lib/packlets/print/PrintEnclosure.js +96 -0
  134. package/lib/packlets/print/index.d.ts +7 -0
  135. package/lib/packlets/print/index.js +12 -0
  136. package/lib/packlets/print/openPrintWindow.d.ts +35 -0
  137. package/lib/packlets/print/openPrintWindow.js +118 -0
  138. package/lib/packlets/responsive/ResponsiveProvider.d.ts +35 -0
  139. package/lib/packlets/responsive/ResponsiveProvider.js +93 -0
  140. package/lib/packlets/responsive/index.d.ts +7 -0
  141. package/lib/packlets/responsive/index.js +13 -0
  142. package/lib/packlets/responsive/useResponsiveLayout.d.ts +48 -0
  143. package/lib/packlets/responsive/useResponsiveLayout.js +121 -0
  144. package/lib/packlets/selectors/EntityRow.d.ts +45 -0
  145. package/lib/packlets/selectors/EntityRow.js +315 -0
  146. package/lib/packlets/selectors/PreferredSelector.d.ts +50 -0
  147. package/lib/packlets/selectors/PreferredSelector.js +287 -0
  148. package/lib/packlets/selectors/index.d.ts +5 -0
  149. package/lib/packlets/selectors/index.js +29 -0
  150. package/lib/packlets/sidebar/CollectionSection.d.ts +82 -0
  151. package/lib/packlets/sidebar/CollectionSection.js +143 -0
  152. package/lib/packlets/sidebar/EntityList.d.ts +105 -0
  153. package/lib/packlets/sidebar/EntityList.js +200 -0
  154. package/lib/packlets/sidebar/FilterBar.d.ts +26 -0
  155. package/lib/packlets/sidebar/FilterBar.js +48 -0
  156. package/lib/packlets/sidebar/FilterRow.d.ts +42 -0
  157. package/lib/packlets/sidebar/FilterRow.js +218 -0
  158. package/lib/packlets/sidebar/GroupedEntityList.d.ts +59 -0
  159. package/lib/packlets/sidebar/GroupedEntityList.js +219 -0
  160. package/lib/packlets/sidebar/SearchBar.d.ts +19 -0
  161. package/lib/packlets/sidebar/SearchBar.js +40 -0
  162. package/lib/packlets/sidebar/SidebarLayout.d.ts +28 -0
  163. package/lib/packlets/sidebar/SidebarLayout.js +98 -0
  164. package/lib/packlets/sidebar/index.d.ts +12 -0
  165. package/lib/packlets/sidebar/index.js +22 -0
  166. package/lib/packlets/theme/ThemeProvider.d.ts +68 -0
  167. package/lib/packlets/theme/ThemeProvider.js +178 -0
  168. package/lib/packlets/theme/index.d.ts +6 -0
  169. package/lib/packlets/theme/index.js +11 -0
  170. package/lib/packlets/top-bar/ModeSelector.d.ts +38 -0
  171. package/lib/packlets/top-bar/ModeSelector.js +52 -0
  172. package/lib/packlets/top-bar/TabBar.d.ts +31 -0
  173. package/lib/packlets/top-bar/TabBar.js +43 -0
  174. package/lib/packlets/top-bar/index.d.ts +7 -0
  175. package/lib/packlets/top-bar/index.js +12 -0
  176. package/lib/packlets/url-sync/index.d.ts +6 -0
  177. package/lib/packlets/url-sync/index.js +12 -0
  178. package/lib/packlets/url-sync/useUrlSync.d.ts +75 -0
  179. package/lib/packlets/url-sync/useUrlSync.js +162 -0
  180. package/package.json +82 -0
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Numeric input with select-all-on-focus and empty-string support.
3
+ *
4
+ * Uses `type="text"` with `inputMode="decimal"` to avoid native number-input
5
+ * quirks (spinner buttons, empty-string rejection). Manages a string internally
6
+ * but exposes `number | undefined` to callers.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ import React from 'react';
11
+ /**
12
+ * Props for the {@link NumericInput} component.
13
+ * @public
14
+ */
15
+ export interface INumericInputProps {
16
+ /** Current numeric value (`undefined` = empty field). */
17
+ readonly value: number | undefined;
18
+ /** Called with the parsed number on blur, or `undefined` if the field is empty. */
19
+ readonly onChange: (value: number | undefined) => void;
20
+ /** Accessible label (maps to `aria-label`). */
21
+ readonly label?: string;
22
+ /** Minimum allowed value (clamped on blur). */
23
+ readonly min?: number;
24
+ /** Maximum allowed value (clamped on blur). */
25
+ readonly max?: number;
26
+ /** Step for arrow-key increment/decrement. Defaults to 1. */
27
+ readonly step?: number;
28
+ /** Additional or replacement CSS classes. */
29
+ readonly className?: string;
30
+ /** Placeholder text shown when the field is empty. */
31
+ readonly placeholder?: string;
32
+ /** Auto-focus the input on mount. */
33
+ readonly autoFocus?: boolean;
34
+ /** Disabled state. */
35
+ readonly disabled?: boolean;
36
+ }
37
+ /**
38
+ * Numeric input that selects all on focus and allows empty values.
39
+ *
40
+ * Internally manages a string so the user can freely type, backspace to empty,
41
+ * and replace the entire value. On blur, the string is parsed to a number
42
+ * (clamped to min/max) and reported to the parent via `onChange`.
43
+ *
44
+ * @public
45
+ */
46
+ export declare function NumericInput(props: INumericInputProps): React.ReactElement;
47
+ //# sourceMappingURL=NumericInput.d.ts.map
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+ /*
3
+ * Copyright (c) 2026 Erik Fortune
4
+ *
5
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ * of this software and associated documentation files (the "Software"), to deal
7
+ * in the Software without restriction, including without limitation the rights
8
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ * copies of the Software, and to permit persons to whom the Software is
10
+ * furnished to do so, subject to the following conditions:
11
+ *
12
+ * The above copyright notice and this permission notice shall be included in all
13
+ * copies or substantial portions of the Software.
14
+ *
15
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ * SOFTWARE.
22
+ */
23
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
24
+ if (k2 === undefined) k2 = k;
25
+ var desc = Object.getOwnPropertyDescriptor(m, k);
26
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
27
+ desc = { enumerable: true, get: function() { return m[k]; } };
28
+ }
29
+ Object.defineProperty(o, k2, desc);
30
+ }) : (function(o, m, k, k2) {
31
+ if (k2 === undefined) k2 = k;
32
+ o[k2] = m[k];
33
+ }));
34
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
35
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
36
+ }) : function(o, v) {
37
+ o["default"] = v;
38
+ });
39
+ var __importStar = (this && this.__importStar) || (function () {
40
+ var ownKeys = function(o) {
41
+ ownKeys = Object.getOwnPropertyNames || function (o) {
42
+ var ar = [];
43
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
44
+ return ar;
45
+ };
46
+ return ownKeys(o);
47
+ };
48
+ return function (mod) {
49
+ if (mod && mod.__esModule) return mod;
50
+ var result = {};
51
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
52
+ __setModuleDefault(result, mod);
53
+ return result;
54
+ };
55
+ })();
56
+ Object.defineProperty(exports, "__esModule", { value: true });
57
+ exports.NumericInput = NumericInput;
58
+ /**
59
+ * Numeric input with select-all-on-focus and empty-string support.
60
+ *
61
+ * Uses `type="text"` with `inputMode="decimal"` to avoid native number-input
62
+ * quirks (spinner buttons, empty-string rejection). Manages a string internally
63
+ * but exposes `number | undefined` to callers.
64
+ *
65
+ * @packageDocumentation
66
+ */
67
+ const react_1 = __importStar(require("react"));
68
+ // ============================================================================
69
+ // Constants
70
+ // ============================================================================
71
+ const NUMERIC_PATTERN = /^-?\d*\.?\d*$/;
72
+ const DEFAULT_CLASS = 'text-sm border border-border rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-focus-ring focus:border-focus-ring';
73
+ // ============================================================================
74
+ // Helpers
75
+ // ============================================================================
76
+ function formatValue(value) {
77
+ return value !== undefined ? String(value) : '';
78
+ }
79
+ function clamp(value, min, max) {
80
+ let result = value;
81
+ if (min !== undefined && result < min) {
82
+ result = min;
83
+ }
84
+ if (max !== undefined && result > max) {
85
+ result = max;
86
+ }
87
+ return result;
88
+ }
89
+ // ============================================================================
90
+ // Component
91
+ // ============================================================================
92
+ /**
93
+ * Numeric input that selects all on focus and allows empty values.
94
+ *
95
+ * Internally manages a string so the user can freely type, backspace to empty,
96
+ * and replace the entire value. On blur, the string is parsed to a number
97
+ * (clamped to min/max) and reported to the parent via `onChange`.
98
+ *
99
+ * @public
100
+ */
101
+ function NumericInput(props) {
102
+ const { value, onChange, label, min, max, step, className, placeholder, autoFocus, disabled } = props;
103
+ const [displayValue, setDisplayValue] = (0, react_1.useState)(() => formatValue(value));
104
+ const focusedRef = (0, react_1.useRef)(false);
105
+ // Sync display when value prop changes externally (but not while focused)
106
+ (0, react_1.useEffect)(() => {
107
+ if (!focusedRef.current) {
108
+ setDisplayValue(formatValue(value));
109
+ }
110
+ }, [value]);
111
+ const handleFocus = (0, react_1.useCallback)((e) => {
112
+ focusedRef.current = true;
113
+ e.target.select();
114
+ }, []);
115
+ const handleChange = (0, react_1.useCallback)((e) => {
116
+ const raw = e.target.value;
117
+ if (raw === '' || NUMERIC_PATTERN.test(raw)) {
118
+ setDisplayValue(raw);
119
+ }
120
+ }, []);
121
+ const handleBlur = (0, react_1.useCallback)(() => {
122
+ focusedRef.current = false;
123
+ const trimmed = displayValue.trim();
124
+ if (trimmed === '' || trimmed === '-' || trimmed === '.') {
125
+ setDisplayValue('');
126
+ onChange(undefined);
127
+ return;
128
+ }
129
+ const parsed = parseFloat(trimmed);
130
+ if (isNaN(parsed)) {
131
+ setDisplayValue('');
132
+ onChange(undefined);
133
+ return;
134
+ }
135
+ const clamped = clamp(parsed, min, max);
136
+ setDisplayValue(String(clamped));
137
+ onChange(clamped);
138
+ }, [displayValue, onChange, min, max]);
139
+ const handleKeyDown = (0, react_1.useCallback)((e) => {
140
+ if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') {
141
+ return;
142
+ }
143
+ e.preventDefault();
144
+ const effectiveStep = step !== null && step !== void 0 ? step : 1;
145
+ const current = parseFloat(displayValue) || 0;
146
+ const next = e.key === 'ArrowUp' ? current + effectiveStep : current - effectiveStep;
147
+ const clamped = clamp(next, min, max);
148
+ // Round to avoid floating-point drift
149
+ const rounded = parseFloat(clamped.toFixed(10));
150
+ setDisplayValue(String(rounded));
151
+ onChange(rounded);
152
+ }, [displayValue, onChange, min, max, step]);
153
+ return (react_1.default.createElement("input", { type: "text", inputMode: "decimal", value: displayValue, onChange: handleChange, onFocus: handleFocus, onBlur: handleBlur, onKeyDown: handleKeyDown, className: className !== null && className !== void 0 ? className : DEFAULT_CLASS, "aria-label": label, placeholder: placeholder, autoFocus: autoFocus, disabled: disabled }));
154
+ }
155
+ //# sourceMappingURL=NumericInput.js.map
@@ -0,0 +1,46 @@
1
+ /**
2
+ * TypeaheadInput — text input with custom autocomplete dropdown supporting
3
+ * tiered suggestions and built-in blur resolution.
4
+ * @packageDocumentation
5
+ */
6
+ import React from 'react';
7
+ import { type ITypeaheadSuggestion } from './useTypeaheadMatch';
8
+ /**
9
+ * Props for the TypeaheadInput component.
10
+ * @public
11
+ */
12
+ export interface ITypeaheadInputProps<TId extends string = string> {
13
+ /** Current text value (controlled). */
14
+ readonly value: string;
15
+ /** Called on every keystroke. */
16
+ readonly onChange: (value: string) => void;
17
+ /** Full catalog of suggestions (lower priority). */
18
+ readonly suggestions: ReadonlyArray<ITypeaheadSuggestion<TId>>;
19
+ /** Optional priority suggestions shown first (e.g. recipe alternates). */
20
+ readonly prioritySuggestions?: ReadonlyArray<ITypeaheadSuggestion<TId>>;
21
+ /** Called when a suggestion is definitively selected (click, Enter, blur auto-resolve). */
22
+ readonly onSelect: (suggestion: ITypeaheadSuggestion<TId>) => void;
23
+ /** Called on blur/Enter/Tab when input cannot be resolved to a single suggestion. */
24
+ readonly onUnresolved?: (text: string) => void;
25
+ /** Placeholder text for the input. */
26
+ readonly placeholder?: string;
27
+ /** Additional CSS class for the input element. */
28
+ readonly className?: string;
29
+ /** If true, the input is disabled. */
30
+ readonly disabled?: boolean;
31
+ /** Maximum dropdown height in pixels. Default: 240. */
32
+ readonly maxHeight?: number;
33
+ /** If true, the input is focused on mount. */
34
+ readonly autoFocus?: boolean;
35
+ }
36
+ /**
37
+ * Text input with a custom autocomplete dropdown supporting tiered suggestions.
38
+ *
39
+ * Priority suggestions (e.g. recipe alternates) appear first, visually separated
40
+ * from the full catalog. On blur, applies resolution logic: exact match → auto-select,
41
+ * single partial match → auto-select, else fires `onUnresolved`.
42
+ *
43
+ * @public
44
+ */
45
+ export declare function TypeaheadInput<TId extends string = string>(props: ITypeaheadInputProps<TId>): React.ReactElement;
46
+ //# sourceMappingURL=TypeaheadInput.d.ts.map
@@ -0,0 +1,243 @@
1
+ "use strict";
2
+ /*
3
+ * Copyright (c) 2026 Erik Fortune
4
+ *
5
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ * of this software and associated documentation files (the "Software"), to deal
7
+ * in the Software without restriction, including without limitation the rights
8
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ * copies of the Software, and to permit persons to whom the Software is
10
+ * furnished to do so, subject to the following conditions:
11
+ *
12
+ * The above copyright notice and this permission notice shall be included in all
13
+ * copies or substantial portions of the Software.
14
+ *
15
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ * SOFTWARE.
22
+ */
23
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
24
+ if (k2 === undefined) k2 = k;
25
+ var desc = Object.getOwnPropertyDescriptor(m, k);
26
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
27
+ desc = { enumerable: true, get: function() { return m[k]; } };
28
+ }
29
+ Object.defineProperty(o, k2, desc);
30
+ }) : (function(o, m, k, k2) {
31
+ if (k2 === undefined) k2 = k;
32
+ o[k2] = m[k];
33
+ }));
34
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
35
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
36
+ }) : function(o, v) {
37
+ o["default"] = v;
38
+ });
39
+ var __importStar = (this && this.__importStar) || (function () {
40
+ var ownKeys = function(o) {
41
+ ownKeys = Object.getOwnPropertyNames || function (o) {
42
+ var ar = [];
43
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
44
+ return ar;
45
+ };
46
+ return ownKeys(o);
47
+ };
48
+ return function (mod) {
49
+ if (mod && mod.__esModule) return mod;
50
+ var result = {};
51
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
52
+ __setModuleDefault(result, mod);
53
+ return result;
54
+ };
55
+ })();
56
+ Object.defineProperty(exports, "__esModule", { value: true });
57
+ exports.TypeaheadInput = TypeaheadInput;
58
+ /**
59
+ * TypeaheadInput — text input with custom autocomplete dropdown supporting
60
+ * tiered suggestions and built-in blur resolution.
61
+ * @packageDocumentation
62
+ */
63
+ const react_1 = __importStar(require("react"));
64
+ const useTypeaheadMatch_1 = require("./useTypeaheadMatch");
65
+ // ============================================================================
66
+ // Component
67
+ // ============================================================================
68
+ /**
69
+ * Text input with a custom autocomplete dropdown supporting tiered suggestions.
70
+ *
71
+ * Priority suggestions (e.g. recipe alternates) appear first, visually separated
72
+ * from the full catalog. On blur, applies resolution logic: exact match → auto-select,
73
+ * single partial match → auto-select, else fires `onUnresolved`.
74
+ *
75
+ * @public
76
+ */
77
+ function TypeaheadInput(props) {
78
+ const { value, onChange, suggestions, prioritySuggestions, onSelect, onUnresolved, placeholder, className, disabled, maxHeight = 240, autoFocus } = props;
79
+ const [isOpen, setIsOpen] = (0, react_1.useState)(false);
80
+ const [focusIndex, setFocusIndex] = (0, react_1.useState)(-1);
81
+ const inputRef = (0, react_1.useRef)(null);
82
+ const dropdownRef = (0, react_1.useRef)(null);
83
+ const itemRefs = (0, react_1.useRef)([]);
84
+ const mouseDownOnDropdownRef = (0, react_1.useRef)(false);
85
+ const matcher = (0, useTypeaheadMatch_1.useTypeaheadMatch)(suggestions, prioritySuggestions);
86
+ // ---- Filtered suggestions ----
87
+ const filtered = (0, react_1.useMemo)(() => matcher.filterSuggestions(value), [matcher, value]);
88
+ const flatItems = (0, react_1.useMemo)(() => [...filtered.priority, ...filtered.catalog], [filtered.priority, filtered.catalog]);
89
+ const hasPriorityItems = filtered.priority.length > 0;
90
+ const hasCatalogItems = filtered.catalog.length > 0;
91
+ const hasItems = flatItems.length > 0;
92
+ // Reset focus index when suggestions change
93
+ (0, react_1.useEffect)(() => {
94
+ setFocusIndex(-1);
95
+ }, [value]);
96
+ // Scroll focused item into view
97
+ (0, react_1.useEffect)(() => {
98
+ var _a;
99
+ if (isOpen && focusIndex >= 0 && itemRefs.current[focusIndex]) {
100
+ (_a = itemRefs.current[focusIndex]) === null || _a === void 0 ? void 0 : _a.scrollIntoView({ block: 'nearest' });
101
+ }
102
+ }, [isOpen, focusIndex]);
103
+ // Close dropdown on outside click
104
+ (0, react_1.useEffect)(() => {
105
+ if (!isOpen)
106
+ return;
107
+ const handler = (e) => {
108
+ if (dropdownRef.current &&
109
+ !dropdownRef.current.contains(e.target) &&
110
+ inputRef.current &&
111
+ !inputRef.current.contains(e.target)) {
112
+ setIsOpen(false);
113
+ }
114
+ };
115
+ document.addEventListener('mousedown', handler);
116
+ return () => {
117
+ document.removeEventListener('mousedown', handler);
118
+ };
119
+ }, [isOpen]);
120
+ // ---- Selection ----
121
+ const handleSelect = (0, react_1.useCallback)((item) => {
122
+ onSelect(item);
123
+ setIsOpen(false);
124
+ setFocusIndex(-1);
125
+ }, [onSelect]);
126
+ const handleResolve = (0, react_1.useCallback)((text) => {
127
+ const trimmed = text.trim();
128
+ if (!trimmed) {
129
+ setIsOpen(false);
130
+ return;
131
+ }
132
+ const match = matcher.resolveOnBlur(trimmed);
133
+ if (match) {
134
+ handleSelect(match);
135
+ }
136
+ else if (onUnresolved) {
137
+ onUnresolved(trimmed);
138
+ setIsOpen(false);
139
+ }
140
+ else {
141
+ setIsOpen(false);
142
+ }
143
+ }, [matcher, handleSelect, onUnresolved]);
144
+ // ---- Input handlers ----
145
+ const handleChange = (0, react_1.useCallback)((e) => {
146
+ onChange(e.target.value);
147
+ if (!isOpen) {
148
+ setIsOpen(true);
149
+ }
150
+ }, [onChange, isOpen]);
151
+ const handleFocus = (0, react_1.useCallback)(() => {
152
+ var _a;
153
+ (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.select();
154
+ if (hasItems) {
155
+ setIsOpen(true);
156
+ }
157
+ }, [hasItems]);
158
+ const handleBlur = (0, react_1.useCallback)(() => {
159
+ // Guard: don't resolve if user is clicking an item in the dropdown
160
+ if (mouseDownOnDropdownRef.current) {
161
+ mouseDownOnDropdownRef.current = false;
162
+ return;
163
+ }
164
+ handleResolve(value);
165
+ }, [value, handleResolve]);
166
+ const handleKeyDown = (0, react_1.useCallback)((e) => {
167
+ if (!isOpen) {
168
+ if (e.key === 'ArrowDown' && hasItems) {
169
+ e.preventDefault();
170
+ setIsOpen(true);
171
+ setFocusIndex(0);
172
+ }
173
+ return;
174
+ }
175
+ switch (e.key) {
176
+ case 'Escape': {
177
+ e.preventDefault();
178
+ e.stopPropagation();
179
+ setIsOpen(false);
180
+ setFocusIndex(-1);
181
+ break;
182
+ }
183
+ case 'ArrowDown': {
184
+ e.preventDefault();
185
+ setFocusIndex((prev) => (prev < flatItems.length - 1 ? prev + 1 : 0));
186
+ break;
187
+ }
188
+ case 'ArrowUp': {
189
+ e.preventDefault();
190
+ setFocusIndex((prev) => (prev > 0 ? prev - 1 : flatItems.length - 1));
191
+ break;
192
+ }
193
+ case 'Enter': {
194
+ e.preventDefault();
195
+ if (focusIndex >= 0 && focusIndex < flatItems.length) {
196
+ handleSelect(flatItems[focusIndex]);
197
+ }
198
+ else {
199
+ handleResolve(value);
200
+ }
201
+ break;
202
+ }
203
+ case 'Tab': {
204
+ // Allow default tab behavior but resolve first
205
+ handleResolve(value);
206
+ break;
207
+ }
208
+ default:
209
+ break;
210
+ }
211
+ }, [isOpen, hasItems, flatItems, focusIndex, handleSelect, handleResolve, value]);
212
+ // ---- Dropdown mouse guard ----
213
+ const handleDropdownMouseDown = (0, react_1.useCallback)(() => {
214
+ mouseDownOnDropdownRef.current = true;
215
+ }, []);
216
+ // ---- Render ----
217
+ const priorityEndIndex = filtered.priority.length;
218
+ return (react_1.default.createElement("div", { className: "relative" },
219
+ react_1.default.createElement("input", { ref: inputRef, type: "text", value: value, onChange: handleChange, onFocus: handleFocus, onBlur: handleBlur, onKeyDown: handleKeyDown, placeholder: placeholder, disabled: disabled, autoFocus: autoFocus, className: className !== null && className !== void 0 ? className : 'w-full px-2 py-1 text-sm border border-border rounded focus:outline-none focus:ring-1 focus:ring-focus-ring focus:border-focus-ring', role: "combobox", "aria-expanded": isOpen, "aria-autocomplete": "list", autoComplete: "off" }),
220
+ isOpen && hasItems && (react_1.default.createElement("div", { ref: dropdownRef, className: "absolute z-50 mt-1 w-full bg-surface border border-border rounded-lg shadow-lg overflow-hidden", style: { maxHeight }, role: "listbox", onMouseDown: handleDropdownMouseDown },
221
+ react_1.default.createElement("div", { className: "overflow-y-auto", style: { maxHeight } },
222
+ hasPriorityItems &&
223
+ filtered.priority.map((item, index) => {
224
+ const isFocused = index === focusIndex;
225
+ return (react_1.default.createElement("button", { key: item.id, ref: (el) => {
226
+ itemRefs.current[index] = el;
227
+ }, role: "option", "aria-selected": isFocused, onClick: () => handleSelect(item), className: `flex items-center w-full px-2.5 py-1.5 text-left text-sm transition-colors border-l-2 ${isFocused
228
+ ? 'bg-surface-alt text-primary border-brand-primary/50'
229
+ : 'text-secondary hover:bg-hover border-brand-primary/20'}` },
230
+ react_1.default.createElement("span", { className: "flex-1 min-w-0 truncate" }, item.name)));
231
+ }),
232
+ hasPriorityItems && hasCatalogItems && react_1.default.createElement("div", { className: "border-t border-border my-0.5" }),
233
+ hasCatalogItems &&
234
+ filtered.catalog.map((item, catalogIndex) => {
235
+ const flatIndex = priorityEndIndex + catalogIndex;
236
+ const isFocused = flatIndex === focusIndex;
237
+ return (react_1.default.createElement("button", { key: item.id, ref: (el) => {
238
+ itemRefs.current[flatIndex] = el;
239
+ }, role: "option", "aria-selected": isFocused, onClick: () => handleSelect(item), className: `flex items-center w-full px-2.5 py-1.5 text-left text-sm transition-colors ${isFocused ? 'bg-surface-alt text-primary' : 'text-secondary hover:bg-hover'}` },
240
+ react_1.default.createElement("span", { className: "flex-1 min-w-0 truncate" }, item.name)));
241
+ }))))));
242
+ }
243
+ //# sourceMappingURL=TypeaheadInput.js.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Editing packlet - generic form field primitives for entity editors.
3
+ * @packageDocumentation
4
+ */
5
+ export { EditField, type IEditFieldProps, EditSection, type IEditSectionProps, TextInput, type ITextInputProps, OptionalTextInput, type IOptionalTextInputProps, TextAreaInput, type ITextAreaInputProps, NumberInput, type INumberInputProps, SelectInput, type ISelectInputProps, TagsInput, type ITagsInputProps, CheckboxInput, type ICheckboxInputProps } from './EditFieldHelpers';
6
+ export { MultiActionButton, type IMultiActionButtonProps, type IMultiActionButtonAction } from './MultiActionButton';
7
+ export { NumericInput, type INumericInputProps } from './NumericInput';
8
+ export { TypeaheadInput, type ITypeaheadInputProps } from './TypeaheadInput';
9
+ export { useTypeaheadMatch, type ITypeaheadSuggestion, type ITypeaheadMatchResult, type IFilteredSuggestions } from './useTypeaheadMatch';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ /**
3
+ * Editing packlet - generic form field primitives for entity editors.
4
+ * @packageDocumentation
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.useTypeaheadMatch = exports.TypeaheadInput = exports.NumericInput = exports.MultiActionButton = exports.CheckboxInput = exports.TagsInput = exports.SelectInput = exports.NumberInput = exports.TextAreaInput = exports.OptionalTextInput = exports.TextInput = exports.EditSection = exports.EditField = void 0;
8
+ var EditFieldHelpers_1 = require("./EditFieldHelpers");
9
+ Object.defineProperty(exports, "EditField", { enumerable: true, get: function () { return EditFieldHelpers_1.EditField; } });
10
+ Object.defineProperty(exports, "EditSection", { enumerable: true, get: function () { return EditFieldHelpers_1.EditSection; } });
11
+ Object.defineProperty(exports, "TextInput", { enumerable: true, get: function () { return EditFieldHelpers_1.TextInput; } });
12
+ Object.defineProperty(exports, "OptionalTextInput", { enumerable: true, get: function () { return EditFieldHelpers_1.OptionalTextInput; } });
13
+ Object.defineProperty(exports, "TextAreaInput", { enumerable: true, get: function () { return EditFieldHelpers_1.TextAreaInput; } });
14
+ Object.defineProperty(exports, "NumberInput", { enumerable: true, get: function () { return EditFieldHelpers_1.NumberInput; } });
15
+ Object.defineProperty(exports, "SelectInput", { enumerable: true, get: function () { return EditFieldHelpers_1.SelectInput; } });
16
+ Object.defineProperty(exports, "TagsInput", { enumerable: true, get: function () { return EditFieldHelpers_1.TagsInput; } });
17
+ Object.defineProperty(exports, "CheckboxInput", { enumerable: true, get: function () { return EditFieldHelpers_1.CheckboxInput; } });
18
+ var MultiActionButton_1 = require("./MultiActionButton");
19
+ Object.defineProperty(exports, "MultiActionButton", { enumerable: true, get: function () { return MultiActionButton_1.MultiActionButton; } });
20
+ var NumericInput_1 = require("./NumericInput");
21
+ Object.defineProperty(exports, "NumericInput", { enumerable: true, get: function () { return NumericInput_1.NumericInput; } });
22
+ var TypeaheadInput_1 = require("./TypeaheadInput");
23
+ Object.defineProperty(exports, "TypeaheadInput", { enumerable: true, get: function () { return TypeaheadInput_1.TypeaheadInput; } });
24
+ var useTypeaheadMatch_1 = require("./useTypeaheadMatch");
25
+ Object.defineProperty(exports, "useTypeaheadMatch", { enumerable: true, get: function () { return useTypeaheadMatch_1.useTypeaheadMatch; } });
26
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,42 @@
1
+ /**
2
+ * A suggestion entry for typeahead matching.
3
+ * @public
4
+ */
5
+ export interface ITypeaheadSuggestion<TId extends string = string> {
6
+ readonly id: TId;
7
+ readonly name: string;
8
+ }
9
+ /**
10
+ * Filtered suggestions split by tier.
11
+ * @public
12
+ */
13
+ export interface IFilteredSuggestions<TId extends string> {
14
+ readonly priority: ReadonlyArray<ITypeaheadSuggestion<TId>>;
15
+ readonly catalog: ReadonlyArray<ITypeaheadSuggestion<TId>>;
16
+ }
17
+ /**
18
+ * Result of the useTypeaheadMatch hook.
19
+ * @public
20
+ */
21
+ export interface ITypeaheadMatchResult<TId extends string> {
22
+ /** Find an exact match by id or case-insensitive name. Checks priority first. */
23
+ findExactMatch: (input: string) => ITypeaheadSuggestion<TId> | undefined;
24
+ /** Resolve on blur: exact match → use it; single partial → use it; else undefined. */
25
+ resolveOnBlur: (input: string) => ITypeaheadSuggestion<TId> | undefined;
26
+ /** Filter suggestions by input text, returning priority and catalog tiers separately. */
27
+ filterSuggestions: (input: string) => IFilteredSuggestions<TId>;
28
+ }
29
+ /**
30
+ * Hook that provides typeahead matching and filtering with tiered priority support.
31
+ *
32
+ * `findExactMatch` matches by exact id or case-insensitive name (checks priority first).
33
+ * `resolveOnBlur` tries exact match, then single partial match (priority first).
34
+ * `filterSuggestions` returns suggestions split by tier, filtered by substring match.
35
+ *
36
+ * @param suggestions - The full catalog of suggestions
37
+ * @param prioritySuggestions - Optional priority suggestions shown first (e.g. recipe alternates)
38
+ * @returns Match and filter functions
39
+ * @public
40
+ */
41
+ export declare function useTypeaheadMatch<TId extends string>(suggestions: ReadonlyArray<ITypeaheadSuggestion<TId>>, prioritySuggestions?: ReadonlyArray<ITypeaheadSuggestion<TId>>): ITypeaheadMatchResult<TId>;
42
+ //# sourceMappingURL=useTypeaheadMatch.d.ts.map