@peasant-labs/fairtrade 0.0.60 → 0.0.62

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.
@@ -0,0 +1,208 @@
1
+ /**
2
+ * A host column.
3
+ *
4
+ * `id` is the host's closed-set member, so the table keys the column by `.wire`
5
+ * and never by a raw string. `render(row)` returns the cell content; the table
6
+ * owns only the cell and its alignment.
7
+ *
8
+ * @template Row
9
+ * @template {import('./closed-value.js').ClosedValue<string, string>} ColumnId
10
+ * @typedef {object} TranscriptLibraryColumn
11
+ * @property {ColumnId} id
12
+ * @property {string} label
13
+ * @property {import('./table/column-align.js').ColumnAlign} align
14
+ * @property {boolean} [numeric] pair counts and durations with tabular figures.
15
+ * @property {(row: Row) => import('react').ReactNode} render
16
+ */
17
+ /**
18
+ * One composed saved view: `groupBy` from the URL and `visibleColumns`/`collapsed`
19
+ * from the device. The host composes it; the table receives it and reports
20
+ * changes back, never reading a URL or storage itself.
21
+ *
22
+ * `visibleColumns` holds the known column members the table shows; the first
23
+ * declared column is the row identity and always shows. An unknown member is
24
+ * dropped when the view is reported back, so the host can rewrite its stored
25
+ * value. `collapsed` lists the qualified group ids the reader closed; an empty
26
+ * list opens every group. The list is page-independent: a collapse recorded on
27
+ * one page is the same qualified id on every page, so it never closes an
28
+ * unrelated group. The host composes both fields from the device and clears
29
+ * `collapsed` when `groupBy` changes; the table reports it unchanged.
30
+ *
31
+ * @template {import('./closed-value.js').ClosedValue<string, string>} GroupBy
32
+ * @template {import('./closed-value.js').ClosedValue<string, string>} ColumnId
33
+ * @typedef {object} SavedView
34
+ * @property {GroupBy} groupBy
35
+ * @property {readonly ColumnId[]} visibleColumns
36
+ * @property {readonly string[]} collapsed
37
+ */
38
+ /**
39
+ * A host offset page. `index` is the 1-based page the server returns (`page` on
40
+ * the wire), `size` is a schema `PageSize` member, and `total` counts
41
+ * transcripts, not rendered rows. `null` means the host renders a complete list:
42
+ * the table hides the pager and renders every row.
43
+ *
44
+ * @typedef {object} TranscriptLibraryPage
45
+ * @property {number} index
46
+ * @property {import('@peasant-labs/schema').PageSize} size
47
+ * @property {number} total
48
+ */
49
+ /**
50
+ * @template Row
51
+ * @template {import('./closed-value.js').ClosedValue<string, string>} GroupBy
52
+ * @template {import('./closed-value.js').ClosedValue<string, string>} ColumnId
53
+ * @typedef {object} TranscriptLibraryTableProps
54
+ * @property {string} caption
55
+ * @property {readonly Row[]} rows
56
+ * @property {(row: Row) => string} rowKey required: the host supplies a stable key; the table never derives one.
57
+ * @property {readonly TranscriptLibraryColumn<Row, ColumnId>[]} columns every host column, in declaration order; the first column is the row identity and always shows.
58
+ * @property {GroupBy} groupBy the host default grouping, used when no saved view names a known member.
59
+ * @property {readonly GroupBy[]} [groupByOptions] the grouping members the picker offers; defaults to `[groupBy]`.
60
+ * @property {(row: Row) => readonly string[]} groupKeys bare keys, such as [row.projectHash].
61
+ * @property {(groupId: string) => string} groupLabel receives `${groupBy.wire}:${key}`.
62
+ * @property {readonly string[] | null} nextGroupKeys bare keys from `page.nextItems`, or null.
63
+ * @property {readonly string[] | null} continuedGroupKeys bare keys from the previous page, or null.
64
+ * @property {TranscriptLibraryPage | null} page
65
+ * @property {SavedView<GroupBy, ColumnId> | null} [viewState] the host's composed grouping, columns, and collapsed groups; a known saved view wins over the host default.
66
+ * @property {(view: SavedView<GroupBy, ColumnId>) => void} [onViewStateChange]
67
+ * @property {(index: number) => void} [onPageChange]
68
+ * @property {(size: import('@peasant-labs/schema').PageSize) => void} [onPageSizeChange]
69
+ * @property {import('react').ReactNode} [emptyState]
70
+ */
71
+ /**
72
+ * The resolved saved view the table renders: the host's saved view read against
73
+ * the columns and grouping members this table actually knows. An unknown member
74
+ * falls back to the host default rather than rendering an empty or broken view.
75
+ *
76
+ * @template {import('./closed-value.js').ClosedValue<string, string>} GroupBy
77
+ * @template {import('./closed-value.js').ClosedValue<string, string>} ColumnId
78
+ * @typedef {object} ResolvedSavedView
79
+ * @property {GroupBy} groupBy
80
+ * @property {readonly GroupBy[]} groupByOptions
81
+ * @property {readonly ColumnId[]} visibleColumns
82
+ * @property {readonly string[]} collapsed the qualified group ids the reader closed.
83
+ */
84
+ /**
85
+ * The canonical transcript library table: one real table with a caption, host
86
+ * columns, group header rows, a compact continuation row for a group that began
87
+ * on an earlier page, a numbered pager, and an end-of-page marker for a group
88
+ * that continues.
89
+ *
90
+ * The table is controlled. It renders the rows, page, and view it is given and
91
+ * reports changes; it holds no storage, reads no URL, and never resets a page.
92
+ * A saved view (when given) supplies the grouping, the visible columns, and the
93
+ * collapsed groups; without one every column shows, every group is open, and the two
94
+ * pickers are absent because they would have nothing to report to.
95
+ *
96
+ * @template Row
97
+ * @template {import('./closed-value.js').ClosedValue<string, string>} GroupBy
98
+ * @template {import('./closed-value.js').ClosedValue<string, string>} ColumnId
99
+ * @param {TranscriptLibraryTableProps<Row, GroupBy, ColumnId>} props
100
+ */
101
+ export function TranscriptLibraryTable<Row, GroupBy extends import("./closed-value.js").ClosedValue<string, string>, ColumnId extends import("./closed-value.js").ClosedValue<string, string>>({ caption, rows, rowKey, columns, groupBy, groupByOptions, groupKeys, groupLabel, nextGroupKeys, continuedGroupKeys, page, viewState, onViewStateChange, emptyState, onPageChange, onPageSizeChange, }: TranscriptLibraryTableProps<Row, GroupBy, ColumnId>): import("react").JSX.Element;
102
+ /**
103
+ * A host column.
104
+ *
105
+ * `id` is the host's closed-set member, so the table keys the column by `.wire`
106
+ * and never by a raw string. `render(row)` returns the cell content; the table
107
+ * owns only the cell and its alignment.
108
+ */
109
+ export type TranscriptLibraryColumn<Row, ColumnId extends import("./closed-value.js").ClosedValue<string, string>> = {
110
+ id: ColumnId;
111
+ label: string;
112
+ align: import("./table/column-align.js").ColumnAlign;
113
+ /**
114
+ * pair counts and durations with tabular figures.
115
+ */
116
+ numeric?: boolean;
117
+ render: (row: Row) => import("react").ReactNode;
118
+ };
119
+ /**
120
+ * One composed saved view: `groupBy` from the URL and `visibleColumns`/`collapsed`
121
+ * from the device. The host composes it; the table receives it and reports
122
+ * changes back, never reading a URL or storage itself.
123
+ *
124
+ * `visibleColumns` holds the known column members the table shows; the first
125
+ * declared column is the row identity and always shows. An unknown member is
126
+ * dropped when the view is reported back, so the host can rewrite its stored
127
+ * value. `collapsed` lists the qualified group ids the reader closed; an empty
128
+ * list opens every group. The list is page-independent: a collapse recorded on
129
+ * one page is the same qualified id on every page, so it never closes an
130
+ * unrelated group. The host composes both fields from the device and clears
131
+ * `collapsed` when `groupBy` changes; the table reports it unchanged.
132
+ */
133
+ export type SavedView<GroupBy extends import("./closed-value.js").ClosedValue<string, string>, ColumnId extends import("./closed-value.js").ClosedValue<string, string>> = {
134
+ groupBy: GroupBy;
135
+ visibleColumns: readonly ColumnId[];
136
+ collapsed: readonly string[];
137
+ };
138
+ /**
139
+ * A host offset page. `index` is the 1-based page the server returns (`page` on
140
+ * the wire), `size` is a schema `PageSize` member, and `total` counts
141
+ * transcripts, not rendered rows. `null` means the host renders a complete list:
142
+ * the table hides the pager and renders every row.
143
+ */
144
+ export type TranscriptLibraryPage = {
145
+ index: number;
146
+ size: import("@peasant-labs/schema").PageSize;
147
+ total: number;
148
+ };
149
+ export type TranscriptLibraryTableProps<Row, GroupBy extends import("./closed-value.js").ClosedValue<string, string>, ColumnId extends import("./closed-value.js").ClosedValue<string, string>> = {
150
+ caption: string;
151
+ rows: readonly Row[];
152
+ /**
153
+ * required: the host supplies a stable key; the table never derives one.
154
+ */
155
+ rowKey: (row: Row) => string;
156
+ /**
157
+ * every host column, in declaration order; the first column is the row identity and always shows.
158
+ */
159
+ columns: readonly TranscriptLibraryColumn<Row, ColumnId>[];
160
+ /**
161
+ * the host default grouping, used when no saved view names a known member.
162
+ */
163
+ groupBy: GroupBy;
164
+ /**
165
+ * the grouping members the picker offers; defaults to `[groupBy]`.
166
+ */
167
+ groupByOptions?: readonly GroupBy[];
168
+ /**
169
+ * bare keys, such as [row.projectHash].
170
+ */
171
+ groupKeys: (row: Row) => readonly string[];
172
+ /**
173
+ * receives `${groupBy.wire}:${key}`.
174
+ */
175
+ groupLabel: (groupId: string) => string;
176
+ /**
177
+ * bare keys from `page.nextItems`, or null.
178
+ */
179
+ nextGroupKeys: readonly string[] | null;
180
+ /**
181
+ * bare keys from the previous page, or null.
182
+ */
183
+ continuedGroupKeys: readonly string[] | null;
184
+ page: TranscriptLibraryPage | null;
185
+ /**
186
+ * the host's composed grouping, columns, and collapsed groups; a known saved view wins over the host default.
187
+ */
188
+ viewState?: SavedView<GroupBy, ColumnId> | null;
189
+ onViewStateChange?: (view: SavedView<GroupBy, ColumnId>) => void;
190
+ onPageChange?: (index: number) => void;
191
+ onPageSizeChange?: (size: import("@peasant-labs/schema").PageSize) => void;
192
+ emptyState?: import("react").ReactNode;
193
+ };
194
+ /**
195
+ * The resolved saved view the table renders: the host's saved view read against
196
+ * the columns and grouping members this table actually knows. An unknown member
197
+ * falls back to the host default rather than rendering an empty or broken view.
198
+ */
199
+ export type ResolvedSavedView<GroupBy extends import("./closed-value.js").ClosedValue<string, string>, ColumnId extends import("./closed-value.js").ClosedValue<string, string>> = {
200
+ groupBy: GroupBy;
201
+ groupByOptions: readonly GroupBy[];
202
+ visibleColumns: readonly ColumnId[];
203
+ /**
204
+ * the qualified group ids the reader closed.
205
+ */
206
+ collapsed: readonly string[];
207
+ };
208
+ export { groupPageRows, groupKeysFor } from "./groupPageRows.js";
@@ -0,0 +1,47 @@
1
+ /**
2
+ * A closed value: a frozen object that carries its wire value, the label a
3
+ * surface shows for it, and the conversion hooks.
4
+ *
5
+ * - `wire` is the exact value persisted, compared, and sent on the wire.
6
+ * - `label` is the human-readable text shown for the member.
7
+ * - `String()` is the Go-style explicit text conversion; it returns `String(wire)`.
8
+ * - `toString()` is the text conversion used by `String(value)` and template literals.
9
+ * - `toJSON()` is the persistence conversion used by `JSON.stringify`; it returns `wire`.
10
+ *
11
+ * @template {string | number} W - the exact value persisted, compared, and sent on the wire.
12
+ * @template {string} L - the human-readable label shown for the member.
13
+ * @typedef {{
14
+ * readonly wire: W,
15
+ * readonly label: L,
16
+ * String: () => string,
17
+ * toString: () => string,
18
+ * toJSON: () => W,
19
+ * }} ClosedValue
20
+ */
21
+ /**
22
+ * Create one frozen closed-set member.
23
+ *
24
+ * @template {string | number} W
25
+ * @template {string} L
26
+ * @param {W} wire - the exact value persisted, compared, and sent on the wire.
27
+ * @param {L} label - the human-readable text shown for the member.
28
+ * @returns {ClosedValue<W, L>} a new frozen member for this `(wire, label)` pair.
29
+ */
30
+ export function closed<W extends string | number, L extends string>(wire: W, label: L): ClosedValue<W, L>;
31
+ /**
32
+ * A closed value: a frozen object that carries its wire value, the label a
33
+ * surface shows for it, and the conversion hooks.
34
+ *
35
+ * - `wire` is the exact value persisted, compared, and sent on the wire.
36
+ * - `label` is the human-readable text shown for the member.
37
+ * - `String()` is the Go-style explicit text conversion; it returns `String(wire)`.
38
+ * - `toString()` is the text conversion used by `String(value)` and template literals.
39
+ * - `toJSON()` is the persistence conversion used by `JSON.stringify`; it returns `wire`.
40
+ */
41
+ export type ClosedValue<W extends string | number, L extends string> = {
42
+ readonly wire: W;
43
+ readonly label: L;
44
+ String: () => string;
45
+ toString: () => string;
46
+ toJSON: () => W;
47
+ };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * One rendered group.
3
+ *
4
+ * - `id` is `${groupBy.wire}:${key}`, or `''` for rows that name no group.
5
+ * - `label` is what `groupLabel(id)` returned; `''` for the ungrouped bucket.
6
+ * - `continued` means this group began on the previous page.
7
+ * - `continues` means this group has rows on the next page.
8
+ *
9
+ * @template Row
10
+ * @typedef {object} TranscriptLibraryGroup
11
+ * @property {string} id
12
+ * @property {string} label
13
+ * @property {readonly Row[]} rows
14
+ * @property {boolean} continued
15
+ * @property {boolean} continues
16
+ */
17
+ /**
18
+ * Group one page of rows.
19
+ *
20
+ * @template Row
21
+ * @template {import('./closed-value.js').ClosedValue<string, string>} GroupBy
22
+ * @param {readonly Row[]} rows - the host's page rows, in display order.
23
+ * @param {GroupBy} groupBy - the host's grouping member; only `.wire` is read.
24
+ * @param {(row: Row) => readonly string[]} groupKeys - bare group keys for a row.
25
+ * @param {(groupId: string) => string} groupLabel - label for a qualified group id.
26
+ * @param {readonly string[] | null} nextGroupKeys - bare keys of `page.nextItems`, or null.
27
+ * @param {readonly string[] | null} [continuedGroupKeys] - bare keys of the previous page, or null.
28
+ * @returns {readonly TranscriptLibraryGroup<Row>[]}
29
+ */
30
+ export function groupPageRows<Row, GroupBy extends import("./closed-value.js").ClosedValue<string, string>>(rows: readonly Row[], groupBy: GroupBy, groupKeys: (row: Row) => readonly string[], groupLabel: (groupId: string) => string, nextGroupKeys: readonly string[] | null, continuedGroupKeys?: readonly string[] | null): readonly TranscriptLibraryGroup<Row>[];
31
+ /**
32
+ * The union of every row's bare group keys, de-duplicated in first-appearance
33
+ * order. A host uses it for `nextGroupKeys` (over `page.nextItems`) and for
34
+ * `continuedGroupKeys` (over the previous page it holds).
35
+ *
36
+ * @template Row
37
+ * @param {readonly Row[]} rows
38
+ * @param {(row: Row) => readonly string[]} groupKeys
39
+ * @returns {readonly string[]}
40
+ */
41
+ export function groupKeysFor<Row>(rows: readonly Row[], groupKeys: (row: Row) => readonly string[]): readonly string[];
42
+ /**
43
+ * One rendered group.
44
+ *
45
+ * - `id` is `${groupBy.wire}:${key}`, or `''` for rows that name no group.
46
+ * - `label` is what `groupLabel(id)` returned; `''` for the ungrouped bucket.
47
+ * - `continued` means this group began on the previous page.
48
+ * - `continues` means this group has rows on the next page.
49
+ */
50
+ export type TranscriptLibraryGroup<Row> = {
51
+ id: string;
52
+ label: string;
53
+ rows: readonly Row[];
54
+ continued: boolean;
55
+ continues: boolean;
56
+ };
@@ -30,6 +30,11 @@ export { default as RepoPicker } from "./RepoPicker.jsx";
30
30
  export { default as VillageObservations } from "./VillageObservations.jsx";
31
31
  export * from "./transcript/index.js";
32
32
  export * from "./transcript/graph/index.js";
33
+ export type SavedView<GroupBy extends import("./closed-value.js").ClosedValue<string, string>, ColumnId extends import("./closed-value.js").ClosedValue<string, string>> = import("./TranscriptLibraryTable.jsx").SavedView<GroupBy, ColumnId>;
34
+ export type TranscriptLibraryColumn<Row, ColumnId extends import("./closed-value.js").ClosedValue<string, string>> = import("./TranscriptLibraryTable.jsx").TranscriptLibraryColumn<Row, ColumnId>;
35
+ export type TranscriptLibraryTableProps<Row, GroupBy extends import("./closed-value.js").ClosedValue<string, string>, ColumnId extends import("./closed-value.js").ClosedValue<string, string>> = import("./TranscriptLibraryTable.jsx").TranscriptLibraryTableProps<Row, GroupBy, ColumnId>;
36
+ export type TranscriptLibraryPage = import("./TranscriptLibraryTable.jsx").TranscriptLibraryPage;
37
+ export type TranscriptLibraryGroup<Row> = import("./groupPageRows.js").TranscriptLibraryGroup<Row>;
33
38
  export type TimelineNavigationAction = import("./graph/timelineNavigation.js").TimelineNavigationAction;
34
39
  export type AppSidebarProps = import("./AppSidebar.jsx").AppSidebarProps;
35
40
  export type AppSidebarLayoutProps = import("./AppSidebar.jsx").AppSidebarLayoutProps;
@@ -59,6 +64,8 @@ export { formatCount, formatInteger, formatCost, formatDecimal, MISSING_NUMBER }
59
64
  export { providerAccent, providerDisplayName } from "./provider-policy.js";
60
65
  export { Skeleton, Progress, Spinner, Toast, FeedbackPanel } from "./Feedback.jsx";
61
66
  export { DataTableToolbar, DataTablePaginationControls } from "./DataTableControls.jsx";
67
+ export { TranscriptLibraryTable, groupPageRows, groupKeysFor } from "./TranscriptLibraryTable.jsx";
68
+ export { ColumnAlign, ALL_COLUMN_ALIGNS, parseColumnAlign } from "./table/column-align.js";
62
69
  export { default as HelperGroup, HelperGroupListItem, HelperThreadRow, useHelperSelection, helperOwnerState, helperSelectionIds, helperOwnerToggle, helperMemberToggle, HELPER_OWNER_STATE } from "./HelperGroup.jsx";
63
70
  export { default as Timeline, TimelineItem, ToolCall, ThinkingBlock } from "./Timeline.jsx";
64
71
  export { default as ToastProvider, useToast } from "./ToastHost.jsx";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @param {string} base the cell or header class.
3
+ * @param {'left' | 'right' | 'center' | 'decimal'} align
4
+ * @returns {string}
5
+ */
6
+ export function alignClass(base: string, align: "left" | "right" | "center" | "decimal"): string;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Resolve a persisted or host-supplied alignment to its member.
3
+ *
4
+ * Accepts a member (returned unchanged) or its wire value as a string. A label is
5
+ * not accepted: labels are chrome and may change, while the wire value is the
6
+ * stable identity. An unknown value throws rather than silently defaulting,
7
+ * because a silent default hides a host that drifted from this set.
8
+ *
9
+ * @param {unknown} raw - a `ColumnAlign` member or its wire value.
10
+ * @returns {ColumnAlign}
11
+ */
12
+ export function parseColumnAlign(raw: unknown): ColumnAlign;
13
+ /**
14
+ * The member type union; every value member is a `ColumnAlign`.
15
+ */
16
+ export type ColumnAlign = (typeof ColumnAlignValues)[keyof typeof ColumnAlignValues];
17
+ /**
18
+ * The alignment a host column declares.
19
+ *
20
+ * - `Start` is left-aligned in a left-to-right grid.
21
+ * - `End` is right-aligned; counts and durations pair it with `numeric`.
22
+ * - `Center` is centered.
23
+ * - `Decimal` right-aligns with tabular numbers. A decimal-point span is
24
+ * `DataTable`'s own; this table does not export it.
25
+ */
26
+ export const ColumnAlign: Readonly<{
27
+ Start: import("../closed-value.js").ClosedValue<"start", "start">;
28
+ End: import("../closed-value.js").ClosedValue<"end", "end">;
29
+ Center: import("../closed-value.js").ClosedValue<"center", "center">;
30
+ Decimal: import("../closed-value.js").ClosedValue<"decimal", "decimal">;
31
+ }>;
32
+ /**
33
+ * The member type union; every value member is a `ColumnAlign`.
34
+ * @typedef {(typeof ColumnAlignValues)[keyof typeof ColumnAlignValues]} ColumnAlign
35
+ */
36
+ /** Every alignment member, in the order a surface lists them. */
37
+ export const ALL_COLUMN_ALIGNS: readonly (import("../closed-value.js").ClosedValue<"start", "start"> | import("../closed-value.js").ClosedValue<"end", "end"> | import("../closed-value.js").ClosedValue<"center", "center"> | import("../closed-value.js").ClosedValue<"decimal", "decimal">)[];
38
+ declare const ColumnAlignValues: Readonly<{
39
+ Start: import("../closed-value.js").ClosedValue<"start", "start">;
40
+ End: import("../closed-value.js").ClosedValue<"end", "end">;
41
+ Center: import("../closed-value.js").ClosedValue<"center", "center">;
42
+ Decimal: import("../closed-value.js").ClosedValue<"decimal", "decimal">;
43
+ }>;
44
+ export {};
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The group-by picker. The trigger names the current grouping, and each offered
3
+ * member is a native radio, so the chosen row is announced as checked.
4
+ *
5
+ * @param {object} props
6
+ * @param {readonly { wire: string, label: string }[]} props.options the host's grouping members.
7
+ * @param {{ wire: string, label: string }} props.value the active grouping member.
8
+ * @param {(next: { wire: string, label: string }) => void} props.onChange receives the chosen member.
9
+ */
10
+ export function GroupByPicker({ options, value, onChange }: {
11
+ options: readonly {
12
+ wire: string;
13
+ label: string;
14
+ }[];
15
+ value: {
16
+ wire: string;
17
+ label: string;
18
+ };
19
+ onChange: (next: {
20
+ wire: string;
21
+ label: string;
22
+ }) => void;
23
+ }): import("react").JSX.Element;
24
+ /**
25
+ * The column picker. The trigger names how many columns show, and every column is
26
+ * a native checkbox. The first declared column is the row identity and always
27
+ * shows, so its checkbox is checked and disabled; the label says why.
28
+ *
29
+ * @param {object} props
30
+ * @param {readonly { id: { wire: string }, label: string }[]} props.columns every host column, in declaration order.
31
+ * @param {readonly { wire: string }[]} props.visibleColumns the columns the saved view shows.
32
+ * @param {(column: { id: { wire: string }, label: string }, checked: boolean) => void} props.onChange receives the toggled column and whether it was checked before the change.
33
+ */
34
+ export function ColumnPicker({ columns, visibleColumns, onChange }: {
35
+ columns: readonly {
36
+ id: {
37
+ wire: string;
38
+ };
39
+ label: string;
40
+ }[];
41
+ visibleColumns: readonly {
42
+ wire: string;
43
+ }[];
44
+ onChange: (column: {
45
+ id: {
46
+ wire: string;
47
+ };
48
+ label: string;
49
+ }, checked: boolean) => void;
50
+ }): import("react").JSX.Element;
@@ -1,4 +1,8 @@
1
- /** Stable source user boundaries, followed by visibility projection. No hidden part is restored.
1
+ /** Runs follow the source prose, not the filtered projection, so a hidden turn never moves a
2
+ * later action forward. Each prose turn starts its own run; a wordless turn appends to the
3
+ * current run; a user turn closes it; a system turn stays where it is and does not split it.
4
+ * A run without a renderable prose anchor keeps its work at the run's first visible work turn,
5
+ * and a run with no work draws no block. No hidden part is restored.
2
6
  * @param {import('./view-model.js').TurnVM[]} turns
3
7
  * @param {import('./view-model.js').TurnVM[]} visibleTurns
4
8
  */