@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.
- package/README.md +23 -0
- package/dist/packlets/messages/MessagesContext.js +79 -3
- package/dist/packlets/messages/MessagesContext.js.map +1 -1
- package/dist/packlets/messages/MessagesLogger.js +10 -31
- package/dist/packlets/messages/MessagesLogger.js.map +1 -1
- package/dist/packlets/messages/StatusBar.js +53 -28
- package/dist/packlets/messages/StatusBar.js.map +1 -1
- package/dist/packlets/messages/Toast.js +7 -4
- package/dist/packlets/messages/Toast.js.map +1 -1
- package/dist/packlets/messages/index.js +2 -2
- package/dist/packlets/messages/index.js.map +1 -1
- package/dist/packlets/messages/model.js +31 -4
- package/dist/packlets/messages/model.js.map +1 -1
- package/lib/packlets/messages/MessagesContext.d.ts +47 -3
- package/lib/packlets/messages/MessagesContext.d.ts.map +1 -1
- package/lib/packlets/messages/MessagesContext.js +79 -2
- package/lib/packlets/messages/MessagesContext.js.map +1 -1
- package/lib/packlets/messages/MessagesLogger.d.ts +8 -3
- package/lib/packlets/messages/MessagesLogger.d.ts.map +1 -1
- package/lib/packlets/messages/MessagesLogger.js +10 -31
- package/lib/packlets/messages/MessagesLogger.js.map +1 -1
- package/lib/packlets/messages/StatusBar.d.ts +10 -1
- package/lib/packlets/messages/StatusBar.d.ts.map +1 -1
- package/lib/packlets/messages/StatusBar.js +52 -27
- package/lib/packlets/messages/StatusBar.js.map +1 -1
- package/lib/packlets/messages/Toast.d.ts.map +1 -1
- package/lib/packlets/messages/Toast.js +6 -3
- package/lib/packlets/messages/Toast.js.map +1 -1
- package/lib/packlets/messages/index.d.ts +2 -2
- package/lib/packlets/messages/index.d.ts.map +1 -1
- package/lib/packlets/messages/index.js +6 -1
- package/lib/packlets/messages/index.js.map +1 -1
- package/lib/packlets/messages/model.d.ts +74 -8
- package/lib/packlets/messages/model.d.ts.map +1 -1
- package/lib/packlets/messages/model.js +32 -4
- package/lib/packlets/messages/model.js.map +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
Shared React UI primitives for application shells in the `@fgv` monorepo.
|
|
4
4
|
|
|
5
|
+
> ⚠️ **Required setup** — this package ships its components as Tailwind-classed JSX. You
|
|
6
|
+
> must add the `@fgv/ts-app-shell` content path to your Tailwind config, or icons and
|
|
7
|
+
> overlays in the messages panel and other primitives will render unsized (invisible
|
|
8
|
+
> buttons, viewport-overflowing absolute overlays). See [Setup](#setup) below; the
|
|
9
|
+
> `MessagesProvider` self-diagnoses this configuration gap on mount and surfaces a
|
|
10
|
+
> warning message linking back here.
|
|
11
|
+
|
|
12
|
+
## Setup
|
|
13
|
+
|
|
14
|
+
See the [Theming](#theming) section for the canonical setup steps:
|
|
15
|
+
|
|
16
|
+
1. Add the Tailwind preset and include `./node_modules/@fgv/ts-app-shell/lib/**/*.{js,jsx}`
|
|
17
|
+
in your `content` array.
|
|
18
|
+
2. Import `@fgv/ts-app-shell/theme.css` once in your application entry point.
|
|
19
|
+
|
|
20
|
+
## Troubleshooting
|
|
21
|
+
|
|
22
|
+
**Icons in the messages panel are invisible or oversized, or you see an
|
|
23
|
+
"@fgv/ts-app-shell styles are not fully loaded" warning message.** Your Tailwind config
|
|
24
|
+
is missing the `@fgv/ts-app-shell` content path. See [Setup](#setup) above and make sure
|
|
25
|
+
`./node_modules/@fgv/ts-app-shell/lib/**/*.{js,jsx}` appears in your `tailwind.config.js`
|
|
26
|
+
`content` array.
|
|
27
|
+
|
|
5
28
|
## Features
|
|
6
29
|
|
|
7
30
|
- **Column Cascade** — Master-detail drill-down with horizontal scrolling, breadcrumb navigation, and min-width columns
|
|
@@ -19,22 +19,64 @@
|
|
|
19
19
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
20
|
* SOFTWARE.
|
|
21
21
|
*/
|
|
22
|
-
import React, { useCallback, useContext, useMemo, useState } from 'react';
|
|
22
|
+
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
23
23
|
import { createMessage } from './model';
|
|
24
24
|
// ============================================================================
|
|
25
25
|
// Context
|
|
26
26
|
// ============================================================================
|
|
27
27
|
const MessagesContext = React.createContext(undefined);
|
|
28
|
+
/**
|
|
29
|
+
* Sentinel Tailwind class used by the probe in {@link MessagesProvider}.
|
|
30
|
+
*
|
|
31
|
+
* This is an arbitrary-value height utility (`h-[7.3215px]`) chosen to be
|
|
32
|
+
* vanishingly unlikely to appear in any consumer's source code: a common
|
|
33
|
+
* utility like `h-3.5` could be generated by Tailwind scanning the consumer's
|
|
34
|
+
* own files, which would mask the missing `@fgv/ts-app-shell` content path.
|
|
35
|
+
* Tailwind JIT regex-scans the package's built JS, finds this literal string,
|
|
36
|
+
* and generates the rule — only when the consumer's `content` array includes
|
|
37
|
+
* the `@fgv/ts-app-shell/lib` glob (see the README "Setup" section).
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
40
|
+
export const TAILWIND_PROBE_CLASS = 'h-[7.3215px]';
|
|
41
|
+
/**
|
|
42
|
+
* Expected probe height in pixels (matches {@link TAILWIND_PROBE_CLASS}).
|
|
43
|
+
* @internal
|
|
44
|
+
*/
|
|
45
|
+
export const TAILWIND_PROBE_HEIGHT_PX = 7.3215;
|
|
46
|
+
/**
|
|
47
|
+
* URL of the setup-instructions anchor surfaced by the Tailwind-content-path
|
|
48
|
+
* diagnostic message when the probe detects that ts-app-shell's geometry utilities
|
|
49
|
+
* are not being generated.
|
|
50
|
+
* @internal
|
|
51
|
+
*/
|
|
52
|
+
export const TAILWIND_SETUP_DOCS_URL = 'https://github.com/ErikFortune/fgv/tree/release/libraries/ts-app-shell#setup';
|
|
53
|
+
/**
|
|
54
|
+
* Text of the diagnostic message emitted by the Tailwind-content-path probe.
|
|
55
|
+
* @internal
|
|
56
|
+
*/
|
|
57
|
+
export const TAILWIND_PROBE_MESSAGE_TEXT = '@fgv/ts-app-shell styles are not fully loaded. Some icons may not render correctly. ' +
|
|
58
|
+
'See setup docs for the required Tailwind content-path entry.';
|
|
28
59
|
/**
|
|
29
60
|
* Provides the global message stream to the component tree.
|
|
61
|
+
*
|
|
62
|
+
* On mount, runs a one-shot probe that detects whether the consuming app's Tailwind
|
|
63
|
+
* configuration is missing the `@fgv/ts-app-shell` content path. The probe applies a
|
|
64
|
+
* sentinel arbitrary-value class ({@link TAILWIND_PROBE_CLASS}) that is vanishingly
|
|
65
|
+
* unlikely to appear in any consumer's source — Tailwind will only generate the rule
|
|
66
|
+
* when it scans the package's built JS via the content path. If the probe finds the
|
|
67
|
+
* element unsized, a `'warning'` message is appended to the stream with a link to the
|
|
68
|
+
* setup documentation. The probe runs exactly once per provider lifecycle (idempotent
|
|
69
|
+
* under React.StrictMode double-render and HMR) and uses inline styles so it is not
|
|
70
|
+
* affected by the failure mode it detects.
|
|
30
71
|
* @public
|
|
31
72
|
*/
|
|
32
73
|
export function MessagesProvider(props) {
|
|
33
74
|
const { maxMessages = 200, children } = props;
|
|
34
75
|
const [messages, setMessages] = useState([]);
|
|
35
76
|
const [dismissedIds, setDismissedIds] = useState(new Set());
|
|
36
|
-
const
|
|
37
|
-
|
|
77
|
+
const probeHasRun = useRef(false);
|
|
78
|
+
const addMessage = useCallback((level, text, options) => {
|
|
79
|
+
const msg = createMessage(level, text, options);
|
|
38
80
|
setMessages((prev) => {
|
|
39
81
|
const next = [...prev, msg];
|
|
40
82
|
return next.length > maxMessages ? next.slice(next.length - maxMessages) : next;
|
|
@@ -49,6 +91,40 @@ export function MessagesProvider(props) {
|
|
|
49
91
|
setDismissedIds(new Set());
|
|
50
92
|
}, []);
|
|
51
93
|
const activeToasts = useMemo(() => messages.filter((m) => !dismissedIds.has(m.id)), [messages, dismissedIds]);
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (probeHasRun.current) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const probe = document.createElement('div');
|
|
99
|
+
probe.className = TAILWIND_PROBE_CLASS;
|
|
100
|
+
probe.setAttribute('aria-hidden', 'true');
|
|
101
|
+
probe.style.position = 'absolute';
|
|
102
|
+
probe.style.visibility = 'hidden';
|
|
103
|
+
probe.style.pointerEvents = 'none';
|
|
104
|
+
document.body.appendChild(probe);
|
|
105
|
+
const frameId = requestAnimationFrame(() => {
|
|
106
|
+
// Mark the guard only once the measurement has actually run; otherwise a
|
|
107
|
+
// StrictMode cleanup-before-rAF would leave `probeHasRun = true` and the
|
|
108
|
+
// second setup would short-circuit, suppressing the diagnostic entirely.
|
|
109
|
+
probeHasRun.current = true;
|
|
110
|
+
const height = probe.getBoundingClientRect().height;
|
|
111
|
+
probe.remove();
|
|
112
|
+
if (height === 0) {
|
|
113
|
+
addMessage('warning', TAILWIND_PROBE_MESSAGE_TEXT, {
|
|
114
|
+
action: {
|
|
115
|
+
label: 'Open setup docs',
|
|
116
|
+
href: TAILWIND_SETUP_DOCS_URL
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
return () => {
|
|
122
|
+
cancelAnimationFrame(frameId);
|
|
123
|
+
if (probe.parentNode !== null) {
|
|
124
|
+
probe.remove();
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}, [addMessage]);
|
|
52
128
|
const value = useMemo(() => ({
|
|
53
129
|
messages,
|
|
54
130
|
addMessage,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MessagesContext.js","sourceRoot":"","sources":["../../../src/packlets/messages/MessagesContext.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAE1E,OAAO,EAAE,aAAa,EAA6C,MAAM,SAAS,CAAC;AAuBnF,+EAA+E;AAC/E,UAAU;AACV,+EAA+E;AAE/E,MAAM,eAAe,GAAqD,KAAK,CAAC,aAAa,CAE3F,SAAS,CAAC,CAAC;AAiBb;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAA6B;IAC5D,MAAM,EAAE,WAAW,GAAG,GAAG,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC;IAC9C,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAa,EAAE,CAAC,CAAC;IACzD,MAAM,CAAC,YAAY,EAAE,eAAe,CAAC,GAAG,QAAQ,CAAc,IAAI,GAAG,EAAE,CAAC,CAAC;IAEzE,MAAM,UAAU,GAAG,WAAW,CAC5B,CAAC,QAAyB,EAAE,IAAY,EAAE,MAAuB,EAAY,EAAE;QAC7E,MAAM,GAAG,GAAG,aAAa,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;QAClD,WAAW,CAAC,CAAC,IAAI,EAAE,EAAE;YACnB,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,EAAE,GAAG,CAAC,CAAC;YAC5B,OAAO,IAAI,CAAC,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAClF,CAAC,CAAC,CAAC;QACH,OAAO,GAAG,CAAC;IACb,CAAC,EACD,CAAC,WAAW,CAAC,CACd,CAAC;IAEF,MAAM,cAAc,GAAG,WAAW,CAAC,CAAC,EAAU,EAAQ,EAAE;QACtD,eAAe,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;IACnD,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,aAAa,GAAG,WAAW,CAAC,GAAS,EAAE;QAC3C,WAAW,CAAC,EAAE,CAAC,CAAC;QAChB,eAAe,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC;IAC7B,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,YAAY,GAAG,OAAO,CAC1B,GAAG,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EACrD,CAAC,QAAQ,EAAE,YAAY,CAAC,CACzB,CAAC;IAEF,MAAM,KAAK,GAAG,OAAO,CACnB,GAAG,EAAE,CAAC,CAAC;QACL,QAAQ;QACR,UAAU;QACV,cAAc;QACd,aAAa;QACb,YAAY;KACb,CAAC,EACF,CAAC,QAAQ,EAAE,UAAU,EAAE,cAAc,EAAE,aAAa,EAAE,YAAY,CAAC,CACpE,CAAC;IAEF,OAAO,oBAAC,eAAe,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,IAAG,QAAQ,CAA4B,CAAC;AACvF,CAAC;AAED,+EAA+E;AAC/E,OAAO;AACP,+EAA+E;AAE/E;;;;GAIG;AACH,MAAM,UAAU,WAAW;IACzB,MAAM,GAAG,GAAG,UAAU,CAAC,eAAe,CAAC,CAAC;IACxC,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC;IACxE,CAAC;IACD,OAAO,GAAG,CAAC;AACb,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, { useCallback, useContext, useMemo, useState } from 'react';\n\nimport { createMessage, IMessage, IMessageAction, MessageSeverity } from './model';\n\n// ============================================================================\n// Context Value\n// ============================================================================\n\n/**\n * Value provided by the MessagesContext.\n * @public\n */\nexport interface IMessagesContextValue {\n /** All messages in the stream (newest last) */\n readonly messages: ReadonlyArray<IMessage>;\n /** Add a message to the stream */\n readonly addMessage: (severity: MessageSeverity, text: string, action?: IMessageAction) => IMessage;\n /** Dismiss a specific message (removes from toast display, keeps in log) */\n readonly dismissMessage: (id: string) => void;\n /** Clear all messages */\n readonly clearMessages: () => void;\n /** Messages currently visible as toasts (not yet dismissed) */\n readonly activeToasts: ReadonlyArray<IMessage>;\n}\n\n// ============================================================================\n// Context\n// ============================================================================\n\nconst MessagesContext: React.Context<IMessagesContextValue | undefined> = React.createContext<\n IMessagesContextValue | undefined\n>(undefined);\n\n// ============================================================================\n// Provider\n// ============================================================================\n\n/**\n * Props for the MessagesProvider.\n * @public\n */\nexport interface IMessagesProviderProps {\n /** Maximum number of messages to retain in the log */\n readonly maxMessages?: number;\n /** Children */\n readonly children: React.ReactNode;\n}\n\n/**\n * Provides the global message stream to the component tree.\n * @public\n */\nexport function MessagesProvider(props: IMessagesProviderProps): React.ReactElement {\n const { maxMessages = 200, children } = props;\n const [messages, setMessages] = useState<IMessage[]>([]);\n const [dismissedIds, setDismissedIds] = useState<Set<string>>(new Set());\n\n const addMessage = useCallback(\n (severity: MessageSeverity, text: string, action?: IMessageAction): IMessage => {\n const msg = createMessage(severity, text, action);\n setMessages((prev) => {\n const next = [...prev, msg];\n return next.length > maxMessages ? next.slice(next.length - maxMessages) : next;\n });\n return msg;\n },\n [maxMessages]\n );\n\n const dismissMessage = useCallback((id: string): void => {\n setDismissedIds((prev) => new Set(prev).add(id));\n }, []);\n\n const clearMessages = useCallback((): void => {\n setMessages([]);\n setDismissedIds(new Set());\n }, []);\n\n const activeToasts = useMemo(\n () => messages.filter((m) => !dismissedIds.has(m.id)),\n [messages, dismissedIds]\n );\n\n const value = useMemo<IMessagesContextValue>(\n () => ({\n messages,\n addMessage,\n dismissMessage,\n clearMessages,\n activeToasts\n }),\n [messages, addMessage, dismissMessage, clearMessages, activeToasts]\n );\n\n return <MessagesContext.Provider value={value}>{children}</MessagesContext.Provider>;\n}\n\n// ============================================================================\n// Hook\n// ============================================================================\n\n/**\n * Hook to access the messages context.\n * Must be used within a MessagesProvider.\n * @public\n */\nexport function useMessages(): IMessagesContextValue {\n const ctx = useContext(MessagesContext);\n if (ctx === undefined) {\n throw new Error('useMessages must be used within a MessagesProvider');\n }\n return ctx;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"MessagesContext.js","sourceRoot":"","sources":["../../../src/packlets/messages/MessagesContext.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, useContext, useEffect, useMemo, useRef, useState } from 'react';\n\nimport { type MessageLogLevel } from '@fgv/ts-utils';\n\nimport { createMessage, ICreateMessageOptions, IMessage } from './model';\n\n// ============================================================================\n// Context Value\n// ============================================================================\n\n/**\n * Value provided by the MessagesContext.\n * @public\n */\nexport interface IMessagesContextValue {\n /** All messages in the stream (newest last) */\n readonly messages: ReadonlyArray<IMessage>;\n /**\n * Add a message to the stream. The `level` drives filtering; pass `options.severity`\n * only to override display styling (e.g. `'success'`).\n */\n readonly addMessage: (level: MessageLogLevel, text: string, options?: ICreateMessageOptions) => IMessage;\n /** Dismiss a specific message (removes from toast display, keeps in log) */\n readonly dismissMessage: (id: string) => void;\n /** Clear all messages */\n readonly clearMessages: () => void;\n /** Messages currently visible as toasts (not yet dismissed) */\n readonly activeToasts: ReadonlyArray<IMessage>;\n}\n\n// ============================================================================\n// Context\n// ============================================================================\n\nconst MessagesContext: React.Context<IMessagesContextValue | undefined> = React.createContext<\n IMessagesContextValue | undefined\n>(undefined);\n\n// ============================================================================\n// Provider\n// ============================================================================\n\n/**\n * Props for the MessagesProvider.\n * @public\n */\nexport interface IMessagesProviderProps {\n /** Maximum number of messages to retain in the log */\n readonly maxMessages?: number;\n /** Children */\n readonly children: React.ReactNode;\n}\n\n/**\n * Sentinel Tailwind class used by the probe in {@link MessagesProvider}.\n *\n * This is an arbitrary-value height utility (`h-[7.3215px]`) chosen to be\n * vanishingly unlikely to appear in any consumer's source code: a common\n * utility like `h-3.5` could be generated by Tailwind scanning the consumer's\n * own files, which would mask the missing `@fgv/ts-app-shell` content path.\n * Tailwind JIT regex-scans the package's built JS, finds this literal string,\n * and generates the rule — only when the consumer's `content` array includes\n * the `@fgv/ts-app-shell/lib` glob (see the README \"Setup\" section).\n * @internal\n */\nexport const TAILWIND_PROBE_CLASS: string = 'h-[7.3215px]';\n\n/**\n * Expected probe height in pixels (matches {@link TAILWIND_PROBE_CLASS}).\n * @internal\n */\nexport const TAILWIND_PROBE_HEIGHT_PX: number = 7.3215;\n\n/**\n * URL of the setup-instructions anchor surfaced by the Tailwind-content-path\n * diagnostic message when the probe detects that ts-app-shell's geometry utilities\n * are not being generated.\n * @internal\n */\nexport const TAILWIND_SETUP_DOCS_URL: string =\n 'https://github.com/ErikFortune/fgv/tree/release/libraries/ts-app-shell#setup';\n\n/**\n * Text of the diagnostic message emitted by the Tailwind-content-path probe.\n * @internal\n */\nexport const TAILWIND_PROBE_MESSAGE_TEXT: string =\n '@fgv/ts-app-shell styles are not fully loaded. Some icons may not render correctly. ' +\n 'See setup docs for the required Tailwind content-path entry.';\n\n/**\n * Provides the global message stream to the component tree.\n *\n * On mount, runs a one-shot probe that detects whether the consuming app's Tailwind\n * configuration is missing the `@fgv/ts-app-shell` content path. The probe applies a\n * sentinel arbitrary-value class ({@link TAILWIND_PROBE_CLASS}) that is vanishingly\n * unlikely to appear in any consumer's source — Tailwind will only generate the rule\n * when it scans the package's built JS via the content path. If the probe finds the\n * element unsized, a `'warning'` message is appended to the stream with a link to the\n * setup documentation. The probe runs exactly once per provider lifecycle (idempotent\n * under React.StrictMode double-render and HMR) and uses inline styles so it is not\n * affected by the failure mode it detects.\n * @public\n */\nexport function MessagesProvider(props: IMessagesProviderProps): React.ReactElement {\n const { maxMessages = 200, children } = props;\n const [messages, setMessages] = useState<IMessage[]>([]);\n const [dismissedIds, setDismissedIds] = useState<Set<string>>(new Set());\n const probeHasRun: React.MutableRefObject<boolean> = useRef<boolean>(false);\n\n const addMessage = useCallback(\n (level: MessageLogLevel, text: string, options?: ICreateMessageOptions): IMessage => {\n const msg = createMessage(level, text, options);\n setMessages((prev) => {\n const next = [...prev, msg];\n return next.length > maxMessages ? next.slice(next.length - maxMessages) : next;\n });\n return msg;\n },\n [maxMessages]\n );\n\n const dismissMessage = useCallback((id: string): void => {\n setDismissedIds((prev) => new Set(prev).add(id));\n }, []);\n\n const clearMessages = useCallback((): void => {\n setMessages([]);\n setDismissedIds(new Set());\n }, []);\n\n const activeToasts = useMemo(\n () => messages.filter((m) => !dismissedIds.has(m.id)),\n [messages, dismissedIds]\n );\n\n useEffect(() => {\n if (probeHasRun.current) {\n return;\n }\n\n const probe: HTMLDivElement = document.createElement('div');\n probe.className = TAILWIND_PROBE_CLASS;\n probe.setAttribute('aria-hidden', 'true');\n probe.style.position = 'absolute';\n probe.style.visibility = 'hidden';\n probe.style.pointerEvents = 'none';\n document.body.appendChild(probe);\n\n const frameId: number = requestAnimationFrame(() => {\n // Mark the guard only once the measurement has actually run; otherwise a\n // StrictMode cleanup-before-rAF would leave `probeHasRun = true` and the\n // second setup would short-circuit, suppressing the diagnostic entirely.\n probeHasRun.current = true;\n const height: number = probe.getBoundingClientRect().height;\n probe.remove();\n if (height === 0) {\n addMessage('warning', TAILWIND_PROBE_MESSAGE_TEXT, {\n action: {\n label: 'Open setup docs',\n href: TAILWIND_SETUP_DOCS_URL\n }\n });\n }\n });\n\n return (): void => {\n cancelAnimationFrame(frameId);\n if (probe.parentNode !== null) {\n probe.remove();\n }\n };\n }, [addMessage]);\n\n const value = useMemo<IMessagesContextValue>(\n () => ({\n messages,\n addMessage,\n dismissMessage,\n clearMessages,\n activeToasts\n }),\n [messages, addMessage, dismissMessage, clearMessages, activeToasts]\n );\n\n return <MessagesContext.Provider value={value}>{children}</MessagesContext.Provider>;\n}\n\n// ============================================================================\n// Hook\n// ============================================================================\n\n/**\n * Hook to access the messages context.\n * Must be used within a MessagesProvider.\n * @public\n */\nexport function useMessages(): IMessagesContextValue {\n const ctx = useContext(MessagesContext);\n if (ctx === undefined) {\n throw new Error('useMessages must be used within a MessagesProvider');\n }\n return ctx;\n}\n"]}
|
|
@@ -29,28 +29,6 @@
|
|
|
29
29
|
*/
|
|
30
30
|
import { Logging, succeed } from '@fgv/ts-utils';
|
|
31
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
32
|
// MessagesLogger
|
|
55
33
|
// ============================================================================
|
|
56
34
|
/**
|
|
@@ -85,19 +63,20 @@ export class MessagesLogger extends Logging.LoggerBase {
|
|
|
85
63
|
this._defaultAction = defaultAction;
|
|
86
64
|
}
|
|
87
65
|
/**
|
|
88
|
-
* Routes a formatted log message into the MessagesContext
|
|
66
|
+
* Routes a formatted log message into the MessagesContext, setting the message `level`
|
|
67
|
+
* straight through from the log call (lossless). Display styling `severity` is left
|
|
68
|
+
* undefined so it is derived from the level for display.
|
|
69
|
+
*
|
|
70
|
+
* Suppression by threshold is handled upstream by {@link @fgv/ts-utils#Logging.LoggerBase | LoggerBase}
|
|
71
|
+
* (`shouldLog` against `logLevel`) before this method is invoked.
|
|
89
72
|
* @param message - The formatted message string
|
|
90
|
-
* @param level - The log level
|
|
91
|
-
* @returns Success with the message
|
|
73
|
+
* @param level - The {@link @fgv/ts-utils#MessageLogLevel | log level}
|
|
74
|
+
* @returns Success with the message that was logged
|
|
92
75
|
* @internal
|
|
93
76
|
*/
|
|
94
77
|
_log(message, level) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
this._addMessage(severity, message, this._defaultAction);
|
|
98
|
-
return succeed(message);
|
|
99
|
-
}
|
|
100
|
-
return succeed(undefined);
|
|
78
|
+
this._addMessage(level, message, { action: this._defaultAction });
|
|
79
|
+
return succeed(message);
|
|
101
80
|
}
|
|
102
81
|
}
|
|
103
82
|
//# sourceMappingURL=MessagesLogger.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MessagesLogger.js","sourceRoot":"","sources":["../../../src/packlets/messages/MessagesLogger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;;;;;;;GAOG;AAEH,OAAO,EAAE,OAAO,EAAiC,OAAO,EAAE,MAAM,eAAe,CAAC;AAKhF,+EAA+E;AAC/E,
|
|
1
|
+
{"version":3,"file":"MessagesLogger.js","sourceRoot":"","sources":["../../../src/packlets/messages/MessagesLogger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;;;;;;;GAOG;AAEH,OAAO,EAAE,OAAO,EAAiC,OAAO,EAAE,MAAM,eAAe,CAAC;AAKhF,+EAA+E;AAC/E,iBAAiB;AACjB,+EAA+E;AAE/E;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,OAAO,cAAe,SAAQ,OAAO,CAAC,UAAU;IAIpD;;;;;OAKG;IACH,YACE,UAA+C,EAC/C,QAAmC,EACnC,aAA8B;QAE9B,KAAK,CAAC,QAAQ,CAAC,CAAC;QAChB,IAAI,CAAC,WAAW,GAAG,UAAU,CAAC;QAC9B,IAAI,CAAC,cAAc,GAAG,aAAa,CAAC;IACtC,CAAC;IAED;;;;;;;;;;;OAWG;IACO,IAAI,CAAC,OAAe,EAAE,KAAsB;QACpD,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,cAAc,EAAE,CAAC,CAAC;QAClE,OAAO,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1B,CAAC;CACF","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\n/**\n * Bridge between the MessagesContext and the ts-utils ILogger/LogReporter system.\n *\n * Provides an ILogger implementation that routes log messages into the\n * MessagesContext, enabling Result.report() integration in React components.\n *\n * @packageDocumentation\n */\n\nimport { Logging, type MessageLogLevel, Success, succeed } from '@fgv/ts-utils';\n\nimport { type IMessageAction } from './model';\nimport { type IMessagesContextValue } from './MessagesContext';\n\n// ============================================================================\n// MessagesLogger\n// ============================================================================\n\n/**\n * An {@link @fgv/ts-utils#Logging.ILogger | ILogger} implementation that routes\n * messages into a {@link IMessagesContextValue | MessagesContext}.\n *\n * Use this as the `logger` parameter when creating a `LogReporter` to get\n * full `Result.report()` integration that feeds into the app's toast/log system.\n *\n * @example\n * ```typescript\n * const { addMessage } = useMessages();\n * const logger = new MessagesLogger(addMessage);\n * const reporter = new LogReporter<unknown>({ logger });\n *\n * // Now Result.report() flows into toasts + log panel\n * someResult.report(reporter, { success: 'info', failure: 'error' });\n * ```\n *\n * @public\n */\nexport class MessagesLogger extends Logging.LoggerBase {\n private readonly _addMessage: IMessagesContextValue['addMessage'];\n private readonly _defaultAction: IMessageAction | undefined;\n\n /**\n * Creates a new MessagesLogger.\n * @param addMessage - The addMessage function from MessagesContext\n * @param logLevel - The minimum log level to display (default: 'info')\n * @param defaultAction - Optional default action to attach to all messages\n */\n public constructor(\n addMessage: IMessagesContextValue['addMessage'],\n logLevel?: Logging.ReporterLogLevel,\n defaultAction?: IMessageAction\n ) {\n super(logLevel);\n this._addMessage = addMessage;\n this._defaultAction = defaultAction;\n }\n\n /**\n * Routes a formatted log message into the MessagesContext, setting the message `level`\n * straight through from the log call (lossless). Display styling `severity` is left\n * undefined so it is derived from the level for display.\n *\n * Suppression by threshold is handled upstream by {@link @fgv/ts-utils#Logging.LoggerBase | LoggerBase}\n * (`shouldLog` against `logLevel`) before this method is invoked.\n * @param message - The formatted message string\n * @param level - The {@link @fgv/ts-utils#MessageLogLevel | log level}\n * @returns Success with the message that was logged\n * @internal\n */\n protected _log(message: string, level: MessageLogLevel): Success<string | undefined> {\n this._addMessage(level, message, { action: this._defaultAction });\n return succeed(message);\n }\n}\n"]}
|
|
@@ -21,8 +21,9 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import React, { useCallback, useMemo, useState } from 'react';
|
|
23
23
|
import { Logging } from '@fgv/ts-utils';
|
|
24
|
-
import { FunnelIcon, DocumentDuplicateIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline';
|
|
24
|
+
import { FunnelIcon, DocumentDuplicateIcon, MagnifyingGlassIcon, XMarkIcon } from '@heroicons/react/24/outline';
|
|
25
25
|
import { CheckIcon } from '@heroicons/react/24/solid';
|
|
26
|
+
import { deriveSeverityFromLevel } from './model';
|
|
26
27
|
import { useResponsive } from '../responsive';
|
|
27
28
|
// ============================================================================
|
|
28
29
|
// Severity display config
|
|
@@ -49,17 +50,25 @@ const LOG_ROW_COLORS = {
|
|
|
49
50
|
// Level filter config
|
|
50
51
|
// ============================================================================
|
|
51
52
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
53
|
+
* Effective display severity for a message: its explicit `severity` styling override,
|
|
54
|
+
* or the severity derived from its canonical log level.
|
|
54
55
|
*/
|
|
55
|
-
function
|
|
56
|
-
|
|
56
|
+
function effectiveSeverity(message) {
|
|
57
|
+
var _a;
|
|
58
|
+
return (_a = message.severity) !== null && _a !== void 0 ? _a : deriveSeverityFromLevel(message.level);
|
|
57
59
|
}
|
|
58
60
|
/**
|
|
59
61
|
* Filter choices exposed in the UI, mapping labels to ReporterLogLevel values.
|
|
62
|
+
*
|
|
63
|
+
* The full log-level granularity is exposed (the core gap fix): `Detail+` lets the panel
|
|
64
|
+
* filter at the same granularity a ts-utils logger records, including `detail` messages
|
|
65
|
+
* that the previous severity-based filter could not represent. `All` additionally surfaces
|
|
66
|
+
* `quiet`-level messages (per {@link @fgv/ts-utils#Logging.shouldLog | shouldLog} semantics,
|
|
67
|
+
* `quiet` is only visible under the `all` threshold).
|
|
60
68
|
*/
|
|
61
69
|
const FILTER_LEVELS = [
|
|
62
70
|
{ label: 'All', level: 'all' },
|
|
71
|
+
{ label: 'Detail+', level: 'detail' },
|
|
63
72
|
{ label: 'Info+', level: 'info' },
|
|
64
73
|
{ label: 'Warn+', level: 'warning' },
|
|
65
74
|
{ label: 'Error', level: 'error' }
|
|
@@ -68,14 +77,15 @@ const FILTER_LEVELS = [
|
|
|
68
77
|
* Collapsible status bar / log panel at the bottom of the application.
|
|
69
78
|
*
|
|
70
79
|
* Collapsed: shows severity counts.
|
|
71
|
-
* Expanded: filterable, searchable, copyable log of all messages
|
|
80
|
+
* Expanded: filterable, searchable, copyable log of all messages, bounded to
|
|
81
|
+
* {@link IStatusBarProps.maxExpandedHeight} with the list scrolling within the bound.
|
|
72
82
|
* @public
|
|
73
83
|
*/
|
|
74
84
|
export function StatusBar(props) {
|
|
75
|
-
const { messages, onClear, initialFilterLevel } = props;
|
|
85
|
+
const { messages, onClear, initialFilterLevel, maxExpandedHeight = '40vh', defaultExpanded = false } = props;
|
|
76
86
|
const { layoutMode } = useResponsive();
|
|
77
87
|
const isMobile = layoutMode === 'mobile';
|
|
78
|
-
const [expanded, setExpanded] = useState(
|
|
88
|
+
const [expanded, setExpanded] = useState(defaultExpanded);
|
|
79
89
|
const [showFilters, setShowFilters] = useState(false);
|
|
80
90
|
const [filterLevel, setFilterLevel] = useState(initialFilterLevel !== null && initialFilterLevel !== void 0 ? initialFilterLevel : 'all');
|
|
81
91
|
const [searchTerm, setSearchTerm] = useState('');
|
|
@@ -83,13 +93,13 @@ export function StatusBar(props) {
|
|
|
83
93
|
const counts = useMemo(() => {
|
|
84
94
|
const result = { info: 0, success: 0, warning: 0, error: 0 };
|
|
85
95
|
for (const msg of messages) {
|
|
86
|
-
result[msg
|
|
96
|
+
result[effectiveSeverity(msg)]++;
|
|
87
97
|
}
|
|
88
98
|
return result;
|
|
89
99
|
}, [messages]);
|
|
90
100
|
const filteredMessages = useMemo(() => {
|
|
91
101
|
return messages.filter((msg) => {
|
|
92
|
-
const passesLevel = Logging.shouldLog(
|
|
102
|
+
const passesLevel = Logging.shouldLog(msg.level, filterLevel);
|
|
93
103
|
const passesSearch = searchTerm === '' || msg.text.toLowerCase().includes(searchTerm.toLowerCase());
|
|
94
104
|
return passesLevel && passesSearch;
|
|
95
105
|
});
|
|
@@ -109,7 +119,7 @@ export function StatusBar(props) {
|
|
|
109
119
|
}, []);
|
|
110
120
|
const copyAllFiltered = useCallback(() => {
|
|
111
121
|
const text = filteredMessages
|
|
112
|
-
.map((msg) => `[${msg.
|
|
122
|
+
.map((msg) => `[${msg.level.toUpperCase()}] ${formatTime(msg.timestamp)} - ${msg.text}`)
|
|
113
123
|
.join('\n');
|
|
114
124
|
copyToClipboard(text, '__all__');
|
|
115
125
|
}, [filteredMessages, formatTime, copyToClipboard]);
|
|
@@ -122,33 +132,48 @@ export function StatusBar(props) {
|
|
|
122
132
|
messages.length === 0 && React.createElement("span", { className: "text-muted" }, "No messages")),
|
|
123
133
|
React.createElement("span", { className: "text-muted" }, expanded ? '\u25BC' : '\u25B2')),
|
|
124
134
|
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' },
|
|
135
|
+
isMobile && (React.createElement("div", { className: "fixed inset-0 z-40 bg-backdrop", onClick: () => setExpanded(false), "aria-hidden": "true" })),
|
|
136
|
+
React.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 } },
|
|
129
137
|
React.createElement("div", { className: "flex items-center justify-between px-4 py-1 bg-surface-alt border-b border-border-subtle shrink-0" },
|
|
130
138
|
React.createElement("span", { className: "text-xs font-medium text-muted" }, isFiltered
|
|
131
139
|
? `${filteredMessages.length} of ${messages.length} messages`
|
|
132
140
|
: `${messages.length} message${messages.length !== 1 ? 's' : ''}`),
|
|
133
141
|
React.createElement("div", { className: "flex items-center gap-1" },
|
|
134
142
|
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")
|
|
143
|
+
React.createElement(FunnelIcon, { className: "h-3.5 w-3.5 text-muted", style: { width: 14, height: 14 } })),
|
|
144
|
+
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", style: { width: 14, height: 14 } })) : (React.createElement(DocumentDuplicateIcon, { className: "h-3.5 w-3.5 text-muted", style: { width: 14, height: 14 } }))),
|
|
145
|
+
React.createElement("button", { onClick: onClear, className: "text-xs text-muted hover:text-secondary ml-1" }, "Clear"),
|
|
146
|
+
React.createElement("button", { onClick: () => setExpanded(false), className: "p-1 rounded hover:bg-surface-raised ml-1", title: "Collapse log panel", "aria-label": "Collapse log panel" },
|
|
147
|
+
React.createElement(XMarkIcon, { className: "h-3.5 w-3.5 text-muted", style: { width: 14, height: 14 } })))),
|
|
138
148
|
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
149
|
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
150
|
? 'bg-status-info-bg border-status-info-border text-status-info-text'
|
|
141
151
|
: '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"
|
|
152
|
+
React.createElement("div", { className: "relative flex-1", style: { position: 'relative' } },
|
|
153
|
+
React.createElement(MagnifyingGlassIcon, { className: "absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted", style: {
|
|
154
|
+
position: 'absolute',
|
|
155
|
+
left: 8,
|
|
156
|
+
top: '50%',
|
|
157
|
+
transform: 'translateY(-50%)',
|
|
158
|
+
width: 14,
|
|
159
|
+
height: 14
|
|
160
|
+
} }),
|
|
144
161
|
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",
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
162
|
+
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", style: {
|
|
163
|
+
position: 'absolute',
|
|
164
|
+
right: 8,
|
|
165
|
+
top: '50%',
|
|
166
|
+
transform: 'translateY(-50%)'
|
|
167
|
+
}, "aria-label": "Clear search" }, "\u00D7"))))),
|
|
168
|
+
React.createElement("div", { className: "flex-1 min-h-0 overflow-y-auto" }, 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) => {
|
|
169
|
+
const severity = effectiveSeverity(msg);
|
|
170
|
+
return (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[severity]}` },
|
|
171
|
+
React.createElement("span", { className: `shrink-0 ${SEVERITY_COLORS[severity]}` }, SEVERITY_ICONS[severity]),
|
|
172
|
+
React.createElement("span", { className: "flex-1 text-secondary" }, msg.text),
|
|
173
|
+
React.createElement("button", { onClick: () => copyToClipboard(msg.text, msg.id), className: `shrink-0 p-0.5 rounded transition-colors ${copySuccessId === msg.id
|
|
174
|
+
? 'opacity-100 bg-status-success-surface'
|
|
175
|
+
: '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", style: { width: 12, height: 12 } })) : (React.createElement(DocumentDuplicateIcon, { className: "h-3 w-3 text-muted", style: { width: 12, height: 12 } }))),
|
|
176
|
+
React.createElement("span", { className: "shrink-0 text-muted" }, formatTime(msg.timestamp))));
|
|
177
|
+
}))))))));
|
|
153
178
|
}
|
|
154
179
|
//# 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 ×\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 ×\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"]}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* SOFTWARE.
|
|
21
21
|
*/
|
|
22
22
|
import React, { useEffect } from 'react';
|
|
23
|
-
import { DEFAULT_TOAST_CONFIG } from './model';
|
|
23
|
+
import { DEFAULT_TOAST_CONFIG, deriveSeverityFromLevel } from './model';
|
|
24
24
|
import { useResponsive } from '../responsive';
|
|
25
25
|
// ============================================================================
|
|
26
26
|
// Severity Styles
|
|
@@ -36,8 +36,10 @@ const SEVERITY_STYLES = {
|
|
|
36
36
|
* @public
|
|
37
37
|
*/
|
|
38
38
|
export function ToastItem(props) {
|
|
39
|
+
var _a;
|
|
39
40
|
const { message, onDismiss } = props;
|
|
40
|
-
const
|
|
41
|
+
const severity = (_a = message.severity) !== null && _a !== void 0 ? _a : deriveSeverityFromLevel(message.level);
|
|
42
|
+
const config = DEFAULT_TOAST_CONFIG[severity];
|
|
41
43
|
const { layoutMode } = useResponsive();
|
|
42
44
|
useEffect(() => {
|
|
43
45
|
if (config.autoDismissMs > 0) {
|
|
@@ -46,10 +48,11 @@ export function ToastItem(props) {
|
|
|
46
48
|
}
|
|
47
49
|
return undefined;
|
|
48
50
|
}, [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[
|
|
51
|
+
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[severity]}`, role: "alert" },
|
|
50
52
|
React.createElement("div", { className: "flex-1 text-sm line-clamp-4", title: message.text }, message.text),
|
|
51
53
|
React.createElement("div", { className: "flex items-center gap-2 shrink-0" },
|
|
52
|
-
message.action &&
|
|
54
|
+
message.action &&
|
|
55
|
+
(message.action.href !== undefined ? (React.createElement("a", { href: message.action.href, target: "_blank", rel: "noopener noreferrer", className: "text-sm font-medium underline hover:no-underline" }, message.action.label)) : (React.createElement("button", { onClick: message.action.onAction, className: "text-sm font-medium underline hover:no-underline" }, message.action.label))),
|
|
53
56
|
React.createElement("button", { onClick: () => onDismiss(message.id), className: "text-current opacity-50 hover:opacity-100", "aria-label": "Dismiss" }, "\u00D7"))));
|
|
54
57
|
}
|
|
55
58
|
/**
|