@mythicalos/preact-ui 0.2.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -1
- package/dist/FileExplorer.d.ts +75 -0
- package/dist/FileExplorer.js +59 -0
- package/dist/GitChip.d.ts +18 -0
- package/dist/GitChip.js +25 -0
- package/dist/Input.d.ts +53 -0
- package/dist/Input.js +112 -6
- package/dist/Popover.d.ts +50 -0
- package/dist/Popover.js +167 -0
- package/dist/QueuePanel.d.ts +38 -0
- package/dist/QueuePanel.js +76 -0
- package/dist/SaveBar.d.ts +20 -0
- package/dist/SaveBar.js +11 -0
- package/dist/SendBar.d.ts +26 -0
- package/dist/SendBar.js +62 -0
- package/dist/SessionCard.d.ts +42 -0
- package/dist/SessionCard.js +43 -0
- package/dist/StatTiles.d.ts +10 -0
- package/dist/StatTiles.js +22 -0
- package/dist/Terminal.d.ts +31 -0
- package/dist/Terminal.js +90 -0
- package/dist/index.d.ts +11 -1
- package/dist/index.js +15 -1
- package/package.json +2 -2
package/dist/Popover.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "preact/jsx-runtime";
|
|
2
|
+
/** @jsxImportSource preact */
|
|
3
|
+
// @mythicalos/preact-ui — the dropdown popover (ds/components-popover, registry row `popover` v1).
|
|
4
|
+
// A chip trigger over an anchored single-select menu: "review lane: **codex cross-model** ▾", ✓ on
|
|
5
|
+
// the current row, `--my-shadow-modal`, high z-index.
|
|
6
|
+
//
|
|
7
|
+
// This file is WIRING ONLY. Every decision — class strings, the viewport-aware flip/align
|
|
8
|
+
// geometry, which key does what, roving-focus index arithmetic that skips disabled rows, the
|
|
9
|
+
// trigger's text composition, and the ARIA maps — is imported from `@mythicalos/ui-core`, so this
|
|
10
|
+
// binding and its React sibling cannot drift. The three things that genuinely cannot live in the
|
|
11
|
+
// core are all this component adds: measuring the DOM, moving focus, and rendering.
|
|
12
|
+
//
|
|
13
|
+
// Behavioural requirements were taken from the design card plus the hand-rolled selector the
|
|
14
|
+
// product ships today (rail/ProjectSelector.tsx): outside-pointerdown and Escape close; Escape and
|
|
15
|
+
// selecting BOTH return focus to the trigger (never stranding it on <body>); focus moves INTO the
|
|
16
|
+
// menu on open, landing on the selected row; Arrow/Home/End rove within it; and an optional
|
|
17
|
+
// head (title + caption) and footer action sit around the rows.
|
|
18
|
+
//
|
|
19
|
+
// Documented residual: the flip/align decision is re-measured on open, on window resize, and on
|
|
20
|
+
// any scroll — but not on a mutation that changes the panel's own size while it is open (no
|
|
21
|
+
// ResizeObserver). `<mythical-select>` measures on open only; this is a superset of the
|
|
22
|
+
// established approach, not a regression against it.
|
|
23
|
+
//
|
|
24
|
+
// Consumer contract (design card, verbatim): "Ancestors must never clip it." The panel is
|
|
25
|
+
// absolutely positioned inside `.my-pop-anchor`, so an ancestor with `overflow: hidden`/`auto`
|
|
26
|
+
// will crop it.
|
|
27
|
+
import { useEffect, useId, useLayoutEffect, useRef, useState } from "preact/hooks";
|
|
28
|
+
import { POPOVER_CARET, POPOVER_CHECK, POPOVER_CLASS, POPOVER_DECORATIVE_ARIA, POPOVER_DEFAULT_POSITION, POPOVER_SEPARATOR_ARIA, edgePopoverIndex, initialPopoverIndex, popoverHasSlotContent, popoverIds, popoverItemAria, popoverItemClass, popoverAppliesToFocus, popoverKeyHandled, popoverMenuAria, popoverPanelAria, popoverPanelClass, popoverPanelKeyAction, popoverTriggerAria, popoverTriggerClass, popoverTriggerKeyAction, popoverTriggerText, resolvePopoverIndex, resolvePopoverPosition, samePopoverPosition, } from "@mythicalos/ui-core/logic";
|
|
29
|
+
export { POPOVER_CARET, POPOVER_CHECK, POPOVER_CLASS, POPOVER_EMPTY_VALUE, popoverItemClass, popoverPanelClass, popoverTriggerClass, popoverTriggerText, resolvePopoverPosition, } from "@mythicalos/ui-core/logic";
|
|
30
|
+
/**
|
|
31
|
+
* The OPEN panel, deliberately hook-free (same testability split as the shell's `SwitcherPanel`):
|
|
32
|
+
* `preact-render-to-string` never runs effects or dispatches events, so the panel markup — and a
|
|
33
|
+
* row's REAL onClick closure, reachable off the returned vnode tree — would otherwise be
|
|
34
|
+
* unreachable from a DOM-free test. Exported from this module for tests; NOT part of the package
|
|
35
|
+
* barrel. `panelRef` rather than `ref` because plain Preact does not forward `ref` to a function
|
|
36
|
+
* component, and the React sibling keeps the same prop name so the two stay symmetric.
|
|
37
|
+
*/
|
|
38
|
+
export function PopoverPanel(props) {
|
|
39
|
+
return (_jsxs("div", { ref: props.panelRef, class: popoverPanelClass(props.pos), tabIndex: -1, ...popoverPanelAria(props.ids), children: [popoverHasSlotContent(props.title) || popoverHasSlotContent(props.caption) ? (_jsxs("div", { class: POPOVER_CLASS.head, children: [popoverHasSlotContent(props.title) ? (_jsx("span", { id: props.ids.title, class: POPOVER_CLASS.title, children: props.title })) : null, popoverHasSlotContent(props.caption) ? _jsx("span", { class: POPOVER_CLASS.caption, children: props.caption }) : null] })) : null, _jsx("div", { class: POPOVER_CLASS.menu, ...popoverMenuAria(props.ids, popoverHasSlotContent(props.title)), children: props.items.map((item) => (_jsxs("button", { type: "button", "data-pop-item": "", class: popoverItemClass(item), disabled: item.disabled, ...popoverItemAria(item), onClick: () => {
|
|
40
|
+
if (item.disabled)
|
|
41
|
+
return; // belt-and-braces: a native disabled button fires no click
|
|
42
|
+
props.onPick(item);
|
|
43
|
+
}, children: [_jsx("span", { class: POPOVER_CLASS.itemLabel, children: item.label }), _jsx("span", { class: POPOVER_CLASS.itemCheck, ...POPOVER_DECORATIVE_ARIA, children: item.selected ? POPOVER_CHECK : "" })] }, item.key))) }), popoverHasSlotContent(props.footer) ? (_jsxs(_Fragment, { children: [_jsx("div", { class: POPOVER_CLASS.divider, ...POPOVER_SEPARATOR_ARIA }), _jsx("div", { class: POPOVER_CLASS.foot, children: props.footer })] })) : null] }));
|
|
44
|
+
}
|
|
45
|
+
/** A single-select dropdown popover anchored to its own chip trigger. */
|
|
46
|
+
export function Popover(props) {
|
|
47
|
+
const { items, disabled = false } = props;
|
|
48
|
+
const generatedId = useId();
|
|
49
|
+
const ids = popoverIds(props.id ?? generatedId);
|
|
50
|
+
const [open, setOpen] = useState(false);
|
|
51
|
+
const [pos, setPos] = useState(POPOVER_DEFAULT_POSITION);
|
|
52
|
+
const anchorRef = useRef(null);
|
|
53
|
+
const triggerRef = useRef(null);
|
|
54
|
+
const panelRef = useRef(null);
|
|
55
|
+
/** Row to focus on the next open (-1 ⇒ let `initialPopoverIndex` decide). */
|
|
56
|
+
const pendingFocus = useRef(-1);
|
|
57
|
+
const itemNodes = () => Array.from(panelRef.current?.querySelectorAll("[data-pop-item]") ?? []);
|
|
58
|
+
const focusRow = (i) => {
|
|
59
|
+
// preventScroll: a bare focus() asks the browser to reveal the row by scrolling every
|
|
60
|
+
// scrollable ancestor — which yanks the page out from under an open popover.
|
|
61
|
+
itemNodes()[i]?.focus({ preventScroll: true });
|
|
62
|
+
};
|
|
63
|
+
const setOpenState = (next) => {
|
|
64
|
+
setOpen(next);
|
|
65
|
+
props.onOpenChange?.(next);
|
|
66
|
+
};
|
|
67
|
+
const openAt = (index) => {
|
|
68
|
+
if (disabled)
|
|
69
|
+
return;
|
|
70
|
+
pendingFocus.current = index;
|
|
71
|
+
setPos(POPOVER_DEFAULT_POSITION); // re-measured below; never reuse a stale flip
|
|
72
|
+
setOpenState(true);
|
|
73
|
+
};
|
|
74
|
+
const closeToTrigger = () => {
|
|
75
|
+
setOpenState(false);
|
|
76
|
+
triggerRef.current?.focus({ preventScroll: true });
|
|
77
|
+
};
|
|
78
|
+
// Placement: measure once the panel is in the DOM but before paint, so it never flashes at the
|
|
79
|
+
// wrong end of the trigger. Guarded by samePopoverPosition so measure → setState can't loop.
|
|
80
|
+
useLayoutEffect(() => {
|
|
81
|
+
if (!open)
|
|
82
|
+
return;
|
|
83
|
+
const measure = () => {
|
|
84
|
+
const trigger = triggerRef.current;
|
|
85
|
+
const panel = panelRef.current;
|
|
86
|
+
if (!trigger || !panel)
|
|
87
|
+
return;
|
|
88
|
+
const next = resolvePopoverPosition(trigger.getBoundingClientRect(), { width: panel.offsetWidth, height: panel.offsetHeight }, { width: document.documentElement.clientWidth, height: document.documentElement.clientHeight });
|
|
89
|
+
setPos((prev) => (samePopoverPosition(prev, next) ? prev : next));
|
|
90
|
+
};
|
|
91
|
+
measure();
|
|
92
|
+
window.addEventListener("resize", measure);
|
|
93
|
+
// capture: scroll does not bubble, so this is the only way to see a scrollable ANCESTOR move
|
|
94
|
+
// the anchor and invalidate the flip decision.
|
|
95
|
+
document.addEventListener("scroll", measure, { capture: true, passive: true });
|
|
96
|
+
return () => {
|
|
97
|
+
window.removeEventListener("resize", measure);
|
|
98
|
+
document.removeEventListener("scroll", measure, { capture: true });
|
|
99
|
+
};
|
|
100
|
+
}, [open, items.length]);
|
|
101
|
+
// a11y: on open, focus moves INTO the menu the role promises — the selected row, else the first
|
|
102
|
+
// enabled one, else the panel itself (an empty or all-disabled list), never left on the trigger.
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (!open)
|
|
105
|
+
return;
|
|
106
|
+
const requested = pendingFocus.current;
|
|
107
|
+
pendingFocus.current = -1;
|
|
108
|
+
const index = requested >= 0 ? requested : initialPopoverIndex(items);
|
|
109
|
+
if (index >= 0)
|
|
110
|
+
focusRow(index);
|
|
111
|
+
else
|
|
112
|
+
panelRef.current?.focus({ preventScroll: true });
|
|
113
|
+
}, [open]);
|
|
114
|
+
// Outside pointerdown closes. Focus is NOT pulled back to the trigger here: the pointer is
|
|
115
|
+
// already establishing focus somewhere else, and stealing it back would fight the user.
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (!open)
|
|
118
|
+
return;
|
|
119
|
+
const onDown = (e) => {
|
|
120
|
+
if (anchorRef.current && !anchorRef.current.contains(e.target))
|
|
121
|
+
setOpenState(false);
|
|
122
|
+
};
|
|
123
|
+
document.addEventListener("pointerdown", onDown, true);
|
|
124
|
+
return () => document.removeEventListener("pointerdown", onDown, true);
|
|
125
|
+
}, [open]);
|
|
126
|
+
// One handler on the anchor catches keys from the trigger AND the panel (both are inside it).
|
|
127
|
+
// The anchor is a plain <div>, but it is never itself focusable or a tab stop — every key this
|
|
128
|
+
// sees has bubbled up from a real <button>, so this is delegation, not a keyboard handler bolted
|
|
129
|
+
// onto a non-interactive element. Enter/Space are absent by design: the trigger is a real
|
|
130
|
+
// <button>, so the browser already synthesises a click from them.
|
|
131
|
+
const onKeyDown = (e) => {
|
|
132
|
+
if (disabled)
|
|
133
|
+
return;
|
|
134
|
+
if (!open) {
|
|
135
|
+
const action = popoverTriggerKeyAction(e.key);
|
|
136
|
+
if (action === null)
|
|
137
|
+
return;
|
|
138
|
+
e.preventDefault();
|
|
139
|
+
openAt(edgePopoverIndex(items, action === "open-first" ? 1 : -1));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const action = popoverPanelKeyAction(e.key);
|
|
143
|
+
// Roving keys apply only on a row (or the panel fallback); the caller's footer content keeps
|
|
144
|
+
// its own arrows/Home/End. Escape and Tab still work from anywhere inside the popover.
|
|
145
|
+
const focused = itemNodes().indexOf(document.activeElement);
|
|
146
|
+
const onRovingSurface = focused >= 0 || document.activeElement === panelRef.current;
|
|
147
|
+
if (!popoverAppliesToFocus(action, onRovingSurface))
|
|
148
|
+
return;
|
|
149
|
+
// Tab ("dismiss") must keep its native behaviour or the popover is a focus trap; moving focus
|
|
150
|
+
// to the trigger first makes the browser continue the tab sequence from AFTER the trigger,
|
|
151
|
+
// which is where tabbing out of a menu belongs.
|
|
152
|
+
if (popoverKeyHandled(action))
|
|
153
|
+
e.preventDefault();
|
|
154
|
+
if (action === "close" || action === "dismiss") {
|
|
155
|
+
closeToTrigger();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const next = resolvePopoverIndex(action, items, focused);
|
|
159
|
+
if (next >= 0)
|
|
160
|
+
focusRow(next);
|
|
161
|
+
};
|
|
162
|
+
const text = popoverTriggerText(items, { label: props.label, placeholder: props.placeholder });
|
|
163
|
+
return (_jsxs("div", { class: POPOVER_CLASS.anchor, ref: anchorRef, onKeyDown: onKeyDown, children: [_jsxs("button", { ref: triggerRef, type: "button", class: popoverTriggerClass({ open, disabled }), disabled: disabled, title: props.triggerTitle, ...popoverTriggerAria(open, ids), onClick: () => (open ? closeToTrigger() : openAt(-1)), children: [text.label !== null ? _jsx("span", { class: POPOVER_CLASS.triggerLabel, children: text.label }) : null, _jsx("span", { class: POPOVER_CLASS.triggerValue, children: text.value }), _jsx("span", { class: POPOVER_CLASS.triggerCaret, ...POPOVER_DECORATIVE_ARIA, children: POPOVER_CARET })] }), open ? (_jsx(PopoverPanel, { panelRef: panelRef, ids: ids, pos: pos, items: items, title: props.title, caption: props.caption, footer: props.footer, onPick: (item) => {
|
|
164
|
+
closeToTrigger(); // a11y — selecting returns focus to the trigger, never to <body>
|
|
165
|
+
props.onSelect(item.key, item);
|
|
166
|
+
} })) : null] }));
|
|
167
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** @jsxImportSource preact */
|
|
2
|
+
import { type QueueItem, type QueueSource, type QueueUnavailableReason } from "@mythicalos/ui-core/logic";
|
|
3
|
+
export { QUEUE_EMPTY_COPY, QUEUE_LOADING_COPY, QUEUE_STALE_COPY, QUEUE_UNAVAILABLE_COPY, canCancelRow, cancelReducer, queueBadgeClass, queueBadgeLabel, queueRowClass, queueView, shouldDisarmCancel, unavailableText, type CancelEvent, type CancelState, type QueueItem, type QueueItemStatus, type QueueSource, type QueueUnavailableReason, type QueueView, } from "@mythicalos/ui-core/logic";
|
|
4
|
+
export interface QueueRowProps {
|
|
5
|
+
item: QueueItem;
|
|
6
|
+
/** The row is showing its two-step confirm instead of its normal presentation. */
|
|
7
|
+
armed?: boolean;
|
|
8
|
+
/** Whether the cancel affordance may exist at all — combined with the status by ui-core. */
|
|
9
|
+
canCancel?: boolean;
|
|
10
|
+
onArm?(): void;
|
|
11
|
+
onConfirm?(): void;
|
|
12
|
+
onDisarm?(): void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* One queue record. The registry atom — usable standalone, or through `QueuePanel`.
|
|
16
|
+
*
|
|
17
|
+
* INVARIANT 4 is enforced HERE, not only by the panel: `armed` is honored only when the record is
|
|
18
|
+
* genuinely cancellable (`canCancelRow`). A caller that arms a `leased`/`delivered`/`canceled` row,
|
|
19
|
+
* or arms a row whose cancellability was revoked, gets the ordinary row back — never a live
|
|
20
|
+
* "Cancel it" button for something that cannot be cancelled. That also covers the render between a
|
|
21
|
+
* revocation and the panel's disarming effect.
|
|
22
|
+
*/
|
|
23
|
+
export declare function QueueRow(props: QueueRowProps): import("preact").JSX.Element;
|
|
24
|
+
export interface QueuePanelProps {
|
|
25
|
+
source: QueueSource;
|
|
26
|
+
/** Whether cancelling is possible at all right now (e.g. the viewer owns the queue). */
|
|
27
|
+
canCancel?: boolean;
|
|
28
|
+
onCancel?(id: string): void;
|
|
29
|
+
/** Changing this clears any pending confirm (e.g. the selected target changed). */
|
|
30
|
+
resetKey?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Extra detail per unavailable reason, APPENDED to this package's own sentence — never
|
|
33
|
+
* substituted for it. Two reasons therefore always render different copy, whatever the caller
|
|
34
|
+
* supplies (see ui-core's `unavailableText`).
|
|
35
|
+
*/
|
|
36
|
+
unavailableDetail?: Partial<Record<QueueUnavailableReason, string>>;
|
|
37
|
+
}
|
|
38
|
+
export declare function QueuePanel(props: QueuePanelProps): import("preact").JSX.Element;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
|
|
2
|
+
/** @jsxImportSource preact */
|
|
3
|
+
// @mythicalos/preact-ui — the delivery queue (ds/components-terminal, spec v2): `QueueRow` is the
|
|
4
|
+
// registry atom; `QueuePanel` is the list that carries the source→view honesty around it.
|
|
5
|
+
//
|
|
6
|
+
// Thin binding: render + wiring only. The source resolution, every copy string, the row/badge class
|
|
7
|
+
// derivation, and the two-step cancel state machine all come from `@mythicalos/ui-core`.
|
|
8
|
+
//
|
|
9
|
+
// Honesty (binding, inherited from ui-core): the empty copy is CAPABILITY-NEUTRAL and renders ONLY
|
|
10
|
+
// on `ok` + zero items — never an operational-emptiness or store-existence claim, and never the
|
|
11
|
+
// design card's "queue empty — deliveries land here by class (ASAP interrupts · ON-DONE waits)";
|
|
12
|
+
// `loading` never renders empty; the three unavailable reasons render DISTINCTLY; `stale` is its own
|
|
13
|
+
// state, flagged as last-known data with no retry claim. The cancel affordance exists ONLY on
|
|
14
|
+
// `queued` records, only when the caller says the record is cancellable, and only while the
|
|
15
|
+
// source is FRESH — a stale (last-known) list carries no cancel controls at all.
|
|
16
|
+
import { useEffect, useReducer } from "preact/hooks";
|
|
17
|
+
import { QUEUE_CANCEL_ASK, QUEUE_CANCEL_LABEL, QUEUE_CANCEL_NO, QUEUE_CANCEL_YES, QUEUE_CLASSES, canCancelRow, cancelReducer, queueBadgeClass, queueBadgeLabel, queueRowClass, queueStatusLabel, queueView, shouldDisarmCancel, } from "@mythicalos/ui-core/logic";
|
|
18
|
+
export { QUEUE_EMPTY_COPY, QUEUE_LOADING_COPY, QUEUE_STALE_COPY, QUEUE_UNAVAILABLE_COPY, canCancelRow, cancelReducer, queueBadgeClass, queueBadgeLabel, queueRowClass, queueView, shouldDisarmCancel, unavailableText, } from "@mythicalos/ui-core/logic";
|
|
19
|
+
/**
|
|
20
|
+
* One queue record. The registry atom — usable standalone, or through `QueuePanel`.
|
|
21
|
+
*
|
|
22
|
+
* INVARIANT 4 is enforced HERE, not only by the panel: `armed` is honored only when the record is
|
|
23
|
+
* genuinely cancellable (`canCancelRow`). A caller that arms a `leased`/`delivered`/`canceled` row,
|
|
24
|
+
* or arms a row whose cancellability was revoked, gets the ordinary row back — never a live
|
|
25
|
+
* "Cancel it" button for something that cannot be cancelled. That also covers the render between a
|
|
26
|
+
* revocation and the panel's disarming effect.
|
|
27
|
+
*/
|
|
28
|
+
export function QueueRow(props) {
|
|
29
|
+
const { item } = props;
|
|
30
|
+
const cancellable = canCancelRow(item.status, !!props.canCancel);
|
|
31
|
+
const badge = _jsx("span", { class: queueBadgeClass(item.cls), children: queueBadgeLabel(item.cls) });
|
|
32
|
+
if (props.armed && cancellable) {
|
|
33
|
+
return (_jsxs("div", { class: queueRowClass(item.status, true), children: [badge, _jsx("span", { class: QUEUE_CLASSES.ask, children: QUEUE_CANCEL_ASK }), _jsxs("span", { class: QUEUE_CLASSES.actions, children: [_jsx("button", { type: "button", class: QUEUE_CLASSES.confirmYes, onClick: props.onConfirm, children: QUEUE_CANCEL_YES }), _jsx("button", { type: "button", class: QUEUE_CLASSES.confirmNo, onClick: props.onDisarm, children: QUEUE_CANCEL_NO })] })] }));
|
|
34
|
+
}
|
|
35
|
+
return (_jsxs("div", { class: queueRowClass(item.status, false), children: [badge, _jsx("span", { class: QUEUE_CLASSES.rowBody, children: item.body }), _jsx("span", { class: QUEUE_CLASSES.rowStatus, children: queueStatusLabel(item.status) }), cancellable ? (_jsx("button", { type: "button", class: QUEUE_CLASSES.cancel, onClick: props.onArm, children: QUEUE_CANCEL_LABEL })) : null] }));
|
|
36
|
+
}
|
|
37
|
+
export function QueuePanel(props) {
|
|
38
|
+
const { source } = props;
|
|
39
|
+
const canCancel = !!props.canCancel;
|
|
40
|
+
const [cancel, dispatch] = useReducer(cancelReducer, { armedId: null });
|
|
41
|
+
// A changed target clears any pending ask.
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
dispatch({ type: "disarm" });
|
|
44
|
+
}, [props.resetKey]);
|
|
45
|
+
// An armed confirm must never outlive its cancelability: ownership loss, or the armed row leaving
|
|
46
|
+
// `queued` in the CURRENT source (leased/delivered/canceled/vanished/unavailable), force-disarms.
|
|
47
|
+
const mustDisarm = shouldDisarmCancel(cancel.armedId, canCancel, source);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (mustDisarm)
|
|
50
|
+
dispatch({ type: "disarm" });
|
|
51
|
+
}, [mustDisarm]);
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (cancel.armedId === null)
|
|
54
|
+
return;
|
|
55
|
+
const onKey = (e) => {
|
|
56
|
+
if (e.key === "Escape")
|
|
57
|
+
dispatch({ type: "disarm" });
|
|
58
|
+
};
|
|
59
|
+
window.addEventListener("keydown", onKey);
|
|
60
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
61
|
+
}, [cancel.armedId]);
|
|
62
|
+
const view = queueView(source, props.unavailableDetail);
|
|
63
|
+
return (_jsxs("div", { class: QUEUE_CLASSES.panel, children: [view.kind === "state" ? _jsx("div", { class: QUEUE_CLASSES.state, children: view.copy }) : null, view.kind === "empty" ? _jsx("div", { class: QUEUE_CLASSES.empty, children: view.copy }) : null, view.kind === "list" ? (_jsxs("div", { class: QUEUE_CLASSES.list, children: [view.staleCopy !== null ? _jsx("div", { class: QUEUE_CLASSES.stale, children: view.staleCopy }) : null, view.items.map((item) => {
|
|
64
|
+
// A STALE list is last-known data, so no row in it may carry a cancel affordance: the
|
|
65
|
+
// row's real status is unknown, and offering a cancel would contradict the banner right
|
|
66
|
+
// above it. `view.fresh` is ui-core's freshness verdict, not a local guess.
|
|
67
|
+
const rowCanCancel = canCancel && view.fresh;
|
|
68
|
+
return (_jsx(QueueRow, { item: item, canCancel: rowCanCancel, armed: cancel.armedId === item.id, onArm: () => dispatch({ type: "arm", id: item.id }), onConfirm: () => {
|
|
69
|
+
// Re-checked at the moment of confirm: the intent only fires if the row is STILL
|
|
70
|
+
// fresh AND cancellable — never a stale-arm cancel.
|
|
71
|
+
if (canCancelRow(item.status, rowCanCancel))
|
|
72
|
+
props.onCancel?.(item.id);
|
|
73
|
+
dispatch({ type: "confirm" });
|
|
74
|
+
}, onDisarm: () => dispatch({ type: "disarm" }) }, item.id));
|
|
75
|
+
})] })) : null] }));
|
|
76
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** @jsxImportSource preact */
|
|
2
|
+
import type { ComponentChildren } from "preact";
|
|
3
|
+
import { SAVE_BAR_DISCARD_LABEL, SAVE_BAR_SAVE_LABEL, SAVE_BAR_PARTS, SAVE_BAR_SEP, saveBarClass, saveBarDirty, saveBarNote, type SaveBarNote } from "@mythicalos/ui-core/logic";
|
|
4
|
+
export { saveBarNote, saveBarDirty, saveBarClass, SAVE_BAR_PARTS, SAVE_BAR_SEP, SAVE_BAR_DISCARD_LABEL, SAVE_BAR_SAVE_LABEL, type SaveBarNote, };
|
|
5
|
+
export interface SaveBarProps {
|
|
6
|
+
/** Human labels of the dirty fields. Empty ⇒ the bar does not render. */
|
|
7
|
+
changed: readonly string[];
|
|
8
|
+
/** A save is in flight: the primary goes loading + inert. */
|
|
9
|
+
saving?: boolean;
|
|
10
|
+
/** Card copy is "Save & apply"; override per surface (the reference product says "Save locally"
|
|
11
|
+
* because its save writes to the local container). */
|
|
12
|
+
saveLabel?: string;
|
|
13
|
+
discardLabel?: string;
|
|
14
|
+
onDiscard: () => void;
|
|
15
|
+
onSave: () => void;
|
|
16
|
+
/** Extra content rendered between the note and the actions (e.g. a validation hint). */
|
|
17
|
+
children?: ComponentChildren;
|
|
18
|
+
class?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function SaveBar(props: SaveBarProps): import("preact").JSX.Element | null;
|
package/dist/SaveBar.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
|
|
2
|
+
import { SAVE_BAR_DISCARD_LABEL, SAVE_BAR_SAVE_LABEL, SAVE_BAR_PARTS, SAVE_BAR_SEP, saveBarClass, saveBarDirty, saveBarNote, } from "@mythicalos/ui-core/logic";
|
|
3
|
+
import { Button } from "./Button.js";
|
|
4
|
+
export { saveBarNote, saveBarDirty, saveBarClass, SAVE_BAR_PARTS, SAVE_BAR_SEP, SAVE_BAR_DISCARD_LABEL, SAVE_BAR_SAVE_LABEL, };
|
|
5
|
+
export function SaveBar(props) {
|
|
6
|
+
const { changed, saving = false, class: cls = "" } = props;
|
|
7
|
+
const note = saveBarNote(changed);
|
|
8
|
+
if (note.count === 0)
|
|
9
|
+
return null;
|
|
10
|
+
return (_jsxs("div", { class: `${saveBarClass()} ${cls}`, children: [_jsxs("span", { class: SAVE_BAR_PARTS.note, children: [_jsx("b", { class: SAVE_BAR_PARTS.count, children: note.countLabel }), SAVE_BAR_SEP, note.detail] }), props.children, _jsxs("span", { class: SAVE_BAR_PARTS.actions, children: [_jsx(Button, { variant: "sec", onClick: props.onDiscard, children: props.discardLabel ?? SAVE_BAR_DISCARD_LABEL }), _jsx(Button, { variant: "pri", loading: saving, onClick: props.onSave, children: props.saveLabel ?? SAVE_BAR_SAVE_LABEL })] })] }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** @jsxImportSource preact */
|
|
2
|
+
import type { JSX } from "preact";
|
|
3
|
+
import { type DeliveryClass } from "@mythicalos/ui-core/logic";
|
|
4
|
+
export { DEFAULT_DELIVERY_CLASS, DELIVERY_CLASSES, DELIVERY_HINT, SEND_DISABLED_FALLBACK, SEND_PLACEHOLDER, canSend, clearDraftOnSend, deliveryClassLabel, keyAction, resolveSend, sendPlaceholder, showDeliveryHint, type DeliveryClass, type KeyAction, } from "@mythicalos/ui-core/logic";
|
|
5
|
+
export interface SendBarProps {
|
|
6
|
+
/** Initial selection. A manual pick then wins — a later prop change never clobbers it. */
|
|
7
|
+
defaultClass?: DeliveryClass;
|
|
8
|
+
/** A send is in flight: the send button is inert but the field stays editable. */
|
|
9
|
+
busy?: boolean;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
/** The honest reason the bar is unusable — surfaced as the placeholder. */
|
|
12
|
+
disabledReason?: string;
|
|
13
|
+
/** A persistent notice rendered below the bar (e.g. a pending stop). */
|
|
14
|
+
notice?: string;
|
|
15
|
+
/** Carried into the enabled placeholder: `Message {name}… (⏎ send · ⇧⏎ newline)`. */
|
|
16
|
+
targetName?: string;
|
|
17
|
+
/** Resolve `true` on success; the draft clears only then (text survives failures). */
|
|
18
|
+
onSend(cls: DeliveryClass, body: string): boolean | Promise<boolean>;
|
|
19
|
+
/**
|
|
20
|
+
* Called when `onSend` REJECTS. The rejection is treated as a failed send (nothing delivered, the
|
|
21
|
+
* draft kept) rather than escaping as an unhandled rejection; this is how a caller still observes
|
|
22
|
+
* it. Omitting it does not hide a success — only a resolved `true` ever counts as sent.
|
|
23
|
+
*/
|
|
24
|
+
onSendError?(error: unknown): void;
|
|
25
|
+
}
|
|
26
|
+
export declare function SendBar(props: SendBarProps): JSX.Element;
|
package/dist/SendBar.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
|
|
2
|
+
/** @jsxImportSource preact */
|
|
3
|
+
// @mythicalos/preact-ui — the delivery send bar (ds/components-terminal, spec v2). Holds the LOCAL
|
|
4
|
+
// draft + class selection and fires `onSend(cls, body)`; the caller owns the delivery call.
|
|
5
|
+
//
|
|
6
|
+
// Thin binding: render + wiring only. The placeholder composition, the send predicate, the
|
|
7
|
+
// draft-clear rule, the keyboard semantics and the hint all come from `@mythicalos/ui-core`.
|
|
8
|
+
//
|
|
9
|
+
// Honesty (binding, inherited from ui-core): the per-class hint is
|
|
10
|
+
// `ASAP takes the first turn gap · ON-DONE waits for full idle.` — the design card's older
|
|
11
|
+
// "ASAP interrupts" wording is FALSE (delivery always waits for a turn boundary) and must never be
|
|
12
|
+
// reintroduced. The hint renders ONLY where the controls are usable; a disabled bar surfaces just
|
|
13
|
+
// the caller's truthful reason.
|
|
14
|
+
//
|
|
15
|
+
// Keyboard: Enter=send, Shift/Alt-Enter=newline, IME composition suppressed (`keyAction`).
|
|
16
|
+
import { useRef, useState } from "preact/hooks";
|
|
17
|
+
import { DEFAULT_DELIVERY_CLASS, DELIVERY_CLASSES, DELIVERY_HINT, SENDBAR_CLASSES, SEND_BUTTON_LABEL, canSend, clearDraftOnSend, deliveryClassButtonClass, deliveryClassLabel, keyAction, makeSendGate, resolveSend, sendBarClass, sendPlaceholder, showDeliveryHint, } from "@mythicalos/ui-core/logic";
|
|
18
|
+
export { DEFAULT_DELIVERY_CLASS, DELIVERY_CLASSES, DELIVERY_HINT, SEND_DISABLED_FALLBACK, SEND_PLACEHOLDER, canSend, clearDraftOnSend, deliveryClassLabel, keyAction, resolveSend, sendPlaceholder, showDeliveryHint, } from "@mythicalos/ui-core/logic";
|
|
19
|
+
export function SendBar(props) {
|
|
20
|
+
const disabled = !!props.disabled;
|
|
21
|
+
const busy = !!props.busy;
|
|
22
|
+
const [cls, setCls] = useState(props.defaultClass ?? DEFAULT_DELIVERY_CLASS);
|
|
23
|
+
const [text, setText] = useState("");
|
|
24
|
+
// Single-flight: `busy` covers a caller that tracks the request itself, but `onSend` may be async
|
|
25
|
+
// and a caller that flips `busy` a tick later (or never) would let two clicks — or a click racing
|
|
26
|
+
// an Enter — deliver the same message twice. The gate is synchronous, so the second attempt is
|
|
27
|
+
// refused before any state update lands; `sending` only exists to re-render the disabled button.
|
|
28
|
+
const gate = useRef(makeSendGate());
|
|
29
|
+
const [sending, setSending] = useState(false);
|
|
30
|
+
const locked = busy || sending;
|
|
31
|
+
const doSend = async () => {
|
|
32
|
+
if (!canSend(text, disabled, locked))
|
|
33
|
+
return;
|
|
34
|
+
if (!gate.current.tryAcquire())
|
|
35
|
+
return;
|
|
36
|
+
setSending(true);
|
|
37
|
+
const submitted = text;
|
|
38
|
+
try {
|
|
39
|
+
// a REJECTED onSend resolves to `false` here (a failed send) instead of escaping the handler
|
|
40
|
+
// as an unhandled rejection; the caller still sees it through `onSendError`
|
|
41
|
+
const sent = await resolveSend(() => props.onSend(cls, submitted), props.onSendError);
|
|
42
|
+
// Functional update: compared against the LIVE draft at resolve time, not this closure's
|
|
43
|
+
// snapshot, so text typed while the request was in flight is never erased.
|
|
44
|
+
if (sent)
|
|
45
|
+
setText((current) => (clearDraftOnSend(sent, submitted, current) ? "" : current));
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
// released on every path, so a failing send can never wedge the bar shut. The draft survives
|
|
49
|
+
// either way (it clears only on `sent === true`).
|
|
50
|
+
gate.current.release();
|
|
51
|
+
setSending(false);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const onKeyDown = (e) => {
|
|
55
|
+
if (keyAction(e) === "send") {
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
void doSend();
|
|
58
|
+
}
|
|
59
|
+
// "newline" / "none": the textarea handles the keystroke natively.
|
|
60
|
+
};
|
|
61
|
+
return (_jsxs("div", { class: SENDBAR_CLASSES.wrap, children: [_jsxs("div", { class: sendBarClass(disabled), children: [_jsx("div", { class: SENDBAR_CLASSES.segment, role: "group", children: DELIVERY_CLASSES.map((c) => (_jsx("button", { type: "button", class: deliveryClassButtonClass(cls === c), disabled: disabled, "aria-pressed": cls === c, onClick: () => setCls(c), children: deliveryClassLabel(c) }, c))) }), _jsx("textarea", { class: SENDBAR_CLASSES.input, rows: 1, placeholder: sendPlaceholder(disabled, props.disabledReason, props.targetName), value: text, disabled: disabled, onInput: (e) => setText(e.target.value), onKeyDown: onKeyDown }), _jsx("button", { type: "button", class: SENDBAR_CLASSES.send, disabled: !canSend(text, disabled, locked), onClick: doSend, children: SEND_BUTTON_LABEL })] }), showDeliveryHint(disabled) ? _jsx("div", { class: SENDBAR_CLASSES.hint, children: DELIVERY_HINT }) : null, props.notice ? _jsx("div", { class: SENDBAR_CLASSES.notice, children: props.notice }) : null] }));
|
|
62
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** @jsxImportSource preact */
|
|
2
|
+
import type { ComponentChildren } from "preact";
|
|
3
|
+
import { ctxBand, ctxReading, ctxBarGeom, normalizeCtxThresholds, ctxMeterClass, ctxNoteText, ctxValueText, sessionAvatarInitial, sessionCardClass, sessionCardIsStale, sessionCardStale, sessionSpineSummary, sessionStatus, sessionStatusClass, sessionStatusText, sessionSubline, spineNodeClass, type CtxThresholds, type SessionSpine, type SessionStatusInput } from "@mythicalos/ui-core/logic";
|
|
4
|
+
export { ctxBand, ctxReading, ctxBarGeom, normalizeCtxThresholds, ctxMeterClass, ctxNoteText, ctxValueText, sessionAvatarInitial, sessionCardClass, sessionCardIsStale, sessionCardStale, sessionSpineSummary, sessionStatus, sessionStatusClass, sessionStatusText, sessionSubline, spineNodeClass, };
|
|
5
|
+
export interface SessionCardProps {
|
|
6
|
+
/** Display name — the card's headline and the source of the avatar initial. */
|
|
7
|
+
name: string;
|
|
8
|
+
/**
|
|
9
|
+
* The meta subline, as a free list joined with ` · `. The design card's own states use it for
|
|
10
|
+
* `role · model · duration`, `role · model · queued: 1 ON-DONE` and `role · last seen 00:12 ago`
|
|
11
|
+
* alike. Absent/blank entries collapse; an all-absent list omits the line entirely.
|
|
12
|
+
*/
|
|
13
|
+
meta?: readonly (string | null | undefined)[];
|
|
14
|
+
/**
|
|
15
|
+
* What the product actually knows about the session. Every field is optional and every absent
|
|
16
|
+
* field means "not reported" — an omitted `activity` never becomes `idle`.
|
|
17
|
+
*/
|
|
18
|
+
status?: SessionStatusInput;
|
|
19
|
+
/** The product's own wording for the derived status (e.g. a wake-specific phrase). The tone,
|
|
20
|
+
* pulse and stale treatment still come from the derivation — only the words change. A blank
|
|
21
|
+
* override, and ANY override on the `unknown` status, is ignored: nothing was claimed, so there
|
|
22
|
+
* is nothing to reword (see ui-core's `sessionStatusText`). */
|
|
23
|
+
statusLabel?: string;
|
|
24
|
+
/** Context reading, 0–100. `undefined`/`null` ⇒ NOT MEASURED: no fill is drawn and the value
|
|
25
|
+
* reads `—`. It is never rendered as 0. */
|
|
26
|
+
contextPct?: number | null;
|
|
27
|
+
/** Product-tunable warn/critical thresholds (default 75/90 — tokens.css rule #4). */
|
|
28
|
+
thresholds?: CtxThresholds;
|
|
29
|
+
/** Spine strip. An unreported `distills` omits the strip entirely. */
|
|
30
|
+
spine?: SessionSpine;
|
|
31
|
+
/** The design's selected state (petrol border + ring). Independent of `stale`. */
|
|
32
|
+
selected?: boolean;
|
|
33
|
+
/** ADD the design's stale treatment (dashed border, muted values) for a reason the card cannot
|
|
34
|
+
* see. It cannot be used to REMOVE the staleness a down link implies — see `sessionCardStale`. */
|
|
35
|
+
stale?: boolean;
|
|
36
|
+
/** Makes the card a real button. Omit for a non-interactive card. */
|
|
37
|
+
onSelect?: () => void;
|
|
38
|
+
class?: string;
|
|
39
|
+
/** Extra content appended inside the card (e.g. a product's own mode glyph). */
|
|
40
|
+
children?: ComponentChildren;
|
|
41
|
+
}
|
|
42
|
+
export declare function SessionCard(props: SessionCardProps): import("preact").JSX.Element;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
|
|
2
|
+
/** @jsxImportSource preact */
|
|
3
|
+
// @mythicalos/preact-ui — the `session-card` atom (design registry spec v1,
|
|
4
|
+
// mythical-design/ds/components-session-card.html): avatar, name, status dot + words, a meta
|
|
5
|
+
// subline, the context meter with its fixed threshold ticks, and the optional spine strip.
|
|
6
|
+
//
|
|
7
|
+
// Extracted from the only product that implemented it (the Control Room's rail SessionCard + its
|
|
8
|
+
// derive.ts / sessionsVm.ts state-dot derivation) with that product's session view-model lifted
|
|
9
|
+
// out — this component takes plain props, never a `SessionVm`.
|
|
10
|
+
//
|
|
11
|
+
// EVERY branch lives in `@mythicalos/ui-core`'s session-card logic module (band/status/stale/class
|
|
12
|
+
// derivation, subline composition, bar geometry, spine summary); this binding renders what those
|
|
13
|
+
// functions return and nothing else, so it and its React sibling cannot drift. The three honesty
|
|
14
|
+
// invariants (absence is not zero · unknown is not idle · thresholds are product-tunable) are
|
|
15
|
+
// enforced there and documented at the top of that file.
|
|
16
|
+
//
|
|
17
|
+
// The avatar is the packaged <Avatar> atom (composed, not re-rolled). The status dot is NOT
|
|
18
|
+
// <StatusLine>: that atom's tone axis has no `unknown` member and it accepts no `class`, so
|
|
19
|
+
// neither the hollow unknown dot nor the ≤1/s pulse could ride on it without reshaping a sibling
|
|
20
|
+
// atom — same tone vocabulary, own elements (see the stylesheet's note).
|
|
21
|
+
//
|
|
22
|
+
// No inline `style=` anywhere (CSP style-src 'self'): the meter's fill and its 75/90 ticks — which
|
|
23
|
+
// the design prototype positioned with `style="width:62%"` / `style="left:75%"` — are SVG rects
|
|
24
|
+
// carrying presentation attributes, which is also the only form that can express product-tuned
|
|
25
|
+
// thresholds.
|
|
26
|
+
import { Fragment } from "preact";
|
|
27
|
+
import { ctxBand, ctxReading, ctxBarGeom, normalizeCtxThresholds, ctxMeterClass, ctxNoteText, ctxValueText, sessionAvatarInitial, sessionCardClass, sessionCardIsStale, sessionCardStale, sessionSpineSummary, sessionStatus, sessionStatusClass, sessionStatusText, sessionSubline, spineNodeClass, CTX_BAR_VIEWBOX, CTX_THRESHOLDS_DEFAULT, } from "@mythicalos/ui-core/logic";
|
|
28
|
+
import { Avatar } from "./Avatar.js";
|
|
29
|
+
export { ctxBand, ctxReading, ctxBarGeom, normalizeCtxThresholds, ctxMeterClass, ctxNoteText, ctxValueText, sessionAvatarInitial, sessionCardClass, sessionCardIsStale, sessionCardStale, sessionSpineSummary, sessionStatus, sessionStatusClass, sessionStatusText, sessionSubline, spineNodeClass, };
|
|
30
|
+
export function SessionCard(props) {
|
|
31
|
+
const { name, meta = [], status: statusInput, statusLabel, contextPct, thresholds = CTX_THRESHOLDS_DEFAULT, spine, selected = false, onSelect, class: cls = "", children, } = props;
|
|
32
|
+
const band = ctxBand(contextPct, thresholds);
|
|
33
|
+
// the band is part of the status derivation, not a second opinion on it: the design card's
|
|
34
|
+
// warn/error states replace the status line with the context claim
|
|
35
|
+
const status = sessionStatus(statusInput, band);
|
|
36
|
+
const stale = sessionCardStale(status, props.stale);
|
|
37
|
+
const bar = ctxBarGeom(contextPct, thresholds);
|
|
38
|
+
const subline = sessionSubline(meta);
|
|
39
|
+
const spineSummary = sessionSpineSummary(spine);
|
|
40
|
+
const body = (_jsxs(Fragment, { children: [_jsxs("span", { class: "my-session-card__head", children: [_jsx(Avatar, { initials: sessionAvatarInitial(name), class: "my-session-card__avatar" }), _jsxs("span", { class: "my-session-card__ident", children: [_jsxs("span", { class: "my-session-card__line1", children: [_jsx("b", { class: "my-session-card__name", children: name }), _jsxs("span", { class: sessionStatusClass(status), children: [_jsx("span", { class: "my-session-card__dot" }), _jsx("span", { class: "my-session-card__status-text", children: sessionStatusText(status, statusLabel) })] })] }), subline.length > 0 ? _jsx("span", { class: "my-session-card__meta", children: subline }) : null] })] }), _jsxs("span", { class: ctxMeterClass({ band, stale }), children: [_jsx("span", { class: "my-session-card__ctx-bar", children: _jsxs("svg", { class: "my-session-card__ctx-svg", viewBox: CTX_BAR_VIEWBOX, preserveAspectRatio: "none", "aria-hidden": "true", children: [_jsx("rect", { class: "my-session-card__ctx-track", x: "0", y: "0", width: bar.span, height: bar.height }), bar.fill === undefined ? null : (_jsx("rect", { class: "my-session-card__ctx-fill", x: "0", y: "0", width: bar.fill, height: bar.height })), bar.ticks.map((t) => (_jsx("rect", { class: "my-session-card__ctx-tick", x: t.x, y: "0", width: t.width, height: bar.height }, t.pct)))] }) }), _jsxs("span", { class: "my-session-card__ctx-legend", children: [_jsx("span", { class: "my-session-card__ctx-note", children: ctxNoteText(band, { stale }) }), _jsx("b", { class: "my-session-card__ctx-value", children: ctxValueText(contextPct, thresholds) })] })] }), spineSummary === undefined ? null : (_jsxs(Fragment, { children: [_jsx("span", { class: "my-session-card__spine", "aria-hidden": "true", children: spineSummary.nodes.map((node, i) => (_jsxs(Fragment, { children: [i > 0 ? _jsx("span", { class: "my-session-card__spine-seg" }) : null, _jsx("span", { class: spineNodeClass(node) })] }, i))) }), _jsxs("span", { class: "my-session-card__ctx-legend", children: [_jsx("span", { class: "my-session-card__spine-note", children: spineSummary.label }), _jsx("b", { class: "my-session-card__spine-value", children: spineSummary.value })] })] })), children] }));
|
|
41
|
+
const classes = sessionCardClass({ selected, stale, extra: cls });
|
|
42
|
+
return onSelect === undefined ? (_jsx("div", { class: classes, children: body })) : (_jsx("button", { type: "button", class: classes, "aria-pressed": selected, onClick: onSelect, children: body }));
|
|
43
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** @jsxImportSource preact */
|
|
2
|
+
import { STAT_TILE_EMPTY, STAT_TILE_MINUS, formatStatCompact, formatStatCount, formatStatPercent, formatStatUsd, STAT_TILE_PARTS, statTileClass, statTilesClass, type StatTile, type StatTileTone } from "@mythicalos/ui-core/logic";
|
|
3
|
+
export { formatStatCompact, formatStatCount, formatStatPercent, formatStatUsd, statTileClass, statTilesClass, STAT_TILE_PARTS, STAT_TILE_EMPTY, STAT_TILE_MINUS, type StatTile, type StatTileTone, };
|
|
4
|
+
export interface StatTilesProps {
|
|
5
|
+
/** The tiles, in display order. A tile with no `sub` renders no sub-caption — the atom never
|
|
6
|
+
* invents one (pass `STAT_TILE_EMPTY` if you want the placeholder line). */
|
|
7
|
+
tiles: readonly StatTile[];
|
|
8
|
+
class?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function StatTiles(props: StatTilesProps): import("preact").JSX.Element;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
|
|
2
|
+
/** @jsxImportSource preact */
|
|
3
|
+
// @mythicalos/preact-ui — the stat-tile row (ds/components-stat-tiles.html): a wrapping row of
|
|
4
|
+
// bordered tiles, each an uppercase micro key, a big mono tabular-nums value and a mono
|
|
5
|
+
// sub-caption. `tone: "accent"` is the card's "brag" number; `warn`/`error` are the band tint.
|
|
6
|
+
//
|
|
7
|
+
// Extracted from the reference implementation in the reference product's session detail pane, which
|
|
8
|
+
// hard-coded ITS five session tiles (tokens in/out, prompt cache, cost, spine savings) off a
|
|
9
|
+
// `SessionVm` inside the component. That composition is the product's; this atom takes an
|
|
10
|
+
// already-composed `StatTile[]` so each surface decides its own tiles and order. The value
|
|
11
|
+
// formatters (and the "—" placeholder that must never become a fabricated `0`) live in
|
|
12
|
+
// `@mythicalos/ui-core` and are re-exported here.
|
|
13
|
+
import { STAT_TILE_EMPTY, STAT_TILE_MINUS, formatStatCompact, formatStatCount, formatStatPercent, formatStatUsd, STAT_TILE_PARTS, statTileClass, statTilesClass, } from "@mythicalos/ui-core/logic";
|
|
14
|
+
export { formatStatCompact, formatStatCount, formatStatPercent, formatStatUsd, statTileClass, statTilesClass, STAT_TILE_PARTS, STAT_TILE_EMPTY, STAT_TILE_MINUS, };
|
|
15
|
+
export function StatTiles(props) {
|
|
16
|
+
const { class: cls = "" } = props;
|
|
17
|
+
// A JS caller can hand a non-array where the type says `StatTile[]` (an unloaded slot); render an
|
|
18
|
+
// empty row rather than crashing on `.map` — the same "absent is not a value" rule the
|
|
19
|
+
// formatters follow.
|
|
20
|
+
const tiles = Array.isArray(props.tiles) ? props.tiles : [];
|
|
21
|
+
return (_jsx("div", { class: `${statTilesClass()} ${cls}`, children: tiles.map((t, i) => (_jsxs("div", { class: statTileClass(t.tone), children: [_jsx("div", { class: STAT_TILE_PARTS.label, children: t.label }), _jsx("div", { class: STAT_TILE_PARTS.value, children: t.value }), t.sub !== undefined ? _jsx("div", { class: STAT_TILE_PARTS.sub, children: t.sub }) : null] }, `${i}:${t.label}`))) }));
|
|
22
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** @jsxImportSource preact */
|
|
2
|
+
import { type TermRow, type TermSource, type TranscriptSegment } from "@mythicalos/ui-core/logic";
|
|
3
|
+
export { TERM_CLASS, TERM_CLASSES, TERM_STALE_COPY, TERM_WAKE_UNAVAILABLE_COPY, transcriptView, type TermRow, type TermRowKind, type TermSource, type TermView, type TranscriptSegment, } from "@mythicalos/ui-core/logic";
|
|
4
|
+
export interface TerminalProps {
|
|
5
|
+
/** The sole transcript input. All six arms are rendered by ui-core's `transcriptView`. */
|
|
6
|
+
source: TermSource;
|
|
7
|
+
/** Title-line text left of ` · tail · noise {on|off}` (e.g. `jacob.jsonl`). */
|
|
8
|
+
name?: string;
|
|
9
|
+
/** The noise FILTER: on ⇒ noise rows are HIDDEN and the title reads `noise on`. */
|
|
10
|
+
noiseFilterEnabled?: boolean;
|
|
11
|
+
/** Omit for a read-only pane — the title then renders as a caption, not a dead button. */
|
|
12
|
+
onToggleNoise?(): void;
|
|
13
|
+
/** Client-synthetic rows appended after the transcript (e.g. an interrupt marker). */
|
|
14
|
+
localRows?: readonly TermRow[];
|
|
15
|
+
/** Render only from the last `boundary` row on (the current wake). */
|
|
16
|
+
currentWakeOnly?: boolean;
|
|
17
|
+
history?: readonly TranscriptSegment[];
|
|
18
|
+
/** Shows the `wake unavailable` banner. Independent of the transcript branches. */
|
|
19
|
+
wakeUnavailable?: boolean;
|
|
20
|
+
/** The blinking stream caret. */
|
|
21
|
+
caret?: boolean;
|
|
22
|
+
/** Supplied truth only — `undefined` renders NO caption rather than guessing. */
|
|
23
|
+
turnInFlight?: boolean;
|
|
24
|
+
/** Omit to render no stop control at all. */
|
|
25
|
+
onStopTurn?(): void;
|
|
26
|
+
/** A stop is already in flight: the control is disabled and Ctrl-C is ignored. */
|
|
27
|
+
stopBusy?: boolean;
|
|
28
|
+
/** VISUAL weight for the stop control only — never a state claim. */
|
|
29
|
+
stopProminent?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export declare function Terminal(props: TerminalProps): import("preact").JSX.Element;
|