@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,117 @@
1
+ /*
2
+ * Copyright (c) 2026 Erik Fortune
3
+ *
4
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ * of this software and associated documentation files (the "Software"), to deal
6
+ * in the Software without restriction, including without limitation the rights
7
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ * copies of the Software, and to permit persons to whom the Software is
9
+ * furnished to do so, subject to the following conditions:
10
+ *
11
+ * The above copyright notice and this permission notice shall be included in all
12
+ * copies or substantial portions of the Software.
13
+ *
14
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ * SOFTWARE.
21
+ */
22
+ /**
23
+ * React context and hooks for the keyboard shortcut registry.
24
+ * @packageDocumentation
25
+ */
26
+ import React, { useContext, useEffect, useMemo, useRef } from 'react';
27
+ import { KeyboardShortcutRegistry } from './registry';
28
+ // ============================================================================
29
+ // Context
30
+ // ============================================================================
31
+ const KeyboardContext = React.createContext(undefined);
32
+ /**
33
+ * Provides a KeyboardShortcutRegistry to the component tree.
34
+ * Should be mounted once at the app root.
35
+ * @public
36
+ */
37
+ export function KeyboardShortcutProvider(props) {
38
+ const registry = useMemo(() => new KeyboardShortcutRegistry(), []);
39
+ useEffect(() => {
40
+ return () => registry.dispose();
41
+ }, [registry]);
42
+ return React.createElement(KeyboardContext.Provider, { value: registry }, props.children);
43
+ }
44
+ // ============================================================================
45
+ // Hooks
46
+ // ============================================================================
47
+ /**
48
+ * Returns the keyboard shortcut registry.
49
+ * Must be used within a KeyboardShortcutProvider.
50
+ * @public
51
+ */
52
+ export function useKeyboardRegistry() {
53
+ const ctx = useContext(KeyboardContext);
54
+ if (ctx === undefined) {
55
+ throw new Error('useKeyboardRegistry must be used within a KeyboardShortcutProvider');
56
+ }
57
+ return ctx;
58
+ }
59
+ /**
60
+ * Registers one or more keyboard shortcuts for the lifetime of the component.
61
+ * Shortcuts are automatically unregistered on unmount.
62
+ *
63
+ * @example
64
+ * ```tsx
65
+ * useKeyboardShortcuts([
66
+ * {
67
+ * binding: { key: 'k', meta: true },
68
+ * description: 'Open command palette',
69
+ * handler: () => { setCommandPaletteOpen(true); }
70
+ * },
71
+ * {
72
+ * binding: { key: 'z', meta: true },
73
+ * description: 'Undo',
74
+ * handler: () => { workspace.undo(); }
75
+ * }
76
+ * ]);
77
+ * ```
78
+ *
79
+ * @param shortcuts - Array of shortcuts to register
80
+ * @public
81
+ */
82
+ export function useKeyboardShortcuts(shortcuts) {
83
+ const registry = useKeyboardRegistry();
84
+ const shortcutsRef = useRef(shortcuts);
85
+ shortcutsRef.current = shortcuts;
86
+ useEffect(() => {
87
+ // Register stable wrapper shortcuts that delegate to the current ref entries.
88
+ // This means the effect only runs when the registry changes or the number of
89
+ // shortcuts changes — not on every render when the array identity changes.
90
+ const registrations = shortcutsRef.current.map((__item, index) => registry.register({
91
+ get binding() {
92
+ var _a, _b;
93
+ return (_b = (_a = shortcutsRef.current[index]) === null || _a === void 0 ? void 0 : _a.binding) !== null && _b !== void 0 ? _b : { key: '' };
94
+ },
95
+ get description() {
96
+ var _a, _b;
97
+ return (_b = (_a = shortcutsRef.current[index]) === null || _a === void 0 ? void 0 : _a.description) !== null && _b !== void 0 ? _b : '';
98
+ },
99
+ get priority() {
100
+ var _a;
101
+ return (_a = shortcutsRef.current[index]) === null || _a === void 0 ? void 0 : _a.priority;
102
+ },
103
+ handler: () => {
104
+ var _a;
105
+ return (_a = shortcutsRef.current[index]) === null || _a === void 0 ? void 0 : _a.handler();
106
+ }
107
+ }));
108
+ return () => {
109
+ for (const reg of registrations) {
110
+ reg.unregister();
111
+ }
112
+ };
113
+ // Re-register only when the registry instance or the number of shortcuts changes.
114
+ // Handler/binding updates are picked up via the ref without re-registration.
115
+ }, [registry, shortcuts.length]);
116
+ }
117
+ //# sourceMappingURL=useKeyboardShortcuts.js.map
@@ -0,0 +1,76 @@
1
+ /*
2
+ * Copyright (c) 2026 Erik Fortune
3
+ *
4
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ * of this software and associated documentation files (the "Software"), to deal
6
+ * in the Software without restriction, including without limitation the rights
7
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ * copies of the Software, and to permit persons to whom the Software is
9
+ * furnished to do so, subject to the following conditions:
10
+ *
11
+ * The above copyright notice and this permission notice shall be included in all
12
+ * copies or substantial portions of the Software.
13
+ *
14
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ * SOFTWARE.
21
+ */
22
+ import React, { useCallback, useContext, useMemo, useState } from 'react';
23
+ import { createMessage } from './model';
24
+ // ============================================================================
25
+ // Context
26
+ // ============================================================================
27
+ const MessagesContext = React.createContext(undefined);
28
+ /**
29
+ * Provides the global message stream to the component tree.
30
+ * @public
31
+ */
32
+ export function MessagesProvider(props) {
33
+ const { maxMessages = 200, children } = props;
34
+ const [messages, setMessages] = useState([]);
35
+ const [dismissedIds, setDismissedIds] = useState(new Set());
36
+ const addMessage = useCallback((severity, text, action) => {
37
+ const msg = createMessage(severity, text, action);
38
+ setMessages((prev) => {
39
+ const next = [...prev, msg];
40
+ return next.length > maxMessages ? next.slice(next.length - maxMessages) : next;
41
+ });
42
+ return msg;
43
+ }, [maxMessages]);
44
+ const dismissMessage = useCallback((id) => {
45
+ setDismissedIds((prev) => new Set(prev).add(id));
46
+ }, []);
47
+ const clearMessages = useCallback(() => {
48
+ setMessages([]);
49
+ setDismissedIds(new Set());
50
+ }, []);
51
+ const activeToasts = useMemo(() => messages.filter((m) => !dismissedIds.has(m.id)), [messages, dismissedIds]);
52
+ const value = useMemo(() => ({
53
+ messages,
54
+ addMessage,
55
+ dismissMessage,
56
+ clearMessages,
57
+ activeToasts
58
+ }), [messages, addMessage, dismissMessage, clearMessages, activeToasts]);
59
+ return React.createElement(MessagesContext.Provider, { value: value }, children);
60
+ }
61
+ // ============================================================================
62
+ // Hook
63
+ // ============================================================================
64
+ /**
65
+ * Hook to access the messages context.
66
+ * Must be used within a MessagesProvider.
67
+ * @public
68
+ */
69
+ export function useMessages() {
70
+ const ctx = useContext(MessagesContext);
71
+ if (ctx === undefined) {
72
+ throw new Error('useMessages must be used within a MessagesProvider');
73
+ }
74
+ return ctx;
75
+ }
76
+ //# sourceMappingURL=MessagesContext.js.map
@@ -0,0 +1,103 @@
1
+ /*
2
+ * Copyright (c) 2026 Erik Fortune
3
+ *
4
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ * of this software and associated documentation files (the "Software"), to deal
6
+ * in the Software without restriction, including without limitation the rights
7
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ * copies of the Software, and to permit persons to whom the Software is
9
+ * furnished to do so, subject to the following conditions:
10
+ *
11
+ * The above copyright notice and this permission notice shall be included in all
12
+ * copies or substantial portions of the Software.
13
+ *
14
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ * SOFTWARE.
21
+ */
22
+ /**
23
+ * Bridge between the MessagesContext and the ts-utils ILogger/LogReporter system.
24
+ *
25
+ * Provides an ILogger implementation that routes log messages into the
26
+ * MessagesContext, enabling Result.report() integration in React components.
27
+ *
28
+ * @packageDocumentation
29
+ */
30
+ import { Logging, succeed } from '@fgv/ts-utils';
31
+ // ============================================================================
32
+ // Level Mapping
33
+ // ============================================================================
34
+ /**
35
+ * Maps ts-utils MessageLogLevel to our MessageSeverity.
36
+ * `detail` maps to `'info'` (shown when logLevel is 'detail' or 'all').
37
+ * `quiet` is always suppressed (only shown when reporter level is 'all', handled upstream).
38
+ * @internal
39
+ */
40
+ function mapLogLevel(level) {
41
+ switch (level) {
42
+ case 'detail':
43
+ case 'info':
44
+ return 'info';
45
+ case 'warning':
46
+ return 'warning';
47
+ case 'error':
48
+ return 'error';
49
+ case 'quiet':
50
+ return undefined;
51
+ }
52
+ }
53
+ // ============================================================================
54
+ // MessagesLogger
55
+ // ============================================================================
56
+ /**
57
+ * An {@link @fgv/ts-utils#Logging.ILogger | ILogger} implementation that routes
58
+ * messages into a {@link IMessagesContextValue | MessagesContext}.
59
+ *
60
+ * Use this as the `logger` parameter when creating a `LogReporter` to get
61
+ * full `Result.report()` integration that feeds into the app's toast/log system.
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * const { addMessage } = useMessages();
66
+ * const logger = new MessagesLogger(addMessage);
67
+ * const reporter = new LogReporter<unknown>({ logger });
68
+ *
69
+ * // Now Result.report() flows into toasts + log panel
70
+ * someResult.report(reporter, { success: 'info', failure: 'error' });
71
+ * ```
72
+ *
73
+ * @public
74
+ */
75
+ export class MessagesLogger extends Logging.LoggerBase {
76
+ /**
77
+ * Creates a new MessagesLogger.
78
+ * @param addMessage - The addMessage function from MessagesContext
79
+ * @param logLevel - The minimum log level to display (default: 'info')
80
+ * @param defaultAction - Optional default action to attach to all messages
81
+ */
82
+ constructor(addMessage, logLevel, defaultAction) {
83
+ super(logLevel);
84
+ this._addMessage = addMessage;
85
+ this._defaultAction = defaultAction;
86
+ }
87
+ /**
88
+ * Routes a formatted log message into the MessagesContext.
89
+ * @param message - The formatted message string
90
+ * @param level - The log level
91
+ * @returns Success with the message if it was logged, or Success with undefined if suppressed
92
+ * @internal
93
+ */
94
+ _log(message, level) {
95
+ const severity = mapLogLevel(level);
96
+ if (severity !== undefined) {
97
+ this._addMessage(severity, message, this._defaultAction);
98
+ return succeed(message);
99
+ }
100
+ return succeed(undefined);
101
+ }
102
+ }
103
+ //# sourceMappingURL=MessagesLogger.js.map
@@ -0,0 +1,154 @@
1
+ /*
2
+ * Copyright (c) 2026 Erik Fortune
3
+ *
4
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ * of this software and associated documentation files (the "Software"), to deal
6
+ * in the Software without restriction, including without limitation the rights
7
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ * copies of the Software, and to permit persons to whom the Software is
9
+ * furnished to do so, subject to the following conditions:
10
+ *
11
+ * The above copyright notice and this permission notice shall be included in all
12
+ * copies or substantial portions of the Software.
13
+ *
14
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ * SOFTWARE.
21
+ */
22
+ import React, { useCallback, useMemo, useState } from 'react';
23
+ import { Logging } from '@fgv/ts-utils';
24
+ import { FunnelIcon, DocumentDuplicateIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline';
25
+ import { CheckIcon } from '@heroicons/react/24/solid';
26
+ import { useResponsive } from '../responsive';
27
+ // ============================================================================
28
+ // Severity display config
29
+ // ============================================================================
30
+ const SEVERITY_ICONS = {
31
+ info: '\u2139',
32
+ success: '\u2713',
33
+ warning: '\u26A0',
34
+ error: '\u2717'
35
+ };
36
+ const SEVERITY_COLORS = {
37
+ info: 'text-status-info-icon',
38
+ success: 'text-status-success-icon',
39
+ warning: 'text-status-warning-icon',
40
+ error: 'text-status-error-icon'
41
+ };
42
+ const LOG_ROW_COLORS = {
43
+ info: '',
44
+ success: 'bg-status-success-bg',
45
+ warning: 'bg-status-warning-bg',
46
+ error: 'bg-status-error-bg'
47
+ };
48
+ // ============================================================================
49
+ // Level filter config
50
+ // ============================================================================
51
+ /**
52
+ * Maps MessageSeverity to MessageLogLevel for shouldLog filtering.
53
+ * 'success' is treated as 'info' since it doesn't exist in the log level hierarchy.
54
+ */
55
+ function severityToLogLevel(severity) {
56
+ return severity === 'success' ? 'info' : severity;
57
+ }
58
+ /**
59
+ * Filter choices exposed in the UI, mapping labels to ReporterLogLevel values.
60
+ */
61
+ const FILTER_LEVELS = [
62
+ { label: 'All', level: 'all' },
63
+ { label: 'Info+', level: 'info' },
64
+ { label: 'Warn+', level: 'warning' },
65
+ { label: 'Error', level: 'error' }
66
+ ];
67
+ /**
68
+ * Collapsible status bar / log panel at the bottom of the application.
69
+ *
70
+ * Collapsed: shows severity counts.
71
+ * Expanded: filterable, searchable, copyable log of all messages.
72
+ * @public
73
+ */
74
+ export function StatusBar(props) {
75
+ const { messages, onClear, initialFilterLevel } = props;
76
+ const { layoutMode } = useResponsive();
77
+ const isMobile = layoutMode === 'mobile';
78
+ const [expanded, setExpanded] = useState(false);
79
+ const [showFilters, setShowFilters] = useState(false);
80
+ const [filterLevel, setFilterLevel] = useState(initialFilterLevel !== null && initialFilterLevel !== void 0 ? initialFilterLevel : 'all');
81
+ const [searchTerm, setSearchTerm] = useState('');
82
+ const [copySuccessId, setCopySuccessId] = useState(null);
83
+ const counts = useMemo(() => {
84
+ const result = { info: 0, success: 0, warning: 0, error: 0 };
85
+ for (const msg of messages) {
86
+ result[msg.severity]++;
87
+ }
88
+ return result;
89
+ }, [messages]);
90
+ const filteredMessages = useMemo(() => {
91
+ return messages.filter((msg) => {
92
+ const passesLevel = Logging.shouldLog(severityToLogLevel(msg.severity), filterLevel);
93
+ const passesSearch = searchTerm === '' || msg.text.toLowerCase().includes(searchTerm.toLowerCase());
94
+ return passesLevel && passesSearch;
95
+ });
96
+ }, [messages, filterLevel, searchTerm]);
97
+ const isFiltered = filteredMessages.length !== messages.length;
98
+ const formatTime = useCallback((timestamp) => {
99
+ return new Date(timestamp).toLocaleTimeString();
100
+ }, []);
101
+ const copyToClipboard = useCallback((text, id) => {
102
+ navigator.clipboard
103
+ .writeText(text)
104
+ .then(() => {
105
+ setCopySuccessId(id);
106
+ setTimeout(() => setCopySuccessId(null), 2000);
107
+ })
108
+ .catch(() => undefined);
109
+ }, []);
110
+ const copyAllFiltered = useCallback(() => {
111
+ const text = filteredMessages
112
+ .map((msg) => `[${msg.severity.toUpperCase()}] ${formatTime(msg.timestamp)} - ${msg.text}`)
113
+ .join('\n');
114
+ copyToClipboard(text, '__all__');
115
+ }, [filteredMessages, formatTime, copyToClipboard]);
116
+ return (React.createElement("div", { className: "border-t border-border bg-surface" },
117
+ React.createElement("button", { onClick: () => setExpanded(!expanded), className: "flex items-center justify-between w-full px-4 py-1.5 text-xs text-secondary hover:bg-hover" },
118
+ React.createElement("div", { className: "flex items-center gap-4" },
119
+ Object.keys(counts).map((severity) => counts[severity] > 0 ? (React.createElement("span", { key: severity, className: `flex items-center gap-1 ${SEVERITY_COLORS[severity]}` },
120
+ React.createElement("span", null, SEVERITY_ICONS[severity]),
121
+ React.createElement("span", null, counts[severity]))) : null),
122
+ messages.length === 0 && React.createElement("span", { className: "text-muted" }, "No messages")),
123
+ React.createElement("span", { className: "text-muted" }, expanded ? '\u25BC' : '\u25B2')),
124
+ expanded && (React.createElement(React.Fragment, null,
125
+ isMobile && (React.createElement("div", { className: "fixed inset-0 z-40 bg-backdrop", onClick: () => setExpanded(false) })),
126
+ React.createElement("div", { className: isMobile
127
+ ? 'fixed inset-x-0 bottom-8 z-50 flex flex-col bg-surface border-t border-border shadow-xl rounded-t-lg max-h-[70vh]'
128
+ : 'border-t border-border-subtle' },
129
+ React.createElement("div", { className: "flex items-center justify-between px-4 py-1 bg-surface-alt border-b border-border-subtle shrink-0" },
130
+ React.createElement("span", { className: "text-xs font-medium text-muted" }, isFiltered
131
+ ? `${filteredMessages.length} of ${messages.length} messages`
132
+ : `${messages.length} message${messages.length !== 1 ? 's' : ''}`),
133
+ React.createElement("div", { className: "flex items-center gap-1" },
134
+ React.createElement("button", { onClick: () => setShowFilters(!showFilters), className: `p-1 rounded hover:bg-surface-raised ${showFilters ? 'bg-surface-raised' : ''}`, title: "Filter messages" },
135
+ React.createElement(FunnelIcon, { className: "h-3.5 w-3.5 text-muted" })),
136
+ React.createElement("button", { onClick: copyAllFiltered, className: `p-1 rounded transition-colors ${copySuccessId === '__all__' ? 'bg-status-success-bg' : 'hover:bg-surface-raised'}`, title: copySuccessId === '__all__' ? 'Copied!' : 'Copy filtered messages' }, copySuccessId === '__all__' ? (React.createElement(CheckIcon, { className: "h-3.5 w-3.5 text-status-success-icon" })) : (React.createElement(DocumentDuplicateIcon, { className: "h-3.5 w-3.5 text-muted" }))),
137
+ React.createElement("button", { onClick: onClear, className: "text-xs text-muted hover:text-secondary ml-1" }, "Clear"))),
138
+ showFilters && (React.createElement("div", { className: "px-4 py-1.5 bg-surface-raised border-b border-border-subtle flex items-center gap-3 shrink-0" },
139
+ React.createElement("div", { className: "flex items-center gap-1" }, FILTER_LEVELS.map(({ label, level }) => (React.createElement("button", { key: level, onClick: () => setFilterLevel(level), className: `text-xs px-2 py-0.5 rounded border ${filterLevel === level
140
+ ? 'bg-status-info-bg border-status-info-border text-status-info-text'
141
+ : 'bg-surface border-border text-secondary hover:bg-hover'}` }, label)))),
142
+ React.createElement("div", { className: "relative flex-1" },
143
+ React.createElement(MagnifyingGlassIcon, { className: "absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted" }),
144
+ React.createElement("input", { type: "text", placeholder: "Search messages...", value: searchTerm, onChange: (e) => setSearchTerm(e.target.value), className: "w-full pl-7 pr-6 py-0.5 text-xs border border-border rounded bg-surface text-primary focus:outline-none focus:ring-1 focus:ring-focus-ring focus:border-focus-ring" }),
145
+ searchTerm.length > 0 && (React.createElement("button", { onClick: () => setSearchTerm(''), className: "absolute right-2 top-1/2 -translate-y-1/2 text-muted hover:text-secondary text-xs", "aria-label": "Clear search" }, "\u00D7"))))),
146
+ React.createElement("div", { className: `overflow-y-auto ${isMobile ? 'flex-1' : 'max-h-48'}` }, filteredMessages.length === 0 ? (React.createElement("div", { className: "px-4 py-3 text-xs text-muted text-center" }, isFiltered ? 'No messages match the current filter' : 'No messages')) : (filteredMessages.map((msg) => (React.createElement("div", { key: msg.id, className: `group flex items-start gap-2 px-4 py-1.5 text-xs border-b border-border-subtle ${LOG_ROW_COLORS[msg.severity]}` },
147
+ React.createElement("span", { className: `shrink-0 ${SEVERITY_COLORS[msg.severity]}` }, SEVERITY_ICONS[msg.severity]),
148
+ React.createElement("span", { className: "flex-1 text-secondary" }, msg.text),
149
+ React.createElement("button", { onClick: () => copyToClipboard(msg.text, msg.id), className: `shrink-0 p-0.5 rounded transition-colors ${copySuccessId === msg.id
150
+ ? 'opacity-100 bg-status-success-surface'
151
+ : 'opacity-0 group-hover:opacity-100 hover:bg-surface-raised'}`, title: copySuccessId === msg.id ? 'Copied!' : 'Copy message' }, copySuccessId === msg.id ? (React.createElement(CheckIcon, { className: "h-3 w-3 text-status-success-icon" })) : (React.createElement(DocumentDuplicateIcon, { className: "h-3 w-3 text-muted" }))),
152
+ React.createElement("span", { className: "shrink-0 text-muted" }, formatTime(msg.timestamp))))))))))));
153
+ }
154
+ //# sourceMappingURL=StatusBar.js.map
@@ -0,0 +1,68 @@
1
+ /*
2
+ * Copyright (c) 2026 Erik Fortune
3
+ *
4
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ * of this software and associated documentation files (the "Software"), to deal
6
+ * in the Software without restriction, including without limitation the rights
7
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ * copies of the Software, and to permit persons to whom the Software is
9
+ * furnished to do so, subject to the following conditions:
10
+ *
11
+ * The above copyright notice and this permission notice shall be included in all
12
+ * copies or substantial portions of the Software.
13
+ *
14
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ * SOFTWARE.
21
+ */
22
+ import React, { useEffect } from 'react';
23
+ import { DEFAULT_TOAST_CONFIG } from './model';
24
+ import { useResponsive } from '../responsive';
25
+ // ============================================================================
26
+ // Severity Styles
27
+ // ============================================================================
28
+ const SEVERITY_STYLES = {
29
+ info: 'bg-status-info-bg border-status-info-border text-status-info-text',
30
+ success: 'bg-status-success-bg border-status-success-border text-status-success-text',
31
+ warning: 'bg-status-warning-bg border-status-warning-border text-status-warning-text',
32
+ error: 'bg-status-error-bg border-status-error-border text-status-error-text'
33
+ };
34
+ /**
35
+ * A single toast notification.
36
+ * @public
37
+ */
38
+ export function ToastItem(props) {
39
+ const { message, onDismiss } = props;
40
+ const config = DEFAULT_TOAST_CONFIG[message.severity];
41
+ const { layoutMode } = useResponsive();
42
+ useEffect(() => {
43
+ if (config.autoDismissMs > 0) {
44
+ const timer = setTimeout(() => onDismiss(message.id), config.autoDismissMs);
45
+ return () => clearTimeout(timer);
46
+ }
47
+ return undefined;
48
+ }, [message.id, config.autoDismissMs, onDismiss]);
49
+ return (React.createElement("div", { className: `flex items-start gap-3 px-4 py-3 rounded-lg border shadow-lg ${layoutMode === 'mobile' ? 'w-full' : 'max-w-sm'} ${SEVERITY_STYLES[message.severity]}`, role: "alert" },
50
+ React.createElement("div", { className: "flex-1 text-sm line-clamp-4", title: message.text }, message.text),
51
+ React.createElement("div", { className: "flex items-center gap-2 shrink-0" },
52
+ message.action && (React.createElement("button", { onClick: message.action.onAction, className: "text-sm font-medium underline hover:no-underline" }, message.action.label)),
53
+ React.createElement("button", { onClick: () => onDismiss(message.id), className: "text-current opacity-50 hover:opacity-100", "aria-label": "Dismiss" }, "\u00D7"))));
54
+ }
55
+ /**
56
+ * Container that renders active toasts in the bottom-right corner.
57
+ * @public
58
+ */
59
+ export function ToastContainer(props) {
60
+ const { toasts, onDismiss, maxVisible = 5 } = props;
61
+ const { layoutMode } = useResponsive();
62
+ const visible = toasts.slice(-maxVisible);
63
+ if (visible.length === 0) {
64
+ return null;
65
+ }
66
+ return (React.createElement("div", { className: `fixed bottom-16 z-50 flex flex-col gap-2 ${layoutMode === 'mobile' ? 'left-4 right-4' : 'right-4'}` }, visible.map((toast) => (React.createElement(ToastItem, { key: toast.id, message: toast, onDismiss: onDismiss })))));
67
+ }
68
+ //# sourceMappingURL=Toast.js.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Messages packlet - observability context, toast system, and log panel.
3
+ * @packageDocumentation
4
+ */
5
+ export { DEFAULT_TOAST_CONFIG, createMessage, generateMessageId } from './model';
6
+ export { MessagesProvider, useMessages } from './MessagesContext';
7
+ export { ToastItem, ToastContainer } from './Toast';
8
+ export { StatusBar } from './StatusBar';
9
+ export { MessagesLogger } from './MessagesLogger';
10
+ export { useLogReporter } from './useLogReporter';
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,56 @@
1
+ /*
2
+ * Copyright (c) 2026 Erik Fortune
3
+ *
4
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ * of this software and associated documentation files (the "Software"), to deal
6
+ * in the Software without restriction, including without limitation the rights
7
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ * copies of the Software, and to permit persons to whom the Software is
9
+ * furnished to do so, subject to the following conditions:
10
+ *
11
+ * The above copyright notice and this permission notice shall be included in all
12
+ * copies or substantial portions of the Software.
13
+ *
14
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ * SOFTWARE.
21
+ */
22
+ /**
23
+ * Default toast configuration by severity.
24
+ * @public
25
+ */
26
+ export const DEFAULT_TOAST_CONFIG = {
27
+ info: { autoDismissMs: 3000 },
28
+ success: { autoDismissMs: 3000 },
29
+ warning: { autoDismissMs: 5000 },
30
+ error: { autoDismissMs: 0 }
31
+ };
32
+ // ============================================================================
33
+ // Helpers
34
+ // ============================================================================
35
+ let _nextId = 0;
36
+ /**
37
+ * Generates a unique message ID.
38
+ * @internal
39
+ */
40
+ export function generateMessageId() {
41
+ return `msg-${++_nextId}-${Date.now().toString(36)}`;
42
+ }
43
+ /**
44
+ * Creates a new message.
45
+ * @public
46
+ */
47
+ export function createMessage(severity, text, action) {
48
+ return {
49
+ id: generateMessageId(),
50
+ severity,
51
+ text,
52
+ timestamp: Date.now(),
53
+ action
54
+ };
55
+ }
56
+ //# sourceMappingURL=model.js.map
@@ -0,0 +1,66 @@
1
+ /*
2
+ * Copyright (c) 2026 Erik Fortune
3
+ *
4
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ * of this software and associated documentation files (the "Software"), to deal
6
+ * in the Software without restriction, including without limitation the rights
7
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ * copies of the Software, and to permit persons to whom the Software is
9
+ * furnished to do so, subject to the following conditions:
10
+ *
11
+ * The above copyright notice and this permission notice shall be included in all
12
+ * copies or substantial portions of the Software.
13
+ *
14
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ * SOFTWARE.
21
+ */
22
+ /**
23
+ * React hook that provides a LogReporter wired into the MessagesContext.
24
+ *
25
+ * @example
26
+ * ```tsx
27
+ * function MyComponent() {
28
+ * const reporter = useLogReporter();
29
+ *
30
+ * const handleSave = () => {
31
+ * const result = workspace.save(data);
32
+ * // Automatically shows success toast or error in log panel
33
+ * result.report(reporter, {
34
+ * success: { level: 'info', message: () => 'Saved successfully' },
35
+ * failure: 'error'
36
+ * });
37
+ * };
38
+ * }
39
+ * ```
40
+ *
41
+ * @packageDocumentation
42
+ */
43
+ import { useMemo } from 'react';
44
+ import { Logging } from '@fgv/ts-utils';
45
+ import { useMessages } from './MessagesContext';
46
+ import { MessagesLogger } from './MessagesLogger';
47
+ /**
48
+ * React hook that creates a {@link @fgv/ts-utils#Logging.LogReporter | LogReporter}
49
+ * backed by the MessagesContext.
50
+ *
51
+ * The returned reporter implements both `ILogger` and `IResultReporter<unknown>`,
52
+ * so it can be used with `Result.report()` and direct logging calls.
53
+ *
54
+ * @param options - Optional configuration
55
+ * @returns A LogReporter that routes messages into the toast/log system
56
+ * @public
57
+ */
58
+ export function useLogReporter(options) {
59
+ const { addMessage } = useMessages();
60
+ const logLevel = options === null || options === void 0 ? void 0 : options.logLevel;
61
+ return useMemo(() => {
62
+ const logger = new MessagesLogger(addMessage, logLevel);
63
+ return new Logging.LogReporter({ logger });
64
+ }, [addMessage, logLevel]);
65
+ }
66
+ //# sourceMappingURL=useLogReporter.js.map