@mythicalos/ui-core 0.2.1 → 0.3.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/dist/index.d.ts +9 -0
- package/dist/index.js +27 -0
- package/dist/logic/file-explorer.d.ts +374 -0
- package/dist/logic/file-explorer.js +624 -0
- package/dist/logic/git-chip.d.ts +79 -0
- package/dist/logic/git-chip.js +118 -0
- package/dist/logic/popover.d.ts +242 -0
- package/dist/logic/popover.js +303 -0
- package/dist/logic/queue.d.ts +142 -0
- package/dist/logic/queue.js +143 -0
- package/dist/logic/save-bar.d.ts +39 -0
- package/dist/logic/save-bar.js +54 -0
- package/dist/logic/sendbar.d.ts +105 -0
- package/dist/logic/sendbar.js +136 -0
- package/dist/logic/session-card.d.ts +263 -0
- package/dist/logic/session-card.js +443 -0
- package/dist/logic/stat-tiles.d.ts +49 -0
- package/dist/logic/stat-tiles.js +102 -0
- package/dist/logic/terminal.d.ts +222 -0
- package/dist/logic/terminal.js +264 -0
- package/package.json +1 -1
- package/src/select/mythical-select.js +5 -1
- package/styles.css +750 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// @mythicalos/ui-core — the dirty-state save bar's pure text composition + class derivation
|
|
2
|
+
// (ds/layouts-settings.html: the bar pinned to the bottom of the settings screen —
|
|
3
|
+
// `1 unsaved change · Base URL` on the left, Discard + the page's single primary on the right).
|
|
4
|
+
//
|
|
5
|
+
// Extracted from the reference implementation in the reference product's settings/projects pages,
|
|
6
|
+
// which took a `(keyof MythicalConfig)[]` — a product config's key union. That product coupling is
|
|
7
|
+
// lifted out here: the atom takes plain `string[]` field labels and composes the same sentence.
|
|
8
|
+
//
|
|
9
|
+
// The count/plural derivation and the separator live here (not in the bindings) so the Preact and
|
|
10
|
+
// React renders can never drift, and so a consumer can assert the exact visible sentence without a
|
|
11
|
+
// DOM — the same contract `composeToastText` holds for the toast.
|
|
12
|
+
/** The separator between the bold "N unsaved change(s)" count and the changed-field list. */
|
|
13
|
+
export const SAVE_BAR_SEP = " · ";
|
|
14
|
+
/** Design-card copy for the two actions (ds/layouts-settings.html). Both are overridable per
|
|
15
|
+
* surface — the reference product's settings page says "Save locally" because its save writes to
|
|
16
|
+
* the local container, not the card's "Save & apply". */
|
|
17
|
+
export const SAVE_BAR_DISCARD_LABEL = "Discard";
|
|
18
|
+
export const SAVE_BAR_SAVE_LABEL = "Save & apply";
|
|
19
|
+
/** Compose the save bar's note from the list of changed-field labels. Pluralization is decided
|
|
20
|
+
* here, once, for both bindings. Non-string / blank entries are dropped rather than rendered as
|
|
21
|
+
* `undefined` in the list — but they are dropped BEFORE the count, so the count always equals the
|
|
22
|
+
* number of fields actually named. */
|
|
23
|
+
export function saveBarNote(changed) {
|
|
24
|
+
const fields = (changed ?? []).filter((f) => typeof f === "string" && f.trim().length > 0);
|
|
25
|
+
const count = fields.length;
|
|
26
|
+
const countLabel = `${count} unsaved ${count === 1 ? "change" : "changes"}`;
|
|
27
|
+
const detail = fields.join(", ");
|
|
28
|
+
return {
|
|
29
|
+
count,
|
|
30
|
+
countLabel,
|
|
31
|
+
detail,
|
|
32
|
+
text: detail.length > 0 ? `${countLabel}${SAVE_BAR_SEP}${detail}` : countLabel,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** The card's "the save bar slides up only when dirty" rule, as a predicate. The bindings return
|
|
36
|
+
* `null` for a not-dirty bar so no surface can ever render "0 unsaved changes" next to a live
|
|
37
|
+
* Save button. */
|
|
38
|
+
export function saveBarDirty(changed) {
|
|
39
|
+
return saveBarNote(changed).count > 0;
|
|
40
|
+
}
|
|
41
|
+
/** Every class the bar renders, root and elements. The bindings import these rather than spelling
|
|
42
|
+
* the strings themselves, so a rename cannot land in one framework and not the other, and the
|
|
43
|
+
* stylesheet test can enumerate the parts instead of restating a hand-written list. */
|
|
44
|
+
export const SAVE_BAR_PARTS = {
|
|
45
|
+
root: "my-savebar",
|
|
46
|
+
note: "my-savebar__note",
|
|
47
|
+
count: "my-savebar__count",
|
|
48
|
+
actions: "my-savebar__actions",
|
|
49
|
+
};
|
|
50
|
+
/** Root class for the bar. No tone axis — the modifier space is reserved for the caller's own
|
|
51
|
+
* passthrough class, which the bindings append. */
|
|
52
|
+
export function saveBarClass() {
|
|
53
|
+
return SAVE_BAR_PARTS.root;
|
|
54
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/** Delivery class. `asap` takes the first turn gap; `on-done` waits for full idle. */
|
|
2
|
+
export type DeliveryClass = "asap" | "on-done";
|
|
3
|
+
export declare const DELIVERY_CLASSES: readonly DeliveryClass[];
|
|
4
|
+
/** The class a bar starts on when the caller expresses no preference. One definition, so the two
|
|
5
|
+
* bindings cannot ship different defaults. */
|
|
6
|
+
export declare const DEFAULT_DELIVERY_CLASS: DeliveryClass;
|
|
7
|
+
/** Verbatim badge/button label for a delivery class. */
|
|
8
|
+
export declare function deliveryClassLabel(cls: DeliveryClass): "ASAP" | "ON-DONE";
|
|
9
|
+
/**
|
|
10
|
+
* HONESTY INVARIANT (binding, do not reword): delivery ALWAYS waits for a turn boundary. The design
|
|
11
|
+
* card's older per-class wording — "ASAP interrupts · ON-DONE waits" — is FALSE: nothing about ASAP
|
|
12
|
+
* interrupts a turn in flight, it merely takes the FIRST gap between turns. This one combined line
|
|
13
|
+
* replaces both of the card's per-class hints. Never reintroduce "interrupts" here.
|
|
14
|
+
*/
|
|
15
|
+
export declare const DELIVERY_HINT = "ASAP takes the first turn gap \u00B7 ON-DONE waits for full idle.";
|
|
16
|
+
/** Enabled placeholder when the caller supplies no target name. */
|
|
17
|
+
export declare const SEND_PLACEHOLDER = "Message\u2026 (\u23CE send \u00B7 \u21E7\u23CE newline)";
|
|
18
|
+
/** Last-resort disabled placeholder when the caller supplies no reason. Says only that sending is
|
|
19
|
+
* unavailable — it never speculates about why. */
|
|
20
|
+
export declare const SEND_DISABLED_FALLBACK = "Sending is unavailable.";
|
|
21
|
+
/**
|
|
22
|
+
* Placeholder copy. Disabled ⇒ the caller's honest reason (or the neutral fallback); enabled ⇒ the
|
|
23
|
+
* design's named prompt `Message {name}… (⏎ send · ⇧⏎ newline)` when a target name is supplied,
|
|
24
|
+
* otherwise the nameless form. A disabled bar NEVER shows the enabled prompt.
|
|
25
|
+
*/
|
|
26
|
+
export declare function sendPlaceholder(disabled: boolean, disabledReason?: string, targetName?: string): string;
|
|
27
|
+
/** Send is possible only with non-blank text, when enabled and not mid-send. */
|
|
28
|
+
export declare function canSend(text: string, disabled: boolean, busy: boolean): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* The ASAP/ON-DONE semantics line renders ONLY where the delivery controls are actually usable — a
|
|
31
|
+
* disabled bar surfaces just the truthful reason (through the placeholder), never per-class
|
|
32
|
+
* semantics for controls the user cannot operate.
|
|
33
|
+
*/
|
|
34
|
+
export declare function showDeliveryHint(disabled: boolean): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Single-flight gate for the send action. `canSend`'s `busy` argument covers the case where the
|
|
37
|
+
* CALLER tracks the in-flight state, but `onSend` may be async and a caller that flips `busy` in a
|
|
38
|
+
* later tick (or not at all) leaves a window in which two clicks — or a click racing an Enter —
|
|
39
|
+
* both call `onSend` and deliver the same message twice. The gate closes that window inside the
|
|
40
|
+
* component: it is synchronous, so the second attempt in the same tick is refused before any state
|
|
41
|
+
* update can land.
|
|
42
|
+
*
|
|
43
|
+
* Lives here rather than in a binding so both bindings get identical semantics, and so the race is
|
|
44
|
+
* testable without a DOM.
|
|
45
|
+
*/
|
|
46
|
+
export interface SendGate {
|
|
47
|
+
/** Takes the lock and returns true; returns false when a send is already in flight. */
|
|
48
|
+
tryAcquire(): boolean;
|
|
49
|
+
/** Releases the lock. Safe to call when not held. */
|
|
50
|
+
release(): void;
|
|
51
|
+
inFlight(): boolean;
|
|
52
|
+
}
|
|
53
|
+
export declare function makeSendGate(): SendGate;
|
|
54
|
+
/**
|
|
55
|
+
* Resolve one send attempt to a plain success/failure.
|
|
56
|
+
*
|
|
57
|
+
* `onSend` is contracted to resolve a boolean, but it is caller code and may REJECT. Letting that
|
|
58
|
+
* rejection escape an async event handler surfaces it as an unhandled rejection — an application
|
|
59
|
+
* error, in many apps a full error overlay — for what is really just a send that did not land. So a
|
|
60
|
+
* rejection is treated as a FAILED send: nothing was delivered, the draft is kept, and the failure
|
|
61
|
+
* is handed to `onError` so the caller can still see it. It is never swallowed silently AND never
|
|
62
|
+
* reported as a success: anything other than a resolved `true` is a failure.
|
|
63
|
+
*
|
|
64
|
+
* Lives here so both bindings behave identically and so the rejection path is testable without a
|
|
65
|
+
* DOM.
|
|
66
|
+
*/
|
|
67
|
+
export declare function resolveSend(attempt: () => boolean | Promise<boolean>, onError?: (error: unknown) => void): Promise<boolean>;
|
|
68
|
+
/**
|
|
69
|
+
* The draft clears ONLY on a delivered/queued success AND only when the field still holds exactly
|
|
70
|
+
* the submitted body — the field stays editable while the request is in flight, so text typed
|
|
71
|
+
* meanwhile is a NEWER draft a completing send must not erase. A failed send keeps the text either
|
|
72
|
+
* way, so the user retries without retyping.
|
|
73
|
+
*/
|
|
74
|
+
export declare function clearDraftOnSend(sent: boolean, submitted: string, current: string): boolean;
|
|
75
|
+
/** Textarea key semantics: Enter=send, Shift/Alt-Enter=newline, IME composition suppressed. */
|
|
76
|
+
export type KeyAction = "send" | "newline" | "none";
|
|
77
|
+
/**
|
|
78
|
+
* HONESTY INVARIANT: an in-progress IME composition NEVER sends — `isComposing` short-circuits to
|
|
79
|
+
* `none` so a CJK/dead-key commit press can't fire a delivery the user did not ask for.
|
|
80
|
+
*/
|
|
81
|
+
export declare function keyAction(ev: unknown): KeyAction;
|
|
82
|
+
/** The send button's label. */
|
|
83
|
+
export declare const SEND_BUTTON_LABEL = "Send";
|
|
84
|
+
/**
|
|
85
|
+
* Every structural class the send bar renders. Named here — not typed literally in a binding — so
|
|
86
|
+
* the Preact and React bindings cannot drift.
|
|
87
|
+
*
|
|
88
|
+
* DEVIATION FROM THE DESIGN CARD (deliberate, carried over from the shipped implementation this was
|
|
89
|
+
* extracted from): the card draws the delivery class as a `<select>`. The shipped bar uses a
|
|
90
|
+
* two-button segmented control with `aria-pressed`, because with exactly two mutually exclusive
|
|
91
|
+
* options both are visible at once — the current class is readable without opening a menu, which is
|
|
92
|
+
* what a delivery decision needs. Everything else in the bar follows the card.
|
|
93
|
+
*/
|
|
94
|
+
export declare const SENDBAR_CLASSES: {
|
|
95
|
+
readonly wrap: "my-sendbar-wrap";
|
|
96
|
+
readonly segment: "my-sendbar__seg";
|
|
97
|
+
readonly input: "my-sendbar__input";
|
|
98
|
+
readonly send: "my-sendbar__send";
|
|
99
|
+
readonly hint: "my-sendbar__hint";
|
|
100
|
+
readonly notice: "my-sendbar__notice";
|
|
101
|
+
};
|
|
102
|
+
/** The bar wrapper: `my-sendbar`, plus `is-disabled` when the whole bar is unusable. */
|
|
103
|
+
export declare function sendBarClass(disabled: boolean): string;
|
|
104
|
+
/** One delivery-class segment button; `is-on` marks the current selection. */
|
|
105
|
+
export declare function deliveryClassButtonClass(selected: boolean): string;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// @mythicalos/ui-core — the send bar half of the terminal set (ds/components-terminal, spec v2):
|
|
2
|
+
// delivery-class selection, draft/keyboard semantics, and the honest per-class hint. Pure — both
|
|
3
|
+
// the Preact and React bindings derive every class string and every user-visible string from here,
|
|
4
|
+
// so the two bindings (and any future one) cannot drift on the copy that carries the honesty.
|
|
5
|
+
export const DELIVERY_CLASSES = ["asap", "on-done"];
|
|
6
|
+
/** The class a bar starts on when the caller expresses no preference. One definition, so the two
|
|
7
|
+
* bindings cannot ship different defaults. */
|
|
8
|
+
export const DEFAULT_DELIVERY_CLASS = "asap";
|
|
9
|
+
/** Verbatim badge/button label for a delivery class. */
|
|
10
|
+
export function deliveryClassLabel(cls) {
|
|
11
|
+
return cls === "asap" ? "ASAP" : "ON-DONE";
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* HONESTY INVARIANT (binding, do not reword): delivery ALWAYS waits for a turn boundary. The design
|
|
15
|
+
* card's older per-class wording — "ASAP interrupts · ON-DONE waits" — is FALSE: nothing about ASAP
|
|
16
|
+
* interrupts a turn in flight, it merely takes the FIRST gap between turns. This one combined line
|
|
17
|
+
* replaces both of the card's per-class hints. Never reintroduce "interrupts" here.
|
|
18
|
+
*/
|
|
19
|
+
export const DELIVERY_HINT = "ASAP takes the first turn gap · ON-DONE waits for full idle.";
|
|
20
|
+
/** Enabled placeholder when the caller supplies no target name. */
|
|
21
|
+
export const SEND_PLACEHOLDER = "Message… (⏎ send · ⇧⏎ newline)";
|
|
22
|
+
/** Last-resort disabled placeholder when the caller supplies no reason. Says only that sending is
|
|
23
|
+
* unavailable — it never speculates about why. */
|
|
24
|
+
export const SEND_DISABLED_FALLBACK = "Sending is unavailable.";
|
|
25
|
+
/**
|
|
26
|
+
* Placeholder copy. Disabled ⇒ the caller's honest reason (or the neutral fallback); enabled ⇒ the
|
|
27
|
+
* design's named prompt `Message {name}… (⏎ send · ⇧⏎ newline)` when a target name is supplied,
|
|
28
|
+
* otherwise the nameless form. A disabled bar NEVER shows the enabled prompt.
|
|
29
|
+
*/
|
|
30
|
+
export function sendPlaceholder(disabled, disabledReason, targetName) {
|
|
31
|
+
if (disabled)
|
|
32
|
+
return disabledReason ?? SEND_DISABLED_FALLBACK;
|
|
33
|
+
const name = targetName?.trim();
|
|
34
|
+
return name ? `Message ${name}… (⏎ send · ⇧⏎ newline)` : SEND_PLACEHOLDER;
|
|
35
|
+
}
|
|
36
|
+
/** Send is possible only with non-blank text, when enabled and not mid-send. */
|
|
37
|
+
export function canSend(text, disabled, busy) {
|
|
38
|
+
return !disabled && !busy && text.trim().length > 0;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The ASAP/ON-DONE semantics line renders ONLY where the delivery controls are actually usable — a
|
|
42
|
+
* disabled bar surfaces just the truthful reason (through the placeholder), never per-class
|
|
43
|
+
* semantics for controls the user cannot operate.
|
|
44
|
+
*/
|
|
45
|
+
export function showDeliveryHint(disabled) {
|
|
46
|
+
return !disabled;
|
|
47
|
+
}
|
|
48
|
+
export function makeSendGate() {
|
|
49
|
+
let held = false;
|
|
50
|
+
return {
|
|
51
|
+
tryAcquire() {
|
|
52
|
+
if (held)
|
|
53
|
+
return false;
|
|
54
|
+
held = true;
|
|
55
|
+
return true;
|
|
56
|
+
},
|
|
57
|
+
release() {
|
|
58
|
+
held = false;
|
|
59
|
+
},
|
|
60
|
+
inFlight() {
|
|
61
|
+
return held;
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Resolve one send attempt to a plain success/failure.
|
|
67
|
+
*
|
|
68
|
+
* `onSend` is contracted to resolve a boolean, but it is caller code and may REJECT. Letting that
|
|
69
|
+
* rejection escape an async event handler surfaces it as an unhandled rejection — an application
|
|
70
|
+
* error, in many apps a full error overlay — for what is really just a send that did not land. So a
|
|
71
|
+
* rejection is treated as a FAILED send: nothing was delivered, the draft is kept, and the failure
|
|
72
|
+
* is handed to `onError` so the caller can still see it. It is never swallowed silently AND never
|
|
73
|
+
* reported as a success: anything other than a resolved `true` is a failure.
|
|
74
|
+
*
|
|
75
|
+
* Lives here so both bindings behave identically and so the rejection path is testable without a
|
|
76
|
+
* DOM.
|
|
77
|
+
*/
|
|
78
|
+
export async function resolveSend(attempt, onError) {
|
|
79
|
+
try {
|
|
80
|
+
return (await attempt()) === true;
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
onError?.(error);
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The draft clears ONLY on a delivered/queued success AND only when the field still holds exactly
|
|
89
|
+
* the submitted body — the field stays editable while the request is in flight, so text typed
|
|
90
|
+
* meanwhile is a NEWER draft a completing send must not erase. A failed send keeps the text either
|
|
91
|
+
* way, so the user retries without retyping.
|
|
92
|
+
*/
|
|
93
|
+
export function clearDraftOnSend(sent, submitted, current) {
|
|
94
|
+
return sent === true && submitted === current;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* HONESTY INVARIANT: an in-progress IME composition NEVER sends — `isComposing` short-circuits to
|
|
98
|
+
* `none` so a CJK/dead-key commit press can't fire a delivery the user did not ask for.
|
|
99
|
+
*/
|
|
100
|
+
export function keyAction(ev) {
|
|
101
|
+
const e = ev;
|
|
102
|
+
if (!e || e.key !== "Enter" || e.isComposing)
|
|
103
|
+
return "none";
|
|
104
|
+
if (e.shiftKey || e.altKey)
|
|
105
|
+
return "newline";
|
|
106
|
+
return "send";
|
|
107
|
+
}
|
|
108
|
+
// ── class derivation ──
|
|
109
|
+
/** The send button's label. */
|
|
110
|
+
export const SEND_BUTTON_LABEL = "Send";
|
|
111
|
+
/**
|
|
112
|
+
* Every structural class the send bar renders. Named here — not typed literally in a binding — so
|
|
113
|
+
* the Preact and React bindings cannot drift.
|
|
114
|
+
*
|
|
115
|
+
* DEVIATION FROM THE DESIGN CARD (deliberate, carried over from the shipped implementation this was
|
|
116
|
+
* extracted from): the card draws the delivery class as a `<select>`. The shipped bar uses a
|
|
117
|
+
* two-button segmented control with `aria-pressed`, because with exactly two mutually exclusive
|
|
118
|
+
* options both are visible at once — the current class is readable without opening a menu, which is
|
|
119
|
+
* what a delivery decision needs. Everything else in the bar follows the card.
|
|
120
|
+
*/
|
|
121
|
+
export const SENDBAR_CLASSES = {
|
|
122
|
+
wrap: "my-sendbar-wrap",
|
|
123
|
+
segment: "my-sendbar__seg",
|
|
124
|
+
input: "my-sendbar__input",
|
|
125
|
+
send: "my-sendbar__send",
|
|
126
|
+
hint: "my-sendbar__hint",
|
|
127
|
+
notice: "my-sendbar__notice",
|
|
128
|
+
};
|
|
129
|
+
/** The bar wrapper: `my-sendbar`, plus `is-disabled` when the whole bar is unusable. */
|
|
130
|
+
export function sendBarClass(disabled) {
|
|
131
|
+
return disabled ? "my-sendbar is-disabled" : "my-sendbar";
|
|
132
|
+
}
|
|
133
|
+
/** One delivery-class segment button; `is-on` marks the current selection. */
|
|
134
|
+
export function deliveryClassButtonClass(selected) {
|
|
135
|
+
return selected ? "my-sendbar__cls is-on" : "my-sendbar__cls";
|
|
136
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/** Product-tunable context thresholds (percent). Defaults per the design card's fixed ticks. */
|
|
2
|
+
export interface CtxThresholds {
|
|
3
|
+
/** ≥ this reading is `warn`. */
|
|
4
|
+
warn: number;
|
|
5
|
+
/** ≥ this reading is `error` ("critical"). */
|
|
6
|
+
critical: number;
|
|
7
|
+
}
|
|
8
|
+
/** The design card's tick positions: warn at 75%, critical at 90% — overridable per product. */
|
|
9
|
+
export declare const CTX_THRESHOLDS_DEFAULT: CtxThresholds;
|
|
10
|
+
/**
|
|
11
|
+
* Normalize a product's threshold pair ONCE, so the band and the bar's ticks can never disagree
|
|
12
|
+
* about where the thresholds are. Two corrections:
|
|
13
|
+
*
|
|
14
|
+
* · A member that is not a finite number strictly inside the rail (0 < t < 100) is not a usable
|
|
15
|
+
* threshold — it marks nothing the meter can draw — so it falls back to the design default
|
|
16
|
+
* rather than silently reclassifying every reading. (`{warn: NaN}` must not quietly make every
|
|
17
|
+
* session look nominal; `{warn: -5}` must not quietly make every session look hot while
|
|
18
|
+
* drawing no tick to explain why.)
|
|
19
|
+
* · The pair is ORDERED (`warn` ≤ `critical`). A mis-ordered pair leaves the higher tick
|
|
20
|
+
* unreachable — with `{warn: 90, critical: 75}` an 80% reading is already `error` while a
|
|
21
|
+
* `warn` tick still sits at 90, so the bar marks a boundary that can never be crossed. Both
|
|
22
|
+
* boundaries the product asked for are kept; only which one is named `warn` is corrected.
|
|
23
|
+
*/
|
|
24
|
+
export declare function normalizeCtxThresholds(thresholds?: CtxThresholds | null): CtxThresholds;
|
|
25
|
+
/**
|
|
26
|
+
* The context band. `"unknown"` is a FIRST-CLASS member, not a fallback to `"ok"`: a session with
|
|
27
|
+
* no reading (undefined/null/NaN/±Infinity) belongs to no health band — it is neither healthy nor
|
|
28
|
+
* hot (invariant 1).
|
|
29
|
+
*/
|
|
30
|
+
export type CtxBand = "unknown" | "ok" | "warn" | "error";
|
|
31
|
+
/**
|
|
32
|
+
* The ONE number the card works from: the reading clamped to 0–100, at FULL precision, or
|
|
33
|
+
* `undefined` when there is no reading. The band and the fill are derived from this — the
|
|
34
|
+
* threshold comparison is against what was actually reported, so an 89.5% session has NOT crossed
|
|
35
|
+
* a 90% critical line. The LABEL then adapts its precision to this same number (see
|
|
36
|
+
* `ctxValueText`) so what the card displays can never contradict what it claims.
|
|
37
|
+
*/
|
|
38
|
+
export declare function ctxReading(pct: number | null | undefined): number | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Band for a context reading, derived from the reading AS REPORTED — 89.5 has not crossed a 90
|
|
41
|
+
* critical line and must not be shown as though it had. Absent ⇒ `"unknown"`. The thresholds are
|
|
42
|
+
* normalized (and ordered) first, so the band and the bar's ticks always agree.
|
|
43
|
+
*/
|
|
44
|
+
export declare function ctxBand(pct: number | null | undefined, thresholds?: CtxThresholds | null): CtxBand;
|
|
45
|
+
/**
|
|
46
|
+
* The 0–100 fill length, or `undefined` when there is no reading. `undefined` means the binding
|
|
47
|
+
* draws NO fill at all — the caller must never substitute 0, which would render as a confident
|
|
48
|
+
* "empty context" (invariant 1).
|
|
49
|
+
*/
|
|
50
|
+
export declare function ctxFillPct(pct: number | null | undefined): number | undefined;
|
|
51
|
+
/** What an absent reading renders as — never `"0%"`. */
|
|
52
|
+
export declare const CTX_UNKNOWN_TEXT = "\u2014";
|
|
53
|
+
/**
|
|
54
|
+
* The meter's right-hand value: `"62%"` for a reading, `"—"` for none (invariant 1).
|
|
55
|
+
*
|
|
56
|
+
* Whole percents are what the design card shows, but a rounded percent must never contradict the
|
|
57
|
+
* band the card is enforcing: at the default thresholds, 89.5% is `warn`, so displaying it as
|
|
58
|
+
* "90%" would put an amber card next to the critical number. The label therefore takes the FIRST
|
|
59
|
+
* precision whose displayed number lands in the same band as the reading — "89.5%" here, plain
|
|
60
|
+
* "62%" in the ordinary case — falling back to the reading verbatim, which by definition agrees.
|
|
61
|
+
*/
|
|
62
|
+
export declare function ctxValueText(pct: number | null | undefined, thresholds?: CtxThresholds | null): string;
|
|
63
|
+
/** The meter's left-hand label and the separator the card composes its notes with. */
|
|
64
|
+
export declare const CTX_LABEL = "context";
|
|
65
|
+
export declare const SESSION_CARD_SEP = " \u00B7 ";
|
|
66
|
+
/**
|
|
67
|
+
* The meter's left-hand note. The design card annotates the band (`context · distill suggested` at
|
|
68
|
+
* warn, `context · distill now` at error, `context · stale` for a stale reading); the unknown state
|
|
69
|
+
* says so outright, and says it BEFORE `stale` — "stale" implies a last-known value, so it must
|
|
70
|
+
* never stand in for "we never measured it".
|
|
71
|
+
*/
|
|
72
|
+
export declare function ctxNoteText(band: CtxBand, opts?: {
|
|
73
|
+
stale?: boolean;
|
|
74
|
+
} | null): string;
|
|
75
|
+
/** Wrapper class for the meter — the band modifier tints the fill AND the value in one place. */
|
|
76
|
+
export declare function ctxMeterClass(input?: {
|
|
77
|
+
band: CtxBand;
|
|
78
|
+
stale?: boolean;
|
|
79
|
+
} | null): string;
|
|
80
|
+
/** The bar's user-space width: percent IS the x axis, so a reading maps 1:1 onto it. */
|
|
81
|
+
export declare const CTX_BAR_SPAN = 100;
|
|
82
|
+
/** The bar's user-space height (the design card's 8px rail). */
|
|
83
|
+
export declare const CTX_BAR_HEIGHT = 8;
|
|
84
|
+
/** `viewBox` for the bar; the binding stretches it to the card width (`preserveAspectRatio="none"`). */
|
|
85
|
+
export declare const CTX_BAR_VIEWBOX = "0 0 100 8";
|
|
86
|
+
/** Tick width in user space (≈1.5px once the bar is stretched to a ~250px card). */
|
|
87
|
+
export declare const CTX_BAR_TICK_WIDTH = 0.6;
|
|
88
|
+
export interface CtxBarTick {
|
|
89
|
+
/** The threshold this tick marks. */
|
|
90
|
+
pct: number;
|
|
91
|
+
/** Left edge in user space — centred on `pct`, then kept inside the rail. */
|
|
92
|
+
x: number;
|
|
93
|
+
width: number;
|
|
94
|
+
}
|
|
95
|
+
export interface CtxBarGeom {
|
|
96
|
+
span: number;
|
|
97
|
+
height: number;
|
|
98
|
+
/** Fill width in user space. `undefined` ⇒ draw no fill rect at all (invariant 1). */
|
|
99
|
+
fill: number | undefined;
|
|
100
|
+
/** In-range, de-duplicated, ascending threshold ticks. */
|
|
101
|
+
ticks: CtxBarTick[];
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Pure bar geometry, from the SAME normalized thresholds `ctxBand` uses — an unusable threshold
|
|
105
|
+
* falls back to the default in both places, so the ticks always mark the boundaries the band is
|
|
106
|
+
* actually enforcing. Duplicates collapse, so `{warn: 90, critical: 90}` draws one tick, not two
|
|
107
|
+
* stacked ones.
|
|
108
|
+
*/
|
|
109
|
+
export declare function ctxBarGeom(pct: number | null | undefined, thresholds?: CtxThresholds | null): CtxBarGeom;
|
|
110
|
+
/** Lifecycle claims a product can make. Absent ⇒ the product reported no lifecycle. */
|
|
111
|
+
export type SessionLifecycle = "spawning" | "active" | "stopping" | "stopped" | "failed" | "paused";
|
|
112
|
+
/** Activity claims a product can make. Absent ⇒ NOT reported — never collapsed to `idle`. */
|
|
113
|
+
export type SessionActivity = "working" | "idle";
|
|
114
|
+
/** Colour axis. `"unknown"` is its own tone: the card renders a HOLLOW dot for it, so an
|
|
115
|
+
* unmeasured session cannot be mistaken for a solid-dot claim of any kind. */
|
|
116
|
+
export type SessionStatusTone = "ok" | "warn" | "error" | "info" | "muted" | "unknown";
|
|
117
|
+
export type SessionStatusKey = "working" | "idle" | "context-high" | "context-critical" | "spawning" | "active" | "stopping" | "stopped" | "failed" | "paused" | "disconnected" | "unknown";
|
|
118
|
+
export interface SessionStatus {
|
|
119
|
+
/** Machine-readable outcome — what the card decided, independent of wording. */
|
|
120
|
+
key: SessionStatusKey;
|
|
121
|
+
/** Default wording; the binding lets a product override it with its own honest phrasing. */
|
|
122
|
+
label: string;
|
|
123
|
+
tone: SessionStatusTone;
|
|
124
|
+
/** ≤1/s dot pulse — transient states only; a steady state never fakes motion. */
|
|
125
|
+
pulse: boolean;
|
|
126
|
+
}
|
|
127
|
+
/** Everything the card needs to state a session's status. Every field is OPTIONAL, and every
|
|
128
|
+
* absent field means "the product did not report this" — never a default claim. */
|
|
129
|
+
export interface SessionStatusInput {
|
|
130
|
+
lifecycle?: SessionLifecycle;
|
|
131
|
+
activity?: SessionActivity;
|
|
132
|
+
/** The session's link/wake readiness. `false` ⇒ down; `undefined` ⇒ not reported. */
|
|
133
|
+
connected?: boolean;
|
|
134
|
+
}
|
|
135
|
+
/** The honest "we were told nothing" status — distinct key AND distinct tone from `idle`. */
|
|
136
|
+
export declare const SESSION_STATUS_UNKNOWN: SessionStatus;
|
|
137
|
+
/**
|
|
138
|
+
* Derive the card's status. Precedence, and why:
|
|
139
|
+
*
|
|
140
|
+
* 1. A DOWN LINK on a session the product calls `active` — or on one whose lifecycle it does not
|
|
141
|
+
* report at all — wins: whatever else was claimed, we are not hearing from it. It does NOT
|
|
142
|
+
* override `spawning`/`stopping`/`stopped`/`failed`, whose own lifecycle already explains the
|
|
143
|
+
* missing link. The label is `"disconnected"`, not the design prototype's "reconnecting…":
|
|
144
|
+
* retry machinery is a product capability this atom cannot verify (a product that really does
|
|
145
|
+
* retry can pass its own `statusLabel`).
|
|
146
|
+
* 2. An ACTIVITY claim, but only for a session that is `active` or has no reported lifecycle —
|
|
147
|
+
* a "working" claim about a stopped session is a contradiction, and the lifecycle is the
|
|
148
|
+
* stronger truth.
|
|
149
|
+
* 3. The LIFECYCLE claim. This is where an active-but-no-activity session lands: `"active"`,
|
|
150
|
+
* never a fabricated `"idle"` or `"working"` (invariant 2).
|
|
151
|
+
* 4. Nothing reported at all ⇒ `"unknown"` (invariant 2).
|
|
152
|
+
*/
|
|
153
|
+
export declare function sessionStatus(input?: SessionStatusInput | null, contextBand?: CtxBand | null): SessionStatus;
|
|
154
|
+
/** Status class: tone modifier + the transient-pulse flag. The dot is a child element, so the
|
|
155
|
+
* modifier tints the words and the dot together. */
|
|
156
|
+
export declare function sessionStatusClass(status: SessionStatus): string;
|
|
157
|
+
/**
|
|
158
|
+
* Whether the card wears the design's STALE treatment (dashed border, muted values) — true exactly
|
|
159
|
+
* when the link is down. Kept here rather than in the binding so both bindings, and any product
|
|
160
|
+
* that wants to pre-compute it, agree on one rule.
|
|
161
|
+
*/
|
|
162
|
+
export declare function sessionCardIsStale(status: SessionStatus): boolean;
|
|
163
|
+
/**
|
|
164
|
+
* The card's stale treatment, including a product's own claim. A product may ADD staleness (its
|
|
165
|
+
* data is old for a reason the card cannot see); it may NOT take away the staleness a down link
|
|
166
|
+
* implies — `stale={false}` on a disconnected session would paint it as a live, solid-bordered
|
|
167
|
+
* card, which is exactly the lie this treatment exists to prevent.
|
|
168
|
+
*/
|
|
169
|
+
export declare function sessionCardStale(status: SessionStatus, productClaim?: boolean): boolean;
|
|
170
|
+
/**
|
|
171
|
+
* The words beside the dot. A product may substitute its own honest phrasing for the derived
|
|
172
|
+
* label — the tone, the pulse and the stale treatment still come from the derivation.
|
|
173
|
+
*
|
|
174
|
+
* Three refusals:
|
|
175
|
+
* · a BLANK override falls back to the derived label, rather than leaving the status as colour
|
|
176
|
+
* alone (token rule #7);
|
|
177
|
+
* · the `unknown` status ignores the override entirely. There is no alternative honest wording
|
|
178
|
+
* for "the product told us nothing" — nothing was claimed, so there is nothing to reword;
|
|
179
|
+
* · an override that states another status's claim is ignored. `statusLabel="idle"` — or
|
|
180
|
+
* `"currently idle"` — on a session whose activity was never reported would state a claim no
|
|
181
|
+
* wire made; invariant 2 is structural here, not a matter of trusting the caller. The two
|
|
182
|
+
* ACTIVITY words are refused anywhere in the override; every other reserved word is refused
|
|
183
|
+
* only as the whole of it. Rewording is still free: anything outside that vocabulary
|
|
184
|
+
* (`"wake unavailable"`, `"stopped (exit 1)"`, …) passes.
|
|
185
|
+
*
|
|
186
|
+
* Documented limit: a status the context escalation has taken over (`context high` /
|
|
187
|
+
* `context critical`) can no longer use an activity word either, even if the wire did report one —
|
|
188
|
+
* the rendered status is the context claim, and the product can word it another way.
|
|
189
|
+
*/
|
|
190
|
+
export declare function sessionStatusText(status: SessionStatus, override?: string | null): string;
|
|
191
|
+
/** Container class — the WHOLE class attribute the card's root carries, including any extra
|
|
192
|
+
* classes a product passes, so neither binding composes a class string of its own. `selected` and
|
|
193
|
+
* `stale` are INDEPENDENT: a selected session can also be disconnected, so they are two
|
|
194
|
+
* modifiers, never one enum. */
|
|
195
|
+
export declare function sessionCardClass(input?: {
|
|
196
|
+
selected?: boolean;
|
|
197
|
+
stale?: boolean;
|
|
198
|
+
extra?: string | null;
|
|
199
|
+
} | null): string;
|
|
200
|
+
/** What an unnameable session's avatar shows — a question mark, not a fabricated initial. */
|
|
201
|
+
export declare const SESSION_AVATAR_UNKNOWN = "?";
|
|
202
|
+
/** Avatar initial: the first alphanumeric of the display name, uppercased; `"?"` when there is
|
|
203
|
+
* none. (Merges the two divergent copies the product carried — the rail card's "first char,
|
|
204
|
+
* em-dash fallback" and derive.ts's "first alphanumeric, ?-fallback" — on the latter, which
|
|
205
|
+
* cannot emit punctuation or whitespace as an initial.) */
|
|
206
|
+
export declare function sessionAvatarInitial(name: string | null | undefined): string;
|
|
207
|
+
/**
|
|
208
|
+
* The card's meta subline — `role · model · duration` on the design card, but deliberately a free
|
|
209
|
+
* list: the spec's own states also use it for `role · model · queued: 1 ON-DONE` and
|
|
210
|
+
* `role · last seen 00:12 ago`. Absent/blank parts COLLAPSE, so there is never a dangling
|
|
211
|
+
* separator, and an all-absent list yields `""` (the binding then omits the element entirely
|
|
212
|
+
* rather than rendering an empty row).
|
|
213
|
+
*/
|
|
214
|
+
export declare function sessionSubline(parts?: readonly (string | null | undefined)[] | null): string;
|
|
215
|
+
export interface SessionSpine {
|
|
216
|
+
/** Distills completed. `undefined` ⇒ the product did not report it — the strip is OMITTED. */
|
|
217
|
+
distills?: number;
|
|
218
|
+
/** Tokens saved. `undefined` ⇒ not reported — renders `"—"`, never `"0 tok"`. */
|
|
219
|
+
savedTok?: number;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* The most nodes the strip will draw. The strip is a fixed-width row of 13px nodes, so beyond this
|
|
223
|
+
* they cannot be told apart anyway — and a runaway count from a malformed wire value must not be
|
|
224
|
+
* able to build an unbounded node list and lock the render. The LABEL beside the strip always
|
|
225
|
+
* states the TRUE count, so the bound costs no honesty: the strip is a bounded illustration, the
|
|
226
|
+
* number is the claim.
|
|
227
|
+
*/
|
|
228
|
+
export declare const SPINE_MAX_NODES = 24;
|
|
229
|
+
export interface SpineNode {
|
|
230
|
+
/** The live tip (hollow); every other node is a completed distill (filled). */
|
|
231
|
+
tip: boolean;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Nodes for the strip: one filled node per completed distill, plus the hollow live tip.
|
|
235
|
+
* An UNREPORTED `distills` yields `[]` — no strip, no fabricated "0 distills" (invariant 1). A
|
|
236
|
+
* REPORTED 0 is a real reading and yields the tip alone. Non-integer/negative counts are floored,
|
|
237
|
+
* and the list is capped at `SPINE_MAX_NODES` so a runaway value cannot lock the render (the
|
|
238
|
+
* strip's label still states the true count).
|
|
239
|
+
*/
|
|
240
|
+
export declare function sessionSpineNodes(distills: number | null | undefined): SpineNode[];
|
|
241
|
+
/** What an unreported token saving renders as. */
|
|
242
|
+
export declare const SPINE_SAVED_UNKNOWN = "\u2014";
|
|
243
|
+
/**
|
|
244
|
+
* The strip's right-hand value. Absent ⇒ `"—"` (invariant 1). A saving is a REDUCTION, so it
|
|
245
|
+
* carries a real minus sign (U+2212); a reported 0 is unsigned; a negative input (context grew)
|
|
246
|
+
* is stated as growth rather than silently re-signed.
|
|
247
|
+
*/
|
|
248
|
+
export declare function sessionSpineSavedText(savedTok: number | null | undefined): string;
|
|
249
|
+
/** The strip's left-hand label: `spine · 3 distills` (singular at 1). */
|
|
250
|
+
export declare function sessionSpineLabel(distills: number): string;
|
|
251
|
+
export interface SpineSummary {
|
|
252
|
+
nodes: SpineNode[];
|
|
253
|
+
label: string;
|
|
254
|
+
value: string;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* The whole strip, or `undefined` when the product reported no distill count — the card then
|
|
258
|
+
* renders no strip at all. `spine.savedTok` is independent: a reported distill count with an
|
|
259
|
+
* unreported saving still draws the strip, with `"—"` for the value.
|
|
260
|
+
*/
|
|
261
|
+
export declare function sessionSpineSummary(spine: SessionSpine | null | undefined): SpineSummary | undefined;
|
|
262
|
+
/** Class for one spine node — filled distill vs. the hollow live tip. */
|
|
263
|
+
export declare function spineNodeClass(node: SpineNode): string;
|