@sigrun/shared 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +9 -2
- package/dist/access.d.ts +55 -0
- package/dist/access.js +72 -0
- package/dist/ambient.d.ts +111 -0
- package/dist/ambient.js +23 -0
- package/dist/blocks/format.d.ts +28 -0
- package/dist/blocks/format.js +199 -0
- package/dist/blocks/icons.d.ts +12 -0
- package/dist/blocks/icons.js +64 -0
- package/dist/blocks/index.d.ts +24 -0
- package/dist/blocks/index.js +20 -0
- package/dist/blocks/lttb.d.ts +16 -0
- package/dist/blocks/lttb.js +67 -0
- package/dist/blocks/ops.d.ts +44 -0
- package/dist/blocks/ops.js +294 -0
- package/dist/blocks/text.d.ts +43 -0
- package/dist/blocks/text.js +790 -0
- package/dist/blocks/transport.d.ts +77 -0
- package/dist/blocks/transport.js +11 -0
- package/dist/blocks/types.d.ts +541 -0
- package/dist/blocks/types.js +17 -0
- package/dist/commands.d.ts +75 -0
- package/dist/commands.js +25 -0
- package/dist/conversations.d.ts +31 -0
- package/dist/conversations.js +94 -0
- package/dist/interactions.d.ts +91 -0
- package/dist/interactions.js +21 -0
- package/dist/jobs.d.ts +109 -0
- package/dist/jobs.js +19 -0
- package/dist/json.d.ts +20 -0
- package/dist/json.js +13 -0
- package/dist/live.d.ts +71 -0
- package/dist/live.js +82 -0
- package/dist/session-meta.d.ts +119 -0
- package/dist/session-meta.js +13 -0
- package/dist/settings.d.ts +408 -0
- package/dist/settings.js +19 -0
- package/dist/themes.d.ts +50 -0
- package/dist/themes.js +156 -0
- package/dist/turns.d.ts +119 -0
- package/dist/turns.js +197 -0
- package/dist/view-delta.d.ts +54 -0
- package/dist/view-delta.js +135 -0
- package/dist/views.d.ts +784 -0
- package/dist/views.js +38 -0
- package/package.json +90 -5
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-10-21
|
|
11
|
+
### Added
|
|
12
|
+
- First published release of the shared leaf (workspace package of
|
|
13
|
+
[@sigrun/sigrun](https://github.com/sigrun-ai/sigrun)): theme tokens,
|
|
14
|
+
conversation/turn shaping, the channel wire types, session-meta,
|
|
15
|
+
commands, interactions, ambient records, access, settings, views,
|
|
16
|
+
view-delta, and jobs — the surfaces the webui, TUI, and server share.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Michael MacLeod
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @sigrun/shared
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The pure client-shared leaf of [sigrun](https://github.com/sigrun-ai/sigrun):
|
|
4
|
+
theme tokens, turn/conversation shaping, and the channel wire types —
|
|
5
|
+
consumed by the webui, the TUI, and the server. Zero runtime dependencies;
|
|
6
|
+
every subpath is a types+code pair (`@sigrun/shared/themes`, `/conversations`,
|
|
7
|
+
`/turns`, …) exported from `dist`.
|
|
8
|
+
|
|
9
|
+
Part of the sigrun workspace; published from the repository root as
|
|
10
|
+
`@sigrun/shared` (see the root `CHANGELOG.md` for release history).
|
package/dist/access.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-resource sharing and content access (core hardening W1 Task 5, spec
|
|
3
|
+
* §5.9 ruling 6/10: per-resource sharing; groups cut).
|
|
4
|
+
*
|
|
5
|
+
* Part of the shared leaf package (`@sigrun/shared`): the server computes
|
|
6
|
+
* access through `accessOf`, the clients render the `sharing` state — one
|
|
7
|
+
* source of truth, no drift. PURE module: no imports at all (the
|
|
8
|
+
* views-purity guard enforces this across `shared/src`).
|
|
9
|
+
*
|
|
10
|
+
* The rules, in order (first match wins):
|
|
11
|
+
*
|
|
12
|
+
* 1. superuser → write;
|
|
13
|
+
* 2. ownerless resource → household write (everyone);
|
|
14
|
+
* 3. user === owner → write;
|
|
15
|
+
* 4. household sharing with a defined user → the shared level;
|
|
16
|
+
* 5. people sharing including the user → the shared level;
|
|
17
|
+
* 6. otherwise none (hidden: 404 on every :id route, absent from lists).
|
|
18
|
+
*
|
|
19
|
+
* Anonymous callers (a principal with no user) get nothing from a shared
|
|
20
|
+
* resource: sharing widens access for identified users only (rule 4/5
|
|
21
|
+
* require a defined user), while ownerless stays open to everyone by
|
|
22
|
+
* rule 2.
|
|
23
|
+
*/
|
|
24
|
+
/** What a conversation's owner grants beyond themselves. */
|
|
25
|
+
export type Sharing = {
|
|
26
|
+
readonly scope: "private";
|
|
27
|
+
} | {
|
|
28
|
+
readonly scope: "household";
|
|
29
|
+
readonly level: "read" | "write";
|
|
30
|
+
} | {
|
|
31
|
+
readonly scope: "people";
|
|
32
|
+
readonly users: string[];
|
|
33
|
+
readonly level: "read" | "write";
|
|
34
|
+
};
|
|
35
|
+
/** The resolved access level of a principal to one resource. */
|
|
36
|
+
export type Access = "none" | "read" | "write";
|
|
37
|
+
/** What accessOf needs of the caller (the server's Principal projects here). */
|
|
38
|
+
export interface AccessPrincipal {
|
|
39
|
+
readonly user?: string;
|
|
40
|
+
readonly superuser?: boolean;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Resolve one principal's access to one resource. Rule order per the
|
|
44
|
+
* module doc; the first matching rule wins.
|
|
45
|
+
*/
|
|
46
|
+
export declare function accessOf(p: AccessPrincipal, r: {
|
|
47
|
+
readonly owner?: string;
|
|
48
|
+
readonly sharing?: Sharing;
|
|
49
|
+
}): Access;
|
|
50
|
+
/**
|
|
51
|
+
* Type guard for the wire value: every scope shape checked strictly
|
|
52
|
+
* (wrong field types are not a Sharing). An empty `people.users` list is
|
|
53
|
+
* valid (shares with nobody yet) — validation is about shape, not policy.
|
|
54
|
+
*/
|
|
55
|
+
export declare function isSharing(value: unknown): value is Sharing;
|
package/dist/access.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-resource sharing and content access (core hardening W1 Task 5, spec
|
|
3
|
+
* §5.9 ruling 6/10: per-resource sharing; groups cut).
|
|
4
|
+
*
|
|
5
|
+
* Part of the shared leaf package (`@sigrun/shared`): the server computes
|
|
6
|
+
* access through `accessOf`, the clients render the `sharing` state — one
|
|
7
|
+
* source of truth, no drift. PURE module: no imports at all (the
|
|
8
|
+
* views-purity guard enforces this across `shared/src`).
|
|
9
|
+
*
|
|
10
|
+
* The rules, in order (first match wins):
|
|
11
|
+
*
|
|
12
|
+
* 1. superuser → write;
|
|
13
|
+
* 2. ownerless resource → household write (everyone);
|
|
14
|
+
* 3. user === owner → write;
|
|
15
|
+
* 4. household sharing with a defined user → the shared level;
|
|
16
|
+
* 5. people sharing including the user → the shared level;
|
|
17
|
+
* 6. otherwise none (hidden: 404 on every :id route, absent from lists).
|
|
18
|
+
*
|
|
19
|
+
* Anonymous callers (a principal with no user) get nothing from a shared
|
|
20
|
+
* resource: sharing widens access for identified users only (rule 4/5
|
|
21
|
+
* require a defined user), while ownerless stays open to everyone by
|
|
22
|
+
* rule 2.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Resolve one principal's access to one resource. Rule order per the
|
|
26
|
+
* module doc; the first matching rule wins.
|
|
27
|
+
*/
|
|
28
|
+
export function accessOf(p, r) {
|
|
29
|
+
// 1. The superuser always writes.
|
|
30
|
+
if (p.superuser === true)
|
|
31
|
+
return "write";
|
|
32
|
+
// 2. Ownerless is household-write for everyone.
|
|
33
|
+
if (r.owner === undefined)
|
|
34
|
+
return "write";
|
|
35
|
+
// 3. The owner always writes.
|
|
36
|
+
if (p.user !== undefined && p.user === r.owner)
|
|
37
|
+
return "write";
|
|
38
|
+
// 4./5. Sharing widens identified users only — an anonymous caller
|
|
39
|
+
// (no user) falls straight through to none.
|
|
40
|
+
if (p.user !== undefined && r.sharing !== undefined) {
|
|
41
|
+
if (r.sharing.scope === "household")
|
|
42
|
+
return r.sharing.level;
|
|
43
|
+
if (r.sharing.scope === "people" && r.sharing.users.includes(p.user)) {
|
|
44
|
+
return r.sharing.level;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// 6. Hidden.
|
|
48
|
+
return "none";
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Type guard for the wire value: every scope shape checked strictly
|
|
52
|
+
* (wrong field types are not a Sharing). An empty `people.users` list is
|
|
53
|
+
* valid (shares with nobody yet) — validation is about shape, not policy.
|
|
54
|
+
*/
|
|
55
|
+
export function isSharing(value) {
|
|
56
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
const record = value;
|
|
60
|
+
switch (record.scope) {
|
|
61
|
+
case "private":
|
|
62
|
+
return true;
|
|
63
|
+
case "household":
|
|
64
|
+
return record.level === "read" || record.level === "write";
|
|
65
|
+
case "people":
|
|
66
|
+
return (Array.isArray(record.users) &&
|
|
67
|
+
record.users.every((user) => typeof user === "string") &&
|
|
68
|
+
(record.level === "read" || record.level === "write"));
|
|
69
|
+
default:
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ambient seam TYPES (spec: docs/approval-ux-seams.md, "Ambient
|
|
3
|
+
* surfaces"): the channel-neutral record shapes for persistent
|
|
4
|
+
* read-mostly state — statusline slots and panel widgets — written by
|
|
5
|
+
* extensions through the host context and rendered by channels
|
|
6
|
+
* generically.
|
|
7
|
+
*
|
|
8
|
+
* The CANONICAL type declarations live here (the shared leaf package);
|
|
9
|
+
* `src/harness/ambient.ts` imports them (type-only) for the doc
|
|
10
|
+
* definitions and validation runtime. The records ride the §5 wire
|
|
11
|
+
* AS-IS (envelope + body + details — never projected), so these shapes
|
|
12
|
+
* are the channel contract too (see `@sigrun/shared/views`).
|
|
13
|
+
*
|
|
14
|
+
* Lifecycle difference from interactions: an ambient surface is replaced
|
|
15
|
+
* repeatedly (write = FULL state replacement, never a delta) and never
|
|
16
|
+
* blocks. One slot key is owned by exactly one extension (allocation,
|
|
17
|
+
* not silent composition — the pi-zentui chrome-skin lesson).
|
|
18
|
+
*
|
|
19
|
+
* Layering mirrors the interaction seam: strict envelope (key, place,
|
|
20
|
+
* budget, refresh, expires) around a body composed only from the closed
|
|
21
|
+
* ambient primitives (line, rows, gauge, ledger), with opaque `details`.
|
|
22
|
+
*/
|
|
23
|
+
import type { JsonObject } from "./json.js";
|
|
24
|
+
/** Where a channel places the surface. */
|
|
25
|
+
export type AmbientPlace = "status" | "footer" | "panel";
|
|
26
|
+
/** One widget row (primitive B). */
|
|
27
|
+
export type AmbientRow = {
|
|
28
|
+
readonly id: string;
|
|
29
|
+
readonly label: string;
|
|
30
|
+
/** Channel-schemed glyph state (todo statuses, agent run states). */
|
|
31
|
+
readonly status?: string;
|
|
32
|
+
/** Severity ordering/color: 0 = severe .. 2 = quiet. */
|
|
33
|
+
readonly severity?: 0 | 1 | 2;
|
|
34
|
+
/** Folded away by default (warden's quiet verdicts). */
|
|
35
|
+
readonly quiet?: boolean;
|
|
36
|
+
readonly detail?: string;
|
|
37
|
+
/** Nesting depth for tree rendering (pi-goal-x task tree). */
|
|
38
|
+
readonly depth?: number;
|
|
39
|
+
/** Dependency ids for marker rendering (rpiv-todo blockedBy). */
|
|
40
|
+
readonly deps?: string[];
|
|
41
|
+
/** Present-continuous spinner label while the row is active. */
|
|
42
|
+
readonly spinnerLabel?: string;
|
|
43
|
+
readonly metrics?: AmbientMetrics;
|
|
44
|
+
};
|
|
45
|
+
/** Numeric metrics with an optional derived-format hint. */
|
|
46
|
+
export type AmbientMetrics = {
|
|
47
|
+
readonly tokens?: number;
|
|
48
|
+
readonly cost?: number;
|
|
49
|
+
readonly current?: number;
|
|
50
|
+
readonly total?: number;
|
|
51
|
+
readonly completed?: number;
|
|
52
|
+
/** Derived display hint for numeric fields ("2.5k", currency) — the
|
|
53
|
+
* host-reuse lesson from every footer extension. */
|
|
54
|
+
readonly format?: "compact" | "usd" | "percent";
|
|
55
|
+
};
|
|
56
|
+
/** The `line` primitive: one keyed statusline/footer line, optionally segmented. */
|
|
57
|
+
export type AmbientLine = {
|
|
58
|
+
readonly text: string;
|
|
59
|
+
/** Segmented composition for powerline-style slots (pi-powerline,
|
|
60
|
+
* pi-zentui): sub-segments with placement zones. */
|
|
61
|
+
readonly segments?: {
|
|
62
|
+
readonly id: string;
|
|
63
|
+
readonly text: string;
|
|
64
|
+
readonly place?: "left" | "middle" | "right";
|
|
65
|
+
}[];
|
|
66
|
+
};
|
|
67
|
+
/** The `gauge` primitive: live progress with optional check rows. */
|
|
68
|
+
export type AmbientGauge = {
|
|
69
|
+
readonly label: string;
|
|
70
|
+
readonly percent?: number;
|
|
71
|
+
readonly phase?: string;
|
|
72
|
+
readonly elapsedMs?: number;
|
|
73
|
+
readonly recentOutput?: string[];
|
|
74
|
+
/** Check rows (pi-goal-x auditor): row-shaped, same schema as rows[]. */
|
|
75
|
+
readonly checks?: AmbientRow[];
|
|
76
|
+
};
|
|
77
|
+
/** One ledger entry (primitive D): append-style feed with per-entry amend. */
|
|
78
|
+
export type AmbientLedgerEntry = {
|
|
79
|
+
readonly id: string;
|
|
80
|
+
readonly at: number;
|
|
81
|
+
readonly kind: string;
|
|
82
|
+
readonly summary: string;
|
|
83
|
+
readonly detail?: string[];
|
|
84
|
+
};
|
|
85
|
+
/** The ambient body: ONLY these keys, each from the closed primitives. */
|
|
86
|
+
export type AmbientBody = {
|
|
87
|
+
readonly line?: AmbientLine;
|
|
88
|
+
readonly rows?: AmbientRow[];
|
|
89
|
+
readonly gauge?: AmbientGauge;
|
|
90
|
+
readonly ledger?: AmbientLedgerEntry[];
|
|
91
|
+
};
|
|
92
|
+
/** The strict envelope + closed-primitive body of one ambient surface. */
|
|
93
|
+
export type AmbientRecord = {
|
|
94
|
+
/** The keyed slot id ("goal", "rpiv-todos", "fabric"). */
|
|
95
|
+
readonly key: string;
|
|
96
|
+
/** Which extension owns this key (set by the host context, not the author). */
|
|
97
|
+
readonly owner: string;
|
|
98
|
+
readonly place: AmbientPlace;
|
|
99
|
+
/** Panel row budget / line-slot width guidance (channel advisory). */
|
|
100
|
+
readonly maxRows?: number;
|
|
101
|
+
/** Overflow policy for panels (rpiv-todo's drop-completed-first rule). */
|
|
102
|
+
readonly overflow?: "drop" | "wrap" | "summarize";
|
|
103
|
+
/** Channel re-render hint between data writes (countdowns, clocks), ms. */
|
|
104
|
+
readonly refreshMs?: number;
|
|
105
|
+
/** Auto-clear rule: "never" stays until replaced with an empty body. */
|
|
106
|
+
readonly expires: "never" | "idle";
|
|
107
|
+
readonly updatedAt: number;
|
|
108
|
+
readonly body?: AmbientBody;
|
|
109
|
+
/** Opaque extension-owned payload; rendered collapsed, never interpreted. */
|
|
110
|
+
readonly details?: JsonObject;
|
|
111
|
+
};
|
package/dist/ambient.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ambient seam TYPES (spec: docs/approval-ux-seams.md, "Ambient
|
|
3
|
+
* surfaces"): the channel-neutral record shapes for persistent
|
|
4
|
+
* read-mostly state — statusline slots and panel widgets — written by
|
|
5
|
+
* extensions through the host context and rendered by channels
|
|
6
|
+
* generically.
|
|
7
|
+
*
|
|
8
|
+
* The CANONICAL type declarations live here (the shared leaf package);
|
|
9
|
+
* `src/harness/ambient.ts` imports them (type-only) for the doc
|
|
10
|
+
* definitions and validation runtime. The records ride the §5 wire
|
|
11
|
+
* AS-IS (envelope + body + details — never projected), so these shapes
|
|
12
|
+
* are the channel contract too (see `@sigrun/shared/views`).
|
|
13
|
+
*
|
|
14
|
+
* Lifecycle difference from interactions: an ambient surface is replaced
|
|
15
|
+
* repeatedly (write = FULL state replacement, never a delta) and never
|
|
16
|
+
* blocks. One slot key is owned by exactly one extension (allocation,
|
|
17
|
+
* not silent composition — the pi-zentui chrome-skin lesson).
|
|
18
|
+
*
|
|
19
|
+
* Layering mirrors the interaction seam: strict envelope (key, place,
|
|
20
|
+
* budget, refresh, expires) around a body composed only from the closed
|
|
21
|
+
* ambient primitives (line, rows, gauge, ledger), with opaque `details`.
|
|
22
|
+
*/
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `formatValue` (UI-blocks spec §5.3): the one value formatter behind
|
|
3
|
+
* `format` props — `stat`, `meter`, table columns, chart axes and card
|
|
4
|
+
* time slots. Pure `Intl` only; no imports outside `shared/src/`.
|
|
5
|
+
*
|
|
6
|
+
* Determinism (spec §14.3): the same input and locale give the same
|
|
7
|
+
* bytes. Everything here is derived from the arguments — no clock
|
|
8
|
+
* reads (the `relative` format takes the timestamp to describe, and
|
|
9
|
+
* `now` comes from `Date.now()` at the CALL SITE inside `toText`
|
|
10
|
+
* rendering... which is equally forbidden to wander). `relative` is
|
|
11
|
+
* therefore stable within a call and pinned by tests that pass a
|
|
12
|
+
* timestamp chosen so the unit boundary cannot flip mid-run.
|
|
13
|
+
*/
|
|
14
|
+
import type { JsonValue } from "../json.js";
|
|
15
|
+
/**
|
|
16
|
+
* Render `value` in `format` for `locale` (spec §5.3's format list).
|
|
17
|
+
*
|
|
18
|
+
* - `undefined` format: numbers group digits; strings pass through;
|
|
19
|
+
* `null` renders empty; booleans spell out; objects and arrays render
|
|
20
|
+
* as JSON with SORTED keys (deterministic bytes regardless of the
|
|
21
|
+
* key order the data arrived in).
|
|
22
|
+
* - `datetime`/`date`/`time` take epoch milliseconds or an ISO string,
|
|
23
|
+
* rendered in `timeZone` when given (defaults to the locale's).
|
|
24
|
+
* - `relative` describes a timestamp relative to `Date.now()`; the unit
|
|
25
|
+
* is chosen by the span, per `Intl.RelativeTimeFormat`.
|
|
26
|
+
* - An unknown format falls back to plain number formatting.
|
|
27
|
+
*/
|
|
28
|
+
export declare function formatValue(value: JsonValue, format: string | undefined, locale: string, timeZone?: string): string;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `formatValue` (UI-blocks spec §5.3): the one value formatter behind
|
|
3
|
+
* `format` props — `stat`, `meter`, table columns, chart axes and card
|
|
4
|
+
* time slots. Pure `Intl` only; no imports outside `shared/src/`.
|
|
5
|
+
*
|
|
6
|
+
* Determinism (spec §14.3): the same input and locale give the same
|
|
7
|
+
* bytes. Everything here is derived from the arguments — no clock
|
|
8
|
+
* reads (the `relative` format takes the timestamp to describe, and
|
|
9
|
+
* `now` comes from `Date.now()` at the CALL SITE inside `toText`
|
|
10
|
+
* rendering... which is equally forbidden to wander). `relative` is
|
|
11
|
+
* therefore stable within a call and pinned by tests that pass a
|
|
12
|
+
* timestamp chosen so the unit boundary cannot flip mid-run.
|
|
13
|
+
*/
|
|
14
|
+
/** The byte-scale units `bytes` walks up through (binary steps of 1024). */
|
|
15
|
+
const BYTE_UNITS = ["B", "KB", "MB", "GB", "TB", "PB"];
|
|
16
|
+
/** The duration steps `duration` walks down through, with their ms sizes. */
|
|
17
|
+
const DURATION_STEPS = [
|
|
18
|
+
{ unit: "d", ms: 86_400_000 },
|
|
19
|
+
{ unit: "h", ms: 3_600_000 },
|
|
20
|
+
{ unit: "m", ms: 60_000 },
|
|
21
|
+
{ unit: "s", ms: 1_000 },
|
|
22
|
+
{ unit: "ms", ms: 1 },
|
|
23
|
+
];
|
|
24
|
+
/** A locale-aware number formatter with at most `max` fraction digits. */
|
|
25
|
+
function numberFormatter(locale, maximumFractionDigits) {
|
|
26
|
+
return new Intl.NumberFormat(locale, { maximumFractionDigits });
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* `bytes`: 1024-based scale with a `maximumFractionDigits: 1` number,
|
|
30
|
+
* e.g. `1536 → "1.5 KB"`, `512 → "512 B"`.
|
|
31
|
+
*/
|
|
32
|
+
function formatBytes(value, locale) {
|
|
33
|
+
let unit = 0;
|
|
34
|
+
let scaled = value;
|
|
35
|
+
while (Math.abs(scaled) >= 1024 && unit < BYTE_UNITS.length - 1) {
|
|
36
|
+
scaled /= 1024;
|
|
37
|
+
unit++;
|
|
38
|
+
}
|
|
39
|
+
return `${numberFormatter(locale, 1).format(scaled)} ${BYTE_UNITS[unit]}`;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* `duration`: milliseconds in, the two most significant non-zero units
|
|
43
|
+
* out, e.g. `11520000 → "3h 12m"`, `1500 → "1s 500ms"`, `0 → "0s"`.
|
|
44
|
+
* Sub-second remainders are truncated (not rounded) so the last unit
|
|
45
|
+
* named is the one actually covered.
|
|
46
|
+
*/
|
|
47
|
+
function formatDuration(ms, locale) {
|
|
48
|
+
let rest = Math.trunc(ms);
|
|
49
|
+
const parts = [];
|
|
50
|
+
for (const step of DURATION_STEPS) {
|
|
51
|
+
if (parts.length === 2)
|
|
52
|
+
break;
|
|
53
|
+
if (rest >= step.ms || (parts.length > 0 && rest > 0)) {
|
|
54
|
+
const size = Math.floor(rest / step.ms);
|
|
55
|
+
parts.push(`${numberFormatter(locale, 0).format(size)}${step.unit}`);
|
|
56
|
+
rest -= size * step.ms;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (parts.length === 0)
|
|
60
|
+
return `0s`;
|
|
61
|
+
return parts.join(" ");
|
|
62
|
+
}
|
|
63
|
+
/** Coerce a datetime-ish JSON value to epoch milliseconds, or `undefined`. */
|
|
64
|
+
function epochMillis(value) {
|
|
65
|
+
if (typeof value === "number")
|
|
66
|
+
return value;
|
|
67
|
+
if (typeof value === "string") {
|
|
68
|
+
const parsed = Date.parse(value);
|
|
69
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Render `value` in `format` for `locale` (spec §5.3's format list).
|
|
75
|
+
*
|
|
76
|
+
* - `undefined` format: numbers group digits; strings pass through;
|
|
77
|
+
* `null` renders empty; booleans spell out; objects and arrays render
|
|
78
|
+
* as JSON with SORTED keys (deterministic bytes regardless of the
|
|
79
|
+
* key order the data arrived in).
|
|
80
|
+
* - `datetime`/`date`/`time` take epoch milliseconds or an ISO string,
|
|
81
|
+
* rendered in `timeZone` when given (defaults to the locale's).
|
|
82
|
+
* - `relative` describes a timestamp relative to `Date.now()`; the unit
|
|
83
|
+
* is chosen by the span, per `Intl.RelativeTimeFormat`.
|
|
84
|
+
* - An unknown format falls back to plain number formatting.
|
|
85
|
+
*/
|
|
86
|
+
export function formatValue(value, format, locale, timeZone) {
|
|
87
|
+
switch (format) {
|
|
88
|
+
case "number":
|
|
89
|
+
case undefined:
|
|
90
|
+
return plain(value, locale);
|
|
91
|
+
case "compact":
|
|
92
|
+
return typeof value === "number"
|
|
93
|
+
? new Intl.NumberFormat(locale, {
|
|
94
|
+
notation: "compact",
|
|
95
|
+
maximumFractionDigits: 1,
|
|
96
|
+
}).format(value)
|
|
97
|
+
: plain(value, locale);
|
|
98
|
+
case "percent":
|
|
99
|
+
return typeof value === "number"
|
|
100
|
+
? new Intl.NumberFormat(locale, {
|
|
101
|
+
style: "percent",
|
|
102
|
+
maximumFractionDigits: 1,
|
|
103
|
+
}).format(value)
|
|
104
|
+
: plain(value, locale);
|
|
105
|
+
case "bytes":
|
|
106
|
+
return typeof value === "number" ? formatBytes(value, locale) : plain(value, locale);
|
|
107
|
+
case "duration":
|
|
108
|
+
return typeof value === "number" ? formatDuration(value, locale) : plain(value, locale);
|
|
109
|
+
case "datetime":
|
|
110
|
+
case "date":
|
|
111
|
+
case "time": {
|
|
112
|
+
const at = epochMillis(value);
|
|
113
|
+
if (at === undefined)
|
|
114
|
+
return plain(value, locale);
|
|
115
|
+
const options = format === "datetime"
|
|
116
|
+
? { dateStyle: "medium", timeStyle: "short" }
|
|
117
|
+
: format === "date"
|
|
118
|
+
? { dateStyle: "medium" }
|
|
119
|
+
: { timeStyle: "medium" };
|
|
120
|
+
return new Intl.DateTimeFormat(locale, timeZone ? { ...options, timeZone } : options).format(at);
|
|
121
|
+
}
|
|
122
|
+
case "relative": {
|
|
123
|
+
const at = epochMillis(value);
|
|
124
|
+
if (at === undefined)
|
|
125
|
+
return plain(value, locale);
|
|
126
|
+
return relativeFrom(at, Date.now(), locale);
|
|
127
|
+
}
|
|
128
|
+
default:
|
|
129
|
+
if (format.startsWith("currency:")) {
|
|
130
|
+
const currency = format.slice("currency:".length);
|
|
131
|
+
if (typeof value !== "number")
|
|
132
|
+
return plain(value, locale);
|
|
133
|
+
try {
|
|
134
|
+
return new Intl.NumberFormat(locale, {
|
|
135
|
+
style: "currency",
|
|
136
|
+
currency,
|
|
137
|
+
}).format(value);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// An invalid ISO 4217 code (validation's job to catch
|
|
141
|
+
// earlier) renders as a plain number, never a throw.
|
|
142
|
+
return numberFormatter(locale, 2).format(value);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return plain(value, locale);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** The no-format rendering: locale-numbered primitives, sorted-key JSON. */
|
|
149
|
+
function plain(value, locale) {
|
|
150
|
+
if (value === null)
|
|
151
|
+
return "";
|
|
152
|
+
switch (typeof value) {
|
|
153
|
+
case "string":
|
|
154
|
+
return value;
|
|
155
|
+
case "number":
|
|
156
|
+
return new Intl.NumberFormat(locale, { maximumFractionDigits: 3 }).format(value);
|
|
157
|
+
case "boolean":
|
|
158
|
+
return value ? "true" : "false";
|
|
159
|
+
default:
|
|
160
|
+
return JSON.stringify(sortKeys(value));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** Recursively copy `value` with every object's keys in sorted order. */
|
|
164
|
+
function sortKeys(value) {
|
|
165
|
+
if (Array.isArray(value))
|
|
166
|
+
return value.map(sortKeys);
|
|
167
|
+
if (value !== null && typeof value === "object") {
|
|
168
|
+
const out = {};
|
|
169
|
+
for (const key of Object.keys(value).sort()) {
|
|
170
|
+
out[key] = sortKeys(value[key]);
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
return value;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* `"<span> ago"` for a timestamp: picks the unit whose threshold the
|
|
178
|
+
* span has crossed (`Intl.RelativeTimeFormat` conventions: seconds
|
|
179
|
+
* under a minute, minutes under an hour, hours under a day, days under
|
|
180
|
+
* a week, then weeks, months, years).
|
|
181
|
+
*/
|
|
182
|
+
function relativeFrom(at, now, locale) {
|
|
183
|
+
const spanSeconds = Math.round((at - now) / 1000);
|
|
184
|
+
const abs = Math.abs(spanSeconds);
|
|
185
|
+
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
|
|
186
|
+
if (abs < 60)
|
|
187
|
+
return rtf.format(spanSeconds, "second");
|
|
188
|
+
if (abs < 3600)
|
|
189
|
+
return rtf.format(Math.round(spanSeconds / 60), "minute");
|
|
190
|
+
if (abs < 86_400)
|
|
191
|
+
return rtf.format(Math.round(spanSeconds / 3600), "hour");
|
|
192
|
+
if (abs < 7 * 86_400)
|
|
193
|
+
return rtf.format(Math.round(spanSeconds / 86_400), "day");
|
|
194
|
+
if (abs < 30 * 86_400)
|
|
195
|
+
return rtf.format(Math.round(spanSeconds / (7 * 86_400)), "week");
|
|
196
|
+
if (abs < 365 * 86_400)
|
|
197
|
+
return rtf.format(Math.round(spanSeconds / (30 * 86_400)), "month");
|
|
198
|
+
return rtf.format(Math.round(spanSeconds / (365 * 86_400)), "year");
|
|
199
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The named icons (UI-blocks spec §10.2): a CLOSED set of 49 names.
|
|
3
|
+
*
|
|
4
|
+
* The webui draws them from a bundled subset of Lucide; the TUI maps
|
|
5
|
+
* each name to a Unicode glyph with an ASCII fallback. An unknown icon
|
|
6
|
+
* name fails validation (U0 Task 2) — `IconName` is the literal union,
|
|
7
|
+
* so a typo is a type error wherever the contract is used literally.
|
|
8
|
+
*/
|
|
9
|
+
/** The v1 icon set, exactly as spec §10.2 pins it. */
|
|
10
|
+
export declare const ICON_NAMES: readonly ["add", "edit", "delete", "refresh", "search", "filter", "share", "copy", "link", "external", "more", "check", "close", "drag", "file", "folder", "calendar", "clock", "user", "users", "tag", "list", "board", "chart", "image", "book", "cart", "home", "bell", "lock", "key", "info", "warn", "error", "success", "pending", "running", "sun", "moon", "cloud", "cloud-sun", "cloud-rain", "cloud-snow", "cloud-lightning", "fog", "wind", "droplets", "thermometer", "umbrella"];
|
|
11
|
+
/** One name from the closed set. */
|
|
12
|
+
export type IconName = (typeof ICON_NAMES)[number];
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The named icons (UI-blocks spec §10.2): a CLOSED set of 49 names.
|
|
3
|
+
*
|
|
4
|
+
* The webui draws them from a bundled subset of Lucide; the TUI maps
|
|
5
|
+
* each name to a Unicode glyph with an ASCII fallback. An unknown icon
|
|
6
|
+
* name fails validation (U0 Task 2) — `IconName` is the literal union,
|
|
7
|
+
* so a typo is a type error wherever the contract is used literally.
|
|
8
|
+
*/
|
|
9
|
+
/** The v1 icon set, exactly as spec §10.2 pins it. */
|
|
10
|
+
export const ICON_NAMES = [
|
|
11
|
+
// navigation and actions
|
|
12
|
+
"add",
|
|
13
|
+
"edit",
|
|
14
|
+
"delete",
|
|
15
|
+
"refresh",
|
|
16
|
+
"search",
|
|
17
|
+
"filter",
|
|
18
|
+
"share",
|
|
19
|
+
"copy",
|
|
20
|
+
"link",
|
|
21
|
+
"external",
|
|
22
|
+
"more",
|
|
23
|
+
"check",
|
|
24
|
+
"close",
|
|
25
|
+
"drag",
|
|
26
|
+
// objects
|
|
27
|
+
"file",
|
|
28
|
+
"folder",
|
|
29
|
+
"calendar",
|
|
30
|
+
"clock",
|
|
31
|
+
"user",
|
|
32
|
+
"users",
|
|
33
|
+
"tag",
|
|
34
|
+
"list",
|
|
35
|
+
"board",
|
|
36
|
+
"chart",
|
|
37
|
+
"image",
|
|
38
|
+
"book",
|
|
39
|
+
"cart",
|
|
40
|
+
"home",
|
|
41
|
+
"bell",
|
|
42
|
+
"lock",
|
|
43
|
+
"key",
|
|
44
|
+
// status
|
|
45
|
+
"info",
|
|
46
|
+
"warn",
|
|
47
|
+
"error",
|
|
48
|
+
"success",
|
|
49
|
+
"pending",
|
|
50
|
+
"running",
|
|
51
|
+
// weather
|
|
52
|
+
"sun",
|
|
53
|
+
"moon",
|
|
54
|
+
"cloud",
|
|
55
|
+
"cloud-sun",
|
|
56
|
+
"cloud-rain",
|
|
57
|
+
"cloud-snow",
|
|
58
|
+
"cloud-lightning",
|
|
59
|
+
"fog",
|
|
60
|
+
"wind",
|
|
61
|
+
"droplets",
|
|
62
|
+
"thermometer",
|
|
63
|
+
"umbrella",
|
|
64
|
+
];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@sigrun/shared/blocks` — the block-set foundation (UI-blocks U0).
|
|
3
|
+
*
|
|
4
|
+
* One entry point for the wire types (§4/§5/§6.1), the data-op applier
|
|
5
|
+
* (§4.3), the icon names (§10.2), display downsampling (§5.4), and the
|
|
6
|
+
* channel-facing surface transport. Consumed by the server, the webui
|
|
7
|
+
* and the TUI alike; re-exported for extensions through
|
|
8
|
+
* `@sigrun/sigrun/blocks` (U0 Task 4).
|
|
9
|
+
*
|
|
10
|
+
* The runtime half is deliberately tiny and dependency-free: the purity
|
|
11
|
+
* guard (`test/views-purity.test.ts`) holds every specifier inside
|
|
12
|
+
* `shared/src/`.
|
|
13
|
+
*/
|
|
14
|
+
export { BLOCK_SET_VERSION } from "./types.js";
|
|
15
|
+
export { DataOpError, applyOps, resolvePointer } from "./ops.js";
|
|
16
|
+
export { ICON_NAMES } from "./icons.js";
|
|
17
|
+
export { lttb } from "./lttb.js";
|
|
18
|
+
export { formatValue } from "./format.js";
|
|
19
|
+
export { toText } from "./text.js";
|
|
20
|
+
export type { Action, ActionButton, Bindable, Binding, Block, BlockBase, BulkAction, CalloutBlock, Card, CardBadge, CardMetric, CardMeta, CardStatus, ChartAxisX, ChartAxisY, ChartBlock, ChartSeries, ChartThreshold, ChoiceField, ChoiceOption, CollectionBlock, CollectionColumn, CollectionGroup, CollectionSort, CollectionTime, ColumnChild, ColumnsBlock, Confirm, CoreTarget, DataOp, DataOps, DataUpdate, DateField, DatePreset, DaterangeField, DatetimeField, DetailsBlock, DiffBlock, Field, FieldBase, FieldsBlock, FieldsRow, FileField, Format, FormBlock, GridBlock, GridChild, ImageBlock, ListField, LogBlock, LogLine, LongtextField, MarkdownBlock, MeterBlock, MeterThreshold, NumberField, PageBlock, PageEmpty, Placement, SectionBlock, SecretField, Size, StackBlock, StatBlock, TextField, ToggleField, Tone, Tree, TreeBlock, TreeNode, WidthCondition, } from "./types.js";
|
|
21
|
+
export type { IconName } from "./icons.js";
|
|
22
|
+
export type { TextOptions } from "./text.js";
|
|
23
|
+
export type { SurfaceAccess, SurfaceActErr, SurfaceActOk, SurfaceActResult, SurfaceChangedEvent, SurfaceFetch, SurfaceLoad, SurfaceTransport, SurfaceUploadBody, } from "./transport.js";
|
|
24
|
+
export type { JsonObject, JsonValue } from "../json.js";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@sigrun/shared/blocks` — the block-set foundation (UI-blocks U0).
|
|
3
|
+
*
|
|
4
|
+
* One entry point for the wire types (§4/§5/§6.1), the data-op applier
|
|
5
|
+
* (§4.3), the icon names (§10.2), display downsampling (§5.4), and the
|
|
6
|
+
* channel-facing surface transport. Consumed by the server, the webui
|
|
7
|
+
* and the TUI alike; re-exported for extensions through
|
|
8
|
+
* `@sigrun/sigrun/blocks` (U0 Task 4).
|
|
9
|
+
*
|
|
10
|
+
* The runtime half is deliberately tiny and dependency-free: the purity
|
|
11
|
+
* guard (`test/views-purity.test.ts`) holds every specifier inside
|
|
12
|
+
* `shared/src/`.
|
|
13
|
+
*/
|
|
14
|
+
// ─── Runtime (pure, dependency-free) ──────────────────────────────────────
|
|
15
|
+
export { BLOCK_SET_VERSION } from "./types.js";
|
|
16
|
+
export { DataOpError, applyOps, resolvePointer } from "./ops.js";
|
|
17
|
+
export { ICON_NAMES } from "./icons.js";
|
|
18
|
+
export { lttb } from "./lttb.js";
|
|
19
|
+
export { formatValue } from "./format.js";
|
|
20
|
+
export { toText } from "./text.js";
|