@fgv/ts-app-shell 5.1.0-31 → 5.1.0-33

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 (37) hide show
  1. package/README.md +23 -0
  2. package/dist/packlets/messages/MessagesContext.js +79 -3
  3. package/dist/packlets/messages/MessagesContext.js.map +1 -1
  4. package/dist/packlets/messages/MessagesLogger.js +10 -31
  5. package/dist/packlets/messages/MessagesLogger.js.map +1 -1
  6. package/dist/packlets/messages/StatusBar.js +53 -28
  7. package/dist/packlets/messages/StatusBar.js.map +1 -1
  8. package/dist/packlets/messages/Toast.js +7 -4
  9. package/dist/packlets/messages/Toast.js.map +1 -1
  10. package/dist/packlets/messages/index.js +2 -2
  11. package/dist/packlets/messages/index.js.map +1 -1
  12. package/dist/packlets/messages/model.js +31 -4
  13. package/dist/packlets/messages/model.js.map +1 -1
  14. package/lib/packlets/messages/MessagesContext.d.ts +47 -3
  15. package/lib/packlets/messages/MessagesContext.d.ts.map +1 -1
  16. package/lib/packlets/messages/MessagesContext.js +79 -2
  17. package/lib/packlets/messages/MessagesContext.js.map +1 -1
  18. package/lib/packlets/messages/MessagesLogger.d.ts +8 -3
  19. package/lib/packlets/messages/MessagesLogger.d.ts.map +1 -1
  20. package/lib/packlets/messages/MessagesLogger.js +10 -31
  21. package/lib/packlets/messages/MessagesLogger.js.map +1 -1
  22. package/lib/packlets/messages/StatusBar.d.ts +10 -1
  23. package/lib/packlets/messages/StatusBar.d.ts.map +1 -1
  24. package/lib/packlets/messages/StatusBar.js +52 -27
  25. package/lib/packlets/messages/StatusBar.js.map +1 -1
  26. package/lib/packlets/messages/Toast.d.ts.map +1 -1
  27. package/lib/packlets/messages/Toast.js +6 -3
  28. package/lib/packlets/messages/Toast.js.map +1 -1
  29. package/lib/packlets/messages/index.d.ts +2 -2
  30. package/lib/packlets/messages/index.d.ts.map +1 -1
  31. package/lib/packlets/messages/index.js +6 -1
  32. package/lib/packlets/messages/index.js.map +1 -1
  33. package/lib/packlets/messages/model.d.ts +74 -8
  34. package/lib/packlets/messages/model.d.ts.map +1 -1
  35. package/lib/packlets/messages/model.js +32 -4
  36. package/lib/packlets/messages/model.js.map +1 -1
  37. package/package.json +5 -5
@@ -59,6 +59,7 @@ const react_1 = __importStar(require("react"));
59
59
  const ts_utils_1 = require("@fgv/ts-utils");
60
60
  const outline_1 = require("@heroicons/react/24/outline");
61
61
  const solid_1 = require("@heroicons/react/24/solid");
62
+ const model_1 = require("./model");
62
63
  const responsive_1 = require("../responsive");
63
64
  // ============================================================================
64
65
  // Severity display config
@@ -85,17 +86,25 @@ const LOG_ROW_COLORS = {
85
86
  // Level filter config
86
87
  // ============================================================================
87
88
  /**
88
- * Maps MessageSeverity to MessageLogLevel for shouldLog filtering.
89
- * 'success' is treated as 'info' since it doesn't exist in the log level hierarchy.
89
+ * Effective display severity for a message: its explicit `severity` styling override,
90
+ * or the severity derived from its canonical log level.
90
91
  */
91
- function severityToLogLevel(severity) {
92
- return severity === 'success' ? 'info' : severity;
92
+ function effectiveSeverity(message) {
93
+ var _a;
94
+ return (_a = message.severity) !== null && _a !== void 0 ? _a : (0, model_1.deriveSeverityFromLevel)(message.level);
93
95
  }
94
96
  /**
95
97
  * Filter choices exposed in the UI, mapping labels to ReporterLogLevel values.
98
+ *
99
+ * The full log-level granularity is exposed (the core gap fix): `Detail+` lets the panel
100
+ * filter at the same granularity a ts-utils logger records, including `detail` messages
101
+ * that the previous severity-based filter could not represent. `All` additionally surfaces
102
+ * `quiet`-level messages (per {@link @fgv/ts-utils#Logging.shouldLog | shouldLog} semantics,
103
+ * `quiet` is only visible under the `all` threshold).
96
104
  */
97
105
  const FILTER_LEVELS = [
98
106
  { label: 'All', level: 'all' },
107
+ { label: 'Detail+', level: 'detail' },
99
108
  { label: 'Info+', level: 'info' },
100
109
  { label: 'Warn+', level: 'warning' },
101
110
  { label: 'Error', level: 'error' }
@@ -104,14 +113,15 @@ const FILTER_LEVELS = [
104
113
  * Collapsible status bar / log panel at the bottom of the application.
105
114
  *
106
115
  * Collapsed: shows severity counts.
107
- * Expanded: filterable, searchable, copyable log of all messages.
116
+ * Expanded: filterable, searchable, copyable log of all messages, bounded to
117
+ * {@link IStatusBarProps.maxExpandedHeight} with the list scrolling within the bound.
108
118
  * @public
109
119
  */
110
120
  function StatusBar(props) {
111
- const { messages, onClear, initialFilterLevel } = props;
121
+ const { messages, onClear, initialFilterLevel, maxExpandedHeight = '40vh', defaultExpanded = false } = props;
112
122
  const { layoutMode } = (0, responsive_1.useResponsive)();
113
123
  const isMobile = layoutMode === 'mobile';
114
- const [expanded, setExpanded] = (0, react_1.useState)(false);
124
+ const [expanded, setExpanded] = (0, react_1.useState)(defaultExpanded);
115
125
  const [showFilters, setShowFilters] = (0, react_1.useState)(false);
116
126
  const [filterLevel, setFilterLevel] = (0, react_1.useState)(initialFilterLevel !== null && initialFilterLevel !== void 0 ? initialFilterLevel : 'all');
117
127
  const [searchTerm, setSearchTerm] = (0, react_1.useState)('');
@@ -119,13 +129,13 @@ function StatusBar(props) {
119
129
  const counts = (0, react_1.useMemo)(() => {
120
130
  const result = { info: 0, success: 0, warning: 0, error: 0 };
121
131
  for (const msg of messages) {
122
- result[msg.severity]++;
132
+ result[effectiveSeverity(msg)]++;
123
133
  }
124
134
  return result;
125
135
  }, [messages]);
126
136
  const filteredMessages = (0, react_1.useMemo)(() => {
127
137
  return messages.filter((msg) => {
128
- const passesLevel = ts_utils_1.Logging.shouldLog(severityToLogLevel(msg.severity), filterLevel);
138
+ const passesLevel = ts_utils_1.Logging.shouldLog(msg.level, filterLevel);
129
139
  const passesSearch = searchTerm === '' || msg.text.toLowerCase().includes(searchTerm.toLowerCase());
130
140
  return passesLevel && passesSearch;
131
141
  });
@@ -145,7 +155,7 @@ function StatusBar(props) {
145
155
  }, []);
146
156
  const copyAllFiltered = (0, react_1.useCallback)(() => {
147
157
  const text = filteredMessages
148
- .map((msg) => `[${msg.severity.toUpperCase()}] ${formatTime(msg.timestamp)} - ${msg.text}`)
158
+ .map((msg) => `[${msg.level.toUpperCase()}] ${formatTime(msg.timestamp)} - ${msg.text}`)
149
159
  .join('\n');
150
160
  copyToClipboard(text, '__all__');
151
161
  }, [filteredMessages, formatTime, copyToClipboard]);
@@ -158,33 +168,48 @@ function StatusBar(props) {
158
168
  messages.length === 0 && react_1.default.createElement("span", { className: "text-muted" }, "No messages")),
159
169
  react_1.default.createElement("span", { className: "text-muted" }, expanded ? '\u25BC' : '\u25B2')),
160
170
  expanded && (react_1.default.createElement(react_1.default.Fragment, null,
161
- isMobile && (react_1.default.createElement("div", { className: "fixed inset-0 z-40 bg-backdrop", onClick: () => setExpanded(false) })),
162
- react_1.default.createElement("div", { className: isMobile
163
- ? '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]'
164
- : 'border-t border-border-subtle' },
171
+ isMobile && (react_1.default.createElement("div", { className: "fixed inset-0 z-40 bg-backdrop", onClick: () => setExpanded(false), "aria-hidden": "true" })),
172
+ react_1.default.createElement("div", { className: `flex flex-col border-t border-border-subtle ${isMobile ? 'relative z-50 bg-surface shadow-xl rounded-t-lg' : ''}`, style: { maxHeight: maxExpandedHeight } },
165
173
  react_1.default.createElement("div", { className: "flex items-center justify-between px-4 py-1 bg-surface-alt border-b border-border-subtle shrink-0" },
166
174
  react_1.default.createElement("span", { className: "text-xs font-medium text-muted" }, isFiltered
167
175
  ? `${filteredMessages.length} of ${messages.length} messages`
168
176
  : `${messages.length} message${messages.length !== 1 ? 's' : ''}`),
169
177
  react_1.default.createElement("div", { className: "flex items-center gap-1" },
170
178
  react_1.default.createElement("button", { onClick: () => setShowFilters(!showFilters), className: `p-1 rounded hover:bg-surface-raised ${showFilters ? 'bg-surface-raised' : ''}`, title: "Filter messages" },
171
- react_1.default.createElement(outline_1.FunnelIcon, { className: "h-3.5 w-3.5 text-muted" })),
172
- react_1.default.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_1.default.createElement(solid_1.CheckIcon, { className: "h-3.5 w-3.5 text-status-success-icon" })) : (react_1.default.createElement(outline_1.DocumentDuplicateIcon, { className: "h-3.5 w-3.5 text-muted" }))),
173
- react_1.default.createElement("button", { onClick: onClear, className: "text-xs text-muted hover:text-secondary ml-1" }, "Clear"))),
179
+ react_1.default.createElement(outline_1.FunnelIcon, { className: "h-3.5 w-3.5 text-muted", style: { width: 14, height: 14 } })),
180
+ react_1.default.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_1.default.createElement(solid_1.CheckIcon, { className: "h-3.5 w-3.5 text-status-success-icon", style: { width: 14, height: 14 } })) : (react_1.default.createElement(outline_1.DocumentDuplicateIcon, { className: "h-3.5 w-3.5 text-muted", style: { width: 14, height: 14 } }))),
181
+ react_1.default.createElement("button", { onClick: onClear, className: "text-xs text-muted hover:text-secondary ml-1" }, "Clear"),
182
+ react_1.default.createElement("button", { onClick: () => setExpanded(false), className: "p-1 rounded hover:bg-surface-raised ml-1", title: "Collapse log panel", "aria-label": "Collapse log panel" },
183
+ react_1.default.createElement(outline_1.XMarkIcon, { className: "h-3.5 w-3.5 text-muted", style: { width: 14, height: 14 } })))),
174
184
  showFilters && (react_1.default.createElement("div", { className: "px-4 py-1.5 bg-surface-raised border-b border-border-subtle flex items-center gap-3 shrink-0" },
175
185
  react_1.default.createElement("div", { className: "flex items-center gap-1" }, FILTER_LEVELS.map(({ label, level }) => (react_1.default.createElement("button", { key: level, onClick: () => setFilterLevel(level), className: `text-xs px-2 py-0.5 rounded border ${filterLevel === level
176
186
  ? 'bg-status-info-bg border-status-info-border text-status-info-text'
177
187
  : 'bg-surface border-border text-secondary hover:bg-hover'}` }, label)))),
178
- react_1.default.createElement("div", { className: "relative flex-1" },
179
- react_1.default.createElement(outline_1.MagnifyingGlassIcon, { className: "absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted" }),
188
+ react_1.default.createElement("div", { className: "relative flex-1", style: { position: 'relative' } },
189
+ react_1.default.createElement(outline_1.MagnifyingGlassIcon, { className: "absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted", style: {
190
+ position: 'absolute',
191
+ left: 8,
192
+ top: '50%',
193
+ transform: 'translateY(-50%)',
194
+ width: 14,
195
+ height: 14
196
+ } }),
180
197
  react_1.default.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" }),
181
- searchTerm.length > 0 && (react_1.default.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"))))),
182
- react_1.default.createElement("div", { className: `overflow-y-auto ${isMobile ? 'flex-1' : 'max-h-48'}` }, filteredMessages.length === 0 ? (react_1.default.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_1.default.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]}` },
183
- react_1.default.createElement("span", { className: `shrink-0 ${SEVERITY_COLORS[msg.severity]}` }, SEVERITY_ICONS[msg.severity]),
184
- react_1.default.createElement("span", { className: "flex-1 text-secondary" }, msg.text),
185
- react_1.default.createElement("button", { onClick: () => copyToClipboard(msg.text, msg.id), className: `shrink-0 p-0.5 rounded transition-colors ${copySuccessId === msg.id
186
- ? 'opacity-100 bg-status-success-surface'
187
- : 'opacity-0 group-hover:opacity-100 hover:bg-surface-raised'}`, title: copySuccessId === msg.id ? 'Copied!' : 'Copy message' }, copySuccessId === msg.id ? (react_1.default.createElement(solid_1.CheckIcon, { className: "h-3 w-3 text-status-success-icon" })) : (react_1.default.createElement(outline_1.DocumentDuplicateIcon, { className: "h-3 w-3 text-muted" }))),
188
- react_1.default.createElement("span", { className: "shrink-0 text-muted" }, formatTime(msg.timestamp))))))))))));
198
+ searchTerm.length > 0 && (react_1.default.createElement("button", { onClick: () => setSearchTerm(''), className: "absolute right-2 top-1/2 -translate-y-1/2 text-muted hover:text-secondary text-xs", style: {
199
+ position: 'absolute',
200
+ right: 8,
201
+ top: '50%',
202
+ transform: 'translateY(-50%)'
203
+ }, "aria-label": "Clear search" }, "\u00D7"))))),
204
+ react_1.default.createElement("div", { className: "flex-1 min-h-0 overflow-y-auto" }, filteredMessages.length === 0 ? (react_1.default.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) => {
205
+ const severity = effectiveSeverity(msg);
206
+ return (react_1.default.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[severity]}` },
207
+ react_1.default.createElement("span", { className: `shrink-0 ${SEVERITY_COLORS[severity]}` }, SEVERITY_ICONS[severity]),
208
+ react_1.default.createElement("span", { className: "flex-1 text-secondary" }, msg.text),
209
+ react_1.default.createElement("button", { onClick: () => copyToClipboard(msg.text, msg.id), className: `shrink-0 p-0.5 rounded transition-colors ${copySuccessId === msg.id
210
+ ? 'opacity-100 bg-status-success-surface'
211
+ : 'opacity-0 group-hover:opacity-100 hover:bg-surface-raised'}`, title: copySuccessId === msg.id ? 'Copied!' : 'Copy message' }, copySuccessId === msg.id ? (react_1.default.createElement(solid_1.CheckIcon, { className: "h-3 w-3 text-status-success-icon", style: { width: 12, height: 12 } })) : (react_1.default.createElement(outline_1.DocumentDuplicateIcon, { className: "h-3 w-3 text-muted", style: { width: 12, height: 12 } }))),
212
+ react_1.default.createElement("span", { className: "shrink-0 text-muted" }, formatTime(msg.timestamp))));
213
+ }))))))));
189
214
  }
190
215
  //# sourceMappingURL=StatusBar.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"StatusBar.js","sourceRoot":"","sources":["../../../src/packlets/messages/StatusBar.tsx"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport React, { useCallback, useMemo, useState } from 'react';\n\nimport { Logging, type MessageLogLevel } from '@fgv/ts-utils';\nimport { FunnelIcon, DocumentDuplicateIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline';\nimport { CheckIcon } from '@heroicons/react/24/solid';\n\nimport { IMessage, MessageSeverity } from './model';\nimport { useResponsive } from '../responsive';\n\n// ============================================================================\n// Severity display config\n// ============================================================================\n\nconst SEVERITY_ICONS: Record<MessageSeverity, string> = {\n info: '\\u2139',\n success: '\\u2713',\n warning: '\\u26A0',\n error: '\\u2717'\n};\n\nconst SEVERITY_COLORS: Record<MessageSeverity, string> = {\n info: 'text-status-info-icon',\n success: 'text-status-success-icon',\n warning: 'text-status-warning-icon',\n error: 'text-status-error-icon'\n};\n\nconst LOG_ROW_COLORS: Record<MessageSeverity, string> = {\n info: '',\n success: 'bg-status-success-bg',\n warning: 'bg-status-warning-bg',\n error: 'bg-status-error-bg'\n};\n\n// ============================================================================\n// Level filter config\n// ============================================================================\n\n/**\n * Maps MessageSeverity to MessageLogLevel for shouldLog filtering.\n * 'success' is treated as 'info' since it doesn't exist in the log level hierarchy.\n */\nfunction severityToLogLevel(severity: MessageSeverity): MessageLogLevel {\n return severity === 'success' ? 'info' : severity;\n}\n\n/**\n * Filter choices exposed in the UI, mapping labels to ReporterLogLevel values.\n */\nconst FILTER_LEVELS: ReadonlyArray<{ readonly label: string; readonly level: Logging.ReporterLogLevel }> = [\n { label: 'All', level: 'all' },\n { label: 'Info+', level: 'info' },\n { label: 'Warn+', level: 'warning' },\n { label: 'Error', level: 'error' }\n] as const;\n\n// ============================================================================\n// Status Bar\n// ============================================================================\n\n/**\n * Props for the StatusBar component.\n * @public\n */\nexport interface IStatusBarProps {\n readonly messages: ReadonlyArray<IMessage>;\n readonly onClear: () => void;\n /** Initial filter level for the log panel. Defaults to 'all'. */\n readonly initialFilterLevel?: Logging.ReporterLogLevel;\n}\n\n/**\n * Collapsible status bar / log panel at the bottom of the application.\n *\n * Collapsed: shows severity counts.\n * Expanded: filterable, searchable, copyable log of all messages.\n * @public\n */\nexport function StatusBar(props: IStatusBarProps): React.ReactElement {\n const { messages, onClear, initialFilterLevel } = props;\n const { layoutMode } = useResponsive();\n const isMobile = layoutMode === 'mobile';\n const [expanded, setExpanded] = useState(false);\n const [showFilters, setShowFilters] = useState(false);\n const [filterLevel, setFilterLevel] = useState<Logging.ReporterLogLevel>(initialFilterLevel ?? 'all');\n const [searchTerm, setSearchTerm] = useState('');\n const [copySuccessId, setCopySuccessId] = useState<string | null>(null);\n\n const counts = useMemo(() => {\n const result: Record<MessageSeverity, number> = { info: 0, success: 0, warning: 0, error: 0 };\n for (const msg of messages) {\n result[msg.severity]++;\n }\n return result;\n }, [messages]);\n\n const filteredMessages = useMemo(() => {\n return messages.filter((msg) => {\n const passesLevel = Logging.shouldLog(severityToLogLevel(msg.severity), filterLevel);\n const passesSearch = searchTerm === '' || msg.text.toLowerCase().includes(searchTerm.toLowerCase());\n return passesLevel && passesSearch;\n });\n }, [messages, filterLevel, searchTerm]);\n\n const isFiltered = filteredMessages.length !== messages.length;\n\n const formatTime = useCallback((timestamp: number): string => {\n return new Date(timestamp).toLocaleTimeString();\n }, []);\n\n const copyToClipboard = useCallback((text: string, id: string): void => {\n navigator.clipboard\n .writeText(text)\n .then(() => {\n setCopySuccessId(id);\n setTimeout(() => setCopySuccessId(null), 2000);\n })\n .catch(() => undefined);\n }, []);\n\n const copyAllFiltered = useCallback((): void => {\n const text = filteredMessages\n .map((msg) => `[${msg.severity.toUpperCase()}] ${formatTime(msg.timestamp)} - ${msg.text}`)\n .join('\\n');\n copyToClipboard(text, '__all__');\n }, [filteredMessages, formatTime, copyToClipboard]);\n\n return (\n <div className=\"border-t border-border bg-surface\">\n {/* Collapsed bar */}\n <button\n onClick={(): void => setExpanded(!expanded)}\n className=\"flex items-center justify-between w-full px-4 py-1.5 text-xs text-secondary hover:bg-hover\"\n >\n <div className=\"flex items-center gap-4\">\n {(Object.keys(counts) as MessageSeverity[]).map((severity) =>\n counts[severity] > 0 ? (\n <span key={severity} className={`flex items-center gap-1 ${SEVERITY_COLORS[severity]}`}>\n <span>{SEVERITY_ICONS[severity]}</span>\n <span>{counts[severity]}</span>\n </span>\n ) : null\n )}\n {messages.length === 0 && <span className=\"text-muted\">No messages</span>}\n </div>\n <span className=\"text-muted\">{expanded ? '\\u25BC' : '\\u25B2'}</span>\n </button>\n\n {/* Expanded log — fixed slide-up sheet on mobile, inline on desktop */}\n {expanded && (\n <>\n {isMobile && (\n <div className=\"fixed inset-0 z-40 bg-backdrop\" onClick={(): void => setExpanded(false)} />\n )}\n <div\n className={\n isMobile\n ? '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]'\n : 'border-t border-border-subtle'\n }\n >\n {/* Header toolbar */}\n <div className=\"flex items-center justify-between px-4 py-1 bg-surface-alt border-b border-border-subtle shrink-0\">\n <span className=\"text-xs font-medium text-muted\">\n {isFiltered\n ? `${filteredMessages.length} of ${messages.length} messages`\n : `${messages.length} message${messages.length !== 1 ? 's' : ''}`}\n </span>\n <div className=\"flex items-center gap-1\">\n <button\n onClick={(): void => setShowFilters(!showFilters)}\n className={`p-1 rounded hover:bg-surface-raised ${showFilters ? 'bg-surface-raised' : ''}`}\n title=\"Filter messages\"\n >\n <FunnelIcon className=\"h-3.5 w-3.5 text-muted\" />\n </button>\n <button\n onClick={copyAllFiltered}\n className={`p-1 rounded transition-colors ${\n copySuccessId === '__all__' ? 'bg-status-success-bg' : 'hover:bg-surface-raised'\n }`}\n title={copySuccessId === '__all__' ? 'Copied!' : 'Copy filtered messages'}\n >\n {copySuccessId === '__all__' ? (\n <CheckIcon className=\"h-3.5 w-3.5 text-status-success-icon\" />\n ) : (\n <DocumentDuplicateIcon className=\"h-3.5 w-3.5 text-muted\" />\n )}\n </button>\n <button onClick={onClear} className=\"text-xs text-muted hover:text-secondary ml-1\">\n Clear\n </button>\n </div>\n </div>\n\n {/* Filter toolbar */}\n {showFilters && (\n <div className=\"px-4 py-1.5 bg-surface-raised border-b border-border-subtle flex items-center gap-3 shrink-0\">\n {/* Level filter buttons */}\n <div className=\"flex items-center gap-1\">\n {FILTER_LEVELS.map(({ label, level }) => (\n <button\n key={level}\n onClick={(): void => setFilterLevel(level)}\n className={`text-xs px-2 py-0.5 rounded border ${\n filterLevel === level\n ? 'bg-status-info-bg border-status-info-border text-status-info-text'\n : 'bg-surface border-border text-secondary hover:bg-hover'\n }`}\n >\n {label}\n </button>\n ))}\n </div>\n\n {/* Search input */}\n <div className=\"relative flex-1\">\n <MagnifyingGlassIcon className=\"absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted\" />\n <input\n type=\"text\"\n placeholder=\"Search messages...\"\n value={searchTerm}\n onChange={(e): void => setSearchTerm(e.target.value)}\n 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\"\n />\n {searchTerm.length > 0 && (\n <button\n onClick={(): void => setSearchTerm('')}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 text-muted hover:text-secondary text-xs\"\n aria-label=\"Clear search\"\n >\n &times;\n </button>\n )}\n </div>\n </div>\n )}\n\n {/* Message list */}\n <div className={`overflow-y-auto ${isMobile ? 'flex-1' : 'max-h-48'}`}>\n {filteredMessages.length === 0 ? (\n <div className=\"px-4 py-3 text-xs text-muted text-center\">\n {isFiltered ? 'No messages match the current filter' : 'No messages'}\n </div>\n ) : (\n filteredMessages.map((msg) => (\n <div\n key={msg.id}\n className={`group flex items-start gap-2 px-4 py-1.5 text-xs border-b border-border-subtle ${\n LOG_ROW_COLORS[msg.severity]\n }`}\n >\n <span className={`shrink-0 ${SEVERITY_COLORS[msg.severity]}`}>\n {SEVERITY_ICONS[msg.severity]}\n </span>\n <span className=\"flex-1 text-secondary\">{msg.text}</span>\n <button\n onClick={(): void => copyToClipboard(msg.text, msg.id)}\n className={`shrink-0 p-0.5 rounded transition-colors ${\n copySuccessId === msg.id\n ? 'opacity-100 bg-status-success-surface'\n : 'opacity-0 group-hover:opacity-100 hover:bg-surface-raised'\n }`}\n title={copySuccessId === msg.id ? 'Copied!' : 'Copy message'}\n >\n {copySuccessId === msg.id ? (\n <CheckIcon className=\"h-3 w-3 text-status-success-icon\" />\n ) : (\n <DocumentDuplicateIcon className=\"h-3 w-3 text-muted\" />\n )}\n </button>\n <span className=\"shrink-0 text-muted\">{formatTime(msg.timestamp)}</span>\n </div>\n ))\n )}\n </div>\n </div>\n </>\n )}\n </div>\n );\n}\n"]}
1
+ {"version":3,"file":"StatusBar.js","sourceRoot":"","sources":["../../../src/packlets/messages/StatusBar.tsx"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport React, { useCallback, useMemo, useState } from 'react';\n\nimport { Logging } from '@fgv/ts-utils';\nimport {\n FunnelIcon,\n DocumentDuplicateIcon,\n MagnifyingGlassIcon,\n XMarkIcon\n} from '@heroicons/react/24/outline';\nimport { CheckIcon } from '@heroicons/react/24/solid';\n\nimport { deriveSeverityFromLevel, IMessage, MessageSeverity } from './model';\nimport { useResponsive } from '../responsive';\n\n// ============================================================================\n// Severity display config\n// ============================================================================\n\nconst SEVERITY_ICONS: Record<MessageSeverity, string> = {\n info: '\\u2139',\n success: '\\u2713',\n warning: '\\u26A0',\n error: '\\u2717'\n};\n\nconst SEVERITY_COLORS: Record<MessageSeverity, string> = {\n info: 'text-status-info-icon',\n success: 'text-status-success-icon',\n warning: 'text-status-warning-icon',\n error: 'text-status-error-icon'\n};\n\nconst LOG_ROW_COLORS: Record<MessageSeverity, string> = {\n info: '',\n success: 'bg-status-success-bg',\n warning: 'bg-status-warning-bg',\n error: 'bg-status-error-bg'\n};\n\n// ============================================================================\n// Level filter config\n// ============================================================================\n\n/**\n * Effective display severity for a message: its explicit `severity` styling override,\n * or the severity derived from its canonical log level.\n */\nfunction effectiveSeverity(message: IMessage): MessageSeverity {\n return message.severity ?? deriveSeverityFromLevel(message.level);\n}\n\n/**\n * Filter choices exposed in the UI, mapping labels to ReporterLogLevel values.\n *\n * The full log-level granularity is exposed (the core gap fix): `Detail+` lets the panel\n * filter at the same granularity a ts-utils logger records, including `detail` messages\n * that the previous severity-based filter could not represent. `All` additionally surfaces\n * `quiet`-level messages (per {@link @fgv/ts-utils#Logging.shouldLog | shouldLog} semantics,\n * `quiet` is only visible under the `all` threshold).\n */\nconst FILTER_LEVELS: ReadonlyArray<{ readonly label: string; readonly level: Logging.ReporterLogLevel }> = [\n { label: 'All', level: 'all' },\n { label: 'Detail+', level: 'detail' },\n { label: 'Info+', level: 'info' },\n { label: 'Warn+', level: 'warning' },\n { label: 'Error', level: 'error' }\n] as const;\n\n// ============================================================================\n// Status Bar\n// ============================================================================\n\n/**\n * Props for the StatusBar component.\n * @public\n */\nexport interface IStatusBarProps {\n readonly messages: ReadonlyArray<IMessage>;\n readonly onClear: () => void;\n /** Initial filter level for the log panel. Defaults to 'all'. */\n readonly initialFilterLevel?: Logging.ReporterLogLevel;\n /**\n * Maximum height of the expanded panel as a CSS length (e.g. `'40vh'`, `'320px'`).\n * The panel is bounded to this height on both desktop and mobile; the message list\n * scrolls within the bound while the header and filter controls stay fixed. Defaults to `'40vh'`.\n */\n readonly maxExpandedHeight?: string;\n /** Whether the panel is expanded on mount. Defaults to `false` (collapsed). */\n readonly defaultExpanded?: boolean;\n}\n\n/**\n * Collapsible status bar / log panel at the bottom of the application.\n *\n * Collapsed: shows severity counts.\n * Expanded: filterable, searchable, copyable log of all messages, bounded to\n * {@link IStatusBarProps.maxExpandedHeight} with the list scrolling within the bound.\n * @public\n */\nexport function StatusBar(props: IStatusBarProps): React.ReactElement {\n const {\n messages,\n onClear,\n initialFilterLevel,\n maxExpandedHeight = '40vh',\n defaultExpanded = false\n } = props;\n const { layoutMode } = useResponsive();\n const isMobile = layoutMode === 'mobile';\n const [expanded, setExpanded] = useState(defaultExpanded);\n const [showFilters, setShowFilters] = useState(false);\n const [filterLevel, setFilterLevel] = useState<Logging.ReporterLogLevel>(initialFilterLevel ?? 'all');\n const [searchTerm, setSearchTerm] = useState('');\n const [copySuccessId, setCopySuccessId] = useState<string | null>(null);\n\n const counts = useMemo(() => {\n const result: Record<MessageSeverity, number> = { info: 0, success: 0, warning: 0, error: 0 };\n for (const msg of messages) {\n result[effectiveSeverity(msg)]++;\n }\n return result;\n }, [messages]);\n\n const filteredMessages = useMemo(() => {\n return messages.filter((msg) => {\n const passesLevel = Logging.shouldLog(msg.level, filterLevel);\n const passesSearch = searchTerm === '' || msg.text.toLowerCase().includes(searchTerm.toLowerCase());\n return passesLevel && passesSearch;\n });\n }, [messages, filterLevel, searchTerm]);\n\n const isFiltered = filteredMessages.length !== messages.length;\n\n const formatTime = useCallback((timestamp: number): string => {\n return new Date(timestamp).toLocaleTimeString();\n }, []);\n\n const copyToClipboard = useCallback((text: string, id: string): void => {\n navigator.clipboard\n .writeText(text)\n .then(() => {\n setCopySuccessId(id);\n setTimeout(() => setCopySuccessId(null), 2000);\n })\n .catch(() => undefined);\n }, []);\n\n const copyAllFiltered = useCallback((): void => {\n const text = filteredMessages\n .map((msg) => `[${msg.level.toUpperCase()}] ${formatTime(msg.timestamp)} - ${msg.text}`)\n .join('\\n');\n copyToClipboard(text, '__all__');\n }, [filteredMessages, formatTime, copyToClipboard]);\n\n return (\n <div className=\"border-t border-border bg-surface\">\n {/* Collapsed bar */}\n <button\n onClick={(): void => setExpanded(!expanded)}\n className=\"flex items-center justify-between w-full px-4 py-1.5 text-xs text-secondary hover:bg-hover\"\n >\n <div className=\"flex items-center gap-4\">\n {(Object.keys(counts) as MessageSeverity[]).map((severity) =>\n counts[severity] > 0 ? (\n <span key={severity} className={`flex items-center gap-1 ${SEVERITY_COLORS[severity]}`}>\n <span>{SEVERITY_ICONS[severity]}</span>\n <span>{counts[severity]}</span>\n </span>\n ) : null\n )}\n {messages.length === 0 && <span className=\"text-muted\">No messages</span>}\n </div>\n <span className=\"text-muted\">{expanded ? '\\u25BC' : '\\u25B2'}</span>\n </button>\n\n {/* Expanded log — bounded bottom-sheet on mobile, bounded inline panel on desktop.\n The panel stays in document flow on both layouts (no `fixed` takeover); the mobile\n backdrop is the only fixed element, dimming the rest and supporting tap-to-dismiss. */}\n {expanded && (\n <>\n {isMobile && (\n <div\n className=\"fixed inset-0 z-40 bg-backdrop\"\n onClick={(): void => setExpanded(false)}\n aria-hidden=\"true\"\n />\n )}\n <div\n className={`flex flex-col border-t border-border-subtle ${\n isMobile ? 'relative z-50 bg-surface shadow-xl rounded-t-lg' : ''\n }`}\n style={{ maxHeight: maxExpandedHeight }}\n >\n {/* Header toolbar */}\n <div className=\"flex items-center justify-between px-4 py-1 bg-surface-alt border-b border-border-subtle shrink-0\">\n <span className=\"text-xs font-medium text-muted\">\n {isFiltered\n ? `${filteredMessages.length} of ${messages.length} messages`\n : `${messages.length} message${messages.length !== 1 ? 's' : ''}`}\n </span>\n <div className=\"flex items-center gap-1\">\n <button\n onClick={(): void => setShowFilters(!showFilters)}\n className={`p-1 rounded hover:bg-surface-raised ${showFilters ? 'bg-surface-raised' : ''}`}\n title=\"Filter messages\"\n >\n <FunnelIcon className=\"h-3.5 w-3.5 text-muted\" style={{ width: 14, height: 14 }} />\n </button>\n <button\n onClick={copyAllFiltered}\n className={`p-1 rounded transition-colors ${\n copySuccessId === '__all__' ? 'bg-status-success-bg' : 'hover:bg-surface-raised'\n }`}\n title={copySuccessId === '__all__' ? 'Copied!' : 'Copy filtered messages'}\n >\n {copySuccessId === '__all__' ? (\n <CheckIcon\n className=\"h-3.5 w-3.5 text-status-success-icon\"\n style={{ width: 14, height: 14 }}\n />\n ) : (\n <DocumentDuplicateIcon\n className=\"h-3.5 w-3.5 text-muted\"\n style={{ width: 14, height: 14 }}\n />\n )}\n </button>\n <button onClick={onClear} className=\"text-xs text-muted hover:text-secondary ml-1\">\n Clear\n </button>\n <button\n onClick={(): void => setExpanded(false)}\n className=\"p-1 rounded hover:bg-surface-raised ml-1\"\n title=\"Collapse log panel\"\n aria-label=\"Collapse log panel\"\n >\n <XMarkIcon className=\"h-3.5 w-3.5 text-muted\" style={{ width: 14, height: 14 }} />\n </button>\n </div>\n </div>\n\n {/* Filter toolbar */}\n {showFilters && (\n <div className=\"px-4 py-1.5 bg-surface-raised border-b border-border-subtle flex items-center gap-3 shrink-0\">\n {/* Level filter buttons */}\n <div className=\"flex items-center gap-1\">\n {FILTER_LEVELS.map(({ label, level }) => (\n <button\n key={level}\n onClick={(): void => setFilterLevel(level)}\n className={`text-xs px-2 py-0.5 rounded border ${\n filterLevel === level\n ? 'bg-status-info-bg border-status-info-border text-status-info-text'\n : 'bg-surface border-border text-secondary hover:bg-hover'\n }`}\n >\n {label}\n </button>\n ))}\n </div>\n\n {/* Search input — `position: relative` inline so the absolutely-positioned\n icon + clear-button fallbacks have a containing block even when Tailwind's\n `relative` utility is absent. */}\n <div className=\"relative flex-1\" style={{ position: 'relative' }}>\n <MagnifyingGlassIcon\n className=\"absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted\"\n style={{\n position: 'absolute',\n left: 8,\n top: '50%',\n transform: 'translateY(-50%)',\n width: 14,\n height: 14\n }}\n />\n <input\n type=\"text\"\n placeholder=\"Search messages...\"\n value={searchTerm}\n onChange={(e): void => setSearchTerm(e.target.value)}\n 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\"\n />\n {searchTerm.length > 0 && (\n <button\n onClick={(): void => setSearchTerm('')}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 text-muted hover:text-secondary text-xs\"\n style={{\n position: 'absolute',\n right: 8,\n top: '50%',\n transform: 'translateY(-50%)'\n }}\n aria-label=\"Clear search\"\n >\n &times;\n </button>\n )}\n </div>\n </div>\n )}\n\n {/* Message list — scrolls within the bounded panel; header/filters stay fixed */}\n <div className=\"flex-1 min-h-0 overflow-y-auto\">\n {filteredMessages.length === 0 ? (\n <div className=\"px-4 py-3 text-xs text-muted text-center\">\n {isFiltered ? 'No messages match the current filter' : 'No messages'}\n </div>\n ) : (\n filteredMessages.map((msg) => {\n const severity = effectiveSeverity(msg);\n return (\n <div\n key={msg.id}\n className={`group flex items-start gap-2 px-4 py-1.5 text-xs border-b border-border-subtle ${LOG_ROW_COLORS[severity]}`}\n >\n <span className={`shrink-0 ${SEVERITY_COLORS[severity]}`}>\n {SEVERITY_ICONS[severity]}\n </span>\n <span className=\"flex-1 text-secondary\">{msg.text}</span>\n <button\n onClick={(): void => copyToClipboard(msg.text, msg.id)}\n className={`shrink-0 p-0.5 rounded transition-colors ${\n copySuccessId === msg.id\n ? 'opacity-100 bg-status-success-surface'\n : 'opacity-0 group-hover:opacity-100 hover:bg-surface-raised'\n }`}\n title={copySuccessId === msg.id ? 'Copied!' : 'Copy message'}\n >\n {copySuccessId === msg.id ? (\n <CheckIcon\n className=\"h-3 w-3 text-status-success-icon\"\n style={{ width: 12, height: 12 }}\n />\n ) : (\n <DocumentDuplicateIcon\n className=\"h-3 w-3 text-muted\"\n style={{ width: 12, height: 12 }}\n />\n )}\n </button>\n <span className=\"shrink-0 text-muted\">{formatTime(msg.timestamp)}</span>\n </div>\n );\n })\n )}\n </div>\n </div>\n </>\n )}\n </div>\n );\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"Toast.d.ts","sourceRoot":"","sources":["../../../src/packlets/messages/Toast.tsx"],"names":[],"mappings":"AAsBA,OAAO,KAAoB,MAAM,OAAO,CAAC;AAEzC,OAAO,EAAwB,QAAQ,EAAmB,MAAM,SAAS,CAAC;AAkB1E;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;CAC1C;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,eAAe,GAAG,KAAK,CAAC,YAAY,CA0CpE;AAMD;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;IACzC,QAAQ,CAAC,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,sDAAsD;IACtD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,oBAAoB,GAAG,KAAK,CAAC,YAAY,GAAG,IAAI,CAoBrF"}
1
+ {"version":3,"file":"Toast.d.ts","sourceRoot":"","sources":["../../../src/packlets/messages/Toast.tsx"],"names":[],"mappings":"AAsBA,OAAO,KAAoB,MAAM,OAAO,CAAC;AAEzC,OAAO,EAAiD,QAAQ,EAAmB,MAAM,SAAS,CAAC;AAkBnG;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;CAC1C;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,eAAe,GAAG,KAAK,CAAC,YAAY,CAqDpE;AAMD;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;IACzC,QAAQ,CAAC,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,sDAAsD;IACtD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,oBAAoB,GAAG,KAAK,CAAC,YAAY,GAAG,IAAI,CAoBrF"}
@@ -73,8 +73,10 @@ const SEVERITY_STYLES = {
73
73
  * @public
74
74
  */
75
75
  function ToastItem(props) {
76
+ var _a;
76
77
  const { message, onDismiss } = props;
77
- const config = model_1.DEFAULT_TOAST_CONFIG[message.severity];
78
+ const severity = (_a = message.severity) !== null && _a !== void 0 ? _a : (0, model_1.deriveSeverityFromLevel)(message.level);
79
+ const config = model_1.DEFAULT_TOAST_CONFIG[severity];
78
80
  const { layoutMode } = (0, responsive_1.useResponsive)();
79
81
  (0, react_1.useEffect)(() => {
80
82
  if (config.autoDismissMs > 0) {
@@ -83,10 +85,11 @@ function ToastItem(props) {
83
85
  }
84
86
  return undefined;
85
87
  }, [message.id, config.autoDismissMs, onDismiss]);
86
- return (react_1.default.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" },
88
+ return (react_1.default.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[severity]}`, role: "alert" },
87
89
  react_1.default.createElement("div", { className: "flex-1 text-sm line-clamp-4", title: message.text }, message.text),
88
90
  react_1.default.createElement("div", { className: "flex items-center gap-2 shrink-0" },
89
- message.action && (react_1.default.createElement("button", { onClick: message.action.onAction, className: "text-sm font-medium underline hover:no-underline" }, message.action.label)),
91
+ message.action &&
92
+ (message.action.href !== undefined ? (react_1.default.createElement("a", { href: message.action.href, target: "_blank", rel: "noopener noreferrer", className: "text-sm font-medium underline hover:no-underline" }, message.action.label)) : (react_1.default.createElement("button", { onClick: message.action.onAction, className: "text-sm font-medium underline hover:no-underline" }, message.action.label))),
90
93
  react_1.default.createElement("button", { onClick: () => onDismiss(message.id), className: "text-current opacity-50 hover:opacity-100", "aria-label": "Dismiss" }, "\u00D7"))));
91
94
  }
92
95
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"Toast.js","sourceRoot":"","sources":["../../../src/packlets/messages/Toast.tsx"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCH,8BA0CC;AAqBD,wCAoBC;AApHD,+CAAyC;AAEzC,mCAA0E;AAC1E,8CAA8C;AAE9C,+EAA+E;AAC/E,kBAAkB;AAClB,+EAA+E;AAE/E,MAAM,eAAe,GAAoC;IACvD,IAAI,EAAE,mEAAmE;IACzE,OAAO,EAAE,4EAA4E;IACrF,OAAO,EAAE,4EAA4E;IACrF,KAAK,EAAE,sEAAsE;CAC9E,CAAC;AAeF;;;GAGG;AACH,SAAgB,SAAS,CAAC,KAAsB;IAC9C,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,KAAK,CAAC;IACrC,MAAM,MAAM,GAAG,4BAAoB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACtD,MAAM,EAAE,UAAU,EAAE,GAAG,IAAA,0BAAa,GAAE,CAAC;IAEvC,IAAA,iBAAS,EAAC,GAAG,EAAE;QACb,IAAI,MAAM,CAAC,aAAa,GAAG,CAAC,EAAE,CAAC;YAC7B,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,aAAa,CAAC,CAAC;YAC5E,OAAO,GAAS,EAAE,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;QACzC,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC,EAAE,CAAC,OAAO,CAAC,EAAE,EAAE,MAAM,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC,CAAC;IAElD,OAAO,CACL,uCACE,SAAS,EAAE,gEACT,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,UACvC,IAAI,eAAe,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EACvC,IAAI,EAAC,OAAO;QAEZ,uCAAK,SAAS,EAAC,6BAA6B,EAAC,KAAK,EAAE,OAAO,CAAC,IAAI,IAC7D,OAAO,CAAC,IAAI,CACT;QACN,uCAAK,SAAS,EAAC,kCAAkC;YAC9C,OAAO,CAAC,MAAM,IAAI,CACjB,0CACE,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,QAAQ,EAChC,SAAS,EAAC,kDAAkD,IAE3D,OAAO,CAAC,MAAM,CAAC,KAAK,CACd,CACV;YACD,0CACE,OAAO,EAAE,GAAS,EAAE,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC,EAC1C,SAAS,EAAC,2CAA2C,gBAC1C,SAAS,aAGb,CACL,CACF,CACP,CAAC;AACJ,CAAC;AAiBD;;;GAGG;AACH,SAAgB,cAAc,CAAC,KAA2B;IACxD,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,GAAG,CAAC,EAAE,GAAG,KAAK,CAAC;IACpD,MAAM,EAAE,UAAU,EAAE,GAAG,IAAA,0BAAa,GAAE,CAAC;IACvC,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,UAAU,CAAC,CAAC;IAE1C,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,CACL,uCACE,SAAS,EAAE,4CACT,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,SAC/C,EAAE,IAED,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CACtB,8BAAC,SAAS,IAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,GAAI,CACnE,CAAC,CACE,CACP,CAAC;AACJ,CAAC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport React, { useEffect } from 'react';\n\nimport { DEFAULT_TOAST_CONFIG, IMessage, MessageSeverity } from './model';\nimport { useResponsive } from '../responsive';\n\n// ============================================================================\n// Severity Styles\n// ============================================================================\n\nconst SEVERITY_STYLES: Record<MessageSeverity, string> = {\n info: 'bg-status-info-bg border-status-info-border text-status-info-text',\n success: 'bg-status-success-bg border-status-success-border text-status-success-text',\n warning: 'bg-status-warning-bg border-status-warning-border text-status-warning-text',\n error: 'bg-status-error-bg border-status-error-border text-status-error-text'\n};\n\n// ============================================================================\n// Single Toast\n// ============================================================================\n\n/**\n * Props for a single Toast item.\n * @public\n */\nexport interface IToastItemProps {\n readonly message: IMessage;\n readonly onDismiss: (id: string) => void;\n}\n\n/**\n * A single toast notification.\n * @public\n */\nexport function ToastItem(props: IToastItemProps): React.ReactElement {\n const { message, onDismiss } = props;\n const config = DEFAULT_TOAST_CONFIG[message.severity];\n const { layoutMode } = useResponsive();\n\n useEffect(() => {\n if (config.autoDismissMs > 0) {\n const timer = setTimeout(() => onDismiss(message.id), config.autoDismissMs);\n return (): void => clearTimeout(timer);\n }\n return undefined;\n }, [message.id, config.autoDismissMs, onDismiss]);\n\n return (\n <div\n className={`flex items-start gap-3 px-4 py-3 rounded-lg border shadow-lg ${\n layoutMode === 'mobile' ? 'w-full' : 'max-w-sm'\n } ${SEVERITY_STYLES[message.severity]}`}\n role=\"alert\"\n >\n <div className=\"flex-1 text-sm line-clamp-4\" title={message.text}>\n {message.text}\n </div>\n <div className=\"flex items-center gap-2 shrink-0\">\n {message.action && (\n <button\n onClick={message.action.onAction}\n className=\"text-sm font-medium underline hover:no-underline\"\n >\n {message.action.label}\n </button>\n )}\n <button\n onClick={(): void => onDismiss(message.id)}\n className=\"text-current opacity-50 hover:opacity-100\"\n aria-label=\"Dismiss\"\n >\n &times;\n </button>\n </div>\n </div>\n );\n}\n\n// ============================================================================\n// Toast Container\n// ============================================================================\n\n/**\n * Props for the ToastContainer.\n * @public\n */\nexport interface IToastContainerProps {\n readonly toasts: ReadonlyArray<IMessage>;\n readonly onDismiss: (id: string) => void;\n /** Maximum number of toasts to show simultaneously */\n readonly maxVisible?: number;\n}\n\n/**\n * Container that renders active toasts in the bottom-right corner.\n * @public\n */\nexport function ToastContainer(props: IToastContainerProps): React.ReactElement | null {\n const { toasts, onDismiss, maxVisible = 5 } = props;\n const { layoutMode } = useResponsive();\n const visible = toasts.slice(-maxVisible);\n\n if (visible.length === 0) {\n return null;\n }\n\n return (\n <div\n className={`fixed bottom-16 z-50 flex flex-col gap-2 ${\n layoutMode === 'mobile' ? 'left-4 right-4' : 'right-4'\n }`}\n >\n {visible.map((toast) => (\n <ToastItem key={toast.id} message={toast} onDismiss={onDismiss} />\n ))}\n </div>\n );\n}\n"]}
1
+ {"version":3,"file":"Toast.js","sourceRoot":"","sources":["../../../src/packlets/messages/Toast.tsx"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport React, { useEffect } from 'react';\n\nimport { DEFAULT_TOAST_CONFIG, deriveSeverityFromLevel, IMessage, MessageSeverity } from './model';\nimport { useResponsive } from '../responsive';\n\n// ============================================================================\n// Severity Styles\n// ============================================================================\n\nconst SEVERITY_STYLES: Record<MessageSeverity, string> = {\n info: 'bg-status-info-bg border-status-info-border text-status-info-text',\n success: 'bg-status-success-bg border-status-success-border text-status-success-text',\n warning: 'bg-status-warning-bg border-status-warning-border text-status-warning-text',\n error: 'bg-status-error-bg border-status-error-border text-status-error-text'\n};\n\n// ============================================================================\n// Single Toast\n// ============================================================================\n\n/**\n * Props for a single Toast item.\n * @public\n */\nexport interface IToastItemProps {\n readonly message: IMessage;\n readonly onDismiss: (id: string) => void;\n}\n\n/**\n * A single toast notification.\n * @public\n */\nexport function ToastItem(props: IToastItemProps): React.ReactElement {\n const { message, onDismiss } = props;\n const severity = message.severity ?? deriveSeverityFromLevel(message.level);\n const config = DEFAULT_TOAST_CONFIG[severity];\n const { layoutMode } = useResponsive();\n\n useEffect(() => {\n if (config.autoDismissMs > 0) {\n const timer = setTimeout(() => onDismiss(message.id), config.autoDismissMs);\n return (): void => clearTimeout(timer);\n }\n return undefined;\n }, [message.id, config.autoDismissMs, onDismiss]);\n\n return (\n <div\n className={`flex items-start gap-3 px-4 py-3 rounded-lg border shadow-lg ${\n layoutMode === 'mobile' ? 'w-full' : 'max-w-sm'\n } ${SEVERITY_STYLES[severity]}`}\n role=\"alert\"\n >\n <div className=\"flex-1 text-sm line-clamp-4\" title={message.text}>\n {message.text}\n </div>\n <div className=\"flex items-center gap-2 shrink-0\">\n {message.action &&\n (message.action.href !== undefined ? (\n <a\n href={message.action.href}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-sm font-medium underline hover:no-underline\"\n >\n {message.action.label}\n </a>\n ) : (\n <button\n onClick={message.action.onAction}\n className=\"text-sm font-medium underline hover:no-underline\"\n >\n {message.action.label}\n </button>\n ))}\n <button\n onClick={(): void => onDismiss(message.id)}\n className=\"text-current opacity-50 hover:opacity-100\"\n aria-label=\"Dismiss\"\n >\n &times;\n </button>\n </div>\n </div>\n );\n}\n\n// ============================================================================\n// Toast Container\n// ============================================================================\n\n/**\n * Props for the ToastContainer.\n * @public\n */\nexport interface IToastContainerProps {\n readonly toasts: ReadonlyArray<IMessage>;\n readonly onDismiss: (id: string) => void;\n /** Maximum number of toasts to show simultaneously */\n readonly maxVisible?: number;\n}\n\n/**\n * Container that renders active toasts in the bottom-right corner.\n * @public\n */\nexport function ToastContainer(props: IToastContainerProps): React.ReactElement | null {\n const { toasts, onDismiss, maxVisible = 5 } = props;\n const { layoutMode } = useResponsive();\n const visible = toasts.slice(-maxVisible);\n\n if (visible.length === 0) {\n return null;\n }\n\n return (\n <div\n className={`fixed bottom-16 z-50 flex flex-col gap-2 ${\n layoutMode === 'mobile' ? 'left-4 right-4' : 'right-4'\n }`}\n >\n {visible.map((toast) => (\n <ToastItem key={toast.id} message={toast} onDismiss={onDismiss} />\n ))}\n </div>\n );\n}\n"]}
@@ -2,8 +2,8 @@
2
2
  * Messages packlet - observability context, toast system, and log panel.
3
3
  * @packageDocumentation
4
4
  */
5
- export { type MessageSeverity, type IMessage, type IMessageAction, type IToastConfig, DEFAULT_TOAST_CONFIG, createMessage, generateMessageId } from './model';
6
- export { type IMessagesContextValue, type IMessagesProviderProps, MessagesProvider, useMessages } from './MessagesContext';
5
+ export { type MessageSeverity, type IMessage, type IMessageAction, type IMessageHrefAction, type IMessageCallbackAction, type ICreateMessageOptions, type IToastConfig, DEFAULT_TOAST_CONFIG, deriveSeverityFromLevel, createMessage, generateMessageId } from './model';
6
+ export { type IMessagesContextValue, type IMessagesProviderProps, MessagesProvider, TAILWIND_PROBE_CLASS, TAILWIND_PROBE_HEIGHT_PX, TAILWIND_PROBE_MESSAGE_TEXT, TAILWIND_SETUP_DOCS_URL, useMessages } from './MessagesContext';
7
7
  export { ToastItem, ToastContainer, type IToastItemProps, type IToastContainerProps } from './Toast';
8
8
  export { StatusBar, type IStatusBarProps } from './StatusBar';
9
9
  export { MessagesLogger } from './MessagesLogger';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/packlets/messages/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,QAAQ,EACb,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,oBAAoB,EACpB,aAAa,EACb,iBAAiB,EAClB,MAAM,SAAS,CAAC;AAEjB,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,gBAAgB,EAChB,WAAW,EACZ,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,KAAK,eAAe,EAAE,KAAK,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAErG,OAAO,EAAE,SAAS,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9D,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAElD,OAAO,EAAE,cAAc,EAAE,KAAK,sBAAsB,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/packlets/messages/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,QAAQ,EACb,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,YAAY,EACjB,oBAAoB,EACpB,uBAAuB,EACvB,aAAa,EACb,iBAAiB,EAClB,MAAM,SAAS,CAAC;AAEjB,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,gBAAgB,EAChB,oBAAoB,EACpB,wBAAwB,EACxB,2BAA2B,EAC3B,uBAAuB,EACvB,WAAW,EACZ,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,KAAK,eAAe,EAAE,KAAK,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAErG,OAAO,EAAE,SAAS,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9D,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAElD,OAAO,EAAE,cAAc,EAAE,KAAK,sBAAsB,EAAE,MAAM,kBAAkB,CAAC"}
@@ -4,13 +4,18 @@
4
4
  * @packageDocumentation
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.useLogReporter = exports.MessagesLogger = exports.StatusBar = exports.ToastContainer = exports.ToastItem = exports.useMessages = exports.MessagesProvider = exports.generateMessageId = exports.createMessage = exports.DEFAULT_TOAST_CONFIG = void 0;
7
+ exports.useLogReporter = exports.MessagesLogger = exports.StatusBar = exports.ToastContainer = exports.ToastItem = exports.useMessages = exports.TAILWIND_SETUP_DOCS_URL = exports.TAILWIND_PROBE_MESSAGE_TEXT = exports.TAILWIND_PROBE_HEIGHT_PX = exports.TAILWIND_PROBE_CLASS = exports.MessagesProvider = exports.generateMessageId = exports.createMessage = exports.deriveSeverityFromLevel = exports.DEFAULT_TOAST_CONFIG = void 0;
8
8
  var model_1 = require("./model");
9
9
  Object.defineProperty(exports, "DEFAULT_TOAST_CONFIG", { enumerable: true, get: function () { return model_1.DEFAULT_TOAST_CONFIG; } });
10
+ Object.defineProperty(exports, "deriveSeverityFromLevel", { enumerable: true, get: function () { return model_1.deriveSeverityFromLevel; } });
10
11
  Object.defineProperty(exports, "createMessage", { enumerable: true, get: function () { return model_1.createMessage; } });
11
12
  Object.defineProperty(exports, "generateMessageId", { enumerable: true, get: function () { return model_1.generateMessageId; } });
12
13
  var MessagesContext_1 = require("./MessagesContext");
13
14
  Object.defineProperty(exports, "MessagesProvider", { enumerable: true, get: function () { return MessagesContext_1.MessagesProvider; } });
15
+ Object.defineProperty(exports, "TAILWIND_PROBE_CLASS", { enumerable: true, get: function () { return MessagesContext_1.TAILWIND_PROBE_CLASS; } });
16
+ Object.defineProperty(exports, "TAILWIND_PROBE_HEIGHT_PX", { enumerable: true, get: function () { return MessagesContext_1.TAILWIND_PROBE_HEIGHT_PX; } });
17
+ Object.defineProperty(exports, "TAILWIND_PROBE_MESSAGE_TEXT", { enumerable: true, get: function () { return MessagesContext_1.TAILWIND_PROBE_MESSAGE_TEXT; } });
18
+ Object.defineProperty(exports, "TAILWIND_SETUP_DOCS_URL", { enumerable: true, get: function () { return MessagesContext_1.TAILWIND_SETUP_DOCS_URL; } });
14
19
  Object.defineProperty(exports, "useMessages", { enumerable: true, get: function () { return MessagesContext_1.useMessages; } });
15
20
  var Toast_1 = require("./Toast");
16
21
  Object.defineProperty(exports, "ToastItem", { enumerable: true, get: function () { return Toast_1.ToastItem; } });
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/packlets/messages/index.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAEH,iCAQiB;AAHf,6GAAA,oBAAoB,OAAA;AACpB,sGAAA,aAAa,OAAA;AACb,0GAAA,iBAAiB,OAAA;AAGnB,qDAK2B;AAFzB,mHAAA,gBAAgB,OAAA;AAChB,8GAAA,WAAW,OAAA;AAGb,iCAAqG;AAA5F,kGAAA,SAAS,OAAA;AAAE,uGAAA,cAAc,OAAA;AAElC,yCAA8D;AAArD,sGAAA,SAAS,OAAA;AAElB,mDAAkD;AAAzC,gHAAA,cAAc,OAAA;AAEvB,mDAA+E;AAAtE,gHAAA,cAAc,OAAA","sourcesContent":["/**\n * Messages packlet - observability context, toast system, and log panel.\n * @packageDocumentation\n */\n\nexport {\n type MessageSeverity,\n type IMessage,\n type IMessageAction,\n type IToastConfig,\n DEFAULT_TOAST_CONFIG,\n createMessage,\n generateMessageId\n} from './model';\n\nexport {\n type IMessagesContextValue,\n type IMessagesProviderProps,\n MessagesProvider,\n useMessages\n} from './MessagesContext';\n\nexport { ToastItem, ToastContainer, type IToastItemProps, type IToastContainerProps } from './Toast';\n\nexport { StatusBar, type IStatusBarProps } from './StatusBar';\n\nexport { MessagesLogger } from './MessagesLogger';\n\nexport { useLogReporter, type IUseLogReporterOptions } from './useLogReporter';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/packlets/messages/index.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAEH,iCAYiB;AAJf,6GAAA,oBAAoB,OAAA;AACpB,gHAAA,uBAAuB,OAAA;AACvB,sGAAA,aAAa,OAAA;AACb,0GAAA,iBAAiB,OAAA;AAGnB,qDAS2B;AANzB,mHAAA,gBAAgB,OAAA;AAChB,uHAAA,oBAAoB,OAAA;AACpB,2HAAA,wBAAwB,OAAA;AACxB,8HAAA,2BAA2B,OAAA;AAC3B,0HAAA,uBAAuB,OAAA;AACvB,8GAAA,WAAW,OAAA;AAGb,iCAAqG;AAA5F,kGAAA,SAAS,OAAA;AAAE,uGAAA,cAAc,OAAA;AAElC,yCAA8D;AAArD,sGAAA,SAAS,OAAA;AAElB,mDAAkD;AAAzC,gHAAA,cAAc,OAAA;AAEvB,mDAA+E;AAAtE,gHAAA,cAAc,OAAA","sourcesContent":["/**\n * Messages packlet - observability context, toast system, and log panel.\n * @packageDocumentation\n */\n\nexport {\n type MessageSeverity,\n type IMessage,\n type IMessageAction,\n type IMessageHrefAction,\n type IMessageCallbackAction,\n type ICreateMessageOptions,\n type IToastConfig,\n DEFAULT_TOAST_CONFIG,\n deriveSeverityFromLevel,\n createMessage,\n generateMessageId\n} from './model';\n\nexport {\n type IMessagesContextValue,\n type IMessagesProviderProps,\n MessagesProvider,\n TAILWIND_PROBE_CLASS,\n TAILWIND_PROBE_HEIGHT_PX,\n TAILWIND_PROBE_MESSAGE_TEXT,\n TAILWIND_SETUP_DOCS_URL,\n useMessages\n} from './MessagesContext';\n\nexport { ToastItem, ToastContainer, type IToastItemProps, type IToastContainerProps } from './Toast';\n\nexport { StatusBar, type IStatusBarProps } from './StatusBar';\n\nexport { MessagesLogger } from './MessagesLogger';\n\nexport { useLogReporter, type IUseLogReporterOptions } from './useLogReporter';\n"]}
@@ -2,11 +2,25 @@
2
2
  * Message model types for the observability system.
3
3
  * @packageDocumentation
4
4
  */
5
+ import { type MessageLogLevel } from '@fgv/ts-utils';
5
6
  /**
6
- * Severity levels for messages.
7
+ * Display styling levels for messages — drives color, icon, and toast behavior.
8
+ *
9
+ * This is the styling axis, orthogonal to the {@link @fgv/ts-utils#MessageLogLevel | log level}
10
+ * filter axis on {@link IMessage.level}. `'success'` lives here (a UI affordance with no
11
+ * log-level analog) and is only ever set explicitly — never derived from a logger bridge.
7
12
  * @public
8
13
  */
9
14
  export type MessageSeverity = 'info' | 'success' | 'warning' | 'error';
15
+ /**
16
+ * Derives a display {@link MessageSeverity} from a canonical
17
+ * {@link @fgv/ts-utils#MessageLogLevel | log level}, used when a message carries no explicit
18
+ * `severity` styling override. `'success'` is never derived — it is a UI-only affordance.
19
+ * @param level - The canonical log level.
20
+ * @returns The styling severity to use for display.
21
+ * @public
22
+ */
23
+ export declare function deriveSeverityFromLevel(level: MessageLogLevel): MessageSeverity;
10
24
  /**
11
25
  * A single message in the observability stream.
12
26
  * @public
@@ -14,8 +28,17 @@ export type MessageSeverity = 'info' | 'success' | 'warning' | 'error';
14
28
  export interface IMessage {
15
29
  /** Unique message ID */
16
30
  readonly id: string;
17
- /** Message severity */
18
- readonly severity: MessageSeverity;
31
+ /**
32
+ * Canonical {@link @fgv/ts-utils#MessageLogLevel | log level} — drives FILTERING.
33
+ * Set losslessly from a ts-utils logger when a message originates from the logger bridge.
34
+ */
35
+ readonly level: MessageLogLevel;
36
+ /**
37
+ * Display styling override (color / icon / toast). Optional; when absent, styling is
38
+ * derived from {@link IMessage.level} via {@link deriveSeverityFromLevel}. `'success'`
39
+ * lives here — set explicitly for UI-originated messages, never from a logger bridge.
40
+ */
41
+ readonly severity?: MessageSeverity;
19
42
  /** Message text */
20
43
  readonly text: string;
21
44
  /** Timestamp (ms since epoch) */
@@ -24,15 +47,39 @@ export interface IMessage {
24
47
  readonly action?: IMessageAction;
25
48
  }
26
49
  /**
27
- * An actionable element attached to a message (e.g., "Go to Session" link).
50
+ * An actionable element attached to a message rendered as an external link.
51
+ * Renderers display this as an `<a>` that opens in a new tab with
52
+ * `noopener noreferrer`.
53
+ * @public
54
+ */
55
+ export interface IMessageHrefAction {
56
+ /** Action label */
57
+ readonly label: string;
58
+ /** External URL to open when the action is clicked. */
59
+ readonly href: string;
60
+ readonly onAction?: undefined;
61
+ }
62
+ /**
63
+ * An actionable element attached to a message rendered as a callback button.
64
+ * Renderers display this as a `<button>` that invokes {@link IMessageCallbackAction.onAction}.
28
65
  * @public
29
66
  */
30
- export interface IMessageAction {
67
+ export interface IMessageCallbackAction {
31
68
  /** Action label */
32
69
  readonly label: string;
33
- /** Callback when the action is triggered */
70
+ /** Callback when the action is triggered. */
34
71
  readonly onAction: () => void;
72
+ readonly href?: undefined;
35
73
  }
74
+ /**
75
+ * An actionable element attached to a message — either an external link
76
+ * ({@link IMessageHrefAction}) or a callback ({@link IMessageCallbackAction}).
77
+ * The discriminated-union shape makes "exactly one of `href` or `onAction`"
78
+ * an enforced contract at type-check time, so callers cannot accidentally
79
+ * supply both or neither.
80
+ * @public
81
+ */
82
+ export type IMessageAction = IMessageHrefAction | IMessageCallbackAction;
36
83
  /**
37
84
  * Configuration for toast auto-dismiss behavior.
38
85
  * @public
@@ -52,8 +99,27 @@ export declare const DEFAULT_TOAST_CONFIG: Record<MessageSeverity, IToastConfig>
52
99
  */
53
100
  export declare function generateMessageId(): string;
54
101
  /**
55
- * Creates a new message.
102
+ * Options for {@link createMessage} (and `addMessage`).
103
+ * @public
104
+ */
105
+ export interface ICreateMessageOptions {
106
+ /**
107
+ * Explicit display styling override. Omit to derive styling from the level via
108
+ * {@link deriveSeverityFromLevel}. Set to `'success'` for UI-originated success messages.
109
+ */
110
+ readonly severity?: MessageSeverity;
111
+ /** Optional action (e.g., a link or callback). */
112
+ readonly action?: IMessageAction;
113
+ }
114
+ /**
115
+ * Creates a new message at the given canonical {@link @fgv/ts-utils#MessageLogLevel | log level}.
116
+ * The level drives filtering; pass `options.severity` only to override display styling
117
+ * (e.g. `'success'`).
118
+ * @param level - The canonical log level (filter axis).
119
+ * @param text - The message text.
120
+ * @param options - Optional styling severity and action.
121
+ * @returns The created message.
56
122
  * @public
57
123
  */
58
- export declare function createMessage(severity: MessageSeverity, text: string, action?: IMessageAction): IMessage;
124
+ export declare function createMessage(level: MessageLogLevel, text: string, options?: ICreateMessageOptions): IMessage;
59
125
  //# sourceMappingURL=model.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../../../src/packlets/messages/model.ts"],"names":[],"mappings":"AAsBA;;;GAGG;AAMH;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;AAMvE;;;GAGG;AACH,MAAM,WAAW,QAAQ;IACvB,wBAAwB;IACxB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,uBAAuB;IACvB,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,mBAAmB;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,iCAAiC;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,iDAAiD;IACjD,QAAQ,CAAC,MAAM,CAAC,EAAE,cAAc,CAAC;CAClC;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,mBAAmB;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,4CAA4C;IAC5C,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAC;CAC/B;AAMD;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,wDAAwD;IACxD,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAED;;;GAGG;AACH,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,eAAe,EAAE,YAAY,CAK7D,CAAC;AAQX;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,cAAc,GAAG,QAAQ,CAQxG"}
1
+ {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../../../src/packlets/messages/model.ts"],"names":[],"mappings":"AAsBA;;;GAGG;AAEH,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAMrD;;;;;;;GAOG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;AAEvE;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,eAAe,GAAG,eAAe,CAW/E;AAMD;;;GAGG;AACH,MAAM,WAAW,QAAQ;IACvB,wBAAwB;IACxB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;IAChC;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,eAAe,CAAC;IACpC,mBAAmB;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,iCAAiC;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,iDAAiD;IACjD,QAAQ,CAAC,MAAM,CAAC,EAAE,cAAc,CAAC;CAClC;AAED;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,mBAAmB;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,uDAAuD;IACvD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,CAAC;CAC/B;AAED;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACrC,mBAAmB;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,6CAA6C;IAC7C,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAC;IAC9B,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,cAAc,GAAG,kBAAkB,GAAG,sBAAsB,CAAC;AAMzE;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,wDAAwD;IACxD,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAED;;;GAGG;AACH,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,eAAe,EAAE,YAAY,CAK7D,CAAC;AAQX;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,eAAe,CAAC;IACpC,kDAAkD;IAClD,QAAQ,CAAC,MAAM,CAAC,EAAE,cAAc,CAAC;CAClC;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,eAAe,EACtB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,qBAAqB,GAC9B,QAAQ,CASV"}
@@ -22,8 +22,29 @@
22
22
  */
23
23
  Object.defineProperty(exports, "__esModule", { value: true });
24
24
  exports.DEFAULT_TOAST_CONFIG = void 0;
25
+ exports.deriveSeverityFromLevel = deriveSeverityFromLevel;
25
26
  exports.generateMessageId = generateMessageId;
26
27
  exports.createMessage = createMessage;
28
+ /**
29
+ * Derives a display {@link MessageSeverity} from a canonical
30
+ * {@link @fgv/ts-utils#MessageLogLevel | log level}, used when a message carries no explicit
31
+ * `severity` styling override. `'success'` is never derived — it is a UI-only affordance.
32
+ * @param level - The canonical log level.
33
+ * @returns The styling severity to use for display.
34
+ * @public
35
+ */
36
+ function deriveSeverityFromLevel(level) {
37
+ switch (level) {
38
+ case 'warning':
39
+ return 'warning';
40
+ case 'error':
41
+ return 'error';
42
+ case 'quiet':
43
+ case 'detail':
44
+ case 'info':
45
+ return 'info';
46
+ }
47
+ }
27
48
  /**
28
49
  * Default toast configuration by severity.
29
50
  * @public
@@ -46,16 +67,23 @@ function generateMessageId() {
46
67
  return `msg-${++_nextId}-${Date.now().toString(36)}`;
47
68
  }
48
69
  /**
49
- * Creates a new message.
70
+ * Creates a new message at the given canonical {@link @fgv/ts-utils#MessageLogLevel | log level}.
71
+ * The level drives filtering; pass `options.severity` only to override display styling
72
+ * (e.g. `'success'`).
73
+ * @param level - The canonical log level (filter axis).
74
+ * @param text - The message text.
75
+ * @param options - Optional styling severity and action.
76
+ * @returns The created message.
50
77
  * @public
51
78
  */
52
- function createMessage(severity, text, action) {
79
+ function createMessage(level, text, options) {
53
80
  return {
54
81
  id: generateMessageId(),
55
- severity,
82
+ level,
83
+ severity: options === null || options === void 0 ? void 0 : options.severity,
56
84
  text,
57
85
  timestamp: Date.now(),
58
- action
86
+ action: options === null || options === void 0 ? void 0 : options.action
59
87
  };
60
88
  }
61
89
  //# sourceMappingURL=model.js.map