@elaraai/east-claude-plugin 1.0.97 → 1.0.99
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/.build/daemon/lsp.js +40 -3
- package/.build/daemon/server.js +40 -3
- package/.claude-plugin/plugin.json +1 -1
- package/index.json +1829 -632
- package/package.json +1 -1
- package/skills/e3/SKILL.md +395 -90
- package/skills/e3-ui/SKILL.md +1248 -277
- package/skills/e3-ui-cli/SKILL.md +2 -2
- package/skills/east/SKILL.md +69 -2
- package/skills/east-contribute/SKILL.md +0 -1
- package/skills/east-py/SKILL.md +53 -11
- package/skills/east-ui/SKILL.md +20 -43
package/skills/e3-ui/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: e3-ui
|
|
3
|
-
description: "e3 + UI bridge — build interactive, reactive decision surfaces as e3 tasks, authored as JSX. Use when: (1) Declaring UI tasks with ui() (e3 tasks of kind 'ui' producing a UIComponentType), (2) Binding reactive workspace data with Data.bind (read/write/has/commit/discard/status against e3.input / task defs) inside a <Reactive>{$ => …}</Reactive> block, (3) Staged vs direct edit modes and reviewing pending changes with the <Diff> tag, (4) Graph/ontology editing with the <Ontology> tag, (5) Calling named package functions (e3.function) RPC-style with Func.bind (call/read/status/error/pending/cancel), (6) Wiring a manifest (reads/writes + bound functions auto-derived from a UI task's IR), (7) Interactive causal-experiment surfaces ('did X change Y?') with the <Experiment> tag, generic over a bound dataset's row and driven by e3.function estimators, (8) The Decide loop — Decision.bind unions reasoning-task decision outputs into one handle (shared selection + commit gate), <DecisionQueue> (urgency-sorted queue with evidence/options/judgement/modify facets, Apply/Reject, grouping, an author-bound Slice scope) and <DecisionJournal> (the resolved read-back), (9) The Studio — Studio.component declares a self-contained East UI function (written exactly like a ui() body) with what the palette shows; the pages operators build are one record of Studio.Types.Pages with one patch write; <Studio.Builder> is the builder — the open page's canvas under one toolbar (its status, the history, Desktop · Tablet, Save as template, Preview and Publish), the palette of the listed components and the project's pages before it, the inspector of the selected placement after it, and the publish preview in its place — every gesture a draft, Save one patch on the page, and a publish stamping each placement with the code it goes live with; <Studio.Library> is a project's templates and pages and where new pages start, opening pages in the builder; <Studio.Page> draws one page's live or draft layout with no chrome, (10) Rendering deployed surfaces in a React app with @elaraai/e3-ui-components — <E3Provider> (E3Config: apiUrl, repo, workspace, token, fetch), <ReactiveDatasetProvider> and <UITaskPreview> — over a server, or over an e3 running in the page (e3-web's createWebE3 and its e3.fetch), (11) Queries — operators build typed jq queries over the datasets a surface binds and save them to one record of Query.Types.Saved with one patch write; <Query.Builder> edits the open query as plain-word steps or as jq, checks it as it is edited, runs it on e3 as a one-shot call — or as a split call over the pieces of a dataset larger than one piece, its plan explained — and shows the result as a Table or a tree; <Query.Library> is the saved queries as a gallery of wireframes and where new queries start, opening them in the builder, (12) The Plan — <Plan>, the one planning canvas, always in its builder frame — one toolbar holding every control it has, the banners, the canvas, the footer's counts: heterogeneous rows on ONE shared { time | number | ordinal } axis from three sources — event kinds over records (Schedule.events placed on Schedule.resources, the kinds a Calendar takes), rows laid out by a series list over local data or a Data.bindPaged source and nested from the data's own structure, and read-only rows (Plan.over a dataset, hand-built rows) — its panes optional props — a library (every event kind's templates, the backlog by when it is due, the series a viewer shows and hides, and cards of the author's own whose drop sets a Schedule.patch) and an inspector of what is selected (one event, several, a row at a bucket, or the window's counts; an event kind's own inspector in place of its form) — with one Slice.bindRecords rail (its rows, each event kind, resource kind and templates record a source, one window), key search, links between runs or events, and an editing session — dropped cards and moved or resized runs, chips, tiles and marks as drafts with Undo / Redo and one checked Save — its authoring vocabulary on `Plan` (Plan.axis, Plan.series.*, Plan.over, Plan.eventRef, Plan.library.*, the value builders), (13) The Sheet — <Sheet>, the one planning spreadsheet, always in its builder frame — one toolbar holding every control it has, the banners, the grid with its docked strip, the footer — its panes optional props: a library (templates, columns, the author's cards) and an inspector of the selected row's every field or the author's own; its rows from an e3 record bound with its patch door (the record's entries, one entry's rows or groups with loose rows between them, or a large record a window at a time — Save one patch commit through the record) or the host's (an array, a bind handle, a Data.bindPaged source with a key search), or the `rows` source of its one Slice.bindRecords rail, paged from it, the rail narrowing its templates records too; typed columns (dates, quantities with units, register lookups, a directed link between register members, stamped codes), a blank tail that invites the next row, an East-function copilot that fills cells and proposes rows, saved-view tabs over every source's narrowing, groups with loose rows between them and read-only sub rows, drag and drop, Excel round-trip, and an editing session — drafts checked as they are made, Undo / Redo and one checked Save — its authoring vocabulary on `Sheet` (Sheet.column.*, Sheet.register.*, Sheet.driver, Sheet.group, Sheet.subRows, Sheet.library.*, Sheet.field, Sheet.apply). (14) Calendar — the record-backed scheduling builder in e3-ui: Calendar, Resources and Timeline layouts, bound resource and template records, the event record’s unscheduled backlog, one Slice.bindRecords rail — a source per event kind, resource kind and templates record, one date window — shared Toolbar/Library/FieldForm/EditSession behavior, and explicit agenda/card actions below 480px. BuilderFrame remains internal. (15) Roster — weekly staffing over the app's own records: its duties a Schedule.events kind (start, end, resource, shift, group, position?, activity?, hours?), its people and agencies Schedule.resources kinds, its weeks Schedule.periods for Publish, its cover Roster.targets, and its shifts, groups, positions, activities and proposals read through Roster.* accessors; Shifts and People layouts; pure coverage/rules over the duty row type; the app's pay in its own codes, asked a person at a time; Find fixes on the app's fit e3.function (Func.bind, Roster.fit(R)); the week's duties read through the slice's duty source; one Slice.bindRecords rail over the duties, targets, people and activities; one history over a session per record, Save per record and Publish saving first, then the week's status; and explicit mobile actions. (16) The Flowchart — <Flowchart>, the state-transition flowchart, always in its builder frame — one toolbar (find state, LR · TD, the freshness chip, the rail of its one Slice.bindRecords, and where it edits the history item), the canvas filling main, the footer's counts: states as nodes in ordered phase lanes (the layout derived, no coordinates), H/V-routed transitions, decision triggers and evidence-weighted links, over an e3 record of flows by name (Flowchart.Types.Flows, its value written with Flowchart.values and checked at build — a lone flow is a record of one entry) or the host's flows or one flow (data, the value's type picking the arm — Flowchart.over builds one flow from the host's own tables with row mappers); its library pane's Flows tab (Flowchart.library.flows()) lists the flows by name — over a record those its rail's `flows` source narrows it to, counted — and opens one, the open flow kept under the flowchart's name, and '+ New flow' starts one, beside state and transition templates, templates records (Flowchart.library.states / transitions), and the author's own cards (Flowchart.library.tab), each following the source of the rail it names, and each card dropped on the canvas — a state template onto a lane, a transition template onto a transition, an author's card where its drop's type says, ⏎ on a card onto the selection; every gesture on the canvas — '+ LANE', a lane renamed (a double-click on its header) or deleted, a state added, edited, moved or deleted, a handle dragged to connect, Del on the selection — one transaction of the open flow's editing session, each flow keeping its own drafts, with Undo / Redo and one checked Save through the record's patch mutation or the host's onApply; its inspector pane, on by default (inspector={false} takes it away) — Details for the state, transition, decision, lane (a click on its header) or several states selected, or the open flow, each through the shared form, a state's or a transition's own Details in its place with inspector={{ state, transition }}, and Issues, each selecting what it names; with selection and path tracing — its types on Flowchart.Types, a row patch from Flowchart.patch. (17) The Allocation — <Allocation>, a day's arrivals booked into timed lane slots and each bin's quantity allocated to resources with room and time for its hold, always in its builder frame, as a Timeline (lanes, links, resource rows) or a Table (a row per slot), over the app's own records: its bins an instant Schedule.events kind (at, resource, arrival, quantity, parts, hold, kind?), its lanes and resources Schedule.resources kinds (offset; min, max, note, hold, jobs), what it books from Allocation.arrivals (each arrival's values a booked bin's own fields), and its days Schedule.periods for Publish — one history over the bins' and the days' sessions, every gesture a draft, Save per record and Publish saving first, then the day's status — the app's fit an e3.function bound with Func.bind (a place or a take question about the drafted day, FitRequest(R) over the bin row type, built on Allocation.answer / fit / lookahead), split loads and Find a fit, the app's bin fields as columns and its own check over CheckContext(R), one Slice.bindRecords rail over the day's bins, its lanes, resources and arrivals, the shared Library, Inspector and session, explicit-action cards below 480px, and pure loads / candidates / check / slots over Day(R) a task runs too."
|
|
3
|
+
description: "e3 + UI bridge — build interactive, reactive decision surfaces as e3 tasks, authored as JSX. Use when: (1) Declaring UI tasks with ui() (e3 tasks of kind 'ui' producing a UIComponentType), (2) Binding reactive workspace data with Data.bind (read/write/has/commit/discard/status against e3.input / task defs) inside a <Reactive>{$ => …}</Reactive> block, (3) Staged vs direct edit modes and reviewing pending changes with the <Diff> tag, (4) Graph/ontology editing with the <Ontology> tag, (5) Calling named package functions (e3.function) RPC-style with Func.bind (call/read/status/error/pending/cancel), (6) Wiring a manifest (reads/writes + bound functions auto-derived from a UI task's IR), (7) Interactive causal-experiment surfaces ('did X change Y?') with the <Experiment> tag, generic over a bound dataset's row and driven by e3.function estimators, (8) The Decide loop — Decision.bind unions reasoning-task decision outputs into one handle (shared selection + commit gate), <DecisionQueue> (urgency-sorted queue with evidence/options/judgement/modify facets, Apply/Reject, grouping, an author-bound Slice scope) and <DecisionJournal> (the resolved read-back), (9) The Studio — Studio.component declares a self-contained East UI function (written exactly like a ui() body) with what the palette shows; the pages operators build are one record of Studio.Types.Pages with one patch write; <Studio.Builder> is the builder — the open page's canvas under one toolbar (its status, the history, Desktop · Tablet, Save as template, Preview and Publish), the palette of the listed components and the project's pages before it, the inspector of the selected placement after it, and the publish preview in its place — every gesture a draft, Save one patch on the page, and Publish one patch making the draft its next live version; <Studio.Library> is a project's templates and pages and where new pages start, opening pages in the builder; <Studio.Page> draws one page's live or draft layout with no chrome, (10) Rendering deployed surfaces in a React app with @elaraai/e3-ui-components — <E3Provider> (E3Config: apiUrl, repo, workspace, token, fetch), <ReactiveDatasetProvider> and <UITaskPreview> — over a server, or over an e3 running in the page (e3-web's createWebE3 and its e3.fetch), (11) Queries — operators build typed jq queries over the datasets a surface binds and save them to one record of Query.Types.Saved with one patch write; <Query.Builder> edits the open query as plain-word steps or as jq, checks it as it is edited, runs it on e3 as a one-shot call — or as a split call over the pieces of a dataset larger than one piece, its plan explained — and shows the result as a Table or a tree; <Query.Library> is the saved queries as a gallery of wireframes and where new queries start, opening them in the builder, (12) The Plan — <Plan>, the one planning canvas, always in its builder frame — one toolbar holding every control it has, the banners, the canvas, the footer's counts: heterogeneous rows on ONE shared { time | number | ordinal } axis from three sources — event kinds over records (Schedule.events placed on Schedule.resources, the kinds a Calendar takes), rows laid out by a series list over local data or a Data.bindPaged source and nested from the data's own structure, and read-only rows (Plan.over a dataset, hand-built rows) — its panes optional props — a library (every event kind's templates, the backlog by when it is due, the series a viewer shows and hides, and cards of the author's own whose drop sets a Schedule.patch) and an inspector of what is selected (one event, several, a row at a bucket, or the window's counts; an event kind's own inspector in place of its form) — with one Slice.bindRecords rail (its rows, each event kind, resource kind and templates record a source, one window; an event kind on paged resources read a cell at a time through a series index and shaded from a summary index zoomed out), key search, links between runs or events, and an editing session — dropped cards and moved or resized runs, chips, tiles and marks as drafts with Undo / Redo and one checked Save — its authoring vocabulary on `Plan` (Plan.axis, Plan.series.*, Plan.over, Plan.eventRef, Plan.library.*, the value builders), (13) The Sheet — <Sheet>, the one planning spreadsheet, always in its builder frame — one toolbar holding every control it has, the banners, the grid with its docked strip, the footer — its panes optional props: a library (templates, columns, the author's cards) and an inspector of the selected row's every field or the author's own; its rows from an e3 record bound with its patch door (the record's entries, one entry's rows or groups with loose rows between them, or a large record a window at a time — Save one patch commit through the record) or the host's (an array, a bind handle, a Data.bindPaged source with a key search), or the `rows` source of its one Slice.bindRecords rail — the record's rows, the source's narrowing drawn over them as the lens (a filtered spreadsheet: only the rows it keeps, one after another, each at its row number, e3 placing them from the index) — the rail narrowing its templates records too; typed columns (dates, quantities with units, register lookups, a directed link between register members, stamped codes), a blank tail that invites the next row, an East-function copilot that fills cells and proposes rows, saved-view tabs over every source's narrowing, groups with loose rows between them and read-only sub rows, drag and drop, Excel round-trip, and an editing session — drafts checked as they are made, Undo / Redo and one checked Save — its authoring vocabulary on `Sheet` (Sheet.column.*, Sheet.register.*, Sheet.driver, Sheet.group, Sheet.subRows, Sheet.library.*, Sheet.field, Sheet.apply). (14) Calendar — the record-backed scheduling builder in e3-ui: Calendar, Resources and Timeline layouts, bound resource and template records, the event record’s unscheduled backlog paged through its source, one Slice.bindRecords rail — a source per event kind, resource kind and templates record, one date window — a resource kind paged a window at a time (Data.bindPaged), the events on it read a cell at a time through a series index and shaded from a summary index past 10,000, shared Toolbar/Library/FieldForm/EditSession behavior, and explicit agenda/card actions below 480px. BuilderFrame remains internal. (15) Roster — weekly staffing over the app's own records: its duties a Schedule.events kind (start, end, resource, shift, group, position?, activity?, hours?), its people and agencies Schedule.resources kinds, its weeks Schedule.periods for Publish, its cover Roster.targets, and its shifts, groups, positions, activities and proposals read through Roster.* accessors; Shifts (Day · Week) and People (Week · Month) layouts — the People week a tile a duty in the shift's colour token (Roster.shifts' color, east-ui's ColorTokenType), the month a column a week, and the app's week patterns (Roster.patterns, Roster.library.patterns()) dropped on a person's week as one step; pure coverage/rules over the duty row type; the app's pay in its own codes, asked a person at a time; Find fixes on the app's fit e3.function (Func.bind, Roster.fit(R)); the week's duties read through the slice's duty source; one Slice.bindRecords rail over the duties, targets, people and activities; one history over a session per record, Save per record and Publish saving first, then the week's status; and explicit mobile actions. (16) The Flowchart — <Flowchart>, the state-transition flowchart, always in its builder frame — one toolbar (find state, LR · TD, the freshness chip, the rail of its one Slice.bindRecords, and where it edits the history item), the canvas filling main, the footer's counts: states as nodes in ordered phase lanes (the layout derived, no coordinates), H/V-routed transitions, decision triggers and evidence-weighted links, over an e3 record of flows by name (Flowchart.Types.Flows, its value written with Flowchart.values and checked at build — a lone flow is a record of one entry) or the host's flows or one flow (data, the value's type picking the arm — Flowchart.over builds one flow from the host's own tables with row mappers); its library pane's Flows tab (Flowchart.library.flows()) lists the flows by name — over a record those its rail's `flows` source narrows it to, counted — and opens one, the open flow kept under the flowchart's name, and '+ New flow' starts one, beside state and transition templates, templates records (Flowchart.library.states / transitions), and the author's own cards (Flowchart.library.tab), each following the source of the rail it names, and each card dropped on the canvas — a state template onto a lane, a transition template onto a transition, an author's card where its drop's type says, ⏎ on a card onto the selection; every gesture on the canvas — '+ LANE', a lane renamed (a double-click on its header) or deleted, a state added, edited, moved or deleted, a handle dragged to connect, Del on the selection — one transaction of the open flow's editing session, each flow keeping its own drafts, with Undo / Redo and one checked Save through the record's patch mutation or the host's onApply; its inspector pane, on by default (inspector={false} takes it away) — Details for the state, transition, decision, lane (a click on its header) or several states selected, or the open flow, each through the shared form, a state's or a transition's own Details in its place with inspector={{ state, transition }}, and Issues, each selecting what it names; with selection and path tracing — its types on Flowchart.Types, a row patch from Flowchart.patch. (17) The Allocation — <Allocation>, a day's arrivals booked into timed lane slots and each bin's quantity allocated to resources with room and time for its hold, always in its builder frame, as a Timeline (lanes, links, resource rows) or a Table (a row per slot), over the app's own records: its bins an instant Schedule.events kind (at, resource, arrival, quantity, parts, hold, kind?), its lanes and resources Schedule.resources kinds (offset; min, max, note, hold, jobs), what it books from Allocation.arrivals (each arrival's values a booked bin's own fields), and its days Schedule.periods for Publish — one history over the bins' and the days' sessions, every gesture a draft, Save per record and Publish saving first, then the day's status — the app's fit an e3.function bound with Func.bind (a place or a take question about the drafted day, FitRequest(R) over the bin row type, built on Allocation.answer / fit / lookahead), split loads and Find a fit, the app's bin fields as columns and its own check over CheckContext(R), one Slice.bindRecords rail over the day's bins, its lanes, resources and arrivals, the shared Library, Inspector and session, explicit-action cards below 480px, and pure loads / candidates / check / slots over Day(R) a task runs too. (18) The Schematic — <Schematic>, the 2D world-coordinate drawing of the app's own records: a kind per record, keyed by String, its fields mapped to the Schematic's roles and its shapes made by accessors over the row's own fields — items (Schematic.items: a centre, a box, a footprint drawn about the centre, a label, a status, a meter, a figure), zones (Schematic.zones: a box, outlined or hatched, a geometry about its centre), links between two items (Schematic.links: from and to naming item slots) and nets joining many to many (Schematic.nets: sources and destinations) — each kind editable bound with its patch door (Record.bind) or read only (Data.bind), read whole or complete through its source of one Slice.bindRecords rail (entries: Data.bindPaged) — a window at a time where that source lists an area index (index.area), its density shaded past 5,000 rows in view — its details a record keyed as it is (Schematic.details), its templates a record (Schematic.templates), its layers the app's rows (Schematic.layers), the library's tabs over every kind (Schematic.library.templates() / backlog() / index() / layers() / tab), each card dropped on the canvas — an item template adds an item at the point, a backlog row is placed there, a link or net template gives a link or a net of its kind its values, an author's card sets its Schematic.patch's fields, ⏎ onto the selection or at the view's centre — the inspector opted into: Details through the shared form over each kind's fields and its details', an item's position, a link's ends and a net's sides, and Issues, each selecting what it names; below 480px of main nothing drags, Details' Connect to… and the Backlog card's Place taking the gestures' place; a camera with a minimap, selection by a tap, Shift and the marquee, and every gesture — Move, Connect and a Shift session growing a net, an end retargeted, a net extended, Del — one step of one history over a session per record it writes, with Undo / Redo and Save sending each record's changes through its patch door, canConnect answering each pair with none or the refusal's words; onSelect hears the whole selection, each entity by its kind's slot and its key — its shapes and types on Schematic (Schematic.circle / polyline / polygon / rect about the anchor, outline / hatch, solid / dashed, Schematic.patch, Schematic.Types)."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# e3-ui — e3 + UI Bridge
|
|
@@ -13,7 +13,7 @@ and commit / discard, a view becomes a place a user commits a decision with its
|
|
|
13
13
|
evidence — not a read-only report.
|
|
14
14
|
|
|
15
15
|
The public surface is **JSX tags + platform helpers**, all from one import
|
|
16
|
-
(`@elaraai/e3-ui`): the e3-specific tags `<Diff>`, `<Ontology>`, `<Experiment>`, `<Plan>`, `<Sheet>`, `<Calendar>`, `<Roster>`, `<Allocation>` and `<
|
|
16
|
+
(`@elaraai/e3-ui`): the e3-specific tags `<Diff>`, `<Ontology>`, `<Experiment>`, `<Plan>`, `<Sheet>`, `<Calendar>`, `<Roster>`, `<Allocation>`, `<Flowchart>` and `<Schematic>`, the `Data` and `Func`
|
|
17
17
|
binding helpers, `Studio` and `Query` components, and the `ui()` task factory. Base UI tags (`<VStack>`, `<Text>`,
|
|
18
18
|
`<Stat>`, …) come from `@elaraai/east-ui`. The factories (`Diff.Root(…)`) are an
|
|
19
19
|
implementation detail under `@elaraai/e3-ui/internal` (also the e3-free,
|
|
@@ -90,7 +90,7 @@ Task → What do you need?
|
|
|
90
90
|
│ ├─ Key search → .seek(query) (none for an Array source)
|
|
91
91
|
│ ├─ The snapshot it serves → .revision() (the content hash; re-fires when it moves)
|
|
92
92
|
│ ├─ Show a confirmed write → .refresh(none) (or .refresh(some(hash)))
|
|
93
|
-
│ └─ Through a record's index → { index:
|
|
93
|
+
│ └─ Through a record's index → { index: "by_status", join?: true }
|
|
94
94
|
│ rows arrive in the INDEX's order as `{ik, key, value, row}`
|
|
95
95
|
│
|
|
96
96
|
├─ Call a named package function (e3.function) from the UI — Func.bind(fn)
|
|
@@ -109,13 +109,13 @@ Task → What do you need?
|
|
|
109
109
|
│
|
|
110
110
|
├─ Offer components for operators to arrange on pages (Studio)
|
|
111
111
|
│ ├─ Declare one — self-contained, like a ui() body → Studio.component(key, meta, fn)
|
|
112
|
-
│ ├─ Store the pages operators build → e3.record("pages", Studio.Types.Pages, new Map())
|
|
112
|
+
│ ├─ Store the pages operators build → e3.record("pages", Studio.Types.Pages, { initial: new Map(), mutations: (mutation) => ({ patch: mutation.patch() }) })
|
|
113
113
|
│ ├─ The builder: canvas, palette, inspector, preview → <Studio.Builder pages components project env? audience? rollout? id? />
|
|
114
114
|
│ ├─ Templates, pages, and where new pages start → <Studio.Library pages components project onOpen? id? />
|
|
115
115
|
│ └─ Show one page, live or draft, with no chrome → <Studio.Page pages components page version? />
|
|
116
116
|
│
|
|
117
117
|
├─ Let users query the bound datasets and save their queries (typed jq)
|
|
118
|
-
│ ├─ Store the queries they save → e3.record("queries", Query.Types.Saved, new Map())
|
|
118
|
+
│ ├─ Store the queries they save → e3.record("queries", Query.Types.Saved, { initial: new Map(), mutations: (mutation) => ({ patch: mutation.patch() }) })
|
|
119
119
|
│ ├─ Ship queries in the record, checked at build → Query.value({ orders, customers }, [{ name, jq, description?, savedAt }])
|
|
120
120
|
│ ├─ The builder: steps or jq, checked, run, saved → <Query.Builder queries datasets query? id? />
|
|
121
121
|
│ └─ The saved queries, and where new ones start → <Query.Library queries datasets onOpen? id? />
|
|
@@ -133,30 +133,34 @@ Task → What do you need?
|
|
|
133
133
|
│ ├─ Its axis: time, number or ordinal → Plan.axis / Plan.axis.number / Plan.axis.ordinal
|
|
134
134
|
│ ├─ Rows, nested by what the data holds → Plan.series.span / buckets / chart / heat / table / cards / events / group / section / views / rows
|
|
135
135
|
│ ├─ Data too large to hold whole → data={paged}, a Data.bindPaged handle
|
|
136
|
-
│ ├─ Narrow its rows, event kinds, resources, templates → slice={{ slice: Slice.bindRecords(key, { rows?, job?, presses?, jobTemplates? }) }} (one window; Schedule.templates(rows, { …, source }))
|
|
137
|
-
│ ├─ Event kinds over records too large to read whole → a kind's source over its interval index (range: "start") + Schedule.events(record, { entries }); with no rail, { window, backlogWindow, entries }; Schedule.resources(Data.bindPaged(record), {…})
|
|
136
|
+
│ ├─ Narrow its rows, event kinds, resources, templates → slice={{ slice: Slice.bindRecords(key, { rows?, job?, presses?, jobTemplates? }) }} (one window, which the rows never follow; Schedule.templates(rows, { …, source }))
|
|
137
|
+
│ ├─ Event kinds over records too large to read whole → a kind's source over its interval index (range: "start") + Schedule.events(record, { entries }), counted first and drawn within 10,000 events; with no rail, { window, backlogWindow, entries } over resources read whole, counted first by the days its events run on; Schedule.resources(Data.bindPaged(record), {…})
|
|
138
|
+
│ ├─ …on paged resources: read a cell at a time → the kind's source lists index.series + index.summary beside its interval index, shaded from the summary zoomed out; any other form is refused, naming both
|
|
139
|
+
│ ├─ …its paged resources narrowed by the rail → a source named after the paged kind's slot, Data.bindPaged(record, { indexes }) over its own record: the canvas pages the narrowing's rows
|
|
138
140
|
│ ├─ A link between two runs, or two events → Plan.link({ from: Plan.ref(…), fromRun, … }) / Plan.link({ from: Plan.eventRef(kind, key), … })
|
|
139
141
|
│ ├─ Its library pane: templates, backlog, series, cards → library={[Plan.library.events(), Plan.library.backlog(), Plan.library.series(), Plan.library.tab(rows, {…})]}
|
|
140
142
|
│ ├─ Its inspector pane: what is selected → inspector (+ an event kind's own: Schedule.events(record, { inspector }))
|
|
141
143
|
│ ├─ Event kinds dragged: templates, backlog, moves → built in with resources + events (+ library); canDrop={fn(Schedule.Types.Candidate) => Option<String>}
|
|
144
|
+
│ ├─ The app's optimiser over the window and the backlog: the Elara eye → optimise={Func.bind(e3.function)} from Plan.Types.OptimiseRequest({ job: Job }, { presses: Press }) to Plan.Types.OptimiseResult({ job: Job }); its answer lands as ONE step of the history (#1409)
|
|
142
145
|
│ ├─ Overlap warnings, without dropping overlapping events → Schedule.events(record, { overlaps: "warn" | "allow" }); the chip selects the first pair
|
|
143
146
|
│ ├─ Save each changed event kind through its patch door → applyMode="batch" (default); "auto" commits each ready gesture
|
|
144
147
|
│ └─ Dropped cards and moves as drafts → a series' edit + editing={{ onUpdate | onApply }}
|
|
145
148
|
│
|
|
146
149
|
├─ Let planners type rows in place — the Sheet, in its frame (see The Sheet below)
|
|
147
|
-
│ ├─ An e3 record edited as a sheet → <Sheet record={…} columns={{…}} /> (record = Record.bind(r
|
|
150
|
+
│ ├─ An e3 record edited as a sheet → <Sheet record={…} columns={{…}} /> (record = Record.bind(r))
|
|
148
151
|
│ │ ├─ One entry's rows, groups or loose rows → entry={{ key, rows: "field", id: "id" }} (+ group)
|
|
149
152
|
│ │ └─ A large record, a window at a time → window={Data.bindPaged(r)}
|
|
150
153
|
│ ├─ The host's rows → <Sheet data={…} id="id" columns={{…}} />
|
|
151
|
-
│ ├─ Narrow its rows and templates — one rail → slice={{ slice: Slice.bindRecords(key, { rows, rowTemplates, … }) }} (
|
|
154
|
+
│ ├─ Narrow its rows and templates — one rail → slice={{ slice: Slice.bindRecords(key, { rows, rowTemplates, … }) }} (the record's rows, `rows`' narrowing drawn over them as the lens; Sheet.templates(rows, { …, source }))
|
|
152
155
|
│ ├─ Its panes, each optional → library={[Sheet.library.rows(), Sheet.library.columns(), Sheet.library.tab(…)]} · inspector (+ fields)
|
|
153
156
|
│ ├─ Its columns, registers and the driver → Sheet.column.* / Sheet.register.* / Sheet.driver
|
|
154
157
|
│ ├─ Groups, loose rows between them, read-only sub rows → Sheet.group / Sheet.Types.Entry / Sheet.subRows
|
|
155
158
|
│ ├─ Data too large to hold whole → data={paged}, a Data.bindPaged handle
|
|
159
|
+
│ ├─ The app's optimiser over the rows in view: the Elara eye → optimise={Func.bind(e3.function)} from Sheet.Types.OptimiseRequest(R) to Sheet.Types.OptimiseResult(R); its answer lands as ONE step of the history (#1409)
|
|
156
160
|
│ └─ Drafts checked as they are made, one checked Save → onUpdate | onApply, ready, newRow
|
|
157
161
|
│
|
|
158
162
|
├─ Draw a state-transition or process flow — the Flowchart (see The Flowchart below)
|
|
159
|
-
│ ├─ An e3 record of flows by name → <Flowchart record={…} flow="…" /> (record = Record.bind(r
|
|
163
|
+
│ ├─ An e3 record of flows by name → <Flowchart record={…} flow="…" /> (record = Record.bind(r), r of Flowchart.Types.Flows)
|
|
160
164
|
│ ├─ The record's value, written as literals → Flowchart.values({ [name]: { lanes, states, links, triggers? } }) (a lone flow: one entry)
|
|
161
165
|
│ ├─ One flow of the host's, an input's value → <Flowchart data={Data.bind(input)} /> (the input's value Flowchart.value({ … }))
|
|
162
166
|
│ ├─ One flow from the app's own tables → <Flowchart data={Flowchart.over(states, { state, links, link, lanes, triggers?, trigger? })} />
|
|
@@ -170,22 +174,42 @@ Task → What do you need?
|
|
|
170
174
|
│ ├─ A card dropped on the canvas, or ⏎ on it → a state template onto a lane, a transition template onto a transition, an author's card where its drop's type says; ⏎ onto the selection
|
|
171
175
|
│ └─ Edit a flow: lanes, states, transitions — Undo, one Save → record={…}, or data={…} + onApply (every canvas gesture a draft; canConnect vetoes a pair)
|
|
172
176
|
│
|
|
177
|
+
├─ Draw the app's records at world coordinates — a floor, a site — the Schematic (see The Schematic below)
|
|
178
|
+
│ ├─ A kind per record, under its slot → <Schematic extent={{ width, height }} items={{ racks: Schematic.items(record, { name, icon, x, y, label, … }) }} zones={{ … }} links={{ … }} nets={{ … }} />
|
|
179
|
+
│ ├─ Editable, or read only → Record.bind(record) · Data.bind(record)
|
|
180
|
+
│ ├─ Every gesture one step: Undo, Save per record → built in over an editable kind: Move, Connect (Shift grows a net), an end retargeted, a net extended, Del
|
|
181
|
+
│ ├─ Narrow its kinds and templates — one rail → slice={{ slice: Slice.bindRecords(key, { <a kind's slot>: { source: Data.bindPaged(record, { indexes }) }, … }) }} (+ the kind's entries: Data.bindPaged(record)) · narrowed="dim"
|
|
182
|
+
│ ├─ A floor too large to read whole: a window at a time → its source lists index.area({ x, y, width, height, at: "centre", cell }) (+ ones led by the rail's fields); past 5,000 rows in view, their density
|
|
183
|
+
│ ├─ Find an item by its key, wherever it stands → built in: Find seeks every item kind's record; a pick flies to the item and selects it
|
|
184
|
+
│ ├─ On a phone: below 480px of main, nothing drags → built in: Details' Connect to… and the Backlog card's Place, beside an item's x and y, a link's ends, a net's sides and Delete
|
|
185
|
+
│ ├─ Shapes from the row's own fields → footprint: (u) => some(Schematic.circle(u.radius)) · a zone's geometry: (a) => some(Schematic.polygon(a.corners))
|
|
186
|
+
│ ├─ What a card draws from another record → Schematic.items(…, { details: Schematic.details(record, { status, meter, metric, sublabel }) })
|
|
187
|
+
│ ├─ The layers, from the app's record → layers={Schematic.layers(systems.read(), { label, order?, tone?, color?, visible?, locked?, opacity? })}
|
|
188
|
+
│ ├─ Templates, the backlog, the index, the layers → Schematic.templates(rows, { …, source }) · library={[Schematic.library.templates(), .backlog(), .index(), .layers()]}
|
|
189
|
+
│ ├─ A card dropped on the canvas, or ⏎ on it → an item template adds an item at the point, a backlog row is placed there, a link or net template onto one of its kind; an author's card onto an entity: Schematic.library.tab(rows, { …, drop: (r) => Schematic.patch(R, { … }) }); ⏎ onto the selection
|
|
190
|
+
│ ├─ The inspector, a kind's form and its own Details → inspector · Schematic.items(record, { …, fields?: { f: Fields.* }, ready?: (row, key) => Readiness, inspector?: East.function([R, FunctionType([R], NullType)], UIComponentType, …) })
|
|
191
|
+
│ └─ What the host hears, and the connect veto → onSelect={fn(Array<Schematic.Types.Ref>)} · canConnect={fn(Schematic.Types.Candidate) → Option<String>}
|
|
192
|
+
│
|
|
173
193
|
├─ Staff a week's shifts from the app's own duties — the Roster (see Roster below)
|
|
174
|
-
│ ├─ The duties: the app's record, a span kind → events={{ duty: Schedule.events(Record.bind(duties
|
|
194
|
+
│ ├─ The duties: the app's record, a span kind → events={{ duty: Schedule.events(Record.bind(duties), { start, end, resource: { field, of }, shift, group, position?, activity?, hours? }) }}
|
|
175
195
|
│ ├─ Who is rostered → resources={{ people: Schedule.resources(staff.read(), { name, icon, label, group, … }), agencies? }}
|
|
176
196
|
│ ├─ The lists it reads, over the app's rows → Roster.shifts / groups / positions / activities / proposals(rows, { … })
|
|
177
|
-
│ ├─ Publish, and the targets: the app's weeks and cover → periods={Schedule.periods(Record.bind(weeks
|
|
197
|
+
│ ├─ Publish, and the targets: the app's weeks and cover → periods={Schedule.periods(Record.bind(weeks), { every: "week", status, dismissed? })} · targets={Roster.targets(Record.bind(cover), { day, group, shift, positions, hours })}
|
|
178
198
|
│ ├─ Costs in the app's own pay codes → pay={fn(Roster.Types.PayRequest(R)) → Roster.Types.Pay} currency="…"
|
|
179
199
|
│ ├─ Who fits a Find fix: the app's fit, on e3 → fit={Func.bind(e3.function)} from Roster.Types.FitRequest(R) to FitResult, over Roster.answer(R) + Roster.fit(R)
|
|
180
|
-
│ ├─
|
|
200
|
+
│ ├─ The app's optimiser over the week: the Elara eye → optimise={Func.bind(e3.function)} from Roster.Types.OptimiseRequest(R) to Roster.Types.OptimiseResult(R); its answer lands as ONE step of the history (#1409)
|
|
201
|
+
│ ├─ Narrow its duties, targets, people, activities and patterns → slice={{ slice: Slice.bindRecords(key, { duty: { source, range: "start" }, targets?, people?, activities?, patterns? }) }}
|
|
202
|
+
│ ├─ People by week or by month, its tiles' and marks' words → view={{ layout: "people", period: "week" | "month", date }} · tileTimes="always" | "different" · letters
|
|
203
|
+
│ ├─ The shifts' colours, and the app's week patterns → shifts={Roster.shifts(rows, { …, color: s => s.colour })} (an Option<ColorToken>) · patterns={Roster.patterns(rows, { name, icon?, label, days, order? })} · library={[…, Roster.library.patterns()]}
|
|
181
204
|
│ └─ The week's figures in a task → Roster.coverage / check / weekHours / rest (R)
|
|
182
205
|
│
|
|
183
206
|
├─ Book a day's arrivals into lane slots, each bin on its resources — the Allocation (see Allocation below)
|
|
184
|
-
│ ├─ The bins: the app's record, an instant kind → events={{ bin: Schedule.events(Record.bind(bins
|
|
207
|
+
│ ├─ The bins: the app's record, an instant kind → events={{ bin: Schedule.events(Record.bind(bins), { at, resource: { field, of: "<lanes>" }, arrival, quantity: { field, unit }, parts: { field, of: "<resources>" }, hold, kind? }) }}
|
|
185
208
|
│ ├─ The lanes and the resources → resources={{ <lanes>: Schedule.resources(rows, { name, icon, label, offset? }), <resources>: Schedule.resources(rows, { name, icon, label, min, max, … }) }}
|
|
186
209
|
│ ├─ What it books from → arrivals={Allocation.arrivals(rows, { label, quantity, hold, kind?, values?, … })}
|
|
187
|
-
│ ├─ Publish: the app's days → periods={Schedule.periods(Record.bind(days
|
|
210
|
+
│ ├─ Publish: the app's days → periods={Schedule.periods(Record.bind(days), { every: "day", status })}
|
|
188
211
|
│ ├─ Who fits where: the app's fit, on e3 → fit={Func.bind(e3.function)} from Allocation.Types.FitRequest(R) to FitResult, over Allocation.answer(R) + Allocation.fit (+ Allocation.lookahead(R))
|
|
212
|
+
│ ├─ The app's optimiser over the day: the Elara eye → optimise={Func.bind(e3.function)} from Allocation.Types.OptimiseRequest(R) to Allocation.Types.OptimiseResult(R); its answer lands as ONE step of the history (#1409)
|
|
189
213
|
│ ├─ Narrow its bins, lanes, resources and arrivals — one rail → slice={{ slice: Slice.bindRecords(key, { bin: { source, range: "at" }, <lanes>?, <resources>?, arrivals? }) }}
|
|
190
214
|
│ ├─ Its panes → library={[Allocation.library.arrivals(), Allocation.library.resources()]} · inspector (+ the bin kind's own inspector)
|
|
191
215
|
│ ├─ The app's bin fields on the board, and its own issues → columns={[{ key, label, text }]} · check={fn(CheckContext(R)) → Array<Issue>}
|
|
@@ -290,7 +314,7 @@ rather than the record's, and each row carries what a view needs to render
|
|
|
290
314
|
without touching the record:
|
|
291
315
|
|
|
292
316
|
```tsx
|
|
293
|
-
const queue = $.let(Data.bindPaged(plans, { index:
|
|
317
|
+
const queue = $.let(Data.bindPaged(plans, { index: "by_status" }));
|
|
294
318
|
// rows: Array<{ ik: StatusKey, key: PlanKey, value: Projection, row: Option<Plan> }>
|
|
295
319
|
<Table data={queue} columns={{
|
|
296
320
|
ik: { header: "Status", value: (ik) => ik.status },
|
|
@@ -298,11 +322,12 @@ const queue = $.let(Data.bindPaged(plans, { index: byStatus }));
|
|
|
298
322
|
}} />
|
|
299
323
|
```
|
|
300
324
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
325
|
+
Name one of the indexes the record declares: the name is checked against the
|
|
326
|
+
record, and the window's type — the index key and the covering projection —
|
|
327
|
+
comes from the index's declaration, so the rows are checked at compile time.
|
|
328
|
+
`join: true` reads each entry's row from the record too; leaving it off is the
|
|
329
|
+
point of a covering projection, since the view then touches the index's
|
|
330
|
+
segments and none of the record's.
|
|
306
331
|
|
|
307
332
|
`.seek` takes a `from..to` range as well as a key or prefix, bounding a leading
|
|
308
333
|
prefix of the key's flattened fields — `late, 3..ok` over a `{status, due}`
|
|
@@ -313,15 +338,89 @@ inside it is text, so `"../config"` finds that key rather than a range.
|
|
|
313
338
|
|
|
314
339
|
### Indexed records and shared Slice
|
|
315
340
|
|
|
316
|
-
Declare field indexes
|
|
317
|
-
|
|
318
|
-
supply the controls; do not redeclare those types in Slice. A
|
|
319
|
-
key uses the index name as its field; a Struct key supplies
|
|
320
|
-
key order. `keys` accepts a scalar or a single-field Struct and
|
|
321
|
-
exact member, keeping pages and counts unique by primary ID.
|
|
322
|
-
compound fan-out remains available through `{ index }`; Slice refuses
|
|
323
|
-
`Data.bindPaged(record, { indexes: [
|
|
324
|
-
types and joins rows by default.
|
|
341
|
+
Declare field indexes in the record's `indexes` with `index.on([fields...])`, or
|
|
342
|
+
computed indexes with `index.key(fn)` / `index.keys(fn)`. Their existing
|
|
343
|
+
declared key types supply the controls; do not redeclare those types in Slice. A
|
|
344
|
+
scalar computed key uses the index name as its field; a Struct key supplies
|
|
345
|
+
named fields in key order. `keys` accepts a scalar or a single-field Struct and
|
|
346
|
+
permits one exact member, keeping pages and counts unique by primary ID.
|
|
347
|
+
Arbitrary compound fan-out remains available through `{ index }`; Slice refuses
|
|
348
|
+
it. `Data.bindPaged(record, { indexes: [names...] })` carries their field/key
|
|
349
|
+
types and joins rows by default. The indexes a binding lists carry one
|
|
350
|
+
projection, or each carries the whole row — as an area index does — or nothing:
|
|
351
|
+
one that carries the row is read without a join, the others with one, and any
|
|
352
|
+
other mix is refused at build, naming the index.
|
|
353
|
+
|
|
354
|
+
**Beside a nested series index** (`index.nested({ collections, tile, summary
|
|
355
|
+
})`), the record's other indexes carry each row without the collections it
|
|
356
|
+
declares, and a binding listing any of them lists it too:
|
|
357
|
+
`Data.bindPaged(machines, { indexes: ["machines_by_site_status",
|
|
358
|
+
"machines_in_time"] })`. Such a binding pages the lean rows (its projection the
|
|
359
|
+
record's row, each listed collection empty) and never joins one: `join: true` is
|
|
360
|
+
refused, and so is an index with a `value` of its own beside it, which is bound
|
|
361
|
+
apart. A row is read whole by its key. The binding's type carries how each
|
|
362
|
+
collection is declared (what it holds, its times, its rollups), and the nested
|
|
363
|
+
index's own entries — items, totals and subtotals — are read a cell at a time: a
|
|
364
|
+
page or a count of it is refused. A component given an indexed binding as `data`
|
|
365
|
+
reads its unfiltered view, a page at a time in its default index's order. A
|
|
366
|
+
`<Plan>` given such a binding as `data`, or a Slice whose `rows` source is one,
|
|
367
|
+
holds its `series` to the index in the editor: a series reading a collection the
|
|
368
|
+
index does not declare, by other times than it files by, items as values, a
|
|
369
|
+
subtotal by a rollup it does not keep, or runs rolled up that it files under no
|
|
370
|
+
parent, fails to compile, the error naming the index and what to write. The Plan
|
|
371
|
+
holds them again as it is built, a series list held in a variable too, and
|
|
372
|
+
refuses as well a function of the row mapping a collection the page carries
|
|
373
|
+
empty (a function it cannot see into, given the row, is refused rather than
|
|
374
|
+
guessed), a subtotal of values drawn as another number than stored or at a
|
|
375
|
+
resolution finer than the index's tile, a read by name where the binding lists
|
|
376
|
+
no nested series index or on an axis other than time, and an index keeping no
|
|
377
|
+
totals. Rows paged whole (`Data.bindPaged(dataset)`) are read as before.
|
|
378
|
+
|
|
379
|
+
**A Plan over them reads a cell at a time** (#1438; the Plan's `plan-nested`
|
|
380
|
+
example, `planNestedAll`: every series kind over two hundred works and ten
|
|
381
|
+
years — chart, events, cards and buckets switched off to start — its series
|
|
382
|
+
picked from the library, a crew's hours typed into). The rail pages the rows lean; for the rows in view and 25
|
|
383
|
+
either side, in blocks of fifty, the Plan reads each collection its series
|
|
384
|
+
draw — one range a row a level its items are filed at — in one batch of
|
|
385
|
+
searches and one read of the entries, and draws them with the same series as
|
|
386
|
+
over rows read whole. A pan reads only the tiles it adds; a zoom reads the
|
|
387
|
+
levels it shows. Past 10,000 items of a collection in the cells it wants, the
|
|
388
|
+
rows shade from the index's totals at its summary tile (runs and chips by the
|
|
389
|
+
share of the bucket booked, marks by how many, values folded as the series
|
|
390
|
+
folds them), or are hatched, a banner saying how many and why. A parent reads
|
|
391
|
+
what e3 keeps for it: a table's or a heat's aggregate and a group's strip are
|
|
392
|
+
its subtotals, one range at the resolution, by the rollup its `fold` and
|
|
393
|
+
`aggregate` make (which the index must keep: `values({ rollup })`); a span's
|
|
394
|
+
bands are the items e3 files under it (`items(…, { rollup: true })`) — so a
|
|
395
|
+
folded parent reads none of the rows beneath it. Editing over them: a gesture
|
|
396
|
+
drafts each row it touches read whole by its key, every row asked meanwhile in
|
|
397
|
+
one request; each drafted row's Save, previewed by e3 (#1437), is laid over
|
|
398
|
+
the cells, so the moved item and every subtotal over it draw before Save,
|
|
399
|
+
while which rows show keeps to what is saved; Save restates each listed
|
|
400
|
+
collection's change by key (`Record.onApply(record, { keyed: true })`), so
|
|
401
|
+
another planner's item added meanwhile is kept. A table's `edit: true` takes
|
|
402
|
+
typed numbers the same way (the ledgers: a reading typed into a leaf moves
|
|
403
|
+
every subtotal above it at once). What each series reads is known as the Plan
|
|
404
|
+
is built, so its series are a list of `Plan.series.*` values: in `series`, or
|
|
405
|
+
in `Plan.pick(key, [...])` passed to `pick` as it returns it, which lets a
|
|
406
|
+
viewer switch each series on and off from the library's Series tab
|
|
407
|
+
(`Plan.library.series()`). A list held as an East expression, or a pick held
|
|
408
|
+
in an East variable (`$.let`), is refused: the Plan could not see what its
|
|
409
|
+
series read. While cells are read — a pan, a zoom, a scroll, a commit — the
|
|
410
|
+
rows waiting on them wash, their plots busy, until the cells land. Its horizon
|
|
411
|
+
spans the time every series reads, shown or not, as e3 keeps it beside the
|
|
412
|
+
index (its span, `<index>_span`): from the first month to the last any row's
|
|
413
|
+
items or values touch, in the collections the series read, read once and again
|
|
414
|
+
at each commit — a series switched off moves no edge of it, and with every
|
|
415
|
+
series off the strip still spans the data — so the brush reaches every part of
|
|
416
|
+
the data from the start, not the window's own margins. Its bars are how much
|
|
417
|
+
the shown series' collections hold in each month, as the span counts it: a
|
|
418
|
+
bar each month, quarter or year — the finest that keeps 4px a bar — toned on
|
|
419
|
+
the heat ramp by its count, the largest in a meta line under the caption
|
|
420
|
+
(`max 1,559 items / mo`), and a bar's hover saying its period and count. Its
|
|
421
|
+
canvas holds its own window, and a zoom zooms it: as many buckets of the new
|
|
422
|
+
resolution as it showed of the old, about the same middle, inside that span —
|
|
423
|
+
the year shows the whole of it.
|
|
325
424
|
|
|
326
425
|
**A builder narrows through `Slice.bindRecords` alone** (#1340): one rail on
|
|
327
426
|
its toolbar, `Slice.bindRecords(key, { [source]: { source:
|
|
@@ -329,15 +428,19 @@ Data.bindPaged(record, { indexes }), label?, icon?, fields, range?, search?,
|
|
|
329
428
|
initial? } }, { presets? })`, passed as `slice={{ slice, affordances?,
|
|
330
429
|
persist? }}`. Each source is named after what it narrows and filters every
|
|
331
430
|
place its records appear — the main area, the Choosers and the Library tab
|
|
332
|
-
that lists them
|
|
333
|
-
|
|
431
|
+
that lists them; the Sheet's `rows` source draws its narrowing over the
|
|
432
|
+
record's rows as the lens instead (#1349) — and the builder reads each
|
|
433
|
+
source's keys itself. Every
|
|
434
|
+
source with a `range` shares the builder's one time window; a Plan's `rows`
|
|
435
|
+
source and its resource kinds' take none, so the dates on screen never decide
|
|
436
|
+
which rows it lists. Calculations —
|
|
334
437
|
coverage, costs, loads, checks, totals — read every record, whatever the
|
|
335
438
|
rail hides. Refused at build, each naming the change: `Slice.bind`, a single
|
|
336
439
|
`Slice.bindRecord`, a source the builder does not take,
|
|
337
440
|
`data={Slice.rows(slice)}` or `window={Slice.rows(slice)}`, the Roster's
|
|
338
441
|
`visiblePeople`, and an event kind's own `slice` or `filter`. Every
|
|
339
442
|
builder has moved: the Allocation, the Roster, the Calendar, the Plan, the
|
|
340
|
-
Sheet and the
|
|
443
|
+
Sheet, the Flowchart and the Schematic.
|
|
341
444
|
|
|
342
445
|
Templates are records: each templates list reads a templates record and names
|
|
343
446
|
its source of the rail (`source`), and a list written in the code is refused.
|
|
@@ -345,7 +448,7 @@ A record a builder reads whole beside its pages — a templates list, the
|
|
|
345
448
|
resources a check reads — binds with `Data.bind`: a UI task's manifest
|
|
346
449
|
preloads it, but never a record `Record.bind` reads beside `Data.bindPaged`,
|
|
347
450
|
whose `read()` then fails on e3. `Record.bind` stays the mutation door a Save
|
|
348
|
-
commits through. east-ui's own Table, Library
|
|
451
|
+
commits through. east-ui's own Table, Library and charts keep
|
|
349
452
|
`Slice.bind`. Existing single-index `Data.bindPaged(record, { index, join })`
|
|
350
453
|
remains separate.
|
|
351
454
|
|
|
@@ -362,15 +465,40 @@ Local arrays use `Slice.bind(key, rows, options?)`; neither arm needs a type
|
|
|
362
465
|
argument for `Slice.rows(slice)`.
|
|
363
466
|
|
|
364
467
|
Calendar event windows preserve overlap with `[from, to)`, including carry-in
|
|
365
|
-
and whole-window spans. Declare
|
|
366
|
-
index (optional leading `on` fields), name
|
|
367
|
-
slot with `range: "start"` (`job: { source:
|
|
368
|
-
[
|
|
369
|
-
Data.bindPaged(jobs)` for its original-key reads. A kind
|
|
370
|
-
source takes no `window` or `backlogWindow`. Do not
|
|
371
|
-
start-only query or guessed lookback. Optional missing ends
|
|
372
|
-
an instant at `from` is included and one at `to` is excluded.
|
|
373
|
-
part of the same authoritative record.
|
|
468
|
+
and whole-window spans. Declare an interval index among the record's indexes,
|
|
469
|
+
`index.interval({ on?, from: "start", to: "end" })` (optional leading `on` fields), name
|
|
470
|
+
the event kind's source after its slot with `range: "start"` (`job: { source:
|
|
471
|
+
Data.bindPaged(jobs, { indexes: ["jobs_by_time"] }), range: "start" }`), and give
|
|
472
|
+
the kind `entries: Data.bindPaged(jobs)` for its original-key reads. A kind
|
|
473
|
+
narrowed through its source takes no `window` or `backlogWindow`. Do not
|
|
474
|
+
replace it with a start-only query or guessed lookback. Optional missing ends
|
|
475
|
+
are instants; an instant at `from` is included and one at `to` is excluded.
|
|
476
|
+
Backlog remains part of the same authoritative record.
|
|
477
|
+
|
|
478
|
+
A **window over a drawing** — the rows whose box meets a viewport — reads
|
|
479
|
+
through an area index (`index.area`, the **e3** skill) listed in a
|
|
480
|
+
source's binding: `Data.bindPaged(racks, { indexes: ["racks_by_area",
|
|
481
|
+
"racks_by_hall_area"] })`. The Schematic reads a kind so wherever the kind's source
|
|
482
|
+
of its rail lists one, the window its view shows (see The Schematic below,
|
|
483
|
+
`schematicRecordIndexes`). A read of a source — its `pageWhere` and
|
|
484
|
+
`countWhere` — takes a read, `{ where, area }`: the narrowing, and for a read
|
|
485
|
+
by area its clause beside it, `some(variant("window", { minX, minY, maxX,
|
|
486
|
+
maxY }))` in world units, or `some(variant("unplaced", null))` for the rows not
|
|
487
|
+
yet placed, on an index whose `x` and `y` are Options; any other read gives
|
|
488
|
+
`area: none`. The narrowing's exact choices of the fields an area index is led
|
|
489
|
+
by pick it. The record slice plans a window as at most 266 searches a
|
|
490
|
+
combination of those choices, sent in one request, and reads every row whose
|
|
491
|
+
box meets the viewport grown by an eighth of its larger side, each once —
|
|
492
|
+
beside rows of the searched cells that it misses, which the reader draws or
|
|
493
|
+
not. The window is the reader's, never the rail's: no choice holds it, and the
|
|
494
|
+
rail's state, its presets and `contains` keep the narrowing alone. A read no
|
|
495
|
+
listed area index serves — choices beside the window no area index is led by
|
|
496
|
+
exactly, a range or a prefix beside it, or more than one value of a Set field,
|
|
497
|
+
which would read a row twice — is refused, naming why. An area index carries
|
|
498
|
+
its rows, so a window reads the index alone, without a join, each entry's
|
|
499
|
+
`value` its row. e3's parts of a generated key are its index's kind's, never a
|
|
500
|
+
field's name (#1432): a row's field named `area` or `__area` is a field of the
|
|
501
|
+
rail like any other, and may lead an area index a window reads through.
|
|
374
502
|
|
|
375
503
|
Sessions retain original-key drafts across paging, narrowing and remount in the
|
|
376
504
|
same UI store. Save includes hidden edits and confirms primary rows at the
|
|
@@ -380,11 +508,15 @@ unsaved drafts. The Roster and the Allocation keep a session for each record
|
|
|
380
508
|
they write — the Roster's duties, weeks and targets, the Allocation's bins and
|
|
381
509
|
days — under one history: Save sends each record's drafts as its own request,
|
|
382
510
|
and their figures read every person, resource and event of the period,
|
|
383
|
-
whatever the rail shows.
|
|
511
|
+
whatever the rail shows. The Schematic keeps one for each kind it writes, and
|
|
512
|
+
a kind read by area keeps its drafts by key wherever the window goes: a row
|
|
513
|
+
moved off the window draws where its draft places it while the narrowing holds
|
|
514
|
+
its stored row, and Save writes it through the kind's patch door.
|
|
384
515
|
|
|
385
516
|
Search examples: `planRecordIndexes`, `sheetRecordIndexes`,
|
|
386
|
-
`calendarRecordIndexes`, `rosterRecordIndexes`,
|
|
387
|
-
|
|
517
|
+
`calendarRecordIndexes`, `rosterRecordIndexes`, `schematicRecordIndexes` (a
|
|
518
|
+
window read by area), their Mobile counterparts, and `flowchartRecordIndexes`
|
|
519
|
+
(scalar, compound and membership computed keys).
|
|
388
520
|
Each shows its full reactive bindings and JSX inline.
|
|
389
521
|
|
|
390
522
|
### `Func.bind(fn)`
|
|
@@ -584,12 +716,11 @@ export const salesCount = Studio.component("sales_count", {
|
|
|
584
716
|
| `tags` / `collections` | the palette's Filter facets |
|
|
585
717
|
| `deprecated` | hidden from the palette; placements keep rendering |
|
|
586
718
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
it.
|
|
719
|
+
One field is derived from `fn`, never written: `reads`, the datasets,
|
|
720
|
+
functions and records its code binds (`deriveManifest` over `fn`) — the palette
|
|
721
|
+
card's meta line, the canvas tile's and the inspector's data list, each dataset
|
|
722
|
+
and record named in words (a task's output by its task's name:
|
|
723
|
+
`.inputs.sales_daily` reads "Sales daily"), never a keypath.
|
|
593
724
|
|
|
594
725
|
A surface lists the components it offers with `$.let([...])`, in palette order,
|
|
595
726
|
and hands the list to the Studio's components, which draw each placement by
|
|
@@ -600,6 +731,8 @@ calling its component's function:
|
|
|
600
731
|
placements of it share them.
|
|
601
732
|
- A placement whose component the list does not hold is a placeholder naming its
|
|
602
733
|
key; a key two listed components share is an error naming it.
|
|
734
|
+
- A placement holds its component's key alone, so a deploy that changes a
|
|
735
|
+
component's code changes every page that places it, its live version too.
|
|
603
736
|
|
|
604
737
|
### Studio pages — `Studio.Types.Pages`
|
|
605
738
|
|
|
@@ -607,11 +740,15 @@ The Studio exports the pages record's type; a solution declares its storage like
|
|
|
607
740
|
any record, with one write, a patch:
|
|
608
741
|
|
|
609
742
|
```ts
|
|
610
|
-
export const pages
|
|
611
|
-
|
|
743
|
+
export const pages = e3.record("pages", Studio.Types.Pages, {
|
|
744
|
+
initial: new Map(),
|
|
745
|
+
mutations: (mutation) => ({
|
|
746
|
+
patch: mutation.patch(),
|
|
747
|
+
}),
|
|
748
|
+
});
|
|
612
749
|
```
|
|
613
750
|
|
|
614
|
-
The builder and the page library bind it with `Record.bind(pages
|
|
751
|
+
The builder and the page library bind it with `Record.bind(pages)`;
|
|
615
752
|
a published page reads it with `Data.bind(pages)`. The type never depends on the
|
|
616
753
|
components, so a deploy that adds, changes or removes one runs no migration.
|
|
617
754
|
|
|
@@ -619,15 +756,13 @@ components, so a deploy that adds, changes or removes one runs no migration.
|
|
|
619
756
|
`Entry` is a `page` — `{ draft, live }`, `live` being `none` until the first
|
|
620
757
|
publish, else `{ version, page }` — or a `template`, a layout new pages start from.
|
|
621
758
|
- A `Page` is `{ title, cells }`, and a `Cell` is `{ key, row, span, height,
|
|
622
|
-
align, title, component
|
|
623
|
-
SnapGrid (rows in the order their keys first appear)
|
|
624
|
-
fingerprint when it was placed, stamped anew each time the page publishes — so
|
|
625
|
-
a live version's cells hold the code each went live with.
|
|
759
|
+
align, title, component }`: a component's key, and where it sits on the
|
|
760
|
+
SnapGrid (rows in the order their keys first appear).
|
|
626
761
|
|
|
627
762
|
Every operator action is one patch commit, computed in East as the diff of the
|
|
628
763
|
one entry it writes: the builder's Save (the page's draft cells), its Publish
|
|
629
|
-
(the live version becomes the draft, numbered one up — 1 the first time
|
|
630
|
-
|
|
764
|
+
(the live version becomes the draft, numbered one up — 1 the first time) and
|
|
765
|
+
its Save as template;
|
|
631
766
|
the page library's new page (a template's cells, or none for Blank grid). A
|
|
632
767
|
patch carries what it changes as it was, so a write drafted on a stale page is a
|
|
633
768
|
`conflict`, refused in the words of the screen that wrote it, and nothing is
|
|
@@ -644,7 +779,7 @@ takes the record bound with its patch:
|
|
|
644
779
|
export const builder = ui("builder", [], East.function([], UIComponentType, _$ => (
|
|
645
780
|
<Reactive>{$ => {
|
|
646
781
|
const components = $.let([kpiRail, revenueTrend, breakdownBars]);
|
|
647
|
-
const record = $.let(Record.bind(pages
|
|
782
|
+
const record = $.let(Record.bind(pages));
|
|
648
783
|
return <Studio.Builder pages={record} components={components} project="Ops console"
|
|
649
784
|
env="Staging" audience="Field ops · 24 users" rollout="Immediate" />;
|
|
650
785
|
}}</Reactive>
|
|
@@ -657,9 +792,11 @@ export const builder = ui("builder", [], East.function([], UIComponentType, _$ =
|
|
|
657
792
|
layout edit the inspector asks for, is a draft; the history item undoes,
|
|
658
793
|
redoes, discards and saves, and ⌘Z / Ctrl+Z undo from anywhere in the
|
|
659
794
|
builder. Save is one patch commit on the page; a save another write beat is
|
|
660
|
-
refused, and the
|
|
661
|
-
|
|
662
|
-
|
|
795
|
+
refused, and the drafts are carried over to the newer page, a part both
|
|
796
|
+
changed waiting to be chosen before Save — with none, the Save goes again
|
|
797
|
+
by itself. The selection bar
|
|
798
|
+
names the selected placement: its component's icon and name, and what its
|
|
799
|
+
code reads, never its key.
|
|
663
800
|
- **The toolbar**, one row: the page's status — `Draft` with an open ring until
|
|
664
801
|
its first publish, `Live` while its draft is its live layout, `Live · edited`
|
|
665
802
|
once they differ, `Template` for a template — the grid chip and the time of
|
|
@@ -673,16 +810,15 @@ export const builder = ui("builder", [], East.function([], UIComponentType, _$ =
|
|
|
673
810
|
- **The palette**, a pane of two tabs before the canvas. Components: every
|
|
674
811
|
listed component but the deprecated, by category, with a search and a Filter
|
|
675
812
|
menu over category, tags and collections; a card shows the component's icon,
|
|
676
|
-
its name, the
|
|
677
|
-
a placement at its component's span
|
|
813
|
+
its name, the data its code reads in words and a lock, and drags onto the canvas as
|
|
814
|
+
a placement at its component's span. The component
|
|
678
815
|
the canvas has selected is drawn placed, `ON CANVAS · ×N`, unsaved drafts
|
|
679
816
|
counted; a click selects its first placement. Pages: the project's pages in
|
|
680
817
|
key order with their status (`LIVE · Vn`, `DRAFT · Vn LIVE` or `DRAFT`); a
|
|
681
818
|
click opens one.
|
|
682
819
|
- **The inspector**, the pane after the canvas: the selected placement's name
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
(`.inputs.sales_daily`); its description, under a lock; and its layout — its
|
|
820
|
+
(never its component's key); what its code reads, each named in words ("Sales
|
|
821
|
+
daily"), never a keypath; its description, under a lock; and its layout — its
|
|
686
822
|
span, held to its row's room, its row, its height (Auto or a preset in px)
|
|
687
823
|
and where it sits in a taller row. Each edit is one gesture of the canvas's
|
|
688
824
|
session. With nothing selected it says so.
|
|
@@ -694,9 +830,8 @@ export const builder = ui("builder", [], East.function([], UIComponentType, _$ =
|
|
|
694
830
|
and Exit, back to the canvas with its drafts as they were; the page as it will
|
|
695
831
|
publish, its unsaved drafts in place; and an aside — "Ready to publish", the
|
|
696
832
|
version it replaces and the one it becomes (`v3 → v4`), the changes since,
|
|
697
|
-
each placement added, removed, moved or resized;
|
|
698
|
-
|
|
699
|
-
live version; the Audience and Rollout rows; Save as draft, which saves the
|
|
833
|
+
each placement added, removed, moved or resized; the Audience and Rollout
|
|
834
|
+
rows; Save as draft, which saves the
|
|
700
835
|
canvas's drafts; and Publish, which saves them first, then commits the
|
|
701
836
|
publish. A page never published is its first version, one live as it stands
|
|
702
837
|
is up to date, and a template is not published.
|
|
@@ -707,7 +842,7 @@ draws no border around itself, so a host frames it or places it bare.
|
|
|
707
842
|
|
|
708
843
|
| Prop | Meaning |
|
|
709
844
|
|---|---|
|
|
710
|
-
| `pages` | **required** — the record bound with its patch: `Record.bind(pages
|
|
845
|
+
| `pages` | **required** — the record bound with its patch: `Record.bind(pages)` |
|
|
711
846
|
| `components` | **required** — the components the surface lists, in palette order |
|
|
712
847
|
| `project` | **required** — the project whose pages it opens |
|
|
713
848
|
| `env` | where the preview publishes to — its Env pill, and "Publish vN to <env>"; omitted, neither names one |
|
|
@@ -723,7 +858,7 @@ commit, so it takes the record bound with its patch:
|
|
|
723
858
|
```tsx
|
|
724
859
|
<Reactive>{$ => {
|
|
725
860
|
const components = $.let([kpiRail, revenueTrend, breakdownBars]);
|
|
726
|
-
const record = $.let(Record.bind(pages
|
|
861
|
+
const record = $.let(Record.bind(pages));
|
|
727
862
|
return <Studio.Library pages={record} components={components} project="Ops console" />;
|
|
728
863
|
}}</Reactive>
|
|
729
864
|
```
|
|
@@ -752,7 +887,7 @@ draws no border around itself.
|
|
|
752
887
|
|
|
753
888
|
| Prop | Meaning |
|
|
754
889
|
|---|---|
|
|
755
|
-
| `pages` | **required** — the record bound with its patch: `Record.bind(pages
|
|
890
|
+
| `pages` | **required** — the record bound with its patch: `Record.bind(pages)` |
|
|
756
891
|
| `components` | **required** — the components the surface lists — what a template's line names |
|
|
757
892
|
| `project` | **required** — the project it shows first |
|
|
758
893
|
| `onOpen` | told when a page opens in the builder, with its key — the host shows the builder |
|
|
@@ -801,8 +936,12 @@ result, and save them. A solution declares one record of saved queries, with
|
|
|
801
936
|
one write, a patch:
|
|
802
937
|
|
|
803
938
|
```ts
|
|
804
|
-
export const queries
|
|
805
|
-
|
|
939
|
+
export const queries = e3.record("queries", Query.Types.Saved, {
|
|
940
|
+
initial: new Map(),
|
|
941
|
+
mutations: (mutation) => ({
|
|
942
|
+
patch: mutation.patch(),
|
|
943
|
+
}),
|
|
944
|
+
});
|
|
806
945
|
```
|
|
807
946
|
|
|
808
947
|
`Query.Types.Saved` is `Dict<String, SavedQuery>`, by name. A saved query is
|
|
@@ -820,11 +959,16 @@ builds, as the builder checks a query it saves. The sources are named as a
|
|
|
820
959
|
query reads them, each the dataset or task `Data.bind` takes:
|
|
821
960
|
|
|
822
961
|
```ts
|
|
823
|
-
export const queries = e3.record("queries", Query.Types.Saved,
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
962
|
+
export const queries = e3.record("queries", Query.Types.Saved, {
|
|
963
|
+
initial: Query.value({ orders, customers }, [
|
|
964
|
+
{ name: "Big orders", jq: ".orders | map(select(.total >= 1000))", savedAt: new Date("2026-10-01T09:00:00Z") },
|
|
965
|
+
{ name: "Orders by region", jq: ".customers as $c | .orders | group_by($c[.customer_id].region) | map(length)",
|
|
966
|
+
description: "How many orders each region placed.", savedAt: new Date("2026-10-01T09:00:00Z") },
|
|
967
|
+
]),
|
|
968
|
+
mutations: (mutation) => ({
|
|
969
|
+
patch: mutation.patch(),
|
|
970
|
+
}),
|
|
971
|
+
});
|
|
828
972
|
```
|
|
829
973
|
|
|
830
974
|
A query that does not check fails the build with the checker's words, as do two
|
|
@@ -836,7 +980,7 @@ export const builder = ui("query_builder", [], East.function([], UIComponentType
|
|
|
836
980
|
<Reactive>{$ => {
|
|
837
981
|
const orders = $.let(Data.bindPaged(d.orders)); // large: paged, never loaded whole
|
|
838
982
|
const customers = $.let(Data.bind(d.customers));
|
|
839
|
-
const saved = $.let(Record.bind(d.queries
|
|
983
|
+
const saved = $.let(Record.bind(d.queries));
|
|
840
984
|
return <Query.Builder queries={saved} datasets={{ orders, customers }} />;
|
|
841
985
|
}}</Reactive>
|
|
842
986
|
)));
|
|
@@ -845,7 +989,7 @@ export const library = ui("query_library", [], East.function([], UIComponentType
|
|
|
845
989
|
<Reactive>{$ => {
|
|
846
990
|
const orders = $.let(Data.bindPaged(d.orders));
|
|
847
991
|
const customers = $.let(Data.bind(d.customers));
|
|
848
|
-
const saved = $.let(Record.bind(d.queries
|
|
992
|
+
const saved = $.let(Record.bind(d.queries));
|
|
849
993
|
return <Query.Library queries={saved} datasets={{ orders, customers }} />;
|
|
850
994
|
}}</Reactive>
|
|
851
995
|
)));
|
|
@@ -881,7 +1025,7 @@ beside it, a Table or a tree with Download (CSV, BEAST2); and a status line.
|
|
|
881
1025
|
|
|
882
1026
|
| Prop | Meaning |
|
|
883
1027
|
|---|---|
|
|
884
|
-
| `queries` | **required** — the record bound with its patch: `Record.bind(queries
|
|
1028
|
+
| `queries` | **required** — the record bound with its patch: `Record.bind(queries)` |
|
|
885
1029
|
| `datasets` | **required** — the data sources a query may read, by the names it reads them: `{ orders, customers }` |
|
|
886
1030
|
| `query` | the saved query it opens first, by name; omitted, a new query on the first data source |
|
|
887
1031
|
| `id` | names the builder — needed only when one surface holds two, and then given to the library that opens queries in it |
|
|
@@ -950,13 +1094,13 @@ return <Plan axis={axis} data={rows} series={series} />;
|
|
|
950
1094
|
|
|
951
1095
|
```
|
|
952
1096
|
<Plan axis={Plan.axis({…})} data={rows} series={[Plan.series.span(Row, {…}), …]} /> — the planning canvas: ONE shared axis — { time | number | ordinal } (#631): a UTC window ÷ resolution, a numeric window ÷ step, or an ordinal list = n bucket columns — over heterogeneous rows — the eight row kinds: span state-runs, bucket allocation lanes, chart measures, heat cells, bucketed table numerals, cards shift chips, event marks, and group strips — sliced, searched and picked as one surface. Rows derive from `data` through `series` as an ordered STREAM (#822): the series list IS the layout (one contiguous block per series, top to bottom), a parent is followed by its subtree, and hierarchy comes only from the data's own nesting — never from a field value. Every row carries a typed id, `Plan.Types.RowId` = { series, path } (the series that made it and the entry keys that lead to it; `Plan.ref` builds one), which every callback reports and `links` address. Nothing behaves differently because `data` is inline or paged
|
|
953
|
-
├─ The frame (#1193): ONE toolbar holding every control the Plan draws outside its canvas, in this order — the rail of its one `Slice.bindRecords` (each source's field chips and the presets, #1344), the key search, Expand/Collapse all, the shared date navigator (on a number axis the leading source's range is a chip of the rail), the configured resolution, the diagnostics; at its end the rail's summary, naming each source a narrowing hides rows of in its own words (#1358), the overlaps chip and the history item (a Plan with none of them has no toolbar). It is one row at every width: the rail folds first, its summary after every step of it, then the date navigator compacts, the resolution folds into a menu and Expand/Collapse all keeps its icon, the key search folds into its icon (which opens the box in a popover, the focus in it), then the time-axis view controls — the fold toggle, the resolution and the overlaps chip — share the date popover, one move, and the history item folds last, to its buttons; on a touch screen every control there is a 44px tap target. The banners — each session's, an event kind's titled with its name (#1194): a Save's conflict, a refusal, an unknown outcome with Retry
|
|
1097
|
+
├─ The frame (#1193): ONE toolbar holding every control the Plan draws outside its canvas, in this order — the rail of its one `Slice.bindRecords` (each source's field chips and the presets, #1344), the key search, Expand/Collapse all, the shared date navigator (on a number axis the leading source's range is a chip of the rail), the configured resolution, the diagnostics; at its end the rail's summary, naming each source a narrowing hides rows of in its own words (#1358), the overlaps chip and the history item (a Plan with none of them has no toolbar). It is one row at every width: the rail folds first, its summary after every step of it, then the date navigator compacts, the resolution folds into a menu and Expand/Collapse all keeps its icon, the key search folds into its icon (which opens the box in a popover, the focus in it), then the time-axis view controls — the fold toggle, the resolution and the overlaps chip — share the date popover, one move, and the history item folds last, to its buttons — below that, where the row is still too wide, the rail's icon drops its caret, so the history and Save stay in the row at a phone's width; on a touch screen every control there is a 44px tap target. The banners — each session's, an event kind's titled with its name (#1194): changes that conflict with newer ones, whose Choose opens the history item's conflicts, a Save's conflict, a refusal, an unknown outcome with Retry. Main — the canvas: the horizon, the ruler, pinned rows and the rows. The footer — the counts: the event kinds' events in the window, their backlog, the changes pending and when an event kind's record was last saved; then the author's `footer`, the one loading line — what the view waits for (`Loading rows 201–400`), a source's new narrowing, or a read that failed with its reason and Retry; nothing while nothing is in flight — and after a Save what left each source's narrowing (`1 print job event left this view`). ⌘Z undoes and ⇧⌘Z redoes from anywhere in the frame; a field being typed into keeps its own undo. The Plan draws no border
|
|
954
1098
|
├─ Props:
|
|
955
|
-
│ ├─ axis (required) — ONE of three kinds; every element instant on the canvas must ride its arm. The kind is also a TYPE: `Plan.axis.number(…)` fixes the tag's `K`, every builder result carries the kind its instants' STATIC types imply (a `DateTimeType` accessor ⇒ `"time"`, a `FloatType` field ⇒ `"number"`, `Plan.at.ordinal(…)` ⇒ `"ordinal"`), a series collects its elements' kinds, and a series (or literal row) on another arm FAILS TO COMPILE at the tag — no wire change, the brand is phantom. Kind-erased values (a stored record, an `Expr<Plan.Types.Instant>`, an East-mapped element list, a `$.let`-bound axis or `$.const`-bound series list, chart rows) constrain nothing at compile time and are held to the axis at render — a row on another arm is a render-time diagnostic naming the row and the arm, never a silent misplacement. Plan.axis({ window?, resolution, resolutions?, now?, format? }) — the TIME shorthand (= Plan.axis.time): half-open [min, max) UTC window; omit window ⇒ the range the sources of its `slice` hold — there is NO fit to the data (#822): a canvas that neither states a window nor has a source holding a range is refused, at build when the axis is written in the tag, else as the render-time NO WINDOW diagnostic (a bound or stored axis); `resolutions` lists the WEEK/DAY segment options; `now` draws the observed/plan divider; `format` overrides tick labels (date tokens; defaults: week ⇒ "W27", day ⇒ "MON"); at a resolution coarser than a row's data a bucket shows ONE value per row — the fold of those in it (#824) · Plan.axis.number({ window?, step, now?, format? }) — a numeric window ÷ `step` (bucket edges on whole steps; omit window ⇒ the leading source's float / integer range, under the same no-fit rule; `format` a Chart.format.* spec; no resolution segment — step IS the declaration) · Plan.axis.ordinal({ values, now? }) — the declared values ARE the buckets, in order (the list is the window: no source's range holds it, no brush, the window keys idle; an unlisted value positions nowhere; an interval END names its LAST bucket, inclusive)
|
|
956
|
-
│ ├─ data (required unless `events`, `rows` or `slice`'s `rows` source, which gives the rows and refuses `data` beside it — a Plan with no rows from any source is refused at build) — a KEYED collection: a `Dict<K, R>` value/expression for the inline arm, or a `$.let`-bound `Data.bindPaged(…)` handle for a WINDOWED arm, recognised by its East type: `pinned` when it names its snapshot, which `editing.onApply` needs; `paged` when it is the shape a UI exported before `revision` / `refresh`, which names none. Any key type `K` — a row's path starts with its entry's key, a String as it is, any other key as its `.east` text (`3`, `(hall="H1", bin=3)`) — and any entry type `R`: a struct, a `RecursiveType` node, or a collection (a `groupToDicts` group). A positional collection is refused — key it at the call site with `rows.toDict((_$, r) => r.id)`. A paged canvas pages by PARENTS (#823): a window is N top-level entries, each with its whole subtree, so a parent never straddles two windows and its bands, means, subtotals and member count are exact — nest children in the source, and group a flat paged source in its dataflow (a group is one entry holding its members; a parent is bounded by a page). Each top-level series is a BLOCK, exactly as inline: a paged canvas pages every block on its own over the same windows (its own bands and resident run; one read of a window serves every block), draws a section's header and hand-built rows once (FIXED blocks), and rebases only the block a far jump lands in; a window landing above the rows in view at a height its estimate missed moves the scroll, never the rows. A paged source may serve SHORT windows (e3 trims pages of wide entries to a byte budget) and the canvas still reads WHOLE ones — its derived source re-requests what a trimmed page left out. When the source's `revision()` moves (`Data.bindPaged` follows each write to its dataset), the canvas re-reads its resident windows at the new snapshot IN PLACE: the rows on screen stay until theirs land (no remount, no empty frame), the scroll position holds, and a standing key search is asked again at the new snapshot,
|
|
957
|
-
│ ├─ series (required unless `pick`) — the row recipes over `data`; the list IS the layout (#822): each series contributes one contiguous block, top to bottom in declared order, its rows in source order, each parent followed by its subtree. Data series: Plan.series.span/buckets/chart/heat/table/cards/events(Row, { key, title, subtitle?, icon?, keyType?, match?, label, id?, stacked?, sub?, value?, status?, expand?, children?, collapsed?, edit?, …kind fields }) — every accessor receives (entry, key); a `RecursiveType` entry arrives as its node.
|
|
958
|
-
│ ├─ pick (optional, exclusive with `series`) — Plan.pick(key, allSeries, { hidden? }): the bound series library — the toolbar has no Series button (#1193): the library pane's Series tab (#1195, when `library` lists `Plan.library.series()`) lists the series by title, subtitle and kind icon in the list's order (that order is the canvas layout); hidden series drop from the canvas (a hidden section, group or views takes its whole block); there are no row counts (#822 — a count means something only with every entry in hand); a pick is STATE, so the Plan must sit inside a <Reactive>
|
|
959
|
-
│ ├─ resources + events (optional) — event kinds over records (#1191), `Schedule`'s — the very values a Calendar takes: `resources={{ presses: Schedule.resources(rows, { name, icon, label, … }) }}` are the rows events are placed on, in this order — `rows` a `Dict`, or a `Data.bindPaged` read of one, which pages the kind (#1199, see Large records, a window at a time) — and `events={{ job: Schedule.events(Record.bind(jobs
|
|
1099
|
+
│ ├─ axis (required) — ONE of three kinds; every element instant on the canvas must ride its arm. The kind is also a TYPE: `Plan.axis.number(…)` fixes the tag's `K`, every builder result carries the kind its instants' STATIC types imply (a `DateTimeType` accessor ⇒ `"time"`, a `FloatType` field ⇒ `"number"`, `Plan.at.ordinal(…)` ⇒ `"ordinal"`), a series collects its elements' kinds, and a series (or literal row) on another arm FAILS TO COMPILE at the tag — no wire change, the brand is phantom. Kind-erased values (a stored record, an `Expr<Plan.Types.Instant>`, an East-mapped element list, a `$.let`-bound axis or `$.const`-bound series list, chart rows) constrain nothing at compile time and are held to the axis at render — a row on another arm is a render-time diagnostic naming the row and the arm, never a silent misplacement. Plan.axis({ window?, resolution, resolutions?, now?, format? }) — the TIME shorthand (= Plan.axis.time): half-open [min, max) UTC window; omit window ⇒ the range the sources of its `slice` holding the window hold — an event kind's, a tab's, never the `rows` source's or a resource kind's (#1344) — there is NO fit to the data (#822): a canvas that neither states a window nor has a source holding a range is refused, at build when the axis is written in the tag, else as the render-time NO WINDOW diagnostic (a bound or stored axis); `resolutions` lists the WEEK/DAY segment options; `now` draws the observed/plan divider; `format` overrides tick labels (date tokens; defaults: week ⇒ "W27", day ⇒ "MON"); at a resolution coarser than a row's data a bucket shows ONE value per row — the fold of those in it (#824) · Plan.axis.number({ window?, step, now?, format? }) — a numeric window ÷ `step` (bucket edges on whole steps; omit window ⇒ the leading source's float / integer range, under the same no-fit rule; `format` a Chart.format.* spec; no resolution segment — step IS the declaration) · Plan.axis.ordinal({ values, now? }) — the declared values ARE the buckets, in order (the list is the window: no source's range holds it, no brush, the window keys idle; an unlisted value positions nowhere; an interval END names its LAST bucket, inclusive)
|
|
1100
|
+
│ ├─ data (required unless `events`, `rows` or `slice`'s `rows` source, which gives the rows and refuses `data` beside it — a Plan with no rows from any source is refused at build) — a KEYED collection: a `Dict<K, R>` value/expression for the inline arm, or a `$.let`-bound `Data.bindPaged(…)` handle for a WINDOWED arm, recognised by its East type: `pinned` when it names its snapshot, which `editing.onApply` needs; `paged` when it is the shape a UI exported before `revision` / `refresh`, which names none. Any key type `K` — a row's path starts with its entry's key, a String as it is, any other key as its `.east` text (`3`, `(hall="H1", bin=3)`) — and any entry type `R`: a struct, a `RecursiveType` node, or a collection (a `groupToDicts` group). A positional collection is refused — key it at the call site with `rows.toDict((_$, r) => r.id)`. A paged canvas pages by PARENTS (#823): a window is N top-level entries, each with its whole subtree, so a parent never straddles two windows and its bands, means, subtotals and member count are exact — nest children in the source, and group a flat paged source in its dataflow (a group is one entry holding its members; a parent is bounded by a page). Each top-level series is a BLOCK, exactly as inline: a paged canvas pages every block on its own over the same windows (its own bands and resident run; one read of a window serves every block), draws a section's header and hand-built rows once (FIXED blocks), and rebases only the block a far jump lands in; a window landing above the rows in view at a height its estimate missed moves the scroll, never the rows. A paged source may serve SHORT windows (e3 trims pages of wide entries to a byte budget) and the canvas still reads WHOLE ones — its derived source re-requests what a trimmed page left out. When the source's `revision()` moves (`Data.bindPaged` follows each write to its dataset), the canvas re-reads its resident windows at the new snapshot IN PLACE: the rows on screen stay until theirs land (no remount, no empty frame), the scroll position holds, and a standing key search is asked again at the new snapshot, the view where it is (its matches named rows of the old one, #1199) — a match its box shows kept while the new snapshot holds it in place, else its query shown again
|
|
1101
|
+
│ ├─ series (required unless `pick`) — the row recipes over `data`; the list IS the layout (#822): each series contributes one contiguous block, top to bottom in declared order, its rows in source order, each parent followed by its subtree. Data series: Plan.series.span/buckets/chart/heat/table/cards/events(Row, { key, title, subtitle?, icon?, keyType?, match?, label, id?, stacked?, sub?, value?, status?, expand?, children?, collapsed?, edit?, …kind fields }) — every accessor receives (entry, key); a `RecursiveType` entry arrives as its node. A series reads the collections its row holds BY NAME (#1436), each a `Dict` field of the row: `Plan.items(field, times, draw)` reads things people drag — runs, decisions, tiles, chips, marks — keyed by an id (a `String`), each item's instants the fields `times` names (`{ from, to }` for a run or a chip, `{ at }` for the rest; a `DateTime` or an `Option` of one — no start draws nowhere, no end is an instant —, a `Float` / `Integer` on the number axis, a `String` on the ordinal) and its look `draw(item, id, row)`'s, everything its element takes but its key and instants (`Plan.items("jobs", { from: "start", to: "end" }, (job) => ({ label: job.ticket, state: job.state }))`); `Plan.values(field, draw)` reads values keyed by their instant (a `DateTime`; a `String` on the ordinal axis), `draw(value, at, row)` giving what each takes besides its instant — a heat, weight, segment or table row's cells (`Plan.heatCells(Plan.values("load", (pct) => ({ value: some(pct) })), { min: 0, max: 100 })`), a table series' cells, a chart layer's points (`Plan.line(Plan.values("ontime", (pct) => ({ y: pct })))`), a span's `ports`, a bucket row's `markers`, a group's `summary`. Each read brands the series with what it reads (phantom: a misnamed field, a time field the item lacks or a look missing a required field fails to compile), a read written outside a series is refused, and a function of the row stays for anything that reads no collection. What a gesture WRITES is declared on the series (#880), through the collection its elements' `Plan.items` names: `edit: true` (span / buckets / cards / events) makes its runs, chips, tiles and marks MOVE — along their row, or to another, the item leaving one row's collection under its id and joining the other's under the same id — and its runs and chips RESIZE (#825), each gesture written into the item's named times; `edit: { create: (drop, entry, key) => item }` takes dropped cards as well, `create`'s item joining the row's collection under the id the canvas mints for it (`drop.id`) — see `editing`. An element's key is its item's id, its identity across every row it can move to: a row's collection keeps its ids unique, so a move onto a row holding the id is refused (the drag showing ⊘). A table's values take typed numbers with `edit: true` — on `Plan.series.table` for its `cells`, or on a position, `Plan.tableSeries({ cells, edit: true })` — its cells a `Plan.values` read drawing each value as stored (`value: some(v)`; `some(v.toFloat())` for an Integer collection): at the Plan's finest zoom, a number typed into a cell replaces the one value its bucket holds, at that value's date, adds one at the bucket's start where none is, and cleared deletes it; at a coarser zoom each cell is a summary and takes none, a double click, Enter or a tap zooming to the finest about it — the zooms are the slice's, so a Plan without one types at the resolution it shows (`Plan.Types.ValueEdit`; each row says which positions take a number, and whether whole numbers only, in `edits.values` — `Plan.Types.TypedPosition`). `children` nests, to any depth, by field: `children: "parts"` names a `Dict` field of the row holding more of THIS series' entries (a `RecursiveType` entry's own children, keyed by an id), `Plan.children("members", [series…])` steps down to a `Dict` field holding child rows of another type, keyed by an id, its series laid out like a top-level list, and an array of step-downs gives several child collections in order (a gesture on a nested row writes back through them); `collapsed: true | (r, k) => …` is a parent's initial state. A parent derives exactly, because its whole subtree rides in its entry: span bands over the subtree's runs (`rollup` "union" | "byStatus" | "sum"; a band sums its runs' quantities unit by unit — the unit rides each `Plan.quantity`), a heat parent with no cells of its own the children's per-bucket `aggregate` (default "mean") painted on its `scale` ({ min?, max?, warnAt? }; default the scale its members share), a table parent its children's subtotal at each position where its own collection holds no values for the row (`aggregate` default "sum" — "count" counts the leaf rows beneath that draw a value in the bucket —, `format`) — each summarising what its children SHOW, their values already folded to the resolution · Plan.series.group(Row, { key, title, label, children, summaryAggregate?, summary?, collapsed? }) — one strip PER ENTRY, its members the entry's children (derived member count; collapsed it rests as its summary strip — explicit `summary` cells OR a `summaryAggregate` of the members' heat cells, never both) · Plan.series.section(Row, { key, title, collapsed?, meta?, value?, status?, summary?, summaryAggregate? }, [series…]) — a fixed titled block over series (it adds no path segment; its header's id is `Plan.sectionRef(key, …parentPath)`) · Plan.series.views(Row, { key, title, match?, children?, collapsed? }, [series…]) — ONE entity shown several ways: one row per member series per entry, ADJACENT and in declared order (a member's own `match` decides whether its row shows; members declare no `children`), the entry's children following its view rows under the first — more of the views' own entries too (a bare accessor), each drawn as the members' rows, to any depth, a span member's `rollup` rolling them up — and a seek on the entry landing on its first view row · Plan.series.rows(Row, identity, rows) — hand-built rows (the kind factories below) placed as one block. Series keys are unique across the WHOLE series tree — a repeat is a build-time Error naming both sites (a list bound as an East value is checked at render: a repeated id draws as a row diagnostic, never a silent drop). There is no `groupBy`: to group a flat source, reshape it first — inline, one `groupToDicts` in the canvas function, each group an entry holding its rows in a field its series step down into (`rows.groupToDicts(($, r) => r.hall, ($, _r, k) => k).map(($, members) => ({ members }))`, entries `{ members: Dict<String, Row> }` keyed by the group); a paged source in the dataflow. A source keyed by another type (`Dict<Integer, R>`) works with series whose accessors ignore the key; one that reads it declares `keyType` and, bound as a list, types it `Plan.Types.Series(R, KeyType)`
|
|
1102
|
+
│ ├─ pick (optional, exclusive with `series`) — Plan.pick(key, allSeries, { hidden? }): the bound series library — the toolbar has no Series button (#1193): the library pane's Series tab (#1195, when `library` lists `Plan.library.series()`) lists the series by title, subtitle and kind icon in the list's order (that order is the canvas layout); hidden series drop from the canvas (a hidden section, group or views takes its whole block); what a viewer switches there is kept under `key` in the browser's storage, so a reload opens the series as they were left (an id the list no longer holds dropped; a pick the tab never wrote opens with `hidden`); there are no row counts (#822 — a count means something only with every entry in hand); a pick is STATE, so the Plan must sit inside a <Reactive>
|
|
1103
|
+
│ ├─ resources + events (optional) — event kinds over records (#1191), `Schedule`'s — the very values a Calendar takes: `resources={{ presses: Schedule.resources(rows, { name, icon, label, … }) }}` are the rows events are placed on, in this order — `rows` a `Dict`, or a `Data.bindPaged` read of one, which pages the kind (#1199, see Large records, a window at a time) — and `events={{ job: Schedule.events(Record.bind(jobs), { name, icon, title, start, end | at, resource: { field, of }, … }) }}` the kinds, each a record of its own committed through its patch mutation. Their rows come first, then `data`'s, then `rows`'. Each resource kind is a block (#1192): every resource a row per way the kinds placed on it draw — bars, tiles, chips, marks, in that order — then its `measures`, under its `group` strips and nested by its `parent`; a row's id is `entry { series: "<slot>.<draw>", path: [group?, …parents, key] }`, an element's key its event's `Schedule.Types.EventRef` as East prints it, and an event no resource holds draws on its kind's Unassigned row after every resource kind. The rows are read over the window the canvas draws, and read again when a record commits. Each kind is a session over its record, under ONE history with `data`'s (#1194): every gesture one transaction, drawn as Save would leave it; Undo and Redo (the history item, ⌘Z / ⇧⌘Z / ⌘Y anywhere in the frame but a field being typed into) step through the gestures in the order they were made, whatever their kind; Discard drops every kind's drafts; Save commits each kind with a change as one patch through its patch mutation, checked against what its drafts began from — one kind's conflict, refusal or unanswered write (Save turns into Retry, the same request) leaving the others' commits, each kind's banner titled with its name. Their events drag (#1196), each gesture one step of that history: a template from the library's Events tab, or an unscheduled event from its Backlog tab, drops on a resource's row at the bucket under the pointer — the template's event made under a new key for its duration (from its `at` on a day or longer) and selected, the backlog event scheduled for its own; an element moves along its row or onto another resource's row, writing the resource field, a selected element moving the selection with it, and a bar's or a chip's edge resizes it, never shorter than one snap; an event dropped on the Backlog tab is unscheduled (a kind whose times are Options); the ghost says what lands where (`Brochure run · Press A1 · Mon, Oct 12, 2026`) or, in red, why not (`Needs presses`, `canDrop`'s message); and an element of an event its drafts changed wears the brand tint in a brand border. An event is scheduled in time, so event kinds on a number or ordinal axis are refused at build, naming the axis and the kinds (an axis held in a variable is refused in the same words as the Plan is evaluated); `resources` without `events`, and a kind's `resource` naming a slot `resources` has not, are refused too. `applyMode` ("batch" default | "auto") says when the kinds' drafts go — on Save, or each ready gesture as it lands; `data`'s session takes `editing.mode` — and `date` the date brought into view first
|
|
960
1104
|
│ ├─ rows (optional) — read-only rows after the event kinds' and `data`'s: hand-built rows (`Plan.chart({ … })`, `Plan.span({ … })`, …) and `Plan.over(data, [series…])`, series over a dataset — a keyed collection, or a `Data.bind` handle over one — which refuses a series declaring `edit` at build (a Plan's edits go through its event kinds and `data`'s session). Each is a FIXED block, drawn once on a paged canvas; a `pinned` row sits under the ruler. Their instants ride the axis's arm, or the tag fails to compile
|
|
961
1105
|
│ ├─ library (optional, #1195) — the start pane's tabs, in order, each a `Plan.library.*` call: `events()` — every event kind's templates, under the kind's name and then the template's group; `backlog()` — every event kind's unscheduled events, grouped by when they are due (this week, overdue included; next week; later; no date — UTC weeks from the axis's `now`, else from when the Plan mounted); `series()` — `Pick.Panel`'s list of what a viewer can hide, in the order the canvas draws it: each resource kind and its measures, the event kinds, `data`'s picked series and the Plan's `rows` — hidden per viewer under the Plan's `id`, and only while the tab is listed; `tab(rows, { name, icon?, label, meta?, group?, drop?, source? })` — a card per row of a `Dict<K, R>` (accessors `(row, key)`), searched by key, label and meta, a click selecting one, following the rail's `source` it names by its rows' keys (#1344); each card drags onto the rows of `data` whose series makes an item of a card (`edit.create`), the Plan taking its own panel's cards with no `id` or `sources` (#1259), and with `drop: (row, key) => Schedule.patch(KindRowType, …)` onto an event, the patch's type naming the event kind it lands on — the patch set on the event under the pointer as one step (#1196), refused between events and on another kind's. The Backlog tab reads every kind's drafts, so a card scheduled leaves it and an event unscheduled joins it at once. Left out, or empty, no library pane. Collapsed, the pane is a rail with the backlog's count, or the first tab's. A tab listed twice, the Events or Backlog tab with no event kinds, the Series tab with nothing to hide, and a `drop` over no event kind's row type (or one two kinds share) are refused at build, naming the tab
|
|
962
1106
|
│ ├─ inspector (optional, #1197) — the end pane: what is selected on the canvas. An event kind's element — a bar, tile, chip or mark — selects its event and its row on a click, Shift / ⌘ / Ctrl adding it to the selection or taking it out, and wears the brand's ring while it is selected; a row's name or plot selects the row, the plot's click naming the bucket under the pointer; Esc clears it all. One event shows its kind, title, when it runs and status, its facts (resource, start and end or its instant, lane, state, quantity), its kind's form over `fields` — or the kind's own inspector, `Schedule.events(record, { …, inspector: (row, update) => UIComponentType })`, in its place — then Duplicate and Delete; several, how many, each kind's count, the list and the bulk edit (state, resource, a shift of a day or an hour); a row, its resource's name, lines and group, its events in the window and its measures at the named bucket; nothing, the window's counts and three hints. Its edits are each one transaction of the Plan's history (#1194): a field of the form — tinted while the record holds it otherwise — the kind's own inspector's `update`, Duplicate (copies under new keys, then selected), Delete, and the bulk edit over every selected event that takes it, across kinds. Refused on a Plan with no event kinds
|
|
@@ -964,21 +1108,23 @@ return <Plan axis={axis} data={rows} series={series} />;
|
|
|
964
1108
|
│ ├─ popover / hover (optional) — generalized element resolvers fn(Plan.Types.ElementRef) => Option<UIComponent> over EVERY element (run / event / chip / mark / cell refs, each carrying the row's id — compare with `East.equal(ev.row, Plan.ref(…))` — and link refs { key, from, to }, a ribbon belonging to no one row); resolved lazily at click/hover time — a `none` result opens nothing
|
|
965
1109
|
│ ├─ expandRender / expandGutter (optional) — the R2 expand-in-place renders fn(Plan.Types.RowId) => UIComponent for rows declaring `expand` (per-row DATA: `{ height?: "168px", axis?: "keep" | "dim" | "off" }` — the render's height, and whether the shared grid + now-line run through it, wash to 40%, or hide inside that row); the focused row GROWS to hold the render (its marks keep their band at the top, its gutter cell grows with it and takes `expandGutter`); every other data row compresses to a 16px strip — never removed: bars / tiles / chips shrink to 7px marks, chart and table rows re-encode as a tone strip, event marks keep their silhouette; a strip click or Esc returns
|
|
966
1110
|
│ ├─ editing (optional) — the editing session (#880), the Sheet's (#879) over the source's TOP-LEVEL entries: { onApply? | onUpdate?, onPatch?, mode?, ready? }. Every dropped card, move and resize drafts the entry its row came from (a gesture on a nested row drafts the entry its whole subtree rides in), drawn at once where it was made — the entry's rows derived again, marked pending, or incomplete / invalid while a check refuses that entry — each gesture ONE undoable transaction. The history item (issues · Undo · Redo · Discard · Save, and its status line) ends the frame's toolbar; ⌘Z / Ctrl+Z undo, ⌘⇧Z / Ctrl+Shift+Z / Ctrl+Y redo. `onApply` fn(Editing.Types.ChangeSet(R, K)) => Editing.Types.ApplyResult (sync or async) commits one checked batch against the base the drafts began from — the inline `Dict`'s snapshot, or a paged source's revision (a paged `onApply` needs the source's `revision` and `refresh`): a batch against a source that moved is refused, never rebased, and a banner says "Source changed — review or discard these drafts"; a Save that throws leaves the outcome unknown, and the banner's "Retry request" resends the SAME request (dedupe by its `requestId`); drafts retire only when the source reads back at the committed revision. `onUpdate` fn(Dict<K, R>) => Null is the inline adapter instead (exclusive with `onApply`): pass `data={handle}` and `onUpdate={handle.write}`, and each batch applies with `Editing.apply` over the handle's LATEST `Dict` and writes the whole result once — idempotent across retries. `onPatch` fn(Plan.Types.PatchEvent(R)) => Null hears every gesture (origin drop / move / resize / undo / redo / discard). `mode` "batch" (default — Save sends) | "auto" (each ready gesture goes at once). `ready` fn(R, K) => Editing.Types.Readiness checks a drafted entry — a refusal names the entry and holds Save (a check that throws refuses its own entry only). Without `editing` the canvas takes no gesture
|
|
967
|
-
│ ├─ slice (optional, #1344) — slice={{ slice: Slice.bindRecords(key, { rows?, <an event kind's slot>?, <a resource kind's slot>?, <a templates list's or a tab's source>? }, { presets? }), affordances?, persist? }}: ONE rail on the toolbar (affordances default ["fields","presets","summary","brush"]), each source named after what it narrows. `rows` gives the canvas its rows, paged from it in its index's order — `data` beside it is refused — edits reaching the record through `editing.onApply` (`Record.onApply(record, { keyed: true })`), the session the record's, a Save that takes rows out of the narrowing said in the footer. A source named after an event kind's slot reads that kind's events in the window and its backlog through its interval index (`
|
|
1111
|
+
│ ├─ slice (optional, #1344) — slice={{ slice: Slice.bindRecords(key, { rows?, <an event kind's slot>?, <a resource kind's slot>?, <a templates list's or a tab's source>? }, { presets? }), affordances?, persist? }}: ONE rail on the toolbar (affordances default ["fields","presets","summary","brush"]), each source named after what it narrows. `rows` gives the canvas its rows, paged from it in its index's order — `data` beside it is refused — edits reaching the record through `editing.onApply` (`Record.onApply(record, { keyed: true })`), the session the record's, a Save that takes rows out of the narrowing said in the footer. A source named after an event kind's slot reads that kind's events in the window and its backlog through its interval index (`index.interval({ on?, from: "start", to: "end" })` among the jobs record's indexes, `range: "start"`), the kind giving `entries: Data.bindPaged(record)` for its reads by key; a draft of an event its narrowing does not hold never shows, and Save sends it. A kind placed on a paged resource kind is read a cell at a time (#1039) — the events of the resources around the rows in view over the tiles the window touches, a range a resource a level (#1433), two requests a change — and shaded from the summary past what it draws: its source lists a series index and a summary index beside the interval index, and any other form is refused, naming both. Every other kind is counted before one of its events is read, and past 10,000 events in the range the canvas reads none is read or drawn (see Large records below). One named after a resource kind's slot narrows that kind's rows: of resources read whole and keyed by String, the rows and the bulk edit's Chooser, which keeps the resources the selected events are on; of a paged kind (`Schedule.resources(Data.bindPaged(record), …)`), its windows — the canvas pages the narrowing's rows themselves, the source `Data.bindPaged(record, { indexes })` over the kind's own record, in its index's order, the counts the narrowing's, so a choice reads only the narrowed resources in view and their cells (`planSeriesByHall`, `planRecordIndexes`). One a templates list (`Schedule.templates(rows, { …, source })`) or a library tab names narrows that tab's cards: each tab counts what it shows (`Customers 6 of 16`) and, emptied, says so with Clear filters; the Backlog counts `N of M`, its Clear filters keeping the window. Every source holding a range of the axis's arm — an event kind's, a tab's that lists what is in the dates on screen — holds ONE window — on a time axis the date navigator's, the leading source's range editor its Range panel and every source's range out of the rail; on a number axis the leading source's range a chip of the rail: the date navigator, the horizon brush, `[` / `]` / `n` and the resolution write each source, and a record's resolution only re-buckets, fetching nothing. The canvas's rows never follow the dates on screen: the `rows` source and a resource kind's take no `range` — refused as the Plan is evaluated, naming the change — so a date field there is a field of the rail the viewer sets, and a pan reads no row; a slice none of whose sources holds the window leaves the canvas its own, which the same controls move, kept for the viewer as the canvas's folds are, so a reload opens on the dates the viewer left. `brush` mounts the horizon band on time and number axes: an OVERVIEW of the sources' range in whole periods (with no source holding the window, the axis's stated window and the window shown, as long again either side, so its lens slides either way), with a lens joining its window to the plot's edges (drag its body to slide the window, an edge to resize it, empty track to draw one; or Tab to its start handle, window and end handle, an arrow moving a period, Shift ten, Home / End the ends, Esc clearing), committing once a gesture — over record sources with no bars yet, over rows read through a nested series index each month's items as bars (see the nested series above); `resolution` the WEEK/DAY segment (time axis); `summary` the rail's counts. Calculations — the footer's counts, the overlaps — read every event the kinds read. Refused at build, each naming the change: the bare handle, `Slice.bind`, a single `Slice.bindRecord`, a source nothing names, `data` beside `rows`, a resource kind keyed otherwise than by String as a source, a paged one's source over another record, a kind's own `slice` or `filter`, and a templates list written in the code or naming no source — and, as the Plan is evaluated, a `range` on the `rows` source or a resource kind's. With a `rows` source there is no key search: the rail narrows the rows — nor over a paged resource kind its source narrows, whose searches run in the source's index's order. On a PAGED `data` the toolbar's KEY SEARCH seeks over the source's `seek` — the box, folding to its icon on a row short of room; the jump REBASES residency at the match (windows in between are never fetched); the query is a whole-key literal, a prefix, leading struct fields, or a `from..to` RANGE over a leading prefix of the key's flattened fields (`late, 3..ok`), which is what a time window or a status band asks for — quoting is the escape: a quoted value is exact and a `..` inside it is text (`"../config"`)
|
|
968
1112
|
│ ├─ grain (optional) — initial grain "resource" (default) | "group" (root groups collapse to summary strips); the `g` key cycles it, and a canvas with a root group mounts the toolbar's GROUP · RESOURCE segment for it — slice or no slice (#632; the narrow layout's tabs own the grain there); onGrainChange observes
|
|
969
1113
|
│ ├─ ui (optional) — the interaction state, held by the HOST (#824): State.bind([Plan.Types.UiState], key, Plan.uiState({ selected?, collapsed?, expanded?, charts?, focus? })) — the canvas draws it from its first frame, takes every outside write (a selection, rows folded or opened, charts expanded) and writes the user's own actions back, once per action; `collapsed` / `expanded` list the rows folded / opened AGAINST their declaration (a row in neither follows it; in both, folded); `focus` is a REQUEST — the canvas opens the rows it nests under, scrolls it to the top of the view, makes it the tab stop (DOM focus stays put) and writes `focus: none` back; a paged row it has not loaded is opened by its window, or sought by its entry's key where the source can seek. Bound, the canvas persists no toggles of its own under its `storageKey`
|
|
970
|
-
│ ├─ id + sources + canDrop (optional) — DnD TARGET (the shared grammar): cards — an author's tab's in the Plan's own library panel (`Plan.library.tab`, #1259), or a Library's beside the Plan, which reaches it by its `id` when `sources` lists the Library — `add` onto rows whose series declares `edit` — span / buckets / events / cards only (chart / heat / table render DERIVED values, and section headers and group strips are wayfinding — they register no cell) — and only with `editing`: a drop is a DRAFT of the session (#880), its item built by the series' `create` from the `Plan.Types.Drop` ({ from: { library, key }, row, at, duplicate } — `at` the start of the bucket it landed in, on the axis arm). `canDrop` sees the drag grammar's `add`: its `into.row` is the row id's canonical TEXT — key host tables by `East.print(Plan.ref(series, …path))`, or read it back with `row.parse(Plan.Types.RowId)`; slot = the bucket start instant per the axis arm — time: Z-less ISO (`slot.parse(DateTimeType)`), number: a decimal (`slot.parse(FloatType)`), ordinal: the value; drop verdicts resolve LIVE where the drag rests — `canDrop` is asked of the event a drop there would deliver (an `add`'s `duplicate` is whether Alt is held), renders the ⊘ stage, and is asked again before the drop becomes a draft — and the landing band previews WHERE the card lands. A moved or resized element (#825) is vetoed the same way: `canDrop` sees the grammar's `move` ({ from, to } cells, `from.event` the element's key) or `resize` ({ event, edge }). The canvas's own elements, and its own panel's cards, need no `id` — only a Library beside the Plan does. A card held at a bounded canvas's edge scrolls it to the rows beyond; by keyboard (#608) → / ← step onto a row and along its buckets and ↑ / ↓ between rows, each bucket announced ("Poster run is over H1-P03, Week of Jul 6, 2026."). `onDrag` is removed (#880) and throws at build. With event kinds, `canDrop` may instead be fn(Schedule.Types.Candidate) => Option<String>, vetting an event kind's drop with its message on the ghost, as the Calendar's — the arm is read from the function's type; it is asked where a drag rests and again of the drop, `from` the template's key, the backlog event's or the moved event's (#1196)
|
|
1114
|
+
│ ├─ id + sources + canDrop (optional) — DnD TARGET (the shared grammar): cards — an author's tab's in the Plan's own library panel (`Plan.library.tab`, #1259), or a Library's beside the Plan, which reaches it by its `id` when `sources` lists the Library — `add` onto rows whose series declares `edit` — span / buckets / events / cards only (chart / heat / table render DERIVED values, and section headers and group strips are wayfinding — they register no cell) — and only with `editing`: a drop is a DRAFT of the session (#880), its item built by the series' `create` from the `Plan.Types.Drop` ({ from: { library, key }, row, at, duplicate, id } — `at` the start of the bucket it landed in, on the axis arm; `id` the key the canvas mints for the new item, unique among every element it holds) and joining the collection the series' `Plan.items` names under that id. `canDrop` sees the drag grammar's `add`: its `into.row` is the row id's canonical TEXT — key host tables by `East.print(Plan.ref(series, …path))`, or read it back with `row.parse(Plan.Types.RowId)`; slot = the bucket start instant per the axis arm — time: Z-less ISO (`slot.parse(DateTimeType)`), number: a decimal (`slot.parse(FloatType)`), ordinal: the value; drop verdicts resolve LIVE where the drag rests — `canDrop` is asked of the event a drop there would deliver (an `add`'s `duplicate` is whether Alt is held), renders the ⊘ stage, and is asked again before the drop becomes a draft — and the landing band previews WHERE the card lands. A moved or resized element (#825) is vetoed the same way: `canDrop` sees the grammar's `move` ({ from, to } cells, `from.event` the element's key) or `resize` ({ event, edge }). The canvas's own elements, and its own panel's cards, need no `id` — only a Library beside the Plan does. A card held at a bounded canvas's edge scrolls it to the rows beyond; by keyboard (#608) → / ← step onto a row and along its buckets and ↑ / ↓ between rows, each bucket announced ("Poster run is over H1-P03, Week of Jul 6, 2026."). `onDrag` is removed (#880) and throws at build. With event kinds, `canDrop` may instead be fn(Schedule.Types.Candidate) => Option<String>, vetting an event kind's drop with its message on the ghost, as the Calendar's — the arm is read from the function's type; it is asked where a drag rests and again of the drop, `from` the template's key, the backlog event's or the moved event's (#1196)
|
|
1115
|
+
│ ├─ optimise (optional, #1409) — `Func.bind` of the app's `e3.function` from `Plan.Types.OptimiseRequest(E, S)` — the plan window in view, the run's number, every event kind's rows in the window and the backlog by key as the session holds them (base plus every unapplied draft, every row whatever the rail shows), and every resource kind's rows read whole as context (a kind paged a window at a time carries none) — to `Plan.Types.OptimiseResult(E)`, the events complete. The Elara eye in the toolbar, immediately before the history item: one call per run; while it is out the history and every gesture are locked, a change of window stops it (Stop too), and a late answer is ignored. Its answer is diffed by key and recorded as ONE step of the history — Undo in one, Redo replays it, Save with everything else — each event it placed from the backlog an add on the canvas, one it moved a change with a ghost where it stood, one it took back a remove; each marked until Apply with its provenance on hover (`Added by Elara · run #7`), the Backlog count and the footer's counts flashing; a result equal to the request records nothing and says so
|
|
971
1116
|
│ ├─ onSelect / onElementClick / onGroupToggle (optional) — row selection, ONE element callback and section toggles; payloads are row ids + element keys + instants, never indices: onSelect the `Plan.Types.RowId`; onElementClick a `Plan.Types.ElementRef` — the SAME ref the popover resolver receives, its arm the element kind (run { row, run } · event { row, event } · mark { row, mark } · chip { row, chip } · cell { row, at } — `at` a Plan.Types.Instant on the axis arm, a folded bucket's start · link { key, from, to }), so `ref.match({ run: …, link: … }, …)` handles the kinds it cares about; onGroupToggle { row, expanded } for any row with children
|
|
972
|
-
│ ├─ footer (optional) — [{ text, tone?, end? }] status-footer items, in the frame's footer after its counts; a paged canvas adds the
|
|
1117
|
+
│ ├─ footer (optional) — [{ text, tone?, end? }] status-footer items, in the frame's footer after its counts; a paged canvas adds the one loading line (what the view waits for, or why a window could not be read, with Retry); rows not read draw as the shared run — label bars and the plot washed, at the rows' pitch — and until the source is exhausted a TOP-LEVEL section's member count is a placeholder bar (its members are entries the windows share out) — every other parent's numbers are exact, its subtree riding whole in one entry
|
|
973
1118
|
│ └─ style (optional) — { height ("fill" fills the parent) and maxHeight — the whole Plan's, its frame's, the canvas filling main and scrolling its own rows; with neither, a host that gives the Plan a height bounds it the same way, and one that gives none lets it grow with its rows — density ("compact" ⇒ dense 24px rows), gutterWidth ("168px", CSS px) }; a bounded canvas pins the PARENT of the rows in view under its header — with its ancestors' path on a deep tree — once the parent's own row scrolls off, inline and paged (a click goes to it, #823)
|
|
974
|
-
├─ Narrow (below 480px of CONTAINER width — a phone, a splitter pane, a task preview; §10): the same definition reflows to a review tool — the frame's toolbar keeps its items, folded to fit (the GROUP · RESOURCE segment leaves: the tabs own the grain), no horizon brush, and Groups · Rows · Measures tabs (the `<Tabs>` line grammar, counts as plain numerals) over ONE slice, then the card list with the shared ruler as its sticky first row (a separator per bucket, labels thinned to what fits): Groups = the group grain as hottest-first strip cards (tap opens its rows) — the LANDING only when it is a map (three or more groups, or any strip); Rows = every data row as a card SECTIONED by group (a section header scopes to that group; `← All rows` returns) with the gutter identity as the card head and the row's plot — the same kind renderer — as the body on the shared window; Measures = chart rows full-width at expanded density; tap selects, a second tap on an `expand` row drills it in place (~148px; neighbours keep their size), a two-finger horizontal drag pans the window; a bounded `height` pins the header and scrolls the list; a paged source shows its resident prefix. Wrap a Plan in `<Box width="360px">` to see it on a desktop page
|
|
975
|
-
├─ Keyboard (#819): the body is a TREEGRID with ONE tab stop that roves between its rows — every row, group band, ⋯ gap band and paged-window band (loading or failed) is a `row` with aria-level / aria-expanded / aria-selected and an `aria-rowindex` that stays exact under virtualization, collapse and paging · ↑ / ↓ step, Home / End go to the first / last, PgUp / PgDn move a viewport · → opens a closed section (a group band, a nesting parent, an `expandable` chart) and steps into an open one's first child; ← closes an open one, else steps to the parent · Enter does the row's click (select; a group band toggles; a rail, strip or ⋯ band returns from the row focus) · Space toggles the section / chart · a step onto an UNLOADED band asks the source for that window and moves on to the row once it lands (Home / End reach the source's own first / last row) · Tab walks the row's widgets — its controls, its elements in time order, tabbables in the expand render — then leaves the canvas; ← / → (Home / End) step between elements in time order; Enter / Space on an element does its click (the popover, the selection — an event kind's element selects its event, Shift+Enter adds it, #1197 — the element callback) · Esc ladder, one rung per press: popover → element back to its row → brush → focus → deselect (the row, its bucket and the events at once) · n recenters the window on `now` · [ / ] pan one period · g cycles the grain — window keys write the window into the sources holding it,
|
|
976
|
-
├─ Screen readers (#819): a polite live region says what changed — a selection, a section or chart opening / closing, a row focus coming or going, the grain, the resolution
|
|
1119
|
+
├─ Narrow (below 480px of CONTAINER width — a phone, a splitter pane, a task preview; §10): the same definition reflows to a review tool — the frame's toolbar keeps its items, folded to fit (the GROUP · RESOURCE segment leaves: the tabs own the grain), no horizon brush, and Groups · Rows · Measures tabs (the `<Tabs>` line grammar, counts as plain numerals) over ONE slice, then the card list with the shared ruler as its sticky first row (a separator per bucket, labels thinned to what fits): Groups = the group grain as hottest-first strip cards (tap opens its rows) — the LANDING only when it is a map (three or more groups, or any strip); Rows = every data row as a card SECTIONED by group (a section header scopes to that group; `← All rows` returns) with the gutter identity as the card head and the row's plot — the same kind renderer — as the body on the shared window; Measures = chart rows full-width at expanded density, its count `not counted` over a paged source whose entries make no fixed number of chart rows; tap selects, a second tap on an `expand` row drills it in place (~148px; neighbours keep their size), a two-finger horizontal drag pans the window; a bounded `height` pins the header and scrolls the list; a paged source shows its resident prefix. Wrap a Plan in `<Box width="360px">` to see it on a desktop page
|
|
1120
|
+
├─ Keyboard (#819): the body is a TREEGRID with ONE tab stop that roves between its rows — every row, group band, ⋯ gap band and paged-window band (loading or failed) is a `row` with aria-level / aria-expanded / aria-selected and an `aria-rowindex` that stays exact under virtualization, collapse and paging · ↑ / ↓ step, Home / End go to the first / last, PgUp / PgDn move a viewport · → opens a closed section (a group band, a nesting parent, an `expandable` chart) and steps into an open one's first child; ← closes an open one, else steps to the parent · Enter does the row's click (select; a group band toggles; a rail, strip or ⋯ band returns from the row focus) · Space toggles the section / chart · a step onto an UNLOADED band asks the source for that window and moves on to the row once it lands (Home / End reach the source's own first / last row) · Tab walks the row's widgets — its controls, its elements in time order, tabbables in the expand render — then leaves the canvas; ← / → (Home / End) step between elements in time order; Enter / Space on an element does its click (the popover, the selection — an event kind's element selects its event, Shift+Enter adds it, #1197 — the element callback) · Esc ladder, one rung per press: popover → element back to its row → brush → focus → deselect (the row, its bucket and the events at once) · n recenters the window on `now` · [ / ] pan one period · g cycles the grain — window keys write the window into the sources holding it, else into the canvas's own; a canvas with no slice keeps the axis's stated window and idles them · with `editing`, ⌘Z / Ctrl+Z undo the last gesture and ⌘⇧Z / Ctrl+Shift+Z / Ctrl+Y redo it (#880) · with `editing`, Space on a movable element picks it up (#825): ← / → move it one bucket, Shift+← / → its end and Alt+← / → its start, ↑ / ↓ carry it to the nearest row above / below that takes it, Space / Enter drop it, Esc / Tab cancel — each step said in the canvas's live region; an event kind's element is carried the same way (#1196), ↑ / ↓ along the rows its kind draws on, a refusal said with why · the toolbar's GROUP · RESOURCE and WEEK · DAY segments are radio groups: one tab stop each, ← / → (Home / End) move and pick
|
|
1121
|
+
├─ Screen readers (#819): a polite live region says what changed — a selection, a section or chart opening / closing, a row focus coming or going, the grain, the resolution; what a paged source reads is the footer's loading line, a polite live region that speaks once a load; every element is named in words — label, span or bucket ("Week of Jul 6, 2026"), state, tone — a chart row reads as an image with a min / max / last summary per layer, and colour-only cells (heat depth, booked weight, segment composition, compressed strips) carry their value as text; narrow, the Groups · Rows · Measures tabs are a real tablist (arrow keys, one tabpanel each)
|
|
977
1122
|
├─ Localization (#820): every word the canvas says itself — toolbar, ruler, footer, diagnostics, bands, narrow tabs and cards, the history bar (the table carries the editing session's words, #880), element names, live-region announcements — comes from ONE typed message table, and every number and date it prints is in the LOCALE: react-aria's `<I18nProvider locale="de-DE">` above the app sets it (the browser's language otherwise), and derived numbers ("2.234,5 k sheets"), ruler ticks ("MO · DI"), the words a reader hears ("29. Juni 2026") and a Chart.format.* axis all follow it. Host React code overrides any subset of the words for a subtree with `<PlanMessagesProvider messages={GERMAN}>` (@elaraai/e3-ui-components; `planMessages` is the English table — each message a function of named, already-formatted parameters, plus the raw `n` for plurals; define the overrides ONCE, not per render — providers nest). What the AUTHOR wrote is data and never translated: labels, footer items, and a date `format` pattern on the axis (East tokens, as written); the label a patch event carries to the host stays in English, as the Sheet's does
|
|
978
1123
|
└─ Factories:
|
|
979
1124
|
├─ Plan.axis({ … }) = Plan.axis.time / Plan.axis.number({ window?, step, now?, format? }) / Plan.axis.ordinal({ values, now? }) — the shared axis declaration, one of three kinds (Plan.Types.Axis); Plan.at.time(d) / Plan.at.number(n) / Plan.at.ordinal(s) build ONE INSTANT explicitly (Plan.Types.Instant) — needed only for element RECORDS written as data (a Plan.Types.HeatCell array, a stored Plan.Types.Run): every element builder below takes a Date / number / string, or a DateTime / Float / Integer / String expression, and wraps it to the arm by its type, so a `start: r.start` DateTime accessor and a `day: FloatType` field both need nothing written
|
|
980
1125
|
├─ Plan.series.* — the data-driven row series (see `series` above); Plan.Types.Series(Row) is one series' East type over `Dict<String, Row>` entries, Plan.Types.Series(Row, KeyType) over another key type
|
|
981
|
-
├─ Plan.
|
|
1126
|
+
├─ Plan.items(field, times, draw) / Plan.values(field, draw) — a series' reads of the collections its row holds, by name (#1436, see `series`): things people drag keyed by an id, and values keyed by their instant
|
|
1127
|
+
├─ Plan.children(field, [series…]) — a step down from an entry to a `Dict` field of child rows of another type, keyed by an id, for a series' `children`
|
|
982
1128
|
├─ Plan.ref(series, …path) / Plan.sectionRef(series, …path) — a row's id (Plan.Types.RowId = entry { series, path } | section { series, path }): `Plan.ref("presses", "H1-P03")`, a nested row `Plan.ref("rollup", "Contract A", "H1-P03")`, a `views` member's row by the MEMBER's key; for links, `East.equal` comparisons in resolvers, and host tables keyed by `East.print(id)` (an id is a variant — it cannot be a Dict key itself)
|
|
983
1129
|
├─ Plan.run({ key, start, end, label, quantity?, state, status?, moved?, icon? }) — one state-run bar (state: "actual"|"in-progress"|"confirmed"|"estimated"|"added"|"recommended"|"removed"|"rejected"; `quantity` is ONE Plan.quantity — the bar prints its caption after the label and a parent's rollup band sums it with its siblings' in the same unit; status "warning" draws the stuck ring; runs past the window mask-fade, never fabricate an end)
|
|
984
1130
|
├─ Plan.quantity(value, { unit?, format?, text? }) — a quantity (Plan.Types.Quantity): `value` is what sums, weighs and compares, `unit` what a rollup sums by (sheets never add to hours), `format` a Format.* spec printing the value in the viewer's locale, `text` a caption printed instead (the value still sums) — a run's and a link's `quantity`
|
|
@@ -986,15 +1132,17 @@ return <Plan axis={axis} data={rows} series={series} />;
|
|
|
986
1132
|
├─ Plan.lane({ key, label? }) / Plan.marker({ at, lane?, status?, message }) — bucket sub-slot lanes + cell status rings (status defaults "danger"; message = the tooltip)
|
|
987
1133
|
├─ Plan.chip({ key, from, to, label, state, icon? }) — one cards shift chip; Plan.mark({ key, at, kind, icon?, label? }) — one event mark (kind: "milestone" | "exception" | Plan.markKind.decision(applied))
|
|
988
1134
|
├─ Plan.decision({ key, at, applied }) / Plan.port({ at, label? }) — span-row decision diamonds (◇ pending / ◆ applied) + quantity in/out ports
|
|
989
|
-
├─ Plan.heatCells(cells, { min?, max?, warnAt?, fold?, format? }) / Plan.weightCells(cells, { fold?, format? }) / Plan.segmentCells(cells, { fold?, format? }) + Plan.segment({ fill, weight, label? }) — the three heat-row cell arms (colour depth / booked-vs-free bars / compositions); `fold` is what a coarser bucket shows of the cells in it (heat and weight "mean", segments "sum"; "sum" | "mean" | "min" | "max" | "last" | "count"), `format` a Format.* spec for the values the arm prints — a heat cell without a `label` prints its value only through a declared `format`, and the words a reader hears for a weight or a segment share follow it
|
|
990
|
-
├─ Plan.tableCells(rawCells) / Plan.tableSeries({ cells, format?, tone?, strong?, rollup?, fold? }) — bucketed numerals (a raw cell's `at` wraps by its field type — DateTime / Float / Integer / String / an instant) (multi-series per row, style declared once per position; explicit text/tone overrides via PlanTableCellType values); a series folds by `fold` (default "sum"), and `Plan.series.table`'s `fold` declares it for its `cells`
|
|
1135
|
+
├─ Plan.heatCells(cells, { min?, max?, warnAt?, fold?, format? }) / Plan.weightCells(cells, { fold?, format? }) / Plan.segmentCells(cells, { fold?, format? }) + Plan.segment({ fill, weight, label? }) — the three heat-row cell arms, `cells` a `Plan.values` read of the row's collection (`Plan.heatCells(Plan.values("load", (pct) => ({ value: some(pct) })))`; `{ fraction, planned }` for weights, `{ segments }` for compositions) or an array of cells (colour depth / booked-vs-free bars / compositions); `fold` is what a coarser bucket shows of the cells in it (heat and weight "mean", segments "sum"; "sum" | "mean" | "min" | "max" | "last" | "count"), `format` a Format.* spec for the values the arm prints — a heat cell without a `label` prints its value only through a declared `format`, and the words a reader hears for a weight or a segment share follow it
|
|
1136
|
+
├─ Plan.tableCells(Plan.values(field, (v) => ({ value: some(v) })) | rawCells) / Plan.tableSeries({ cells, format?, tone?, strong?, rollup?, fold?, edit? }) — bucketed numerals (`edit: true` over a values read: the position takes typed numbers, #1436) (a raw cell's `at` wraps by its field type — DateTime / Float / Integer / String / an instant) (multi-series per row, style declared once per position; explicit text/tone overrides via PlanTableCellType values); a series folds by `fold` (default "sum"), and `Plan.series.table`'s `fold` declares it for its `cells`
|
|
1137
|
+
├─ Plan.line / Plan.area / Plan.column / Plan.scatter / Plan.band(Plan.values(field, draw), { axis?, breach?, series?, fold? }) — a chart row's layer over the row's values, a point at each value's instant (`draw` gives `{ y }`, a band's `{ lo, hi }`; columns stack by `series`, `breach` on lines and columns, scatter and band draw every point) (#1436)
|
|
991
1138
|
├─ Plan.layer(chartLayer, { axis?, breach?, series?, fold? }) + Plan.fixed("120px") — chart rows consume Chart.Line/Column/Area/Scatter/Band/ref* builder results AS DATA on the shared scale (the x accessor's static type picks the arm — DateTimeType ⇒ time, Float/Integer ⇒ number, String ⇒ ordinal — and must match the canvas axis at render; Chart.Bar is a build-time error on every kind); Plan.layer adds the y-axis side, breach threshold, stack series and the fold a coarser bucket shows (columns "sum", lines and areas "mean"; scatter and band layers draw every point)
|
|
992
1139
|
├─ Plan.span/buckets/chart/heat/table/cards/events/group({ key, label, id?, sub?, value?, meta?, stacked?, swatches?, collapsed?, pinned?, height?, status?, expand?, …kind fields, rows? }) — literal kind factories returning row STREAMS (the row, then its nested `rows:` in order; parents DECLARE rollup/aggregate and the renderer derives the numbers); ride them beside data-driven series via Plan.series.rows — a hand-built row's id is that series' key plus the factory `key`s that lead to it (`Plan.ref("works", "shutdown", "elec")`), so keys must be unique among siblings (a repeat draws as a DUPLICATE ID row diagnostic)
|
|
993
1140
|
├─ Plan.link({ key, from, fromRun, to, toRun, quantity? }) — one link-graph edge; `key` is what a ribbon click's `link` ref names it by, `from` / `to` are row ids (`Plan.ref`) with their runs, or events (`Plan.eventRef`) with none, `quantity` a Plan.quantity
|
|
994
1141
|
├─ Plan.eventRef(kind, key) — an event of one of the Plan's event kinds, named for a link's end (Plan.Types.RunRef) · Plan.over(data, [series…]) — series over a dataset, read only, for `rows` (#1191)
|
|
995
1142
|
├─ Plan.library.events() / backlog() / series() / tab(rows, config) — the library pane's tabs (#1195, see `library`) · Schedule.patch(R, { field: value, … }) — a patch over an event kind's row type (Schedule.Types.Patch(R): every field an `Option`, the fields left out `none`), what an author's card dropped on an event sets
|
|
996
1143
|
├─ Plan.uiState({ selected?, collapsed?, expanded?, charts?, focus? }) — the seed of a bound `ui` state (Plan.Types.UiState): rows named by id (`Plan.ref`), omitted fields empty
|
|
997
|
-
|
|
1144
|
+
├─ Plan.Types.OptimiseRequest(E, S) / Plan.Types.OptimiseResult(E) — what the app's optimiser is asked and answers (#1409): `E` and `S` the event and resource kinds' row types by slot (`Plan.Types.OptimiseRequest({ job: Job }, { presses: Press })`, a kind keyed otherwise than by String `{ key, row }`); the request `{ from, to, run, events, resources }`, each kind's rows by key; the result `{ events }`, the events complete, a new event's key the optimiser's own
|
|
1145
|
+
└─ Plan.Types.PatchEvent(R) — what `editing.onPatch` receives for entries of `R` · Plan.Types.Drop ({ from: { library, key }, row: RowId, at: Instant, duplicate, id: String }) — what an `edit.create` builds its item from, `id` the key its item goes in under · Plan.Types.Gesture (drop(Drop) | move(Move) | value(ValueEdit)) and Plan.Types.RowEdits ({ drop, move: Option<MoveEdits>, values: Array<Integer> } — the gestures a row takes, `values` the table positions taking typed numbers) · Plan.Types.ValueEdit ({ row, position, at, value: Option<Float> } — a number typed into a table cell, `at` the date of the one value its bucket holds, else its bucket's start; `none` when cleared, #1436) · Plan.Types.Move ({ key, from: RowId, to: RowId, start, end } — a moved or resized element, #825) — the editing wire (#880); the session's contracts are the shared `Editing.Types.*` (ChangeSet(R, K), ApplyResult, Readiness)
|
|
998
1146
|
```
|
|
999
1147
|
|
|
1000
1148
|
#### The series list is the layout, the data is the hierarchy (#822)
|
|
@@ -1006,32 +1154,40 @@ step down into a child collection — so a parent's bands, means and subtotals
|
|
|
1006
1154
|
are exact, and the canvas reads the same inline or paged. There is no
|
|
1007
1155
|
`groupBy`: to group a flat source, reshape it first.
|
|
1008
1156
|
|
|
1157
|
+
A row's collections are `Dict`s it holds, read by name (#1436): things people
|
|
1158
|
+
drag keyed by an id, with their times as fields, through `Plan.items`; values
|
|
1159
|
+
keyed by their instant through `Plan.values`; child rows keyed by an id,
|
|
1160
|
+
through `Plan.children` or a tree's own field.
|
|
1161
|
+
|
|
1009
1162
|
```tsx
|
|
1010
|
-
//
|
|
1011
|
-
const
|
|
1012
|
-
const
|
|
1163
|
+
// A press's jobs by id, its load by the instant of each reading.
|
|
1164
|
+
const Job = StructType({ ticket: StringType, start: DateTimeType, end: DateTimeType, state: EventStateType });
|
|
1165
|
+
const HallRow = StructType({ label: StringType, hall: StringType, jobs: DictType(StringType, Job), load: DictType(DateTimeType, FloatType) });
|
|
1166
|
+
// Grouping is a DATA step: each hall an entry holding its rows, in the field its series step down into.
|
|
1167
|
+
const halls = $.let(rows.groupToDicts(($, r) => r.hall, ($, _r, k) => k).map(($, members) => ({ members })));
|
|
1168
|
+
const Hall = StructType({ members: DictType(StringType, HallRow) });
|
|
1013
1169
|
const series = $.const([
|
|
1014
1170
|
// One strip PER HALL; its rows stepped down into, laid out like a top-level list.
|
|
1015
|
-
Plan.series.group(
|
|
1171
|
+
Plan.series.group(Hall, {
|
|
1016
1172
|
key: "halls", title: "Halls",
|
|
1017
|
-
label: (
|
|
1173
|
+
label: (_h, hall) => hall,
|
|
1018
1174
|
summaryAggregate: "mean",
|
|
1019
|
-
children: Plan.children(
|
|
1175
|
+
children: Plan.children("members", [
|
|
1020
1176
|
Plan.series.span(HallRow, {
|
|
1021
1177
|
key: "hall-jobs", title: "Jobs",
|
|
1022
1178
|
match: r => r.jobs.size().greater(0n),
|
|
1023
1179
|
label: r => r.label,
|
|
1024
|
-
runs:
|
|
1180
|
+
runs: Plan.items("jobs", { from: "start", to: "end" }, (job) => ({ label: job.ticket, state: job.state })),
|
|
1025
1181
|
}),
|
|
1026
1182
|
Plan.series.heat(HallRow, {
|
|
1027
1183
|
key: "hall-load", title: "Load",
|
|
1028
|
-
match: r => r.
|
|
1184
|
+
match: r => r.load.size().greater(0n),
|
|
1029
1185
|
label: r => r.label,
|
|
1030
|
-
cells:
|
|
1186
|
+
cells: Plan.heatCells(Plan.values("load", (pct) => ({ value: some(pct) })), { min: 0, max: 100 }),
|
|
1031
1187
|
}),
|
|
1032
1188
|
]),
|
|
1033
1189
|
}),
|
|
1034
|
-
], ArrayType(Plan.Types.Series(
|
|
1190
|
+
], ArrayType(Plan.Types.Series(Hall)));
|
|
1035
1191
|
// <Plan axis={axis} data={halls} series={series} />
|
|
1036
1192
|
```
|
|
1037
1193
|
|
|
@@ -1040,12 +1196,12 @@ derives its numbers from its subtree:
|
|
|
1040
1196
|
|
|
1041
1197
|
```tsx
|
|
1042
1198
|
const OrderRow = RecursiveType((self) => StructType({
|
|
1043
|
-
name: StringType, act:
|
|
1199
|
+
name: StringType, act: DictType(DateTimeType, FloatType), children: DictType(StringType, self),
|
|
1044
1200
|
}));
|
|
1045
1201
|
Plan.series.table(OrderRow, {
|
|
1046
1202
|
key: "orders", title: "Orders", label: r => r.name,
|
|
1047
|
-
cells:
|
|
1048
|
-
children:
|
|
1203
|
+
cells: Plan.tableCells(Plan.values("act", (n) => ({ value: some(n) }))), // where a parent holds no values of its own…
|
|
1204
|
+
children: "children", aggregate: "sum", // …it shows its subtree's subtotal, at every depth
|
|
1049
1205
|
format: Format.Number({ maximumFractionDigits: 0n }),
|
|
1050
1206
|
})
|
|
1051
1207
|
```
|
|
@@ -1056,9 +1212,11 @@ member series, side by side, and each row has its own id:
|
|
|
1056
1212
|
```tsx
|
|
1057
1213
|
Plan.series.views(OpsRow, { key: "presses", title: "Presses", match: r => r.pick.equal("presses") }, [
|
|
1058
1214
|
Plan.series.span(OpsRow, { key: "press-jobs", title: "Press jobs", label: r => r.label,
|
|
1059
|
-
runs:
|
|
1060
|
-
Plan.series.chart(OpsRow, { key: "press-util", title: "Press · utilisation", label: r => r.label,
|
|
1061
|
-
|
|
1215
|
+
runs: Plan.items("jobs", { from: "start", to: "end" }, (job) => ({ label: job.label, state: job.state })) }),
|
|
1216
|
+
Plan.series.chart(OpsRow, { key: "press-util", title: "Press · utilisation", label: r => r.label,
|
|
1217
|
+
layers: [Plan.line(Plan.values("util", (pct) => ({ y: pct })))] }),
|
|
1218
|
+
Plan.series.table(OpsRow, { key: "press-sheets", title: "Press · sheets", label: r => r.label,
|
|
1219
|
+
cells: Plan.tableCells(Plan.values("sheets", (n) => ({ value: some(n) }))) }),
|
|
1062
1220
|
])
|
|
1063
1221
|
// a click on p03's utilisation row reports Plan.ref("press-util", "p03")
|
|
1064
1222
|
```
|
|
@@ -1067,10 +1225,12 @@ Plan.series.views(OpsRow, { key: "presses", title: "Presses", match: r => r.pick
|
|
|
1067
1225
|
| --- | --- | --- |
|
|
1068
1226
|
| **Layout and nesting** | | |
|
|
1069
1227
|
| `series={[s1, s2, …]}` | The list is the layout: one block per series, top to bottom; a pick's list order is the same layout. | `planTargetState` |
|
|
1070
|
-
| `
|
|
1071
|
-
| `
|
|
1228
|
+
| `Plan.items(field, { from, to } \| { at }, draw: (item, id, row) => look)` | Things people drag — runs, decisions, tiles, chips, marks — read from a `Dict` field of the row keyed by an id, each item's instants its named fields; `draw` gives everything its element takes but its key and instants. Over a collection keyed by an instant, or a field that is no `Dict`, it is refused in words naming the field. | `planTargetState`, `planRowDrop` |
|
|
1229
|
+
| `Plan.values(field, draw: (value, at, row) => look)` | Values read from a `Dict` field keyed by their instant: cells, a table series' cells, chart layers (`Plan.line` … `Plan.band`), a span's `ports`, a bucket row's `markers`, a group's `summary`. A collection keyed by `String` rides only an ordinal axis. | `planTargetState`, `planMeasures` |
|
|
1230
|
+
| `children: "children"` | More of this series: a `Dict` field of a `RecursiveType` entry, keyed by an id, to any depth. A parent's span bands (`rollup`, summed per quantity unit), heat `aggregate` (on its `scale`) or table subtotals (`aggregate`, `format`) derive from its subtree. | `planTargetState`, `planMeasures` |
|
|
1231
|
+
| `children: Plan.children(field, series: Series[])` | A step down to a `Dict` field of child rows of another type, keyed by an id, laid out like a top-level list; an array of step-downs gives several collections, in order. | `planTargetState` |
|
|
1072
1232
|
| `Plan.series.group(G, { key, title, label, children, summaryAggregate?, summary?, collapsed? })` **❗** | One strip per entry, its members the entry's children; the removed `by` form throws. | `planTargetState` |
|
|
1073
|
-
| `rows.groupToDicts(($, r) => r.hall, ($, _r, k) => k)` | Grouping is a data step; a paged source is grouped in its dataflow. | `planTargetState` |
|
|
1233
|
+
| `rows.groupToDicts(($, r) => r.hall, ($, _r, k) => k).map(($, members) => ({ members }))` | Grouping is a data step, each group an entry holding its rows in a field; a paged source is grouped in its dataflow. | `planTargetState` |
|
|
1074
1234
|
| `Plan.series.section(R, { key, title, collapsed?, meta?, value?, status?, summaryAggregate? }, members: Series[])` | A fixed titled block over series; it adds no path segment. | `planTargetState` |
|
|
1075
1235
|
| `Plan.series.views(R, { key, title, match?, children?, collapsed? }, members: Series[])` **❗** | One row per member per entry, adjacent; the entry's children follow under the first view row, and a seek lands there. A member may not declare `children`. Its children may be more of its own entries (a bare accessor), each drawn as the members' rows; a span member's `rollup` rolls them up. | `planTargetState` |
|
|
1076
1236
|
| `Plan.series.rows(R, { key, title }, [Plan.span({ key, label, rows? }), …])` | Hand-built rows as one block; a row's id is the series key plus the factory keys to it. | `planTargetState` |
|
|
@@ -1083,7 +1243,7 @@ Plan.series.views(OpsRow, { key: "presses", title: "Presses", match: r => r.pick
|
|
|
1083
1243
|
| `onSelect` / `expandRender` / `expandGutter`: `fn(Plan.Types.RowId)`; element refs, `onGroupToggle`, a drop (`Plan.Types.Drop`): `{ row: RowId, … }` | Every callback names a row by its id. | `planVariants`, `planTargetState`, `planRowDrop` |
|
|
1084
1244
|
| series keys unique across the whole tree **❗** | A repeated key is a build-time error naming both series; a repeated run-time id draws a DUPLICATE ID row diagnostic. | — |
|
|
1085
1245
|
| **Window** | | |
|
|
1086
|
-
| `Plan.axis({ window: { min, max }, … })` or a source of `slice={{ slice }}` holding a `range` **❗** | The window is stated, or it is the range its sources hold (#1344). A canvas with neither is refused, and nothing fits the axis to the data. | `planTargetState`, `planNumberAxis` |
|
|
1246
|
+
| `Plan.axis({ window: { min, max }, … })` or a source of `slice={{ slice }}` holding a `range` — an event kind's or a tab's, never the `rows` source's or a resource kind's **❗** | The window is stated, or it is the range its sources hold (#1344); while no source holds it the canvas holds its own, which its navigation moves without reading a row. A canvas with neither is refused, and nothing fits the axis to the data; a `range` on the `rows` source or a resource kind's is refused as the Plan is evaluated, naming the change. | `planTargetState`, `planNumberAxis`, `planRecordIndexes`, `slicePlanChrome` |
|
|
1087
1247
|
|
|
1088
1248
|
A paged canvas pages by parents, block by block (#823): a window holds its
|
|
1089
1249
|
entries whole, so every parent derives exactly, and each top-level series is a
|
|
@@ -1106,9 +1266,9 @@ element click is one callback over the same ref the popover resolver receives,
|
|
|
1106
1266
|
and the interaction state can live with the host.
|
|
1107
1267
|
|
|
1108
1268
|
```tsx
|
|
1109
|
-
Plan.
|
|
1110
|
-
quantity: Plan.quantity(
|
|
1111
|
-
Plan.heatCells(
|
|
1269
|
+
runs: Plan.items("jobs", { from: "start", to: "end" }, (job) => ({ label: job.ticket, state: job.state,
|
|
1270
|
+
quantity: Plan.quantity(job.sheets, { unit: "k sheets", format: Format.Number({ maximumFractionDigits: 0n }) }) })),
|
|
1271
|
+
cells: Plan.heatCells(Plan.values("load", (pct) => ({ value: some(pct) })), { min: 0, max: 100, fold: "max", format: Format.Number({ maximumFractionDigits: 0n }) }),
|
|
1112
1272
|
|
|
1113
1273
|
// The host holds what is selected, folded, opened and expanded; a write brings a row into view.
|
|
1114
1274
|
const ui = $.let(State.bind([Plan.Types.UiState], "ops.plan.ui", Plan.uiState({ collapsed: [Plan.ref("halls", "Hall 3")] })));
|
|
@@ -1177,15 +1337,15 @@ const onPatch = $.const(East.function([Plan.Types.PatchEvent(Hall)], NullType, (
|
|
|
1177
1337
|
library={[Plan.library.tab(templates.read(), { name: "Jobs", label: t => t.name })]} // a card drags onto a press
|
|
1178
1338
|
series={[Plan.series.span(Hall, {
|
|
1179
1339
|
key: "halls", title: "Halls", label: h => h.name, runs: _h => [],
|
|
1180
|
-
// A gesture on a press writes back into its hall through this
|
|
1181
|
-
children: Plan.children(
|
|
1340
|
+
// A gesture on a press writes back into its hall through this field.
|
|
1341
|
+
children: Plan.children("presses", [
|
|
1182
1342
|
Plan.series.span(Press, {
|
|
1183
1343
|
key: "presses", title: "Presses", label: (_p, k) => k,
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
create: (drop
|
|
1188
|
-
|
|
1344
|
+
// Its jobs, by id: a run each, from its start to its end.
|
|
1345
|
+
runs: Plan.items("jobs", { from: "start", to: "end" }, (job) => ({ label: job.label, state: job.state })),
|
|
1346
|
+
edit: { // a run moves and resizes; a dropped card joins `jobs`
|
|
1347
|
+
create: (drop) => ({ // …under the id the canvas mints, drop.id
|
|
1348
|
+
label: drop.from.key,
|
|
1189
1349
|
start: drop.at.unwrap("time"), end: drop.at.unwrap("time").addWeeks(2n),
|
|
1190
1350
|
state: variant("proposed", variant("added", null)),
|
|
1191
1351
|
}),
|
|
@@ -1199,9 +1359,10 @@ const onPatch = $.const(East.function([Plan.Types.PatchEvent(Hall)], NullType, (
|
|
|
1199
1359
|
| Signature | Description | Example |
|
|
1200
1360
|
| --- | --- | --- |
|
|
1201
1361
|
| **What a gesture writes (on the series)** | | |
|
|
1202
|
-
| `edit: {
|
|
1203
|
-
| `edit:
|
|
1204
|
-
| `
|
|
1362
|
+
| `edit: { create: (drop: Plan.Types.Drop, entry, key) => Item }` beside `Plan.items(F, …)`; span / buckets / cards / events **❗** | A library card dropped on a row joins `F` as `create`'s item, under the id the canvas mints (`drop.id`, unique among every element it holds): `drop.from` is the card, `drop.at` the start of the bucket it landed in, on the axis arm. Elements given any other way than `Plan.items` fail the build, in words naming the read to give. | `planRowDrop` |
|
|
1363
|
+
| `edit: true` beside `Plan.items(F, { from, to } \| { at }, draw)` (span / buckets / cards / events) **❗** | A run, chip, tile or mark moves by its body, and a run or chip resizes by either end, with the pointer, a touch or the keyboard (#825), each gesture written into the item's named times. It moves in whole buckets; Shift moves one finer unit (a day under a week, an hour under a day, 15 minutes under an hour). Onto another row whose items are the same type, the item leaves its row's collection under its id and joins that row's under the same id, as one gesture over both entries. An element's key is its item's id — its identity across every row it can move to: a row's collection keeps its ids unique, so a move onto a row holding the id is refused (⊘ while dragging). Over a read of values, `edit` is refused: what people move is keyed by an id. | `planRowDrop` |
|
|
1364
|
+
| `edit: true` on `Plan.series.table` (its `cells`) or `Plan.tableSeries({ cells, edit: true })` — its cells `Plan.tableCells(Plan.values(F, draw))` **❗** | A number typed into a cell is the collection's value at the bucket's start: set, added where none is, deleted when cleared; an Integer collection takes a whole number. Its draw gives each value as stored (`value: some(v)`) — show it another way through `format`; any other draw, or cells given as a function, fails the build in words naming the series. On the canvas (#1438), Enter, F2, a double click or a digit opens an input over the cell: the Sheet's number field at the cell's width, with no stepper — below 480px of main, a tap opens it in a popover anchored to the cell. Enter writes, Esc leaves the cell as it was, ↑ and ↓ step the number, and Tab and Shift+Tab move along the row. Its text reads through the position's `format` (a percent's `82` or `82%` writes `0.82`), and an Integer collection's input refuses a fraction. A cell takes a number where its bucket holds one value of the collection, or none (every bucket in the window draws a cell then); a parent's cell drawing its children's subtotals takes none, and a bucket folding several values takes none, its tooltip naming the resolution its values take one at. Over rows read lean beside a nested series index, the first number typed on a row reads it whole, and every subtotal above it moves as e3 previews it. | — |
|
|
1365
|
+
| `children: "kids"` / `Plan.children("kids", …)` **❗** | A gesture below an entry writes back through its `children`, each a field of the entry; a field that is no `Dict` of rows keyed by an id, or a tree's field holding another type than the row's, fails the build. | `planRowDrop` |
|
|
1205
1366
|
| **The session (on the root)** | | |
|
|
1206
1367
|
| `editing={{ onUpdate: handle.write }}` with `data={handle}` **❗** | The inline adapter: Save applies the batch with `Editing.apply` over the handle's latest `Dict` and writes it once — idempotent across retries. It needs the handle itself as `data`. | Inline adapter shown above; the record-backed `planRowDrop` uses `Record.onApply` |
|
|
1207
1368
|
| `editing={{ onApply: fn(Editing.Types.ChangeSet(R, K)) => Editing.Types.ApplyResult }}` **❗** | The host's transaction, sync or async, checked against the base the drafts began from. A paged source needs `revision` and `refresh`; the host applies atomically, dedupes by `requestId` and returns the committed revision. | `planRowDrop` (`Record.onApply`) |
|
|
@@ -1215,15 +1376,58 @@ const onPatch = $.const(East.function([Plan.Types.PatchEvent(Hall)], NullType, (
|
|
|
1215
1376
|
pending, or incomplete / invalid while a check refuses its entry — each
|
|
1216
1377
|
entry marked for its own issues. The narrow layout's cards carry the same marks.
|
|
1217
1378
|
- **One gesture, one transaction.** The history item ends the frame's toolbar:
|
|
1218
|
-
issues · Undo · Redo · Discard · Save, and its
|
|
1219
|
-
undo; ⌘⇧Z / Ctrl+Shift+Z / Ctrl+Y redo.
|
|
1379
|
+
issues · conflicts (while one waits) · Undo · Redo · Discard · Save, and its
|
|
1380
|
+
status line. ⌘Z / Ctrl+Z undo; ⌘⇧Z / Ctrl+Shift+Z / Ctrl+Y redo.
|
|
1220
1381
|
- **The base is the source's.** An inline `Dict` is checked by its snapshot, a
|
|
1221
|
-
paged source by its revision.
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1382
|
+
paged source by its revision. When the source moves under the drafts —
|
|
1383
|
+
another person's save, a run's — each drafted entry is carried over to its
|
|
1384
|
+
newer value by East's three-way patch merge (the history item says
|
|
1385
|
+
"Checking your changes against the newest data" meanwhile): changes to
|
|
1386
|
+
different parts combine, and a part both changed to different values is a
|
|
1387
|
+
conflict. The draft keeps its own value there, and Save waits until every
|
|
1388
|
+
conflict is chosen, the draft's value or the source's, each choice one step
|
|
1389
|
+
Undo takes back. While one waits, the history item leads with the
|
|
1390
|
+
conflicts button (≠ and how many, before Issues), Save waits
|
|
1391
|
+
aria-disabled (its tooltip says why), each item holding one wears the warn
|
|
1392
|
+
ring until it is chosen (the optimiser's highlight in the warn colour: 1px
|
|
1393
|
+
at its edge, inside a cell, and a soft glow, over the highlight where an
|
|
1394
|
+
item has both), and the banner band under
|
|
1395
|
+
the toolbar — the ≠ mark, the word Conflict — says how many changes
|
|
1396
|
+
conflict, its Choose opening the same popover: a card for each conflict —
|
|
1397
|
+
the item as the banners name it (an event by its title), with Go to, which
|
|
1398
|
+
shows the item and selects it — the groups folded over it opened, and the
|
|
1399
|
+
view moved to its dates, week or day where it shows others — the popover
|
|
1400
|
+
staying open off it, by a side of the frame that leaves it clear where it
|
|
1401
|
+
would cover it (where neither does, and in a frame under 480px, it
|
|
1402
|
+
closes); the part's label; Yours and Theirs, each
|
|
1403
|
+
value in plain words; who changed it last, where the source said so when
|
|
1404
|
+
it refused a Save, and what both began from — with Keep all mine and Use
|
|
1405
|
+
all theirs at its foot. An event's start and end read as one range: a
|
|
1406
|
+
conflict at either is one card, Dates, the range whole on each side, its
|
|
1407
|
+
two chosen together and counted once — and with them the fields a move
|
|
1408
|
+
writes with the times, which place the event with them: a Roster duty's
|
|
1409
|
+
shift and group, an Allocation bin's bay. While it waits each side's place
|
|
1410
|
+
stands whole, and choosing takes one side's whole: Theirs takes their
|
|
1411
|
+
shift back with their times, never their times in your shift. It never
|
|
1412
|
+
opens by itself.
|
|
1413
|
+
A Save another write got there first is refused, and once its drafts carry
|
|
1414
|
+
over with nothing to choose it is sent again by itself — three times in a
|
|
1415
|
+
row at most, then the person's Save sends it; with a conflict, the person
|
|
1416
|
+
saves once it is chosen. A Save whose outcome is unknown retries the SAME request ("Retry
|
|
1417
|
+
request"), and drafts retire only once the source reads back with the
|
|
1418
|
+
Save in it ("Saved — reading it back…"): at the revision it committed or
|
|
1419
|
+
a later one, so another Save landing just after it holds nothing up.
|
|
1420
|
+
- **Kept through a reload.** The drafts are kept in the tab's storage as
|
|
1421
|
+
each gesture is made, and a reload of the tab brings them back, carried
|
|
1422
|
+
over to what the source holds then: as they were where it has not moved,
|
|
1423
|
+
combined with a change since or a conflict to choose where it has, and
|
|
1424
|
+
none where a Save landed before the reload. They are the workspace's they
|
|
1425
|
+
were made in: the same view over another workspace, repository or server
|
|
1426
|
+
in the tab holds none of them, before a reload or after. Undo starts at
|
|
1427
|
+
them. They go when the tab closes, and at a sign-out; storage that
|
|
1428
|
+
refuses them, or drafts a reload cannot restore, is a banner. The Sheet,
|
|
1429
|
+
the Calendar, the Roster, the Allocation board, the Schematic and the
|
|
1430
|
+
Flowchart keep theirs the same way.
|
|
1227
1431
|
- **Moves (#825).** A move along its row, or a resize, drafts the item's
|
|
1228
1432
|
instants in place. A move to another row takes the item out of its list
|
|
1229
1433
|
and appends it to the target row's, and a batch that takes an item and
|
|
@@ -1287,12 +1491,13 @@ unmapped fields hold their type's default.
|
|
|
1287
1491
|
| Event `fields`, `Schedule.field.*` | The shared `Fields` vocabulary: `text`, `number`, `select`, `tags`, `checklist`, `reference`, `readonly`, `hidden`; other editors derive from the field type. | `planEvents`, `planPrintWorks` |
|
|
1288
1492
|
| Event `ready`, `inspector` | Readiness returns `Editing.Types.Readiness` and can hold Save. A custom inspector receives the typed row and its session writer. | `planPrintWorks` |
|
|
1289
1493
|
| Event source, `entries` | The builder's `Slice.bindRecords` narrows a kind through a source named after its slot, over an interval index: its events in the window and its backlog read through it (the Calendar, #1343; the Plan, #1344), and the Roster's week of duties and the Allocation's day of bins, the range their own navigation writes (#1368, #1371). `entries: Data.bindPaged(record)` supplies original-key reads for editing. A kind's own `slice` and `filter` are refused at build, naming the change. See Indexed records and shared Slice above. | `calendarRecordIndexes`, `calendarOperations`, `planPrintWorks`, `planWindows`, `rosterWindowed`, `allocationWindowed` |
|
|
1290
|
-
| Event `window`, `backlogWindow`, `entries` | The separate single-index paging form, described under Large records below. A kind narrowed through a source of its builder's rail takes neither `window` nor `backlogWindow
|
|
1291
|
-
| `Schedule.periods(Record.bind(r
|
|
1494
|
+
| Event `window`, `backlogWindow`, `entries` | The separate single-index paging form, described under Large records below. A kind narrowed through a source of its builder's rail takes neither `window` nor `backlogWindow`, and a Plan refuses it on a kind placed on a paged resource kind, which is read a cell at a time through its source (#1039). | `calendarWindowed` |
|
|
1495
|
+
| `Schedule.periods(Record.bind(r), { every, status: { field, draft, published }, dismissed?, entries? })` **❗** | What a builder publishes (#1367): the app's record of periods, `Dict<DateTime, P>`, a row each keyed by its start — a UTC midnight, a week's first day by the builder's week start for `"week"`, or a `"day"`. `status.field` names a variant field of `P`, not an Option, and `draft` and `published` two of its cases; the published case holds `Null` or `{ at: DateTime }`. `dismissed`, the Roster's, names a `Set<String>` field: the proposals a week dismissed. `entries: Data.bindPaged(r)` reads the periods by key — the period, the one before it, the open picker's span, each one key search and one page — else the record is read whole. Without `periods`, a builder has no status chip and no Publish. | `rosterWarehouse`, `rosterWindowed`, `allocationKilnYard`, `allocationWindowed` |
|
|
1292
1496
|
|
|
1293
1497
|
A period whose status is the published case is read-only in every layout,
|
|
1294
1498
|
phones included, and so is one in a case that is neither, its chip naming the
|
|
1295
|
-
case; a period with no row is Not started
|
|
1499
|
+
case; a period with no row is Not started once its read lands; until then the
|
|
1500
|
+
status chip is a placeholder in its slot. Publish saves the period's drafts
|
|
1296
1501
|
first, and once every record reads back as saved writes the published case —
|
|
1297
1502
|
the time in `at` when the case holds one — and saves the period: a Save that
|
|
1298
1503
|
fails leaves the period a draft with its drafts kept, so a published period
|
|
@@ -1340,7 +1545,7 @@ key, never its row: a draft can move the event, and the link follows it.
|
|
|
1340
1545
|
|
|
1341
1546
|
```tsx
|
|
1342
1547
|
const presses = $.let(Record.bind(planPrintPresses, []));
|
|
1343
|
-
const jobs = $.let(Record.bind(planLinkJobs
|
|
1548
|
+
const jobs = $.let(Record.bind(planLinkJobs));
|
|
1344
1549
|
const stock = $.let(Data.bind(planLinkStock));
|
|
1345
1550
|
<Plan axis={axis}
|
|
1346
1551
|
resources={{ presses: Schedule.resources(presses.read(), { name: "Presses", icon: "print", label: p => p.name, group: p => p.hall }) }}
|
|
@@ -1357,7 +1562,7 @@ const stock = $.let(Data.bind(planLinkStock));
|
|
|
1357
1562
|
|
|
1358
1563
|
| Signature | Description | Example |
|
|
1359
1564
|
| --- | --- | --- |
|
|
1360
|
-
| `resources: { [slot]: Schedule.resources(rows, { name, icon, label, … }) }` + `events: { [slot]: Schedule.events(Record.bind(r
|
|
1565
|
+
| `resources: { [slot]: Schedule.resources(rows, { name, icon, label, … }) }` + `events: { [slot]: Schedule.events(Record.bind(r), { name, icon, title, start, end \| at, resource: { field, of }, … }) }` **❗** | Event kinds over records, on the resources' rows, ahead of `data`'s. `resources` without `events`, or a `resource` naming a slot `resources` has not, fails the build. | `planEvents`, `planPrintWorks` |
|
|
1361
1566
|
| event kinds on `Plan.axis.number` / `Plan.axis.ordinal` **❗** | Refused at build, naming the axis and the kinds: an event is scheduled in time. An axis held in a variable is refused in the same words as the Plan is evaluated. | — |
|
|
1362
1567
|
| `data`, beside `events` or `rows` | Optional: a Plan of event kinds, or of `rows` alone, needs none. A Plan with no rows from any source fails the build. | `planEvents` |
|
|
1363
1568
|
| `rows: [Plan.chart({ …, pinned? }), Plan.over(…), …]` | Read-only rows after the event kinds' and `data`'s, each a fixed block; a pinned one sits under the ruler. | `planPrintWorks`, `planEventLinks` |
|
|
@@ -1417,9 +1622,9 @@ places it bare.
|
|
|
1417
1622
|
|
|
1418
1623
|
| Signature | Description | Example |
|
|
1419
1624
|
| --- | --- | --- |
|
|
1420
|
-
| the toolbar | Every control the Plan draws outside its canvas, one row at every width: the rail of its one `Slice.bindRecords` folds first, its summary after every step of it, then the Plan's own steps and the key search into its icon; the fold toggle, the resolution and the overlaps chip into the date navigator's View, one move; the history item folds last. | `planTargetState`, `planRowDrop`, `planPrintWorks` |
|
|
1421
|
-
| the banners | Each session's — `data`'s, and each event kind's titled with its name (#1194): a Save's conflict, a refusal, an unknown outcome with Retry
|
|
1422
|
-
| the footer | The counts — the event kinds' events in the window, their backlog, the changes pending and the last save of an event kind's record — then the author's `footer` and the
|
|
1625
|
+
| the toolbar | Every control the Plan draws outside its canvas, one row at every width: the rail of its one `Slice.bindRecords` folds first, its summary after every step of it, then the Plan's own steps and the key search into its icon; the fold toggle, the resolution and the overlaps chip into the date navigator's View, one move; the history item folds last, and below that, at a phone's width, the rail's icon drops its caret, the history and Save staying in the row. | `planTargetState`, `planRowDrop`, `planPrintWorks` |
|
|
1626
|
+
| the banners | Each session's — `data`'s, and each event kind's titled with its name (#1194): changes that conflict with newer ones, whose Choose opens the history item's conflicts, a Save's conflict, a refusal, an unknown outcome with Retry; each leaves when what it reports does, and one kind's leaves the other kinds' commits standing. | `planRowDrop`, `planPrintWorks` |
|
|
1627
|
+
| the footer | The counts — the event kinds' events in the window, their backlog, the changes pending and the last save of an event kind's record — then the author's `footer` and the one loading line. | `planPrintWorks` |
|
|
1423
1628
|
| `style={{ height: "fill" }}` / `maxHeight` | The whole Plan's: the canvas fills main and scrolls its own rows. | `planTargetState` |
|
|
1424
1629
|
| a host's height, and no `style.height` | The canvas fills main and scrolls there; a host that gives none lets it grow with its rows. | — |
|
|
1425
1630
|
|
|
@@ -1437,6 +1642,14 @@ query moves once a gesture), a resolution's rewrite of the window (it
|
|
|
1437
1642
|
re-buckets) and templates written in the code (`Schedule.templates` over a
|
|
1438
1643
|
record). Each form still written is refused at build, naming the change.
|
|
1439
1644
|
|
|
1645
|
+
Changed with #1344 too: the canvas's rows never follow the dates on screen. A
|
|
1646
|
+
`range` on the `rows` source or a resource kind's — which held the window, so
|
|
1647
|
+
every pan narrowed the rows again — is refused as the Plan is evaluated,
|
|
1648
|
+
naming the change: leave it out, and the field stays a field of the rail the
|
|
1649
|
+
viewer sets. A slice with no source holding the window leaves the canvas its
|
|
1650
|
+
own window, which the date navigator, the brush and the keys move, and which a
|
|
1651
|
+
reload opens on, as the canvas's folds are kept.
|
|
1652
|
+
|
|
1440
1653
|
#### The library pane (#1195)
|
|
1441
1654
|
|
|
1442
1655
|
`library` lists the start pane's tabs, in order, each a `Plan.library.*`
|
|
@@ -1451,8 +1664,8 @@ const customers = $.let(Data.bind(planPrintCustomers));
|
|
|
1451
1664
|
const jobTemplates = $.let(Data.bind(planPrintJobTemplates));
|
|
1452
1665
|
// One rail (#1344): the templates' and the customers' sources narrow their tabs.
|
|
1453
1666
|
const slice = $.let(Slice.bindRecords("ops.plan", {
|
|
1454
|
-
jobTemplates: { source: Data.bindPaged(planPrintJobTemplates, { indexes: [
|
|
1455
|
-
customers: { source: Data.bindPaged(planPrintCustomers, { indexes: [
|
|
1667
|
+
jobTemplates: { source: Data.bindPaged(planPrintJobTemplates, { indexes: ["plan_print_job_templates_by_group"] }), fields: { group: { label: "Job templates" } } },
|
|
1668
|
+
customers: { source: Data.bindPaged(planPrintCustomers, { indexes: ["plan_print_customers_by_district"] }), fields: { district: { label: "District" } } },
|
|
1456
1669
|
}));
|
|
1457
1670
|
<Plan axis={axis} slice={{ slice }}
|
|
1458
1671
|
resources={{ presses: Schedule.resources(presses.read(), { name: "Presses", icon: "print", label: p => p.name, group: p => p.hall }) }}
|
|
@@ -1481,7 +1694,7 @@ const slice = $.let(Slice.bindRecords("ops.plan", {
|
|
|
1481
1694
|
| --- | --- | --- |
|
|
1482
1695
|
| `Plan.library.events()` **❗** | Every event kind's templates, under the kind's name and then the template's `group` (`Print job · Jobs`). A card is its kind's icon, its name and `Print job · 6 h · presses`: its kind, how long it runs (an instant kind's runs no time) and the resource kinds it is placed on. Dropped on a resource's row it makes its event there (#1196). Each kind's list follows its source of the rail (#1344): the tab counts what it shows (`Events 8 of 12`) and, when the rail leaves a list none, says so with Clear filters for it. Refused on a Plan with no event kinds. | `planEvents`, `planPrintWorks` |
|
|
1483
1696
|
| `Plan.library.backlog()` **❗** | Every event kind's unscheduled events (a kind with Option times and a `backlog`), grouped by when they are due: Due this week (overdue included), Due next week, Later, No date — UTC weeks, from the Monday of the week the axis's `now` is in, else the week the Plan mounted in. A card is `6 h · Press A · due Fri 16` (`Unassigned` on no resource). It is read again as a kind's record commits, with every kind's drafts in place (#1196): a card dropped on a resource's row is scheduled and leaves it, an event dropped on the tab is unscheduled and joins it. Collapsed, the pane's rail counts it. A kind narrowed through its source (#1344) shows its narrowing's backlog, the tab counting `N of M` — what shows beside the stored events the choices hide — and its Clear filters keeping the window. Refused on a Plan with no event kinds. | `planEvents`, `planPrintWorks` |
|
|
1484
|
-
| `Plan.library.series()` **❗** | `Pick.Panel`'s list in the order the canvas draws: each resource kind and its measures, the event kinds, `data`'s series when they are picked (`pick`), and the Plan's `rows` — a hand-built row by its key, a `Plan.over` series by its own. Hiding an event kind hides its elements everywhere, its Unassigned row and any row only it draws on with them (a resource's first row is its own, and stays); a resource kind its rows; a measure, or one of `rows`, its rows — a `Plan.over` series' nested series with it. `data`'s series hide through their pick
|
|
1697
|
+
| `Plan.library.series()` **❗** | `Pick.Panel`'s list in the order the canvas draws: each resource kind and its measures, the event kinds, `data`'s series when they are picked (`pick`), and the Plan's `rows` — a hand-built row by its key, a `Plan.over` series by its own. Hiding an event kind hides its elements everywhere, its Unassigned row and any row only it draws on with them (a resource's first row is its own, and stays), and leaves it out of every figure of the canvas's — the footer's counts, the inspector's stats and a row's facts — nothing saying so, its backlog the Backlog tab's still; a resource kind its rows; a measure, or one of `rows`, its rows — a `Plan.over` series' nested series with it. `data`'s series hide through their pick, kept per viewer under its `key`; the rest under the Plan's `id`, and only while the library lists this tab. Refused on a Plan with nothing a viewer can hide. | `planPrintWorks` |
|
|
1485
1698
|
| `Plan.library.tab(rows: Dict<K, R>, { name, icon?, label, meta?, group?, drop?, source? })` **❗** | `source` names the rail's source it follows, by its rows' keys (`Customers 6 of 16`, #1344). A card per row, its rows read as `Schedule.resources` reads them, accessors `(row, key)`: its label, its meta, the tab's icon, grouped by `group`, searched by key, label and meta. A click selects a card, and a click on the selected card lets it go. A key of another type than String is its card's key as East prints it. Its cards drag onto the rows of `data` whose series makes an item of a card (`edit.create`) — the drop's `from.key` the card's key, so `canDrop` and `create` read it as a Library card's — and the Plan takes them with no `id` or `sources` (#1259). | `planEvents`, `planPrintWorks`, `planRowDrop` |
|
|
1486
1699
|
| `drop: (row, key) => Schedule.patch(KindRowType, { … })` **❗** | With it, each card drags onto an event too, while one of the patch's kind is on the canvas. What a card dropped on an event sets (#1196): the patch's type names the event kind it lands on, so a patch over no kind's row type, or over one two kinds share, is refused at build, naming the tab. Dropped on the event under the pointer it sets the patch's fields as one step (`Harbour Arts Society → Spring catalogue` on the ghost); refused between events (`Drop a customer onto an event`) and on another kind's (`Stops take no customer`). | `planEvents`, `planPrintWorks` |
|
|
1487
1700
|
| `Schedule.patch(R, { field: value, … })` / `Schedule.Types.Patch(R)` | A patch over an event kind's row type: every field an `Option`, each field left out `none`; one set to `none` clears an Option field. | `planEvents` |
|
|
@@ -1525,7 +1738,7 @@ const inspectStop = $.const(East.function([PrintStop, FunctionType([PrintStop],
|
|
|
1525
1738
|
|
|
1526
1739
|
| Signature | Description | Example |
|
|
1527
1740
|
| --- | --- | --- |
|
|
1528
|
-
| `inspector` (`inspector={true}`) **❗** | The end pane, 320px. One event: its kind, its title, when it runs and its status; its resource, its start and end or its instant, its lane, its state and its quantity; its kind's form over `fields`, the fields its roles read left to their own lines; Duplicate and Delete; an overlaps banner lists this event's pairs. Several: how many, each kind's count, the list, and the bulk edit — the state, the resource, a shift of a day or an hour. A row: its resource's name, its lines and its group, its events in the window — how many, their hours, their quantities by unit — its overlapping pairs, and its measures at the bucket a click on its plot named; an Unassigned row, its kind's events on no resource; any other row, what it draws at that bucket. Nothing: the window's events, hours and backlog
|
|
1741
|
+
| `inspector` (`inspector={true}`) **❗** | The end pane, 320px. One event: its kind, its title, when it runs and its status; its resource, its start and end or its instant, its lane, its state and its quantity; its kind's form over `fields`, the fields its roles read left to their own lines; Duplicate and Delete; an overlaps banner lists this event's pairs. Several: how many, each kind's count, the list, and the bulk edit — the state, the resource, a shift of a day or an hour. A row: its resource's name, its lines and its group, its events in the window — how many, their hours, their quantities by unit; a paged resource's placeholders until its cells land, never 0 — its overlapping pairs, and its measures at the bucket a click on its plot named; an Unassigned row, its kind's events on no resource; any other row, what it draws at that bucket. Nothing: the window's events, hours and backlog — the hours those of the kinds it reads — and three hints. Collapsed, a rail naming what is selected. Refused on a Plan with no event kinds; `false`, or left out, no pane. | `planPrintWorks` |
|
|
1529
1742
|
| `Schedule.events(record, { …, inspector: fn(R, fn(R) => Null) => UIComponentType })` **❗** | The kind's own inspector for one event, in place of its form, over the event's row; `update(edited)` writes the edited event back. Passed through untouched, called only where the pane draws it. A function over another row type, or with no writer, fails the build. Plan and Calendar use it when their optional `inspector` pane is present; read-only Calendar never calls its editing callback. | `planPrintWorks` |
|
|
1530
1743
|
| the edits | Each one transaction of the Plan's history (#1194): a field of the kind's form, tinted while the record holds it otherwise; the kind's own inspector's `update`; Duplicate — each copy under a new key (a `String` key `-2`, `-3`, … after its own, an `Integer` key past the largest; a kind keyed otherwise has none), the copies then selected; Delete; and the bulk edit across kinds — the state, the resource (each event placed on that resource's kind), a shift of a day or an hour. They are off while a selected event's kind is saving or its record is not yet read. | `planPrintWorks` |
|
|
1531
1744
|
|
|
@@ -1558,27 +1771,37 @@ A record too large to read whole is read a window at a time: an event kind
|
|
|
1558
1771
|
through its record's indexes, and a resource kind paged as the canvas
|
|
1559
1772
|
scrolls. An event kind narrowed through its source of the Plan's slice
|
|
1560
1773
|
(#1344) reads its window and its backlog through the source's interval index,
|
|
1561
|
-
as `
|
|
1562
|
-
(
|
|
1563
|
-
|
|
1564
|
-
|
|
1774
|
+
as `planPrintWorks`' jobs do — counted first, and read only within 10,000
|
|
1775
|
+
events (#1039, below) — and one placed on a paged resource kind is read a cell
|
|
1776
|
+
at a time through its source, as `planWindows`' jobs are (below); what follows
|
|
1777
|
+
is the form for a kind with no rail, over resources read whole
|
|
1778
|
+
(`calendarWindowed`). An event kind given `window` — a paged read of a day
|
|
1779
|
+
index over its record, keyed by `Schedule.days` — reads only the days the
|
|
1780
|
+
canvas draws, on a Plan counted first by the entries the index files under
|
|
1781
|
+
them and read only within 10,000 (#1039, below); given `backlogWindow`, a paged read of an index keyed by
|
|
1565
1782
|
`Schedule.unscheduled`, its backlog; and given `entries`, a paged read of the
|
|
1566
1783
|
record's own entries, one event by its key. A resource kind given its
|
|
1567
1784
|
resources as a paged read pages them, 200 to a window, the key search seeking
|
|
1568
|
-
one
|
|
1785
|
+
one — and every event kind placed on it is read a cell at a time: one with no
|
|
1786
|
+
source of the slice, read whole or through its day index, is refused at
|
|
1787
|
+
build, naming the series and the summary index to declare over its own
|
|
1788
|
+
fields. Neither record is ever read whole: not to draw, not to edit, not to
|
|
1789
|
+
save.
|
|
1569
1790
|
|
|
1570
1791
|
```tsx
|
|
1571
|
-
// Package side: the jobs record
|
|
1572
|
-
//
|
|
1573
|
-
//
|
|
1792
|
+
// Package side: the jobs record declares its two indexes, each keyed by Schedule's own function.
|
|
1793
|
+
// indexes: (index) => ({
|
|
1794
|
+
// jobs_by_day: index.keys(East.function(… Schedule.days(start, end) …)),
|
|
1795
|
+
// jobs_unscheduled: index.keys(East.function(… Schedule.unscheduled(job.start, job.due) …)),
|
|
1796
|
+
// }),
|
|
1574
1797
|
<Reactive>{$ => {
|
|
1575
|
-
const presses = $.let(Data.
|
|
1576
|
-
const jobs = $.let(Record.bind(jobsRecord
|
|
1577
|
-
const days = $.let(Data.bindPaged(jobsRecord, { index:
|
|
1578
|
-
const backlog = $.let(Data.bindPaged(jobsRecord, { index:
|
|
1798
|
+
const presses = $.let(Data.bind(pressesTask)); // read whole: a paged kind's jobs are read a cell at a time (below)
|
|
1799
|
+
const jobs = $.let(Record.bind(jobsRecord));
|
|
1800
|
+
const days = $.let(Data.bindPaged(jobsRecord, { index: "jobs_by_day", join: true }));
|
|
1801
|
+
const backlog = $.let(Data.bindPaged(jobsRecord, { index: "jobs_unscheduled", join: true }));
|
|
1579
1802
|
const entries = $.let(Data.bindPaged(jobsRecord)); // one job by its key
|
|
1580
1803
|
return <Plan axis={axis}
|
|
1581
|
-
resources={{ presses: Schedule.resources(presses, { name: "Presses", icon: "print", label: p => p.name, sub: p => some(p.hall) }) }}
|
|
1804
|
+
resources={{ presses: Schedule.resources(presses.read(), { name: "Presses", icon: "print", label: p => p.name, sub: p => some(p.hall) }) }}
|
|
1582
1805
|
events={{ job: Schedule.events(jobs, {
|
|
1583
1806
|
name: "Print job", icon: "file-lines", title: "title", start: "start", end: "end",
|
|
1584
1807
|
resource: { field: "press", of: "presses" },
|
|
@@ -1590,10 +1813,11 @@ one. Neither record is ever read whole: not to draw, not to edit, not to save.
|
|
|
1590
1813
|
|
|
1591
1814
|
| Signature | Description | Example |
|
|
1592
1815
|
| --- | --- | --- |
|
|
1593
|
-
| `Schedule.events(record, { …, window: Data.bindPaged(record, { index, join: true }) })` **❗** | The kind's events over the days the canvas draws, read through a day index keyed by `Schedule.days` (a scheduled event filed under every day it runs into): the first
|
|
1816
|
+
| `Schedule.events(record, { …, window: Data.bindPaged(record, { index, join: true }) })` **❗** | The kind's events over the days the canvas draws, read through a day index keyed by `Schedule.days` (a scheduled event filed under every day it runs into): the days sought from the first's midnight to the window's end — where they start in the index and how many entries they hold — and the index's pages read from there; a pan seeks the days it brings in. On a Plan that count comes first, and past 10,000 entries no page is read (#1039, below). An event filed under several days draws once, and the drafts draw over the window. Refused at build: an index of another record, or keyed otherwise; a `window` over Option times without `backlogWindow`; a `window` without `entries`; either beside a source of the rail; and, on a Plan, a kind so read placed on a paged resource kind, naming the source to name after it and its series and summary indexes (#1039). An index read without `join: true` is refused as it is read. | `calendarWindowed` |
|
|
1594
1817
|
| `backlogWindow: Data.bindPaged(record, { index, join: true })` **❗** | The kind's backlog — the library's Backlog tab and the footer's count — read through an index keyed by `Schedule.unscheduled`. Refused on a kind whose times are plain `DateTime`s. | `calendarWindowed` |
|
|
1595
1818
|
| `entries: Data.bindPaged(record)` **❗** | The record's own entries: one event by its key — the inspector's, a gesture's before its first draft, a Save's read back, a conflict's — its key sought and a one-row page read. Required beside `window` or a source of the rail, refused without either. The kind's editing is a session over the record's revision: each change checked by the record against what it began from, and a Save's conflict naming each event another write moved, read by its key at the record's new revision, with who changed the record last. | `planWindows` |
|
|
1596
|
-
| `Schedule.resources(Data.bindPaged(record), { name, icon, label, … })` **❗** | A resource kind paged as the canvas scrolls: a window of resources at a time, the events on them drawn with them, the key search seeking a resource by its key, its types its page's `Dict`'s. Its resources keyed by `String`, with no `group` or `parent`; one paged kind per Plan, and none beside a paged `data`. A link's end on its rows is named by the row's id; the inspector and the backlog's cards name a resource once it is read by its key. An event on a resource it does not have draws on no row, and the bulk edit offers none of its resources. The Calendar
|
|
1819
|
+
| `Schedule.resources(Data.bindPaged(record), { name, icon, label, … })` **❗** | A resource kind paged as the canvas scrolls: a window of resources at a time, the events on them drawn with them, the key search seeking a resource by its key, its types its page's `Dict`'s. Its resources keyed by `String`, with no `group` or `parent`; one paged kind per Plan, and none beside a paged `data`. Every event kind placed on it is read a cell at a time through its source of the slice (#1039, below): one read whole, one read through its day index (`window`) and one placed beside another resource kind are refused at build, the refusal naming the series and the summary index to declare over the kind's own fields. A link's end on its rows is named by the row's id; the inspector and the backlog's cards name a resource once it is read by its key. An event on a resource it does not have draws on no row, and the bulk edit offers none of its resources. The Calendar takes one too, under the same rules (#1424; see Calendar below). | `planWindows` |
|
|
1820
|
+
| a source of the slice named after the paged kind's slot **❗** | The narrowing's rows are the kind's windows (#1344): `Data.bindPaged(record, { indexes })` over the kind's own record, paged in the source's index's order, 200 to a window — never every resource read, nor a window of them filtered by key — the rail and the canvas counting the narrowing's, the frame following none of its keys. A choice keeps the last view standing, retained, until its first window lands; a bounded canvas puts the resource at its top back where the narrowing's first windows draw it, and else starts at the top. It has no key search: a narrowing's searches run in its index's order. A source over another record's rows is refused at build. | `planSeriesByHall`, `planRecordIndexes` |
|
|
1597
1821
|
| `Schedule.resources(rows, { window })` **❗** | Gone (#1199): refused at build, naming the paged form. | — |
|
|
1598
1822
|
| a `ui()` task over them | A record bound with `Record.bind` and read with `Data.bindPaged` too is in its manifest's `records` and `pages`, never its `paths`: it is never preloaded or polled whole. | `planWindows` |
|
|
1599
1823
|
|
|
@@ -1602,6 +1826,76 @@ to the jobs reads the presses' windows again over the same snapshot, and the
|
|
|
1602
1826
|
search keeps its text, its matches and the view. Only a new snapshot of the
|
|
1603
1827
|
presses asks it again, there.
|
|
1604
1828
|
|
|
1829
|
+
#### A cell at a time, shaded zoomed out (#1039)
|
|
1830
|
+
|
|
1831
|
+
An event kind placed on a paged resource kind reads only the events of the
|
|
1832
|
+
resources around the rows in view: its source lists a **series index** of its
|
|
1833
|
+
record beside the interval index, and a **summary index** with it
|
|
1834
|
+
(`planWindows`). A series index files each event once under its resource, at
|
|
1835
|
+
the level its length fits — its tile, or a coarser one up to a year — and past
|
|
1836
|
+
a year under each year it touches, carrying its row (#1433); a summary holds,
|
|
1837
|
+
under each resource and tile, how many events touch it and the milliseconds of
|
|
1838
|
+
it they cover (the **e3** skill). A kind on a paged resource kind read any other way — its source
|
|
1839
|
+
listing its interval index alone, or a summary and no series; no source, its
|
|
1840
|
+
record read whole or the days in view through its day index; or placed
|
|
1841
|
+
beside another resource kind — is refused, naming both declarations over the
|
|
1842
|
+
kind's own fields: `index.series({ key: "press", from: "start", to: "end",
|
|
1843
|
+
tile: "week" })` and `index.summary({ key: "press", from: "start", to: "end",
|
|
1844
|
+
tile: "month" })` among the jobs record's indexes. Nothing else in the Plan
|
|
1845
|
+
changes:
|
|
1846
|
+
|
|
1847
|
+
```tsx
|
|
1848
|
+
// Package side, among the jobs record's indexes, beside the interval index:
|
|
1849
|
+
// jobs_by_press_week: index.series({ key: "press", from: "start", to: "end", tile: "week" }),
|
|
1850
|
+
// jobs_by_press_month: index.summary({ key: "press", from: "start", to: "end", tile: "month" }),
|
|
1851
|
+
const slice = $.let(Slice.bindRecords("ops.plan", {
|
|
1852
|
+
job: { source: Data.bindPaged(jobs, { indexes: ["jobs_by_time", "jobs_by_press_week", "jobs_by_press_month"] }), range: "start" },
|
|
1853
|
+
}));
|
|
1854
|
+
```
|
|
1855
|
+
|
|
1856
|
+
A **cell** is the events of a block of fifty of the paged kind's resources at
|
|
1857
|
+
one level — of a kind its source of the slice narrows, fifty of the narrowing's,
|
|
1858
|
+
in its index's order, so only the narrowed resources in view have their events
|
|
1859
|
+
read, and a choice reads none before its own rows are drawn
|
|
1860
|
+
(`planSeriesByHall`, #1344). The Plan wants the blocks meeting the rows in view and twenty-five
|
|
1861
|
+
resources either side, at each level over the tiles the range it reads touches
|
|
1862
|
+
— its window and the periods laid out beyond each edge — one tile either side,
|
|
1863
|
+
and the tile before those, whose events may run into them: a range a resource
|
|
1864
|
+
a level, whatever the span (#1433). Every wanted cell's searches go in one
|
|
1865
|
+
request, which counts its entries before one is read, and the entries of the
|
|
1866
|
+
tiles it does not hold in one more. A pan or a
|
|
1867
|
+
scroll within the cells it holds reads nothing, and a commit reads the wanted
|
|
1868
|
+
cells again, the old standing in until they land. The rail's window and its
|
|
1869
|
+
exact count stay the interval index's.
|
|
1870
|
+
|
|
1871
|
+
| Signature | Description | Example |
|
|
1872
|
+
| --- | --- | --- |
|
|
1873
|
+
| a source listing `index.series` + `index.summary` beside the interval index **❗** | The kind read a cell at a time, two requests a change: a block of fifty resources, a range a resource a level, whatever the span (#1433). An event past a year is under every year it touches and draws once; the drafts draw where they place their events, read or not; a link's event end the cells do not hold is read by its key, in one read. | `planWindows`, `planRecordIndexes` |
|
|
1874
|
+
| past 10,000 events in the cells wanted | The summary's cells instead, at the largest of its tiles that divides the canvas's bucket — a day, an ISO week (which divides no month), a month, a quarter or a year — each resource's share of each bucket booked for bars and chips, how many events touch it for tiles and marks. With none that divides it, the events over the window while their count there is within the cap; past it the rows are hatched, and a banner names how many events there are and the resolution that shades them. | `planWindows` |
|
|
1875
|
+
| a narrowing | A series index led by the narrowed fields serves it exactly; any other field is asked of the cells' events in one read. A summary shades a narrowing only when led by its fields. | `planWindows` |
|
|
1876
|
+
| the footer and the inspector | The window's events counted exactly from the source, as the rail counts them; their hours and overlaps over the rows drawn — "Hours in view", once the cells have landed — and the backlog read whole through the source's unset start. A resource selected while its cells are not read — scrolled out of the blocks read, or before the first read lands — shows its figures as placeholders until they land, never 0. | `planWindows` |
|
|
1877
|
+
| a kind on a paged resource kind whose source lists its interval index alone, or a summary and no series; one read whole, or through its day index; one placed beside another resource kind **❗** | Refused, each naming the series and the summary index to declare over the kind's own fields — as the payload builds where the indexes are known there, else as the Plan is evaluated, in the same words. | — |
|
|
1878
|
+
| a series index without a summary; an index filed by another field than the kind's resource field, or over other times than its own; a series index for a kind placed on resources read whole, or on none; two series or two summaries of one tile over the same fields **❗** | Refused as the payload builds, each naming the change. | — |
|
|
1879
|
+
|
|
1880
|
+
Every other event kind is **counted first** (#1039): before one of its events
|
|
1881
|
+
is read, it is counted over the range the canvas reads — its window and the
|
|
1882
|
+
periods it lays out beyond each edge, for every resource. A kind read through
|
|
1883
|
+
its source is counted through its interval index, the count its window's read
|
|
1884
|
+
asks first, so counting asks e3 nothing of its own; a kind read through its
|
|
1885
|
+
day index by the entries the index files under the days of the range, in the
|
|
1886
|
+
search its read asks first — an event under each day it runs on, so an upper
|
|
1887
|
+
bound on its events, never their number; and a kind whose record is read
|
|
1888
|
+
whole in memory. Within 10,000 it is read over the window as ever; past them
|
|
1889
|
+
none of its events is read or drawn:
|
|
1890
|
+
|
|
1891
|
+
| Signature | Description | Example |
|
|
1892
|
+
| --- | --- | --- |
|
|
1893
|
+
| a kind past 10,000 events, its source listing a summary **❗** | Its rows shade from the summary as a kind read by its cells does, at the largest of its tiles that divides the canvas's bucket, and no event of the range is read. | — |
|
|
1894
|
+
| a kind past 10,000 events with no summary that shades the bucket **❗** | Its rows are hatched, and the banner says how many events there are and the resolution its summary shades at — with none, to narrow the filters or zoom in. A pan, a zoom, a narrowing or a commit that brings the range within the cap reads its events again. | — |
|
|
1895
|
+
| a kind read whole, past 10,000 events | Counted in memory, and drawn the same: hatched, with the banner. | — |
|
|
1896
|
+
| the footer and the inspector over a kind past the cap | Its events in the window counted — through the source as the rail counts them, or in memory — never read; a counted kind is read over the window only once its count over the range the canvas reads is within the cap. A resource's row counts none of its events: the inspector says so (`Print jobs not counted: too many to draw at this resolution`), never 0. With nothing selected it counts its events but not their hours — the hours those of the kinds it reads, none where no kind's are — and names it, in one line with any kind it counts none of (`Print job hours not counted: too many to draw at this resolution`). A kind hidden in the Series tab is in no figure, and nothing says so. | — |
|
|
1897
|
+
| a kind read through its day index (`window`), past 10,000 entries **❗** | Counted first through its day index, its count the days its events run on: none of its days is read, its rows are hatched, and its events are not counted anywhere — the banner says so (`Print job events in view: not counted`), the footer (`13 events, print jobs not counted`) and the inspector too, never a number it does not have. Within 10,000 entries its days are read as ever. | `calendarWindowed` |
|
|
1898
|
+
|
|
1605
1899
|
### The Sheet — `<Sheet>`
|
|
1606
1900
|
|
|
1607
1901
|
`<Sheet>` is the planning spreadsheet, and there is one: it renders in its
|
|
@@ -1619,8 +1913,11 @@ sources:
|
|
|
1619
1913
|
through `onApply`, `onUpdate` and `onPatch`; with none of them the sheet is
|
|
1620
1914
|
read only.
|
|
1621
1915
|
- the **`rows` source of its `slice`** — `slice={{ slice: Slice.bindRecords(key,
|
|
1622
|
-
{ rows, … }) }}` (#1345): the
|
|
1623
|
-
|
|
1916
|
+
{ rows, … }) }}` (#1345): the source's record's rows, every one in the
|
|
1917
|
+
record's order, each row's id its record key. While the source narrows them
|
|
1918
|
+
the sheet draws the narrowing over them as the **lens** (#1349), a filtered
|
|
1919
|
+
spreadsheet: only the rows the narrowing keeps, one after another in the
|
|
1920
|
+
record's order, each at its row number. Save commits through
|
|
1624
1921
|
`onApply={Record.onApply(record, { keyed: true })}` by original primary key.
|
|
1625
1922
|
|
|
1626
1923
|
Its columns are typed — a date, a quantity with its unit, an integer, text, a
|
|
@@ -1629,9 +1926,9 @@ register lookup, reference or enum, a set of register members, a directed
|
|
|
1629
1926
|
read-only code, a custom parse / print pair — with a blank tail that invites
|
|
1630
1927
|
the next row, typed parsing with a docked candidate strip, a copilot that
|
|
1631
1928
|
fills cells and proposes whole rows from author East functions, one rail over
|
|
1632
|
-
its `Slice.bindRecords` — its rows,
|
|
1633
|
-
name a source — with saved-view tabs, drag
|
|
1634
|
-
a key search over a paged source. Every change is a draft
|
|
1929
|
+
its `Slice.bindRecords` — its rows, whose narrowing it draws as the lens, its
|
|
1930
|
+
templates records and the tabs that name a source — with saved-view tabs, drag
|
|
1931
|
+
and drop, Excel round-tripping, and a key search over a paged source. Every change is a draft
|
|
1635
1932
|
of an editing session, checked as it is made and applied as one checked
|
|
1636
1933
|
batch. Where east-ui's `<Table>` displays rows, the Sheet is where a planner
|
|
1637
1934
|
types them: reach for it when the rows are authored in place and written back.
|
|
@@ -1650,10 +1947,12 @@ and `sheetMessages` — are `@elaraai/e3-ui-components`'.
|
|
|
1650
1947
|
/** @jsxImportSource @elaraai/e3-ui */
|
|
1651
1948
|
import { Box, Reactive } from '@elaraai/east-ui';
|
|
1652
1949
|
import { Record, Sheet } from '@elaraai/e3-ui';
|
|
1653
|
-
// Package side: export const jobs = e3.record("jobs", DictType(StringType, Job),
|
|
1654
|
-
//
|
|
1950
|
+
// Package side: export const jobs = e3.record("jobs", DictType(StringType, Job), {
|
|
1951
|
+
// initial: new Map([…]),
|
|
1952
|
+
// mutations: (mutation) => ({ patch: mutation.patch() }),
|
|
1953
|
+
// });
|
|
1655
1954
|
<Reactive>{$ => {
|
|
1656
|
-
const record = $.let(Record.bind(jobs
|
|
1955
|
+
const record = $.let(Record.bind(jobs));
|
|
1657
1956
|
return (
|
|
1658
1957
|
<Box height="560px">
|
|
1659
1958
|
<Sheet record={record} columns={{
|
|
@@ -1667,12 +1966,12 @@ import { Record, Sheet } from '@elaraai/e3-ui';
|
|
|
1667
1966
|
```
|
|
1668
1967
|
|
|
1669
1968
|
```
|
|
1670
|
-
<Sheet record={Record.bind(r
|
|
1671
|
-
├─ The frame: ONE toolbar holding every control the sheet has, in this order — the view tabs, the key search, the rail over its `Slice.bindRecords` and its summary (each source a narrowing hides rows of, named in its own words, #1358), the scope badge and the history item (a sheet with none of them, read only with no slice, has no toolbar). It is one row at every width: the rail folds first, then its summary, then the sheet's own steps — the tabs into `+n`, the strip closing up — then the key search into its icon (which opens the box in a popover, the focus in it) and the view tabs into one chip naming the open view, its menu every view, `+ TAB` and the open view's close (#1221); where the sheet has a key search, the chip folds last into the search's popover, its views over the box (#1345); and the history item last, to its buttons; on a touch screen every control there is a 44px tap target. The banners — a Save's conflict naming its rows and who changed the record last, a refusal with its reason, an unknown outcome and a failed confirmation read each with Retry, the out-of-date notice with Discard, each source the library follows whose narrowing could not be read, with Retry, and an `entry` the record does not hold; main — the grid, filling the room the panes leave and scrolling its own rows, the strip docked under it; the footer — the sheet's, over a record its last save, and over the slice's `rows` source the rows a Save took out of the narrowing. ⌘Z undoes and ⇧⌘Z / ⌘Y redo from anywhere in the frame; a field being typed into keeps its own undo. The sheet fills its parent and draws no border: give it a box of its own height
|
|
1969
|
+
<Sheet record={Record.bind(r)} | data={rows} id="id" | slice={{ slice: Slice.bindRecords(key, { rows, … }) }} columns={{ start: Sheet.column.date(Row, {…}), qty: Sheet.column.quantity(Row, Driver, {…}), … }} library?={[…]} inspector? /> — the planning SPREADSHEET in its builder frame: typed columns over its rows (date · quantity + unit · integer · text · register lookup / reference / enum · a set of register members · a directed `from > to` LINK between register members as a TYPED value · stamped read-only codes · a custom parse / print pair), a blank tail that invites the next row, typed parsing with a docked candidate strip (nothing ever floats over the grid), a copilot that fills cells and proposes whole rows from author East functions, one rail over its Slice.bindRecords with the LENS — the `rows` source's narrowing drawn over the record's rows, only the rows it keeps, each at its row number (#1349) — and saved-view tabs over every source's narrowing, drag and drop, Excel round-tripping, and a key search over a paged source. Declared the way east-ui's Table and the Plan are: every per-row fact is an accessor, the builders take the row type FIRST, and nothing at the author's side is addressed by a string name
|
|
1970
|
+
├─ The frame: ONE toolbar holding every control the sheet has, in this order — the view tabs, the lens's count line (while the `rows` source narrows the rows, #1349), the key search, the rail over its `Slice.bindRecords` and its summary (each source a narrowing hides rows of, named in its own words, #1358), the scope badge and the history item (a sheet with none of them, read only with no slice, has no toolbar). It is one row at every width: the rail folds first, then its summary, then the sheet's own steps — the tabs into `+n`, the count line, the strip closing up — then the key search into its icon (which opens the box in a popover, the focus in it) and the view tabs into one chip naming the open view, its menu every view, `+ TAB` and the open view's close (#1221); where the sheet has a key search, the chip folds last into the search's popover, its views over the box (#1345); and the history item last, to its buttons — below that, where the row is still too wide, the rail's icon drops its caret, so the history and Save stay in the row at a phone's width; on a touch screen every control there is a 44px tap target. The banners — a Save's conflict naming its rows and who changed the record last, a refusal with its reason, an unknown outcome and a failed confirmation read each with Retry, the out-of-date notice with Discard, each source the library follows whose narrowing could not be read, with Retry, and an `entry` the record does not hold; main — the grid, filling the room the panes leave and scrolling its own rows, the strip docked under it; the footer — the sheet's, over a record its last save, and over the slice's `rows` source the rows a Save took out of the narrowing. ⌘Z undoes and ⇧⌘Z / ⌘Y redo from anywhere in the frame; a field being typed into keeps its own undo. The sheet fills its parent and draws no border: give it a box of its own height
|
|
1672
1971
|
├─ Rows — ONE of three sources (two, or none, is refused):
|
|
1673
|
-
│ ├─ record — `Record.bind(r
|
|
1674
|
-
│ ├─ data — the host's rows: an `Array<R>` value / expression or a `$.let`-bound whole-value handle (`State.bind` / `Data.bind`) for the INLINE arm; a paged source for a WINDOWED arm — a `$.let`-bound `Data.bindPaged(…)` handle, recognised by its East type: `pinned` when it names its snapshot, which `onApply` needs, `paged` when it is the shape a UI exported before `revision` / `refresh`, which names none — positional (`Array<R>` windows) or keyed (`Dict<String, R>` windows; the key is the row id). When a pinned source's `revision()` moves (`Data.bindPaged` follows each write to its dataset), the sheet re-reads its resident windows at the new snapshot IN PLACE: the rows on screen stay until theirs land (no remount, no empty frame), the scroll position and row heights hold, and a standing key search is cleared.
|
|
1675
|
-
│ └─ the `rows` source of `slice` — slice={{ slice: Slice.bindRecords(key, { rows: { source: Data.bindPaged(r, { indexes }), fields, range?, search? }, … }) }} (#1345): the
|
|
1972
|
+
│ ├─ record — `Record.bind(r)` over a `Dict` record: its entries are the rows, in key order, each row's id its key's text (any key type: a non-String key by its `.east` text). `window={Data.bindPaged(r)}` reads the same record a window at a time. `entry={{ key, rows: F, id: I }}` is one entry's Array field `F`, in its own order, each row identified by the `String` field `I`; each entry keeps its own drafts, and a key the record does not hold opens empty and read only, named in a banner. With `group`, the entry's field holds groups, or `Sheet.Types.Entry(P, "lines")` entries with loose rows between them (`id` then names a field of both types); a record of groups is grouped the same way. Save is ONE patch commit, checked against what each row was when its edit began, confirmed by the rows the record reads back. Over a record `id`, `onApply` and `onUpdate` are refused (the record identifies and commits its rows); `entry` and `window` need a record
|
|
1973
|
+
│ ├─ data — the host's rows: an `Array<R>` value / expression or a `$.let`-bound whole-value handle (`State.bind` / `Data.bind`) for the INLINE arm; a paged source for a WINDOWED arm — a `$.let`-bound `Data.bindPaged(…)` handle, recognised by its East type: `pinned` when it names its snapshot, which `onApply` needs, `paged` when it is the shape a UI exported before `revision` / `refresh`, which names none — positional (`Array<R>` windows) or keyed (`Dict<String, R>` windows; the key is the row id). When a pinned source's `revision()` moves (`Data.bindPaged` follows each write to its dataset), the sheet re-reads its resident windows at the new snapshot IN PLACE: the rows on screen stay until theirs land (no remount, no empty frame), the scroll position and row heights hold, and a standing key search is cleared. Rows not read draw as the shared run — the sheet's own rows at its pitch, a flat sheet's real row numbers — and a failure stays where it happened: a window the source cannot read is said once, in its run's first row in view (`Couldn't read rows 201–400`, Retry), its reason on the footer's loading line, while the windows around it keep working and the rows after it keep their numbers; a `total()` or `revision()` that throws is said on the footer's loading line with a Retry while the rows stay; only a source that fails before anything lands replaces the grid, with a Retry. A row that throws while it draws is a one-row diagnostic. A `Dict` inline is refused (a sorted map would sit rows in key order, not the planner's) — a record's entries come through `record`. `id` (required on a positional source) names the `String` field that identifies a row; over entries of groups and loose rows, a field of BOTH types
|
|
1974
|
+
│ └─ the `rows` source of `slice` — slice={{ slice: Slice.bindRecords(key, { rows: { source: Data.bindPaged(r, { indexes }), fields, range?, search? }, … }) }} (#1345): the source's record's rows, every one in the record's order, each row's id its record key, a grouped sheet's groups its records; while the source narrows them, the sheet draws the narrowing over them as the LENS (#1349), a filtered spreadsheet — only the rows the narrowing keeps, one after another in the record's order, each at its row number, its number marked; nothing stands between them for the rows it leaves out. e3 places the narrowing's rows from its index alone, and the lens reads only the record's segments that hold matches near the view — a page and one ask of which of its rows the narrowing holds, each — so rows with no match are never read, whatever the record's size; a segment not read yet is one band of its matches at the rows' pitch, so the sheet is as tall as its matches before their rows land and after, and they land where the band stood. The count line says `n matches`, n e3's count. A segment whose rows disagree with where e3 placed the narrowing's is one failed band saying the record changed, with a Retry — never a match the index does not hold. A grouped sheet's lens narrows its groups: a matching group's every line shows. Edits through `onApply={Record.onApply(record, { keyed: true })}`, a Save that takes rows out of the narrowing said in the footer; the key search seeks in the record's order and lists the rows its key finds wherever they are, a row the narrowing leaves out naming the choices that hide it as the rail words them — `· hidden by Status: Ready — picking it clears that filter`, which of them hide it asked of e3 through the record's indexes, `· hidden by the filters` until it says — and a pick of it clears just those choices, every other one kept (every choice of the source, before e3 has said), and goes to the row at its number; a choice cleared so is a change of the rail like any other, a saved view's tab marked changed and Esc putting it back. `record`, `data`, `window` and `entry` beside it are refused, each naming the change
|
|
1676
1975
|
├─ Props:
|
|
1677
1976
|
│ ├─ columns (required) — keyed by the row's fields and checked per key (a key that is not a field, a date under a `String` field, a builder over another row type: type errors): Sheet.column.text(R, cfg) · date(R, { base?, format?, level?, actual? }) — the common date field (`dd / mm / yyyy` segments); pasted text takes the B§3 grammar (`+3d`, `4d` from `base`, `fri`, ISO, `d/m[/yy]`); `level: r => …` reads each row's date at a Sheet.Types.DateLevel ("week" | "day" | "range" | "time" — no shifts: a host with shifts uses a `custom` kind) and `actual: r => Option<DateTime>` is when the work really happened (the cell then prints it with its difference, the wanted date becoming the cell's detail) · quantity(R, cfg) or quantity(R, D, { uom: d => d.uom, format? }) — a float with the DRIVER row's unit in the common number field, in the viewer's language (#852): the cell groups (1,234.5 — German 1.234,5; through `format` when declared), the edit box and copy are bare (1234.5 — German 1234,5), typed and pasted text reads the viewer's separators (pasted `1.2k` / `1.2m`, German `1,5k`, still parse; a typed quantity rounds to a whole number), and ⏎ on an unchanged edit box writes nothing · integer(R, cfg) · lookup(R, { options? }) — the DRIVER column only (scored candidates from its register) · reference(R, register, cfg) — a lookup over a flat member list · enum(R, register, { options? }) — an upper-cased register word with a valence dot · set(R, register, { members?, multiple?, store? }) — comma members, the link grammar without an arrow · link(R, D, register, { to? | from?, members, multiple?, sides?, arity?, check?, store?, options? }) — `from > to`, the split cell; `members: [{ kind, identified?, countable?, resolvesTo?, ranged? }]` — `ranged` offers and prints runs of consecutive codes as one range. `options` (enum / lookup / link) is fn(Sheet.Types.DraftContext(R, D)) => Option<Array<String>> — the member keys the row is OFFERED (`none` = the whole register; typed text still resolves against the whole register) · stamped(R, { owner? }) — read-only, skipped by paste and clear · custom(R, { accepts, parse, print }) — an author parse / print pair over the field's payload. Every kind takes { header, sub, width, editable?, fill?, detail? } (`header` + `sub` are the two header lines; `fill` = providers, the first that yields wins; `detail: r => String | Option<String>` is the text the hover and the strip show beyond the value); text / date / quantity / integer also take `value: r => …` for a derived READ-ONLY projection on any field. A set / link column sits on a `Sheet.Types.Link` field, an `Array<Sheet.Types.Member>` field (the other half named by `to` / `from`), or a `String` field the grammar parses on read and prints on commit per `store` ("asTyped" | "canonical" — the register's labels)
|
|
1678
1977
|
│ ├─ group (optional) — Sheet.group(P, "lines", { title, sub?, cells?, folded?, noun? }): the rows are GROUPS whose lines live in one `Array<L>` field; `columns` are declared over `L`; `cells` = band cells (Sheet.group.cell.*) keyed by the line column they sit under; `noun: { singular, plural }` is the word the renderer prints for a group (omitted, the sheet says its own in the viewer's language — "group" / "groups" in English, #861). LOOSE rows between the groups (#846): rows of Sheet.Types.Entry(P, "lines") entries — each `variant("group", P)` or `variant("row", L)` — and a row entry draws as a plain row: no band or rail, numbered in the groups' sequence, its own cells. The seam above a band or beside a loose row inserts a loose row (a line's seam and a group's blank line still insert a line); loose rows delete, paste and count on their own ("2 groups · 3 lines · 3 loose rows"), fold-all passes them by and no band sticks over them. A new line there gets its `id` field minted unless `newRow` supplies one; drafts, checks and the change set carry the entry (a draft is `variant("group", …)` or `variant("row", …)`, a loose row's DraftContext has `group: none`)
|
|
@@ -1681,27 +1980,28 @@ import { Record, Sheet } from '@elaraai/e3-ui';
|
|
|
1681
1980
|
│ ├─ registers (optional) — { name: Sheet.register.members(rows, { kind, key, label, aliases?, meta?, parent?, tone? }) | Sheet.register.concat([…]) }: the lookup tables reference / enum / set / link columns resolve against; accessors receive `(value, key)` (a `Dict<String, T>` register reads its key as the second argument; an Array's key is its index); duplicate keys fold, first wins — so a family kind ("CNC router" from every router) declares one member per distinct value
|
|
1682
1981
|
│ ├─ owned (optional) — accessor r => Bool: rows the upstream system owns — no copilot, stamped columns read-only
|
|
1683
1982
|
│ ├─ suggest (optional) — { ahead?, triggers?, ghost?, propose: [fn] }: the copilot's row proposers — East functions (sync, or async for a model call) over Sheet.Types.DraftContext(R, D) — current draft row and neighbours, rowIndex, driver: Option<D> and today; draft fields are missing | value(T) | invalid(String), so guard with hasTag("value") before unwrap("value") — returning Array<Sheet.Types.Proposal(R)> ({ patch: Sheet.patch(R, { … }), meta }); a column's `fill` providers are the same shape returning Option<Sheet.Types.Fill(T)> ({ value, meta }). The first that yields wins; fills CHAIN in column order (a later column sees the earlier fills as if taken); an async one shows a pending chip in the strip and the newest context wins; a rejected fill / proposal is remembered for the session
|
|
1684
|
-
│ ├─ slice (optional) — slice={{ slice: Slice.bindRecords(key, { rows?, <a templates list's source>…, <a tab's source>… }, { presets? }), affordances?, persist? }} (#1345): ONE rail on the toolbar over its sources — `rows` narrows the sheet's rows (above), each templates list's source its cards in the Rows tab, and an author's tab's source that tab's cards; a sheet over `record` or `data` takes a slice of templates sources alone. The bare handle, `Slice.bind`, a single `Slice.bindRecord`, a source nothing names, `affordances` / `persist` outside `slice` and the `brush` affordance (the sheet has no axis) are refused, each naming the change.
|
|
1983
|
+
│ ├─ slice (optional) — slice={{ slice: Slice.bindRecords(key, { rows?, <a templates list's source>…, <a tab's source>… }, { presets? }), affordances?, persist? }} (#1345): ONE rail on the toolbar over its sources — `rows` narrows the sheet's rows (above), each templates list's source its cards in the Rows tab, and an author's tab's source that tab's cards; a sheet over `record` or `data` takes a slice of templates sources alone. The bare handle, `Slice.bind`, a single `Slice.bindRecord`, a source nothing names, `affordances` / `persist` outside `slice` and the `brush` affordance (the sheet has no axis) are refused, each naming the change. While `rows` narrows the rows, the sheet draws the narrowing over the record's rows as the lens (above, #1349). Over a keyed paged source the key search seeks over `seek` (the jump rebases residency and, once the match's window is in, lands the ring on the match and scrolls to it — until then the jump owns the viewport, so a scroll report from where the sheet was cannot undo it; a match whose window cannot be read shows its failed band instead). The ring, a range and an open editor stay on their rows as windows land around them
|
|
1685
1984
|
│ ├─ views / activeView (optional) — `views` is a whole-value bind handle of Array<Sheet.Types.RecordView> (`State.bind` keeps them per viewer, `Data.bind` shares them): saved views, each every source's narrowing by the source's name (#1345), as TABS — the pinned whole-sheet tab, which clears every source, `+ TAB` snapshot (named from the sources' searches), each tab's count its `rows` narrowing's through the record's index, the dirty dot when the rail drifts from the tab, × or middle-click close, double-click rename, drag reorder — on a row short of room, one chip naming the open view, its menu every view, `+ TAB` and the open view's close (#1221), and with a key search last folded into its popover (#1345); every change lands at once and is written back through the handle (`onViewsChange` is refused); `activeView` opens a tab and is followed when the host moves it. The tabs show on a sheet with a `rows` source; a sheet without one takes `views` and shows none
|
|
1686
1985
|
│ ├─ onUpdate (optional, `data` only, INLINE arm) — fn(Array<R>) => Null over a LIVE data bind handle: reads the latest collection, checks the batch base and writes the complete result once. Hidden fields survive. Pass data={rows} and onUpdate={rows.write}; a captured array is refused for this adapter
|
|
1687
1986
|
│ ├─ onPatch / onApply (optional) — onPatch: fn(Sheet.Types.PatchEvent(E)) => Null observes one draft gesture including incomplete values (either source); onApply (`data` only): sync/async fn(Sheet.Types.ChangeSet(E)) => Sheet.Types.ApplyResult persists a complete checked batch. Paged writes require revision/refresh, atomic host application and request-id deduplication
|
|
1688
1987
|
│ ├─ applyMode (optional) — "batch" (default: the history item's Save sends the batch) | "auto" (each ready gesture goes at once, through the same acknowledgement path)
|
|
1988
|
+
│ ├─ optimise (optional, #1409) — `Func.bind` of the app's `e3.function` from `Sheet.Types.OptimiseRequest(R)` (`OptimiseRequest(R, K)` over a record keyed otherwise than by String) — the run's number and the rows in view by key as the session holds them, base plus every unapplied draft, the lens's rows read whole — to `Sheet.Types.OptimiseResult(R)`, the rows complete. The Elara eye before the history item: one call per run, the history and every edit locked while it is out, a change of view or tab stopping it. Its answer is diffed by key and recorded as ONE step of the history — Undo in one, Save with everything else — a row added or changed marked cell by cell until Apply (a blank filled an add, a value changed a change, its provenance on hover), the toolbar's count flashing; a result equal to the request records nothing and says so
|
|
1689
1989
|
│ ├─ newRow / newGroup / ready (optional) — newRow: fn(NewRow) => Patch(R) supplies defaults, including hidden fields; newGroup: fn(NewGroup) => Patch(G) a new group's; ready.row: fn(Draft(R), DraftContext(R, D)) => Readiness adds business rules; ready.group: fn(DraftGroup(G, "rows")) => Readiness checks grouped drafts. Required fields and valid parsing are always checked; optional absence becomes none
|
|
1690
1990
|
│ ├─ edits (optional) — insertRows / removeRows gate structure independently of cell edits; insertGroups / removeGroups require grouping. moveRows: "none" | "within" | "between" and moveGroups say what a grip may move. Keyed top-level creation uses key order; positional insertion uses gutter buttons, the selection strip or Alt+Insert / Alt+Shift+Insert
|
|
1691
1991
|
│ ├─ onSelect / selection (optional) — the ring reported as { rowId, line, key }; give `selection` and the ring is CONTROLLED (follows the value, scrolls into view, every move still reports)
|
|
1692
1992
|
│ ├─ newRowId (optional) — fn() => String minting inserted rows' ids (else the renderer mints one); over a record keyed by a non-String key, the new row's key as its `.east` text — required there unless `edits` inserts no row
|
|
1693
1993
|
│ ├─ readOnly / blanks / density (optional) — the whole sheet read-only · padding rows below the last real one (default 18; typing into one INSERTS a row after the last real one — blanks are padding, never rows) · row rhythm
|
|
1694
|
-
│ ├─ footer (optional) — [{ text, tone? }] counts; a paged source adds the
|
|
1994
|
+
│ ├─ footer (optional) — [{ text, tone? }] counts; a paged source adds the one loading line (what the view waits for, or why a read failed, with Retry)
|
|
1695
1995
|
│ ├─ style (optional) — { gutterWidth } — the sheet fills the box it is given and scrolls its own rows there
|
|
1696
1996
|
│ └─ name (optional) — names the sheet when a surface holds two: its panes' open tab and collapsed state, the columns a viewer hides, its library's drag source and its drop target are kept under it
|
|
1697
1997
|
├─ Panes — optional props, each its own; the examples show every combination (none, a library, an inspector, both):
|
|
1698
1998
|
│ ├─ library (optional) — the start pane's tabs, in order, each a `Sheet.library.*` call: `rows()` — the templates, by their group, each card saying what it sets, each list following its source of the rail — the tab counting what it shows (`Rows 5 of 11`) and, when its sources leave it none, saying so with Clear filters for them; `columns()` — the declared columns, each with its kind and an eye that hides it from the grid per viewer (the last shown stays; a sheet whose library lists no Columns tab hides none); `tab(data, { name, icon?, key, label, meta?, group?, drop?, source? })` — the author's own cards from an Array or a `Dict<String, T>` (its key the accessors' second argument), searched by key, label and meta, following the rail's `source` it names as the Rows tab does; `drop: (row, key) => Sheet.patch(R | G, …)` sets a row's fields where a card lands, or a band's when the patch is over the group type. Left out, or empty, no library pane (SB59). Collapsed, the pane is a rail with the templates' count
|
|
1699
1999
|
│ ├─ templates (optional) — Sheet.templates(templates.read(), { name, group?, values, source }), or a list of them: templates RECORDS (#1345), bound with `Data.bind`, one card per row in the record's key order, each list naming its source of `slice` — a grouped sheet's row templates and group templates two records and two sources, a group template's `values` over the group type; a dropped card is `newRow`'s (`newGroup`'s) defaults with the template's fields over them. A list written in the code, one naming no source of the slice or one it lacks, one naming `rows`, two naming one source, and values over another type are refused, each naming the change
|
|
1700
|
-
│ ├─ inspector (optional) — given alone (`inspector`, `inspector={true}`): the end pane's Details shows the selected row's every field — through its column's kind, or by its type (a field no column shows included) — tinted where it differs from what the source holds, each edit ONE transaction; several rows selected, a column set across them as one step; a band, the group's own fields, Add line, Duplicate and Delete with its lines; Issues lists the batch's issues by row, each going to its cell (over a paged source, sought by its key). `inspector={East.function([R, FunctionType([R], NullType)], UIComponentType, ($, row, update) => …)}` — the author's own Details for a complete row (SB58), passed through untouched and called where Details draws; `update(edited)` is one transaction; a row whose draft is still incomplete shows the form. With nothing selected, Details counts the rows, the pending drafts and the issues, and over a record says the last commit and who made it. Left out, no inspector pane
|
|
2000
|
+
│ ├─ inspector (optional) — given alone (`inspector`, `inspector={true}`): the end pane's Details shows the selected row's every field — through its column's kind, or by its type (a field no column shows included) — tinted where it differs from what the source holds, each edit ONE transaction; several rows selected, a column set across them as one step; a band, the group's own fields, Add line, Duplicate and Delete with its lines; Issues lists the batch's issues by row, each going to its cell (over a paged source, sought by its key) — an issue on a row the `rows` source's narrowing hides naming the choices that hide it, and a click clearing just those and going to its cell, as a pick of the key search does. `inspector={East.function([R, FunctionType([R], NullType)], UIComponentType, ($, row, update) => …)}` — the author's own Details for a complete row (SB58), passed through untouched and called where Details draws; `update(edited)` is one transaction; a row whose draft is still incomplete shows the form. With nothing selected, Details counts the rows, the pending drafts and the issues, and over a record says the last commit and who made it. Left out, no inspector pane
|
|
1701
2001
|
│ └─ fields / groupFields (optional, with `inspector`) — the inspector form's hints by field — `Sheet.field.*`, east-ui's `Fields`: a label, a help line, an editor, read only or hidden; a column's kind is its field's default (SB10). Given without an inspector they are refused
|
|
1702
2002
|
├─ Drag and drop: a template lands on the seam it is dropped on — a row beside a row, a line in the group under the pointer, a group between groups, or, on a keyed source, where its key sorts; an author's card sets its patch's fields on the row or band it lands on; a row's, a line's or a band's grip moves it, as `edits` allows (a keyed source's rows and groups have no grip). The ghost's caption says where, and turns red where a drop is refused; ⏎ on a card does what a drop below the ring would; each drop is one transaction. On a coarse pointer a frame too narrow for the touch gutter beside the first column folds it (#1215): each row's actions go into one 44px row-actions button — the row's grip and its menu
|
|
1703
2003
|
├─ Keyboard (B§6): arrows / ⇧arrows (↓ on the last row appends — on a flat sheet, once a paged source is exhausted) · ⇥ / ⇧⇥ walk the copilot's fills, then take rows, then move · ⏎ takes the next suggestion, else edits with the value selected (F2 too); a printable key seeds a fresh edit · ⌘⏎ fills the row (one undo step), ⌘⇧⏎ takes everything · esc ladder, one rung per press: editor → chip selection → selected proposal → row fill → every suggestion → range → dirty tab revert → the whole sheet · ⌫ clears cells (never a stamped one) or deletes whole selected rows · ⌘C / ⌘V round-trip with Excel (a link cell as two columns) · ⌘/ and ⌘F reach the key search — its box, or, folded to its icon, the box in its popover, opened (#1221) — else the row's first search box. In a link editor: `,` resolves a member, `>` hops From → To, ⇥ takes the ghost → a predicted chip → hops → commits right, ⌫ pops the last chip
|
|
1704
|
-
├─ Localization (#861): every word the sheet says itself — the toolbar and the view tabs, the header, the rows
|
|
2004
|
+
├─ Localization (#861): every word the sheet says itself — the toolbar and the view tabs, the header, the rows and bands, the strip and the editor, the footer and the history item, the banners, the panes, the insertion chips, and the message each gesture leaves — comes from ONE typed message table, its counts in the LOCALE react-aria's `<I18nProvider locale="de-DE">` sets (the browser's language otherwise). Host React code overrides any subset of the words for a subtree with `<SheetMessagesProvider messages={GERMAN}>` (@elaraai/e3-ui-components; `sheetMessages` is the English table — each message a function of named, already-formatted parameters, plus the raw `n` for plurals; define the overrides ONCE, not per render — providers nest). The message a gesture leaves is kept as data and worded as it shows, so a new table re-words it. What the AUTHOR wrote is data and never translated: headers and subs, a group's `noun`, register labels and metas, lock tags, footer items, library tab names; the date and link GRAMMARS keep their forms (day-first dates, `TBC`, `N x kind`), and the issues a patch event carries to the host stay in English (the sheet shows them in its words)
|
|
1705
2005
|
└─ Factories:
|
|
1706
2006
|
├─ Sheet.column.text / date / quantity / integer / lookup / reference / enum / set / link / stamped / custom(R, …) — the column builders (see `columns`); Sheet.driver(column, rows, accessors) — the driver; Sheet.register.members(rows, accessors) / Sheet.register.concat([…]) — registers
|
|
1707
2007
|
├─ Sheet.link.parse(text, members) / Sheet.link.print(link) — the link grammar as East functions (`R2140, Bay 2 > 4 x CNC router`: codes and aliases, ranges `R2140-45`, counted `N x kind`, `TBC`, free text kept as a `text` member — never a refusal) · Sheet.link.arity(half, implied) — how many members a half should hold, `implied: fn(Context(R, D)) => Option<Sheet.Types.Counted>` (the strip reads "n × kind implied · k named") · Sheet.link.check.exists() and author checks fn(Sheet.Types.CheckContext(R)) => Option<String> — a `some(message)` FLAGS the member (warn chip + title), never blocks
|
|
@@ -1709,6 +2009,7 @@ import { Record, Sheet } from '@elaraai/e3-ui';
|
|
|
1709
2009
|
├─ Sheet.subRows(R, sources) / Sheet.subRow({ code?, name, chips?, facets?, id? }) — sub rows (see `subRows`); a left-out `code` / `id` is "", `chips` / `facets` []
|
|
1710
2010
|
├─ Sheet.library.rows() / columns() / tab(data, config) — the library's tabs; Sheet.templates(rows, { name, group?, values, source }) — a templates record's cards for the Rows tab (#1345); Sheet.field.* — the inspector's hints (east-ui's `Fields`)
|
|
1711
2011
|
├─ Sheet.apply(E, idField) — the checked batch applier as an East function: fn(entries, Sheet.Types.ChangeSet(E), revision: Option<String>) => Sheet.Types.Applied(E) — the base (snapshot or revision) is checked first, then the whole batch applies or none of it does (`conflict` says why); `E` is the row struct or a Sheet.Types.Entry(G, "rows") union of groups and ungrouped rows. Request deduplication stays with the host's `onApply`. It IS east-ui's shared `Editing.apply` (#879); a keyed Dict source applies with `Editing.apply(DictType(K, E))` — entries addressed by key, placed `keyOrder`
|
|
2012
|
+
├─ Sheet.Types.OptimiseRequest(R, K?) / Sheet.Types.OptimiseResult(R, K?) — what the app's optimiser is asked and answers (#1409): `{ run, rows }`, the rows in view by key as the session holds them; `{ rows }`, the rows complete, a new row's key the optimiser's own
|
|
1712
2013
|
└─ Sheet.Types.DraftContext(R, D) / Draft(R) / Fill(T) / Patch(R) / Proposal(R) / PatchEvent(E) / ChangeSet(E) / Applied(E) / Entry(G, "rows") / ApplyResult / Readiness / CheckContext(R) — typed contracts for providers, drafts, checked application and checks; grouped contexts use (G, "rows", D). The transaction and draft types are east-ui's shared editing contract's (`Editing.Types.*`, #879) under their Sheet names — the same values. Sheet.Types.Link / Member / Cell / Row / Line / SubRow / Facet / DateLevel / Noun / RecordView / Selection / Counted / Sides / RegisterMember are shared value and wire types.
|
|
1713
2014
|
```
|
|
1714
2015
|
|
|
@@ -1740,9 +2041,11 @@ read and shaped before the sheet sees them — commit through `Record.onApply`
|
|
|
1740
2041
|
over that entry's rows:
|
|
1741
2042
|
|
|
1742
2043
|
```tsx
|
|
1743
|
-
// Package side: export const plans = e3.record("plans", DictType(StringType, Plan),
|
|
1744
|
-
//
|
|
1745
|
-
|
|
2044
|
+
// Package side: export const plans = e3.record("plans", DictType(StringType, Plan), {
|
|
2045
|
+
// initial: new Map([["week", { jobs: [] }]]),
|
|
2046
|
+
// mutations: (mutation) => ({ patch: mutation.patch() }),
|
|
2047
|
+
// });
|
|
2048
|
+
const record = $.let(Record.bind(plans));
|
|
1746
2049
|
const jobs = $.let(record.read().get("week").jobs);
|
|
1747
2050
|
const onApply = $.const(Record.onApply(record, {
|
|
1748
2051
|
entry: "week",
|
|
@@ -1810,13 +2113,14 @@ the row projection, so they follow the host's rows, not an open edit.
|
|
|
1810
2113
|
| Signature | Description | Example |
|
|
1811
2114
|
| --- | --- | --- |
|
|
1812
2115
|
| **The rows** | | |
|
|
1813
|
-
| `record: Record.bind(r
|
|
2116
|
+
| `record: Record.bind(r)` **❗** | A `Dict` record bound with its patch door; its entries are the rows, in key order. | `sheetBasic` |
|
|
1814
2117
|
| `window: Data.bindPaged(r)` **❗** | The same record's entries, a window at a time; never with `entry`. | `sheetPaged` |
|
|
1815
2118
|
| `entry: { key, rows: F, id: I }` **❗** | One entry's Array field `F`, in its own order; `I` a `String` field of its rows. | `sheetWeeks` |
|
|
1816
2119
|
| `entry` + `group: Sheet.group(P, "lines", …)` | `F` holds groups, or `Sheet.Types.Entry(P, "lines")` entries, loose rows between the groups. | `sheetBatches`, `sheetLoose` |
|
|
1817
2120
|
| `group` over the record's entries | Each entry a group, its lines its Array field. | `sheetWorkshop` |
|
|
1818
2121
|
| `data` + `id` (+ `onApply` / `onUpdate`) | The host's rows: an array, a bind handle or a paged source. | `sheetVariants` |
|
|
1819
|
-
| `slice={{ slice: Slice.bindRecords(key, { rows, … }) }}` + `onApply={Record.onApply(record, { keyed: true })}` **❗** | The
|
|
2122
|
+
| `slice={{ slice: Slice.bindRecords(key, { rows, … }) }}` + `onApply={Record.onApply(record, { keyed: true })}` **❗** | The `rows` source's record's rows, in the record's order, each row's id its record key (#1345); `record`, `data`, `window` and `entry` beside it are refused. | `sheetWorkshop`, `sheetStress`, `sheetRecordIndexes` |
|
|
2123
|
+
| the lens: the `rows` source narrowed **❗** | Its narrowing drawn over the record's rows (#1349), a filtered spreadsheet: only the rows it keeps, one after another, each at its row number, and the count line, its matches — e3 placing them from the index, the lens reading only the segments near the view. | `sheetRecordIndexes` |
|
|
1820
2124
|
| `record` with `data`, or neither; `id` / `onApply` / `onUpdate` over a record; `entry` / `window` with `data` **❗** | Refused, naming the prop and the fix. | — |
|
|
1821
2125
|
| **The panes** | | |
|
|
1822
2126
|
| `library: [Sheet.library.rows(), Sheet.library.columns(), Sheet.library.tab(data, { name, key, label, drop?, … })]` **❗** | The start pane's tabs, in order; none, no pane. A tab's `drop` patch over the row type lands on rows, over the group type on bands. | `sheetLibrary`, `sheetWorkshop` |
|
|
@@ -1825,7 +2129,7 @@ the row projection, so they follow the host's rows, not an open edit.
|
|
|
1825
2129
|
| `inspector: fn(R, fn(R) => Null) => UIComponentType` **❗** | The author's own Details for a complete row; a row still missing a field shows the form. | `sheetWeeks` |
|
|
1826
2130
|
| `fields: { f: Sheet.field.readonly() \| Sheet.field.hidden() \| … }` / `groupFields` **❗** | The inspector form's hints, by field; refused without an inspector. | `sheetWorkshop`, `sheetBatches` |
|
|
1827
2131
|
| **The frame** | | |
|
|
1828
|
-
| the toolbar | Every control the sheet has, one row at every width: the rail folds first, then its summary, then the sheet's own steps, then the key search into its icon and the view tabs into one chip (#1221) — with a key search, last into its popover (#1345); the history item folds last. ⌘F reaches the key search in either form. | `sheetWorkshop`, `sheetPaged` |
|
|
2132
|
+
| the toolbar | Every control the sheet has, one row at every width: the rail folds first, then its summary, then the sheet's own steps, then the key search into its icon and the view tabs into one chip (#1221) — with a key search, last into its popover (#1345); the history item folds last, and below that, at a phone's width, the rail's icon drops its caret, the history and Save staying in the row. ⌘F reaches the key search in either form. | `sheetWorkshop`, `sheetPaged` |
|
|
1829
2133
|
| `views: State.bind([ArrayType(Sheet.Types.RecordView)], key, [])` **❗** | The saved views, each every source's narrowing by name (#1345), as a bind handle; `onViewsChange` is refused. | `sheetWorkshop`, `sheetStress` |
|
|
1830
2134
|
| `name: String` | Names the sheet when a surface holds two; its panes' state, the columns hidden, its drag source and its drop target follow it. | every example but `sheetBasic` |
|
|
1831
2135
|
|
|
@@ -1838,8 +2142,16 @@ Removed with #1345: the bare `slice={slice}`, `data={Slice.rows(slice)}` and
|
|
|
1838
2142
|
`affordances` and `persist` outside `slice`, `Slice.bind` and a single
|
|
1839
2143
|
`Slice.bindRecord` (`Slice.bindRecords`), `Sheet.Types.View`
|
|
1840
2144
|
(`Sheet.Types.RecordView`) and templates written in the code (`Sheet.templates`
|
|
1841
|
-
over a record) — each refused at build, naming the change.
|
|
1842
|
-
|
|
2145
|
+
over a record) — each refused at build, naming the change.
|
|
2146
|
+
|
|
2147
|
+
Changed with #1349: the `rows` source's rows are its record's, every one in the
|
|
2148
|
+
record's order, the source's narrowing drawn over them as the lens — where they
|
|
2149
|
+
were the narrowing's rows alone, in its index's order. The lens is a filtered
|
|
2150
|
+
spreadsheet: it draws only the rows the narrowing keeps, each at its row
|
|
2151
|
+
number, with no rows beside them, no gaps and no context switch, and a saved
|
|
2152
|
+
view keeps no `context` and no `reveals` — `Sheet.Types.Reveal` is gone. An
|
|
2153
|
+
application's own record of saved views changes type with it: a view saved
|
|
2154
|
+
before reads no more, and resets.
|
|
1843
2155
|
|
|
1844
2156
|
### The Flowchart — `<Flowchart>`
|
|
1845
2157
|
|
|
@@ -1848,10 +2160,9 @@ phase lanes — its layout derived from the lanes and the links, never from
|
|
|
1848
2160
|
coordinates — H/V-routed transition arrows, optional per-link decision
|
|
1849
2161
|
triggers (lettered diamonds) and evidence-weighted strokes. The dim-ladder
|
|
1850
2162
|
highlight and the selection grammar are built in, and what is selected shows
|
|
1851
|
-
in its inspector. Where
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
process flow.
|
|
2163
|
+
in its inspector. Where the Schematic (below) places items at world
|
|
2164
|
+
coordinates, the Flowchart lays its states out itself, for reading a process
|
|
2165
|
+
flow.
|
|
1855
2166
|
|
|
1856
2167
|
There is one Flowchart, and it renders in its builder frame wherever it is
|
|
1857
2168
|
used (#1245): one toolbar — find state, LR · TD, the freshness chip, the rail
|
|
@@ -1870,9 +2181,9 @@ draws no border: give it a box of its own height.
|
|
|
1870
2181
|
Its flows come from one of two sources:
|
|
1871
2182
|
|
|
1872
2183
|
- **`record`** — an e3 record of `Flowchart.Types.Flows`, flows by name, bound
|
|
1873
|
-
with its patch mutation: `Record.bind(r
|
|
2184
|
+
with its patch mutation: `Record.bind(r)`. The usual
|
|
1874
2185
|
case: the canvas shows the flow opened last, else `flow`, else the first flow
|
|
1875
|
-
by name. A record always holds flows by name —
|
|
2186
|
+
by name. A record always holds flows by name — a patch mutation writes
|
|
1876
2187
|
only keyed records — so a lone flow is a record of one entry, or the host's
|
|
1877
2188
|
`data`; a record of one flow is refused at build.
|
|
1878
2189
|
- **`data`** — the host's flows by name or one flow, the value's type picking
|
|
@@ -1923,24 +2234,28 @@ import { Flowchart, Record } from '@elaraai/e3-ui';
|
|
|
1923
2234
|
import e3 from '@elaraai/e3';
|
|
1924
2235
|
|
|
1925
2236
|
// The depot's flows, by name: each written as a literal, checked when the package builds.
|
|
1926
|
-
export const depotFlows = e3.record("depot_flows", Flowchart.Types.Flows,
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
}
|
|
1939
|
-
|
|
2237
|
+
export const depotFlows = e3.record("depot_flows", Flowchart.Types.Flows, {
|
|
2238
|
+
initial: Flowchart.values({
|
|
2239
|
+
"Inbound parcels": {
|
|
2240
|
+
description: "From the trailer to the van",
|
|
2241
|
+
lanes: [{ key: "intake", label: "Intake" }, { key: "sort", label: "Sort" }, { key: "load", label: "Load" }],
|
|
2242
|
+
states: [
|
|
2243
|
+
{ key: "ARV", label: "Arrived", lane: "intake" },
|
|
2244
|
+
{ key: "CH*", label: "Sort chutes", lane: "sort", members: 12n },
|
|
2245
|
+
{ key: "LDD", label: "Loaded", lane: "load" },
|
|
2246
|
+
],
|
|
2247
|
+
links: [{ from: "ARV", to: "CH*", trigger: "route" }, { from: "CH*", to: "LDD" }],
|
|
2248
|
+
triggers: [{ key: "route", label: "route", owner: "sort-planner" }],
|
|
2249
|
+
},
|
|
2250
|
+
}),
|
|
2251
|
+
mutations: (mutation) => ({
|
|
2252
|
+
patch: mutation.patch(),
|
|
2253
|
+
}),
|
|
2254
|
+
});
|
|
1940
2255
|
|
|
1941
2256
|
const flowchart = East.function([], UIComponentType, _$ => (
|
|
1942
2257
|
<Reactive>{$ => {
|
|
1943
|
-
const flows = $.let(Record.bind(depotFlows
|
|
2258
|
+
const flows = $.let(Record.bind(depotFlows));
|
|
1944
2259
|
return (
|
|
1945
2260
|
<Box height="480px">
|
|
1946
2261
|
<Flowchart record={flows} flow="Inbound parcels" library={[Flowchart.library.flows()]} />
|
|
@@ -2002,7 +2317,7 @@ the same request id again, and e3 answers it with the first write's commit
|
|
|
2002
2317
|
rather than writing twice (#1275):
|
|
2003
2318
|
|
|
2004
2319
|
```tsx
|
|
2005
|
-
const flows = $.let(Record.bind(depotFlows
|
|
2320
|
+
const flows = $.let(Record.bind(depotFlows));
|
|
2006
2321
|
const onApply = $.const(East.asyncFunction([StringType, PatchType(Flowchart.Types.Flows)], Editing.Types.ApplyResult, ($2, requestId, patch) => {
|
|
2007
2322
|
const outcome = $2.let(flows.commit.patch(requestId, patch));
|
|
2008
2323
|
const answer = $2.let(variant("rejected", [{ entry: "", row: none, field: none, message: "The write was refused" }]), Editing.Types.ApplyResult);
|
|
@@ -2018,9 +2333,9 @@ const onApply = $.const(East.asyncFunction([StringType, PatchType(Flowchart.Type
|
|
|
2018
2333
|
|
|
2019
2334
|
```
|
|
2020
2335
|
<Flowchart record={…} | data={…} /> — state-transition flowchart in its builder frame: states as nodes in ORDERED phase lanes (layout derived — no coordinates), H/V-routed transition arrows, optional per-link decision triggers (lettered diamonds); dim-ladder highlight built in; the inspector, on by default, shows what is selected
|
|
2021
|
-
├─ The frame (#1245): ONE toolbar holding every control the flowchart has — find state (a search over the open flow's states by key and by label; a pick selects the state and scrolls it into view), LR · TD, the freshness chip, at its end the rail of its one `Slice.bindRecords` and its summary, naming each source a narrowing hides rows of in its own words (#1346, #1358), and where it edits the history item (#1246, #1247: the open flow's session — its status, its issues, Undo, Redo, Discard and Save). It is one row at every width: the rail folds first, its summary after every step of it, then the freshness chip goes, LR · TD folds into one chip naming the orientation (its menu both) and find state's box into its icon (which opens the box in a popover, the focus in it) — with a rail, LR · TD then folds into that popover, its View, one move, so a phone's row keeps the rail and Save — and the history item last, to its buttons; on a touch screen every control there is a 44px tap target. The banners — the open flow's session's: a Save's conflict or refusal, an unknown outcome and a failed confirmation read, each with Retry, and the source changed under its drafts, with Discard; a source of the rail whose narrowing could not be read, with Retry; and a `flow` the flowchart doesn't hold, named with the flow shown in its place (`Gone isn't a flow here — showing Inbound parcels`) until the viewer opens a flow or the record gains that name. Main — the canvas, filling it and scrolling both ways inside it, its lanes running main's whole height; over flows by name with none — an empty record — the shared empty state, with "+ New flow" where it edits. The footer — the counts: over many flows the open flow's name first, the transitions and their planned · observed · unresolved split, where it edits the changes waiting on Save (`3 pending` — each lane, state, transition and decision the drafts add, change or remove), and over a record its last save. The start pane is `library`'s, an optional prop; the end pane is the inspector, on by default. The flowchart fills its parent and draws no border: give it a box of its own height
|
|
2336
|
+
├─ The frame (#1245): ONE toolbar holding every control the flowchart has — find state (a search over the open flow's states by key and by label; a pick selects the state and scrolls it into view), LR · TD, the freshness chip, at its end the rail of its one `Slice.bindRecords` and its summary, naming each source a narrowing hides rows of in its own words (#1346, #1358), and where it edits the history item (#1246, #1247: the open flow's session — its status, its issues, Undo, Redo, Discard and Save). It is one row at every width: the rail folds first, its summary after every step of it, then the freshness chip goes, LR · TD folds into one chip naming the orientation (its menu both) and find state's box into its icon (which opens the box in a popover, the focus in it) — with a rail, LR · TD then folds into that popover, its View, one move, so a phone's row keeps the rail and Save — and the history item last, to its buttons, and below that, where the row is still too wide, the rail's icon drops its caret, the history and Save staying in the row; on a touch screen every control there is a 44px tap target. The banners — the open flow's session's: a Save's conflict or refusal, an unknown outcome and a failed confirmation read, each with Retry, and the source changed under its drafts, with Discard; a source of the rail whose narrowing could not be read, with Retry; and a `flow` the flowchart doesn't hold, named with the flow shown in its place (`Gone isn't a flow here — showing Inbound parcels`) until the viewer opens a flow or the record gains that name. Main — the canvas, filling it and scrolling both ways inside it, its lanes running main's whole height; over flows by name with none — an empty record — the shared empty state, with "+ New flow" where it edits. The footer — the counts: over many flows the open flow's name first, the transitions and their planned · observed · unresolved split, where it edits the changes waiting on Save (`3 pending` — each lane, state, transition and decision the drafts add, change or remove), and over a record its last save. The start pane is `library`'s, an optional prop; the end pane is the inspector, on by default. The flowchart fills its parent and draws no border: give it a box of its own height
|
|
2022
2337
|
├─ Its flows — ONE of two sources (both, or neither, is refused):
|
|
2023
|
-
│ ├─ record — Record.bind(r
|
|
2338
|
+
│ ├─ record — Record.bind(r): r of Flowchart.Types.Flows, flows by name (the canvas opens the flow the viewer opened last, kept in the UI store under `name`; else `flow`; else the first by name). Each flow keeps its own session (#1246): its drafts stay while another flow is open, and Save sends the open flow's as ONE patch through the record's patch mutation — `Record.onApply(record, { keyed: true })`, the flow's insert, update or delete by name; the drafts retire once the record reads it back. A record always holds flows by name — e3's patch mutation writes only keyed records — so a lone flow is a record of one entry, or `data`; a record of one flow is refused
|
|
2024
2339
|
│ └─ data — a value, an expression or a bind handle of Flowchart.Types.Flows (+ `flow`) or Flowchart.Types.Flow, the value's type picking the arm; read only unless given onApply (async, (String, PatchType(the value's type)) → Editing.Types.ApplyResult — `East.asyncFunction([StringType, PatchType(T)], Editing.Types.ApplyResult, ($, requestId, patch) => …)`), the host's commit: Save hands it the session's request id — the same again on a Retry, so a host that keys its write by it (`commit.patch(requestId, patch)`) writes once (#1275) — and one patch of the value — over flows by name the open flow's insert, update or delete by name, never the whole value replaced; over one flow the flow's own patch — and the drafts retire once the host's value holds what it was handed
|
|
2025
2340
|
├─ Flowchart.over(states, { state?, links, link?, lanes, lane?, triggers?, trigger? }) — one Flowchart.Types.Flow from the app's tables:
|
|
2026
2341
|
│ ├─ states + links + lanes (required) — the three tables (lanes accept a literal [{ key, label? }] array; array order = band order)
|
|
@@ -2105,6 +2420,565 @@ Removed with #1346: `window={Slice.rows(slice)}` (the `flows` source of
|
|
|
2105
2420
|
read from an Array or an input (templates records) and a templates list's
|
|
2106
2421
|
`key` — each refused at build, naming the change.
|
|
2107
2422
|
|
|
2423
|
+
### The Schematic — `<Schematic>`
|
|
2424
|
+
|
|
2425
|
+
`<Schematic>` is the 2D world-coordinate drawing of the app's own records:
|
|
2426
|
+
placed items (racks, cooling units, power distribution units), zones (halls,
|
|
2427
|
+
cages, hot and cold aisles), links between two items (fibre runs) and nets
|
|
2428
|
+
joining many to many (a power distribution unit feeding a row of racks), each
|
|
2429
|
+
where its row places it and the canvas scaled to fit the extent. Where the
|
|
2430
|
+
Flowchart lays its states out itself, the Schematic draws what the records
|
|
2431
|
+
place.
|
|
2432
|
+
|
|
2433
|
+
It stores nothing in a shape of its own (#1386): each kind is a record of the
|
|
2434
|
+
app's, keyed by String, its fields mapped to the Schematic's roles —
|
|
2435
|
+
`Schematic.items`, `zones`, `links` and `nets`, each under a slot of its
|
|
2436
|
+
sort's prop. A role names a field of the row: its type is checked when the
|
|
2437
|
+
code compiles and again at build, refused naming the role, the field and its
|
|
2438
|
+
type (`` `x` names a Float field, or an Option of one — "name" holds .String ``).
|
|
2439
|
+
A role the app computes — a second line, a meter, a tone — is an accessor
|
|
2440
|
+
`(row, key) => …`, reified once and called per row. A shape is one too
|
|
2441
|
+
(#1401): a footprint, or a zone's geometry, is built from the row's own fields
|
|
2442
|
+
with `Schematic.circle`, `polygon`, `polyline` or `rect`, so no field of the
|
|
2443
|
+
app's record holds a type of the Schematic's — a shape role naming a field is
|
|
2444
|
+
refused, naming the accessor to write. The layers are the app's rows as well,
|
|
2445
|
+
read through `Schematic.layers` as the Roster reads its lists, an entity's
|
|
2446
|
+
`layer` field naming one by its record's key. A gesture writes only the
|
|
2447
|
+
fields its roles map: every other field of the row reads back as it was.
|
|
2448
|
+
|
|
2449
|
+
A kind's binding says whether it is editable and how it is read; any other
|
|
2450
|
+
binding is refused, naming the one to use:
|
|
2451
|
+
|
|
2452
|
+
| Read | Editable | Read only |
|
|
2453
|
+
| --- | --- | --- |
|
|
2454
|
+
| whole | `Record.bind(record)` | `Data.bind(record)` |
|
|
2455
|
+
| complete, through its source of the slice | `Record.bind(…)` + `entries: Data.bindPaged(record)` | `Data.bindPaged(record)` |
|
|
2456
|
+
| a window at a time, by area — its source lists an area index | the same, its source `Data.bindPaged(record, { indexes: ["by_area", …] })` | the same |
|
|
2457
|
+
|
|
2458
|
+
A source of `slice={{ slice: Slice.bindRecords(key, { … }) }}` named after a
|
|
2459
|
+
kind's slot reads that kind through it — `Data.bindPaged(record, { indexes })`
|
|
2460
|
+
over the kind's record — every row of its narrowing; an indexed read is the
|
|
2461
|
+
source, never the kind's record. A kind's details (`Schematic.details`) are
|
|
2462
|
+
bound as their kind is read: whole beside a kind read whole, by key beside one
|
|
2463
|
+
read through its source — and beside one read by area, by the runs its
|
|
2464
|
+
window's cells are of the area index, e3 reading the keys from the index so
|
|
2465
|
+
none is sent, each cell's once, at every zoom, so a dot draws its item's
|
|
2466
|
+
status as a card does. Bound with their patch door —
|
|
2467
|
+
`Record.bind(record)`, with `entries:
|
|
2468
|
+
Data.bindPaged(record)` beside a kind read through its source — the inspector
|
|
2469
|
+
edits them beside their kind's row; `Data.bind` or `Data.bindPaged` alone
|
|
2470
|
+
reads them only.
|
|
2471
|
+
|
|
2472
|
+
A kind whose source lists an area index — `index.area({ x, y, width, height,
|
|
2473
|
+
at: "centre", cell })` among its record's indexes, the **e3** skill — is read by
|
|
2474
|
+
area (#1393), a window at a time: the rows whose box meets the view and a
|
|
2475
|
+
margin about it, read from the index alone. Past 5,000 rows in the view it
|
|
2476
|
+
draws their density from the index's counts, in the theme's heat steps, the
|
|
2477
|
+
footer saying how many it shades; zoomed in, the rows themselves, their
|
|
2478
|
+
details read by the runs the window's cells are of the area index. A choice
|
|
2479
|
+
of the rail reads through an area index led by its fields, else through the
|
|
2480
|
+
one led by none, the rest asked of the window's rows in one request; a source
|
|
2481
|
+
whose area indexes are all led by fields, or box the rows otherwise than the
|
|
2482
|
+
kind's roles place them, is refused as the kind is read: its read fails, with
|
|
2483
|
+
Retry, the reason naming what to declare. Its
|
|
2484
|
+
Backlog reads the index's rows with no position, and the Index tab a zone's
|
|
2485
|
+
items by the zone's box, each with the drafts in place wherever the window
|
|
2486
|
+
goes: a row a draft unplaces is in the Backlog, and one a draft moves is under
|
|
2487
|
+
the zone it now stands in. Every edit is a draft by the row's key, whatever
|
|
2488
|
+
the window: a row moved off the window it was read in draws where its draft
|
|
2489
|
+
places it while the narrowing holds its stored row, and Save writes it
|
|
2490
|
+
through the kind's patch door — the index moving it, so a window there reads
|
|
2491
|
+
it back (`schematicRecordIndexes`, `schematicRecordIndexesMobile`). A selected
|
|
2492
|
+
row stays selected as the window moves off it — Details reads it by its key,
|
|
2493
|
+
Del deletes it, and `onSelect` hears no camera move — and is let go of only as
|
|
2494
|
+
a read shows it gone (its record holds none of it, it has no position, or the
|
|
2495
|
+
narrowing leaves it out), as a draft deletes or unplaces it, or as a layer
|
|
2496
|
+
hides or locks it. The drawing's counts take such a kind from its source,
|
|
2497
|
+
never its window, and Issues checks the rows read around the view and every
|
|
2498
|
+
unsaved change (below).
|
|
2499
|
+
|
|
2500
|
+
The canvas draws each kind's rows: an item at its centre, as a card, a
|
|
2501
|
+
labelled pin or a dot by the zoom, its footprint about its centre; a zone in
|
|
2502
|
+
its box, outlined or hatched, its shape about the box's centre; a link routed
|
|
2503
|
+
between its ends, two of one pair fanning into lanes; a net as a bus. An item
|
|
2504
|
+
draws as its card only where the card draws whole — one held to the item's
|
|
2505
|
+
`width` once that width holds its words — and clear of its neighbours, else as
|
|
2506
|
+
a pin, its dot on the item and its words after it, clear of every other
|
|
2507
|
+
item's mark, else as a dot (ruled 23 Oct 2026): what an item draws as follows
|
|
2508
|
+
where the items stand, never a pan. Fit frames the drawing inside a margin,
|
|
2509
|
+
centred, and the zoom reaches in until its smallest item held to its own
|
|
2510
|
+
width draws as its card. A kind drawn as its density shades it in the theme's
|
|
2511
|
+
five heat steps. Its links and
|
|
2512
|
+
nets draw in the Plan's and the Allocation's links look (#1403): cased in the
|
|
2513
|
+
paper, in the muted ink or their style's tone, each ending on the edges of the
|
|
2514
|
+
items it joins where they draw at the zoom — a card's side, a labelled pin's
|
|
2515
|
+
edge, a dot's rim — a link's head where it arrives at its `to` item and a
|
|
2516
|
+
net's on each of its destination legs, a caption — its `label` and its
|
|
2517
|
+
`metric`, `uplink · 100G` — on a knockout at its middle from the label zoom;
|
|
2518
|
+
the one under the pointer lit, and a selected one, in the strong ink, their
|
|
2519
|
+
ends ringed. An item kind
|
|
2520
|
+
whose `x` and `y` are Options keeps its rows with no position as its backlog,
|
|
2521
|
+
drawn nowhere. A record another session commits is drawn again. `onSelect`
|
|
2522
|
+
hears the whole selection after each change — items, zones, links and nets
|
|
2523
|
+
alike, each a `Schematic.Types.Ref`, `{ kind, key }`, and `[]` once it is
|
|
2524
|
+
cleared — one call a change, however many entities it selects, and none for a
|
|
2525
|
+
change that leaves the selection as it was. Its frame came with #1387: one
|
|
2526
|
+
toolbar — the rail of its `slice`, the tools, the zoom and Fit, Find, the
|
|
2527
|
+
steps and, where it edits, the history item — over the canvas, and the
|
|
2528
|
+
footer, which says only what is live (below); its library pane with #1389, its
|
|
2529
|
+
inspector with #1391, its
|
|
2530
|
+
narrowing with #1392 and Find over every item kind's record with #1404
|
|
2531
|
+
(below).
|
|
2532
|
+
|
|
2533
|
+
Its narrowing (#1392) is the one rail of its `slice`, each source narrowing
|
|
2534
|
+
its kind wherever the kind appears: on the canvas, in the library's Backlog,
|
|
2535
|
+
its Index and the templates the source names, in the inspector's Choosers and
|
|
2536
|
+
what an item is joined to, and in the drawing's counts; a link or a net draws
|
|
2537
|
+
only while its ends do. By default a row the narrowing leaves out is hidden:
|
|
2538
|
+
its kind reads only what its narrowing holds. With `narrowed="dim"`, or
|
|
2539
|
+
`{ show: "dim", … }`, the kind reads every row through its source, never the
|
|
2540
|
+
narrowing, so a choice reads no row again, and a row the narrowing leaves out
|
|
2541
|
+
draws as context — faded to `opacity` (0.35), drained of colour while
|
|
2542
|
+
`desaturate` (on by default), a bare marker with `dot` — as do a link to one
|
|
2543
|
+
and a net whose members on a side are all context. Context takes no tap, no
|
|
2544
|
+
marquee, no step and no drop, and leaves every list and count but Find's.
|
|
2545
|
+
Whatever `show` is, `emphasis` rings each match — `halo`, or `pulse`, which
|
|
2546
|
+
breathes — `frame` boxes the matches, and `fit` flies the camera to them when
|
|
2547
|
+
the narrowing changes, once what it flies to is read, never as a zoom or a
|
|
2548
|
+
Move changes what matches — to a kind read by area's matches by the box its
|
|
2549
|
+
narrowing's rows stand in, as its area index's counts answer it. Which rows
|
|
2550
|
+
match is each source's shown keys, never a predicate over the rows, and a
|
|
2551
|
+
choice builds the Schematic no more.
|
|
2552
|
+
|
|
2553
|
+
Find (#1404) seeks every item kind's record by key, never what the canvas
|
|
2554
|
+
draws, as the Sheet's Search Records seeks its record: a kind read through
|
|
2555
|
+
its source is sought through e3 in its record's own order — a find of its keys
|
|
2556
|
+
and a page of the rows its list shows, never the record read whole — and a
|
|
2557
|
+
kind read whole in its rows. Its box takes a key's start, a quoted key or a
|
|
2558
|
+
range, keys case-sensitive, and asks again as what its lines say moves — a
|
|
2559
|
+
record, a narrowing, the layers — never as the canvas reads a window. A pick
|
|
2560
|
+
shows the item's key in the box and its place among the matches (`3 of 7`),
|
|
2561
|
+
and Next and Previous step on from it, each showing its match; asked again,
|
|
2562
|
+
the box keeps a match wherever the matches still hold it. Each line reads
|
|
2563
|
+
the item's key, its label where it is not its key, its kind's name, what
|
|
2564
|
+
keeps it off the canvas — `on a hidden layer`, or `no position` — and, where
|
|
2565
|
+
the rail's filters leave it out, the choices that do, as every builder's
|
|
2566
|
+
Search Records names them: `· hidden by Owner: Northwind Media — picking it
|
|
2567
|
+
clears that filter`, asked of e3 through the kind's indexes, `· hidden by the
|
|
2568
|
+
filters` until it has said — and, under `narrowed="dim"`, which draws the item
|
|
2569
|
+
as context, `· dimmed by Owner: Northwind Media — picking it clears that
|
|
2570
|
+
filter` and `· dimmed by the filters`. A pick of such an
|
|
2571
|
+
item clears just those choices — every choice of its kind's source while e3
|
|
2572
|
+
has not said — then goes to it as any pick does, and selects it once the
|
|
2573
|
+
narrowing they leave draws it as itself. A pick flies to an item drawn as
|
|
2574
|
+
itself and selects it; to one on a hidden layer it flies to where it stands,
|
|
2575
|
+
selects nothing, and the footer says why — `A04 is on a hidden layer`; `A06
|
|
2576
|
+
has no position: it is in the Backlog`. In a toolbar's row the box is 28px, as
|
|
2577
|
+
every builder's key search is there.
|
|
2578
|
+
|
|
2579
|
+
The footer says only what is live, and nothing while nothing is (ruled 23 Oct
|
|
2580
|
+
2026): a kind drawn as its density in place of items — a kind read by area
|
|
2581
|
+
(#1393) past the rows a window draws — how many rows it shades in the view and
|
|
2582
|
+
that zooming in draws them (`Density: 20,000 racks — zoom in to draw them`,
|
|
2583
|
+
`about` that many where the view cuts a cell, `the filters not applied` under a
|
|
2584
|
+
narrowing its area index doesn't lead); why the last gesture was refused,
|
|
2585
|
+
until the viewer's next action — a pan, a zoom, Fit or a fly, a Find pick, or
|
|
2586
|
+
a press on the canvas — a resize alone keeping it; each Save's notice of what
|
|
2587
|
+
left a source's narrowing; and where Find's last pick landed off the canvas.
|
|
2588
|
+
The drawing's counts and each record's last save are the inspector's Details',
|
|
2589
|
+
and whether changes wait on Save the history's.
|
|
2590
|
+
|
|
2591
|
+
The library (#1389) is the frame's start pane, 272px open, holding the tabs
|
|
2592
|
+
`library` lists, in its order, each with its count; its open tab and whether
|
|
2593
|
+
it is collapsed are kept per viewer under the Schematic's `id`, collapsed it
|
|
2594
|
+
is a rail counting the backlog (else the first tab's cards), and below 560px
|
|
2595
|
+
of frame it opens from its rail over the canvas:
|
|
2596
|
+
|
|
2597
|
+
- **Templates** — every kind's templates, item, link and net kinds alike, each
|
|
2598
|
+
card its template's name over its kind's, grouped by its kind and then its
|
|
2599
|
+
template's `group`. Each kind's list follows the source its templates record
|
|
2600
|
+
names: the tab counts what it shows (`3 of 5`) and, when the rail leaves a
|
|
2601
|
+
list none, says so above the rest, with Clear filters.
|
|
2602
|
+
- **Backlog** — every item kind's rows with no position, the drafts in place,
|
|
2603
|
+
each card its label, its kind's name and its second line, grouped by its
|
|
2604
|
+
kind. A kind read through its source lists the rows its narrowing holds, and
|
|
2605
|
+
a narrowing that leaves none says so, with Clear filters.
|
|
2606
|
+
- **Index** — the zones nested by containment, each counting the items it
|
|
2607
|
+
holds, its nested zones' among them; an open zone lists its items, then its
|
|
2608
|
+
nested zones, and the items in no zone come last; a hatched zone is an
|
|
2609
|
+
annotation, holding none. A zone's row flies to the zone, selects it and
|
|
2610
|
+
opens it; an item's flies to the item and selects it; the zone the viewport
|
|
2611
|
+
is in is marked. Its search keeps the items whose label, key or kind's name
|
|
2612
|
+
holds it, under their zones. The tree is one tab stop — ↑ ↓ Home End move,
|
|
2613
|
+
→ opens a zone or goes in, ← closes it or goes up, Enter does a row's click
|
|
2614
|
+
— and only the rows in view are mounted.
|
|
2615
|
+
- **Layers** — each layer in its order: its swatch in its colour token, else
|
|
2616
|
+
its tone, its name and how many entities it holds, with Isolate, Lock and
|
|
2617
|
+
Show or Hide, and Show all while a layer is hidden (the layers its author
|
|
2618
|
+
hides at first among them; the locks stay). The viewer's choices are kept
|
|
2619
|
+
under the Schematic's `id`, over the author's defaults: a hidden layer's
|
|
2620
|
+
entities never draw, and a locked one's never select — a tap, the marquee
|
|
2621
|
+
and the steps pass them by, and Find and the Index fly to them and select
|
|
2622
|
+
nothing — and locking a layer lets go of what is selected on it.
|
|
2623
|
+
- **An author's tab** — its cards, each its label and its meta, grouped by its
|
|
2624
|
+
`group`, following the source of the rail it names (`Racks 2 of 7`).
|
|
2625
|
+
|
|
2626
|
+
A card tab's search finds within that tab alone, never writing the rail; a
|
|
2627
|
+
click picks a card and a second lets it go.
|
|
2628
|
+
|
|
2629
|
+
The cards drop on the canvas (#1390) through the shared drag engine, as the
|
|
2630
|
+
Flowchart's do, each drop one step of the history and the ghost's caption
|
|
2631
|
+
saying where the card lands, or why it can't:
|
|
2632
|
+
|
|
2633
|
+
- **An item template** lands at the drop point: a new item, its key minted
|
|
2634
|
+
unique, its `x` and `y` the point's and every other field the template's
|
|
2635
|
+
`values`. The point is written at the screen's resolution, so the caption
|
|
2636
|
+
says what is written, after the zone holding it (`Cage A · 12.00, 1.00 m`).
|
|
2637
|
+
The item is selected.
|
|
2638
|
+
- **A backlog row** lands at the drop point, its `x` and `y` written, and is
|
|
2639
|
+
selected.
|
|
2640
|
+
- **A link or a net template** lands on a link or a net of its kind: its
|
|
2641
|
+
`values` over the row, never its ends, its sides or its key (`onto A03 →
|
|
2642
|
+
ODF-1`).
|
|
2643
|
+
- **An author's card** — a tab whose `drop` is `(row) => Schematic.patch(R, {
|
|
2644
|
+
… })` — lands on an entity of the kind whose row type is `R` and sets those
|
|
2645
|
+
fields (`onto A03`). A tab without `drop` doesn't drag.
|
|
2646
|
+
- **⏎ on a card** drops it on the selection — an author's card on every
|
|
2647
|
+
selected entity of its kind, one step — or a template or a backlog row at
|
|
2648
|
+
the view's centre; on a touch screen, a tap on the selected card does the
|
|
2649
|
+
same and leaves it selected. Refused, the footer says why.
|
|
2650
|
+
|
|
2651
|
+
A drop is refused — the caption red, the cursor not allowed — outside the
|
|
2652
|
+
drawing (`Outside the drawing`), with no entity of the card's kind under it
|
|
2653
|
+
(`Drop onto a rack`), or on a kind bound read only (`Racks are read only`); a
|
|
2654
|
+
locked layer's entities and a hatched zone take no drop, and an item's card
|
|
2655
|
+
covers the zones and links beneath it. The canvas marks where a card lands:
|
|
2656
|
+
the zone holding the point tinted, a link's or a net's line washed, a ring
|
|
2657
|
+
round the marker or the point, and a card in the brand. A drop that changes
|
|
2658
|
+
nothing records nothing. While a link or net template is picked in the
|
|
2659
|
+
library, Connect draws that kind's links with its values.
|
|
2660
|
+
|
|
2661
|
+
Every gesture is one step of one history over a session per record it writes
|
|
2662
|
+
(#1388) — each kind bound with its patch door, and each such kind's details —
|
|
2663
|
+
as the Roster's and the Allocation's are:
|
|
2664
|
+
|
|
2665
|
+
- **Move** — the tool, over an editable item kind — drags the item under the
|
|
2666
|
+
pointer, or the selection with it when the item is selected, by the mouse
|
|
2667
|
+
or a touch, every centre kept within the extent; Esc mid-drag puts it back.
|
|
2668
|
+
- **Connect** — the tool, over an editable link kind, its menu picking the
|
|
2669
|
+
kind (the first, until the viewer picks) — drags from an item onto another:
|
|
2670
|
+
a link of that kind between them. Shift held on the next drag grows them
|
|
2671
|
+
into a net of the first editable net kind whose sides take their items —
|
|
2672
|
+
the first drag's two items one each side, an item joined from a member
|
|
2673
|
+
downstream, one joined into a member upstream, a member never changing side
|
|
2674
|
+
— which absorbs the links the drags drew; with no such kind, each drag adds
|
|
2675
|
+
a link. A Shift-drag from a selected link's end starts the session with
|
|
2676
|
+
that link.
|
|
2677
|
+
- A selected link's **end** drags onto another item, retargeting that end;
|
|
2678
|
+
with Connect, a Shift-drag from a selected net's member onto an item
|
|
2679
|
+
**extends** the net's destinations.
|
|
2680
|
+
- **Del** (or Backspace) deletes the selection: a link or a net; a net's leg,
|
|
2681
|
+
its item taken off that side, the net deleted once a side holds none; and
|
|
2682
|
+
items, with every link and every net leg naming one. Refused, in the
|
|
2683
|
+
footer's words, when a link or a net bound read only names an item it
|
|
2684
|
+
would delete (`A01 isn't deleted: Fibre run u1 names it, and is read only`).
|
|
2685
|
+
|
|
2686
|
+
`canConnect` is asked of each pair a draft would join, as a
|
|
2687
|
+
`Schematic.Types.Candidate` — the slot of the link or net kind the gesture
|
|
2688
|
+
writes, and its two items — and answers `none`, which allows it, or the
|
|
2689
|
+
refusal's words; a pair whose end or side holds no item of that slot is
|
|
2690
|
+
refused the same way (`Fibre run can't end at PDU-B`). A refused draft never
|
|
2691
|
+
snaps: the item rings danger, the draft says why in the drag caption, and
|
|
2692
|
+
letting go records nothing. A gesture writes only the fields its roles map —
|
|
2693
|
+
an item's `x` and `y`, a link's ends, a net's sides — every other field of the
|
|
2694
|
+
row as it was; a new link or net is made under a key minted unique
|
|
2695
|
+
(`crypto.randomUUID()`), its other fields its type's default. The canvas draws
|
|
2696
|
+
the drafts where Save would leave them; a draft of a row the rail's narrowing
|
|
2697
|
+
leaves out waits in its session until the rail lets it back, and a new row
|
|
2698
|
+
draws always. Undo and Redo — the history item, or ⌘Z, ⇧⌘Z and ⌘Y anywhere in
|
|
2699
|
+
the frame but a field being typed into — step through the gestures in the
|
|
2700
|
+
order they were made, whatever their kind; Discard drops every draft; Save
|
|
2701
|
+
sends each record's changes as its own request through its patch door,
|
|
2702
|
+
checked against what its drafts began from: one record's conflict or refusal
|
|
2703
|
+
keeps its drafts under a banner titled with its kind's name, the others'
|
|
2704
|
+
commits standing, and a write with no answer turns Save into Retry, which
|
|
2705
|
+
resends the same request. After a Save the footer says what left each kind's
|
|
2706
|
+
narrowing. A kind bound read only, and every
|
|
2707
|
+
kind under `readOnly`, takes no gesture; with nothing editable there is no
|
|
2708
|
+
history item.
|
|
2709
|
+
|
|
2710
|
+
The inspector (#1391), given `inspector`, is the frame's end pane, 320px open:
|
|
2711
|
+
its tabs Details and Issues, Issues with its count; collapsed, a rail counting
|
|
2712
|
+
the issues and naming what is selected. Whether it is collapsed, and its open
|
|
2713
|
+
tab, are kept per viewer under the Schematic's `id`, and below 560px of frame
|
|
2714
|
+
it opens from its rail over the canvas. Enter, or a double-click on an
|
|
2715
|
+
entity, opens what is selected in Details. Details draws each field through
|
|
2716
|
+
the shared form, by the input its type takes — a field the drafts changed
|
|
2717
|
+
tinted, the head's chip Pending (New for a row its record holds none of):
|
|
2718
|
+
|
|
2719
|
+
- **An item** — its kind's form over the fields no role writes (the kind's
|
|
2720
|
+
`fields`, `Fields` hints over its row), or the kind's own `inspector` in its
|
|
2721
|
+
place; its details record's form, or the details' own; its position, `x`
|
|
2722
|
+
and `y`, with Unplace where the kind keeps a backlog; Delete; and what it is
|
|
2723
|
+
joined to, each link and net a control that selects it.
|
|
2724
|
+
- **A zone** — its form.
|
|
2725
|
+
- **A link** — its ends, each a Chooser over the items of the slots its role
|
|
2726
|
+
names, and its form; Delete.
|
|
2727
|
+
- **A net** — its sides, each tags over its slot's items' keys, a side never
|
|
2728
|
+
emptied (Delete takes the net), and its form; Delete. **A net's leg** — the
|
|
2729
|
+
stub a tap selected — its item, and Remove, which takes it off its side.
|
|
2730
|
+
- **Several** — how many of each kind, and Delete.
|
|
2731
|
+
- **Nothing** — the drawing's counts, and each record's last save with who
|
|
2732
|
+
made it. A kind read by area is counted from its source's indexes, never its
|
|
2733
|
+
window, its drafts laid over the count and a placeholder while it is read;
|
|
2734
|
+
what no read can count is named on a line of its own, with why (`Links not
|
|
2735
|
+
counted: what they join is read as the view moves`).
|
|
2736
|
+
|
|
2737
|
+
Every edit is one step of the history: a field through its record, an own
|
|
2738
|
+
Details' `update(edited)` with the whole row, a position, the ends or the
|
|
2739
|
+
sides through the kind's roles, and Delete as Del. Read only — a kind bound
|
|
2740
|
+
read only, or `readOnly` — every field is printed and none edits, an own
|
|
2741
|
+
Details giving way to the form. Issues lists the drawing's issues — an item
|
|
2742
|
+
outside the drawing, a shape reaching past its box, a colour naming no theme
|
|
2743
|
+
colour, a link or a net naming an item no kind holds — and its sessions': a
|
|
2744
|
+
drafted row the kind's `ready` refuses, which holds Save, and a Save refused
|
|
2745
|
+
or in conflict. Each issue selects what it names and flies to it. Over a kind
|
|
2746
|
+
read by area, Issues checks the rows read around the view and every unsaved
|
|
2747
|
+
change, and one line says the kind's other rows are checked only as the view
|
|
2748
|
+
reaches them (`Racks out of view are checked only as the view reaches them`).
|
|
2749
|
+
|
|
2750
|
+
Below 480px of main (#1394) — a phone, or a narrow pane in a wide window — the
|
|
2751
|
+
canvas pans with one finger, pinches with two and selects with a tap, and
|
|
2752
|
+
nothing drags: the View's tools hold no Move or Connect, no library card
|
|
2753
|
+
drags, the canvas is no drop cell and a selected link draws no handles.
|
|
2754
|
+
Details takes the gestures' place, beside an item's `x` and `y`, a link's
|
|
2755
|
+
ends, a net's sides and Delete:
|
|
2756
|
+
|
|
2757
|
+
- **Connect to…** — on an item, a Chooser of the items a link may end at,
|
|
2758
|
+
beside the link kind while more than one editable kind starts at the item:
|
|
2759
|
+
a choice records the link as a drag of Connect would, a picked template's
|
|
2760
|
+
values with it.
|
|
2761
|
+
- **Place** — a Backlog card picked shows its row in Details, `Not placed`,
|
|
2762
|
+
and Place puts it at the view's centre, selected; a tap on the selected card
|
|
2763
|
+
does the same.
|
|
2764
|
+
|
|
2765
|
+
Every route that connects — Connect to…, a link's ends, a net's sides — is
|
|
2766
|
+
held to `canConnect`, its words said in the footer. The layout follows main's
|
|
2767
|
+
width, not the window's: a box narrowed and widened again keeps its selection
|
|
2768
|
+
and its drafts (`schematicRecordIndexesMobile`; `schematicDataCentre` on a
|
|
2769
|
+
phone).
|
|
2770
|
+
|
|
2771
|
+
```tsx
|
|
2772
|
+
/** @jsxImportSource @elaraai/e3-ui */
|
|
2773
|
+
import { DictType, East, FloatType, StringType, StructType } from '@elaraai/east';
|
|
2774
|
+
import { Box, Reactive, UIComponentType } from '@elaraai/east-ui';
|
|
2775
|
+
import { Data, Schematic } from '@elaraai/e3-ui';
|
|
2776
|
+
import e3 from '@elaraai/e3';
|
|
2777
|
+
|
|
2778
|
+
// The app's racks: a record keyed by String — each rack's centre, box, name, model and power.
|
|
2779
|
+
export const FloorRack = StructType({ name: StringType, x: FloatType, y: FloatType, w: FloatType, h: FloatType, model: StringType, kw: FloatType, capacity: FloatType });
|
|
2780
|
+
export const floorRacks = e3.record("floor_racks", DictType(StringType, FloorRack), {
|
|
2781
|
+
initial: new Map([
|
|
2782
|
+
["A01", { name: "A01", x: 2.4, y: 2.9, w: 3.2, h: 1.2, model: "42U · 8 kW", kw: 5.2, capacity: 8.0 }],
|
|
2783
|
+
["A02", { name: "A02", x: 6.0, y: 2.9, w: 3.2, h: 1.2, model: "42U · 8 kW", kw: 7.4, capacity: 8.0 }],
|
|
2784
|
+
]),
|
|
2785
|
+
});
|
|
2786
|
+
|
|
2787
|
+
const floor = East.function([], UIComponentType, (_$) => (
|
|
2788
|
+
<Reactive>{$ => {
|
|
2789
|
+
const racks = $.let(Data.bind(floorRacks)); // read only, and read whole
|
|
2790
|
+
return (
|
|
2791
|
+
<Box height="440px">
|
|
2792
|
+
<Schematic id="floor" extent={{ width: 18, height: 12 }} scaleUnit="m"
|
|
2793
|
+
items={{ racks: Schematic.items(racks, {
|
|
2794
|
+
name: "Rack", icon: "server", x: "x", y: "y", width: "w", height: "h", label: "name",
|
|
2795
|
+
sublabel: (r) => r.model,
|
|
2796
|
+
meter: (r) => ({ value: r.kw, max: r.capacity }),
|
|
2797
|
+
}) }} />
|
|
2798
|
+
</Box>
|
|
2799
|
+
);
|
|
2800
|
+
}}</Reactive>
|
|
2801
|
+
));
|
|
2802
|
+
```
|
|
2803
|
+
|
|
2804
|
+
A floor over several records, its racks editable and read through their
|
|
2805
|
+
source of the rail, their loads a details record keyed as they are, edited in
|
|
2806
|
+
the inspector, their templates a record of their own, its cooling units round by their own radius,
|
|
2807
|
+
its fibre trunks a net kind a Shift session grows runs into, its layers a
|
|
2808
|
+
record of systems, its tenants an author's tab whose cards set a rack's owner,
|
|
2809
|
+
what is selected kept by the host and a run between two frames refused in the
|
|
2810
|
+
veto's words (`schematicDataCentre`):
|
|
2811
|
+
|
|
2812
|
+
```tsx
|
|
2813
|
+
// Inside Reactive: the racks through their patch door, read by key beside their source of the rail.
|
|
2814
|
+
const racks = $.let(Record.bind(dcRacks));
|
|
2815
|
+
const rackEntries = $.let(Data.bindPaged(dcRacks)); // one rack by its key
|
|
2816
|
+
const loads = $.let(Record.bind(dcRackLoads)); // the racks' loads, edited in the inspector
|
|
2817
|
+
const loadEntries = $.let(Data.bindPaged(dcRackLoads)); // by key, beside a kind read through its source
|
|
2818
|
+
const frames = $.let(Data.bind(dcFrames));
|
|
2819
|
+
const cooling = $.let(Record.bind(dcCooling));
|
|
2820
|
+
const areas = $.let(Data.bind(dcAreas)); // read only, and read whole
|
|
2821
|
+
const fibre = $.let(Record.bind(dcFibre));
|
|
2822
|
+
const trunks = $.let(Record.bind(dcTrunks)); // none yet: a Shift session grows one
|
|
2823
|
+
const systems = $.let(Data.bind(dcSystems)); // the layers: a record of the app's
|
|
2824
|
+
const rackTemplates = $.let(Data.bind(dcRackTemplates));
|
|
2825
|
+
const owners = $.let(Data.bind(dcOwners)); // the tenants: an author's tab whose cards set a rack's owner
|
|
2826
|
+
// coolingDetails: a cooling unit's own Details — East.function([DcCooling, FunctionType([DcCooling], NullType)], UIComponentType, …)
|
|
2827
|
+
// The whole selection after each change, each entity by its kind's slot and key, kept where the app reads it.
|
|
2828
|
+
const selected = $.let(State.bind([ArrayType(Schematic.Types.Ref)], "ops.floor.selected", []));
|
|
2829
|
+
const onSelect = $.const(East.function([ArrayType(Schematic.Types.Ref)], NullType, ($2, refs) => { $2(selected.write(refs)); }));
|
|
2830
|
+
// Two frames are joined by a cross-connect, never a fibre run: the veto's words refuse the pair.
|
|
2831
|
+
const canConnect = $.const(East.function([Schematic.Types.Candidate], OptionType(StringType), ($2, candidate) => {
|
|
2832
|
+
const reason = $2.let(none, OptionType(StringType));
|
|
2833
|
+
$2.if(candidate.from.kind.equal("network").and(() => candidate.to.kind.equal("network")), ($3) => {
|
|
2834
|
+
$3.assign(reason, some("Two frames are joined by a cross-connect"));
|
|
2835
|
+
});
|
|
2836
|
+
return reason;
|
|
2837
|
+
}));
|
|
2838
|
+
// One rail: the racks by name, hall and owner, and their templates by group.
|
|
2839
|
+
const slice = $.let(Slice.bindRecords("ops.floor", {
|
|
2840
|
+
racks: { source: Data.bindPaged(dcRacks, { indexes: ["racks_by_name", "racks_by_hall", "racks_by_owner"] }), search: ["name", "owner"],
|
|
2841
|
+
fields: { name: { label: "Rack" }, hall: { label: "Hall", hints: ["Cage A"] }, owner: { label: "Owner", hints: ["Harbour Labs"] } } },
|
|
2842
|
+
rackTemplates: { source: Data.bindPaged(dcRackTemplates, { indexes: ["rack_templates_by_group"] }), fields: { group: { label: "Group", hints: ["Racks"] } } },
|
|
2843
|
+
}));
|
|
2844
|
+
<Schematic id="floor" inspector extent={{ width: 28, height: 14 }} scaleUnit="m" slice={{ slice }} narrowed="dim"
|
|
2845
|
+
onSelect={onSelect} canConnect={canConnect}
|
|
2846
|
+
items={{
|
|
2847
|
+
racks: Schematic.items(racks, {
|
|
2848
|
+
name: "Rack", icon: "server", x: "x", y: "y", width: "w", height: "h", label: "name", layer: "system", entries: rackEntries,
|
|
2849
|
+
details: Schematic.details(loads, { // what each card draws from the rack's load
|
|
2850
|
+
entries: loadEntries,
|
|
2851
|
+
meter: (l) => ({ value: l.kw, max: l.capacity }),
|
|
2852
|
+
status: { field: "alarm", cases: {
|
|
2853
|
+
ok: { label: "OK", tone: variant("success", null), ring: false },
|
|
2854
|
+
hot: { label: "Hot", tone: variant("danger", null), ring: false },
|
|
2855
|
+
} },
|
|
2856
|
+
}),
|
|
2857
|
+
templates: Schematic.templates(rackTemplates.read(), { // every field of a rack but x and y
|
|
2858
|
+
name: (t) => t.name, group: (t) => some(t.group), values: (t) => t.values, source: "rackTemplates",
|
|
2859
|
+
}),
|
|
2860
|
+
}),
|
|
2861
|
+
network: Schematic.items(frames, { name: "Network frame", icon: "network-wired", x: "x", y: "y", width: "w", height: "h", label: "name", layer: "system" }),
|
|
2862
|
+
cooling: Schematic.items(cooling, {
|
|
2863
|
+
name: "Cooling unit", icon: "fan", x: "x", y: "y", width: "w", height: "h", label: "name", layer: "system",
|
|
2864
|
+
footprint: (c) => some(Schematic.circle(c.radius)), // a circle of the unit's own radius, about its centre
|
|
2865
|
+
inspector: coolingDetails, // its own Details, in place of its form
|
|
2866
|
+
}),
|
|
2867
|
+
}}
|
|
2868
|
+
zones={{ areas: Schematic.zones(areas, {
|
|
2869
|
+
name: "Area", icon: "vector-square", x: "x", y: "y", width: "w", height: "h", label: "name", layer: "system",
|
|
2870
|
+
pattern: (a) => a.kind.match({ // outlined or hatched by what the area is
|
|
2871
|
+
room: (_$) => Schematic.outline(), cold: (_$) => Schematic.hatch({ tone: "brand" }), hot: (_$) => Schematic.hatch({ tone: "warning" }),
|
|
2872
|
+
}),
|
|
2873
|
+
}) }}
|
|
2874
|
+
links={{ fibre: Schematic.links(fibre, {
|
|
2875
|
+
name: "Fibre run", icon: "bolt", layer: "system", via: "path", // an Array of { x, y } Floats field
|
|
2876
|
+
from: { field: "from", of: { rack: "racks", frame: "network" } }, to: { field: "to", of: { rack: "racks", frame: "network" } },
|
|
2877
|
+
label: (f) => f.label,
|
|
2878
|
+
style: (f) => f.cable.match({ fibre: (_$) => Schematic.solid(), logical: (_$) => Schematic.dashed() }),
|
|
2879
|
+
}) }}
|
|
2880
|
+
nets={{ trunks: Schematic.nets(trunks, { // racks to a frame: what a Shift session grows its runs into
|
|
2881
|
+
name: "Fibre trunk", icon: "diagram-project", layer: "system",
|
|
2882
|
+
sources: { field: "racks", of: "racks" }, destinations: { field: "frames", of: "network" }, // Array<String> fields of keys
|
|
2883
|
+
label: (t) => t.label,
|
|
2884
|
+
}) }}
|
|
2885
|
+
layers={Schematic.layers(systems.read(), { // a layer per system, an entity's `layer` naming it by key
|
|
2886
|
+
label: (s) => s.label, color: (s) => some(s.colour), order: (s) => s.order, locked: (s) => s.fixed, opacity: (s) => s.dim,
|
|
2887
|
+
})}
|
|
2888
|
+
library={[Schematic.library.templates(), Schematic.library.backlog(), Schematic.library.index(), Schematic.library.layers(),
|
|
2889
|
+
Schematic.library.tab(owners.read(), { // a card dropped on a rack sets its owner
|
|
2890
|
+
name: "Owners", icon: "building", label: (o) => o.name, meta: (o) => some(o.contact),
|
|
2891
|
+
drop: (o) => Schematic.patch(DcRack, { owner: o.name }),
|
|
2892
|
+
})]} />
|
|
2893
|
+
```
|
|
2894
|
+
|
|
2895
|
+
```
|
|
2896
|
+
<Schematic extent={{ width, height }} items={{ <slot>: Schematic.items(record, { … }) }} /> — the 2D world-coordinate drawing of the app's own records: a kind per record, under its slot
|
|
2897
|
+
├─ Props:
|
|
2898
|
+
│ ├─ extent (required) — the drawing's world bounds, { width, height }, each a positive number of world units; the canvas fits them
|
|
2899
|
+
│ ├─ items (required) — the item kinds, by slot, one at least: Schematic.items(record, { name, icon, x, y, label, width?, height?, footprint?, layer?, sublabel?, metric?, meter?, status?, tone?, color?, weight?, fillOpacity?, details?, templates?, entries?, fields?, inspector?, ready? }) — `x` and `y` Float fields, the item's centre, or both Options of Float: a row with neither has no position, the kind's backlog, drawn nowhere; `width` and `height` Float fields, its box about its centre; `footprint` an accessor `(row, key) => Option<Schematic.Types.Geometry>` built from the row's own fields with Schematic.circle / polygon / polyline / rect, drawn about its centre, refused without the box or naming a field; `label` a String field, or an accessor; `icon` a Font Awesome solid icon, or an accessor; `layer` a String field naming a layer by its record's key; `status: { field, cases }` a variant field, a Status per case; `fields` the inspector form's hints over the fields no role writes (`Fields`); `inspector` the kind's own Details, fn(R, fn(R) => Null) => UIComponentType, in place of its form; `ready` the app's check of a drafted row, fn(R, String) => Editing.Types.Readiness, its refusal listed in Issues and holding Save — each typed over the kind's own row; the rest accessors `(row, key) => …`
|
|
2900
|
+
│ ├─ zones (optional) — the zone kinds, by slot: Schematic.zones(record, { name, icon, x, y, width, height, label, pattern?, geometry?, layer?, tone?, color?, fillOpacity?, weight?, details?, entries?, fields?, inspector?, ready? }) — `x`, `y`, `width` and `height` the Float fields of its box, its top-left corner and its size; `pattern` an accessor, outline (the default) or hatch — a hatched zone is an annotation, holding no items and taking no tap; `geometry` an accessor `(row, key) => Option<Schematic.Types.Geometry>` built from the row's own fields, drawn about its box's centre, refused naming a field. A zone takes no templates and no gesture on the canvas
|
|
2901
|
+
│ ├─ links (optional) — the link kinds, by slot: Schematic.links(record, { name, icon, from: { field, of }, to: { field, of }, via?, label?, metric?, style?, route?, layer?, details?, templates?, entries?, fields?, inspector?, ready? }) — an end is a String field holding a key of the item slot `of` names, or a variant of String cases with an item slot per case (`of: { rack: "racks", frame: "network" }`) — never an Option: a link has both its ends; `via` an `Array` of `{ x, y }` Floats field, its waypoints
|
|
2902
|
+
│ ├─ nets (optional) — the net kinds, by slot: Schematic.nets(record, { name, icon, sources: { field, of }, destinations: { field, of }, via?, label?, metric?, style?, route?, layer?, details?, templates?, … }) — each side a `Set<String>` or `Array<String>` field of keys of the item slot `of` names; drawn as a bus — a header bar over each side of many members, a stub per member, the trunk between them, a dot only where a tap joins a bar; a press on the trunk, a bar or a junction selects the net wherever its bus draws, one on a stub its leg
|
|
2903
|
+
│ ├─ scaleUnit (optional) — the scale bar's unit
|
|
2904
|
+
│ ├─ slice (optional) — slice={{ slice: Slice.bindRecords(key, { <a kind's slot>?, <a templates list's or a tab's source>? }, { presets? }), affordances?, persist? }}: ONE rail, each source named after what it narrows — a kind's slot, that kind, read complete through it, or a window at a time where it lists an area index; a templates list's or a tab's, its cards. The brush is refused: a 2D canvas has no axis
|
|
2905
|
+
│ ├─ narrowed (optional) — how a row the narrowing leaves out draws: "hide" (the default — its kind reads only what its narrowing holds), "dim" (its kind reads every row; what the narrowing leaves out draws as context), or { show, opacity? (0.35), desaturate? (on), dot?, emphasis?: "halo" | "pulse", frame?, fit? } — `emphasis`, `frame` and `fit` take the matches whatever `show` is
|
|
2906
|
+
│ ├─ layers (optional) — Schematic.layers(rows, { label, order?, tone?, color?, visible?, locked?, opacity? }): the app's own rows, a `Dict` keyed by String (a record's `read()`), each row a layer under its key, through accessors `(row, key) => …` as the Roster's lists are — in their `order`, lowest first, then key order; `color` an Option of a theme colour token over `tone`; `visible`, `locked` and `opacity` the author's defaults, which the viewer's choices in the Layers tab override. An entity's `layer` field names one by its key; a locked layer's entities never select, a hidden one's never draw
|
|
2907
|
+
│ ├─ library (optional) — the library pane's tabs, in order, each over every kind as the Calendar's are: Schematic.library.templates() (every kind's templates, each kind's under its name) · backlog() (every item kind's rows with no position, each kind's under its name) · index() (the zones, with zone kinds) · layers() (with layers) · tab(rows, { name, icon?, label, meta?, group?, drop?, source? }) (`drop` a `Schematic.patch(R, …)` over one kind's row type: the fields a card dropped on an entity of that kind sets); each listed once
|
|
2908
|
+
│ ├─ inspector (optional) — `inspector` (true) shows the end pane, 320px: Details of what is selected and Issues (below); left out, or `false`, no pane, as the Plan's. A kind's own Details is its kind's `inspector`, its details' their own `inspector`
|
|
2909
|
+
│ ├─ readOnly / grid / minimap / id (optional) — every kind read only, taking no gesture; the metric grid (on); the minimap (from 25 items); `id` keeps the Schematic's panes, layers and tools apart when one surface holds two, as the Plan's does
|
|
2910
|
+
│ ├─ canConnect (optional) — fn(Schematic.Types.Candidate) => Option<String>: asked of each pair a connect drag would join — `{ kind, from, to }`, the slot of the link or net kind it writes and its two items, each a `Schematic.Types.Ref` — before the draft snaps; `none` allows it, and words refuse it — the draft says them in its caption, and letting go records nothing; a veto that throws allows the pair and logs it
|
|
2911
|
+
│ └─ onSelect (optional) — fn(Array<Schematic.Types.Ref>) => Null: the whole selection after each change — items, zones, links and nets alike, each { kind: its slot, key: its row's key }, `[]` once cleared — one call a change, none for a change that leaves it as it was
|
|
2912
|
+
├─ Editing (#1388) — every gesture ONE step of one history over a session per record it writes, each kind bound with its patch door and each such kind's details: Move (the tool, over an editable item kind) drags the item under the pointer, or the selection with it, every centre kept within the extent, Esc mid-drag putting it back; Connect (the tool, over an editable link kind, its menu picking the kind) drags from an item onto another, a link of that kind, and Shift held on the next drag grows the drags into a net of the first editable net kind whose sides take their items, absorbing their links (with none, each drag adds a link); a selected link's end drags onto another item; with Connect, a Shift-drag from a selected net's member extends its destinations; Del or Backspace deletes the selection — a link, a net, a net's leg, items with every link and net leg naming one — refused in the footer's words where a link or a net bound read only names an item it would delete. An end or a side holding no item of a slot refuses the pair as `canConnect` does (`Fibre run can't end at PDU-B`). A gesture writes only the fields its roles map; a new link's or net's key is minted unique (`crypto.randomUUID()`), its other fields their type's defaults. The canvas draws the drafts where Save would leave them; a draft of a row the narrowing leaves out waits in its session, and a new row draws always. The history item ends the toolbar — Undo, Redo, Discard and Save, and ⌘Z, ⇧⌘Z and ⌘Y anywhere in the frame but a field being typed into; Save sends each record's changes as its own request through its patch door, one record's conflict or refusal kept under a banner titled with its kind's name while the others' commits stand, a write with no answer turning Save into Retry, the same request. After a Save the footer says what left each kind's narrowing
|
|
2913
|
+
├─ Drops (#1390) — each card dropped on the canvas ONE step of the history, the ghost's caption saying where it lands or why it can't: an item template adds an item at the point (its key minted, its `x` and `y` the point's at the screen's resolution, every other field the template's `values`), selected; a backlog row is placed at the point, selected; a link or net template gives a link or a net of its kind its `values`, never its ends, sides or key; an author's card sets its `drop`'s `Schematic.patch` on an entity of that patch's kind. ⏎ on a card — on a touch screen a tap on the selected card — drops it on the selection (an author's card on every selected entity of its kind) or a template or backlog row at the view's centre, refused in the footer's words. Refused outside the drawing, with no entity of the card's kind under it, or on a kind bound read only; a locked layer's entities and a hatched zone take none, and an item's card covers the zones and links beneath it. A picked link or net template gives Connect its values
|
|
2914
|
+
├─ Inspector (#1391) — the end pane, its tabs Details and Issues (Issues with its count), opened on Details by Enter or a double-click on an entity: Details through the shared form, each field by the input its type takes, a field the drafts changed tinted and the head's chip Pending (New for a row its record holds none of) — an item's kind's form over the fields no role writes, or the kind's own `inspector`, its details' form or own Details, its position (`x`, `y`) and Unplace (a kind with a backlog), Delete, and what it is joined to, each selecting it; a zone's form; a link's ends as Choosers over the items of their slots, its form, Delete; a net's sides as tags over its slot's keys (a side never emptied: Delete takes the net), its form, Delete; a net's leg, its item and Remove; several, how many of each kind, and Delete; nothing, the drawing's counts — a kind read by area counted from its source's indexes, never its window, a line naming what no read can count — and each record's last save with who made it — each edit ONE step of the history; read only, every field printed. Issues: an item outside the drawing, a shape past its box, a colour naming no theme colour, a link or net naming an item no kind holds, a drafted row `ready` refuses (holding Save), a Save refused or in conflict — each selecting what it names; over a kind read by area, the rows read around the view and every unsaved change, one line saying its rows out of view are checked only as the view reaches them
|
|
2915
|
+
├─ Narrowing (#1392) — each source of the rail narrows its kind on the canvas, in the Backlog, the Index and the templates it names, in the inspector's Choosers and what an item is joined to, and in the drawing's counts; a link or a net draws only while its ends do. Hidden by default; dimmed, its kind reads every row and draws what the narrowing leaves out as context — faded, greyed or a bare marker, a link to it and a net all of whose members on a side are it among it — which selects by no path and counts nowhere. A match — a row its source's shown keys hold — wears `emphasis` (a ring on its card or marker, an edge on a zone, a halo under a link's or a net's line), `frame` boxes two or more, and `fit` flies the camera to them when the narrowing changes, once what it flies to is read, never as a zoom or a Move changes what matches — a kind read by area's by the box its narrowing's rows stand in, from its area index's counts. A draft the narrowing leaves out waits in its session, hidden, or draws where its draft has it, dimmed; Save sends it either way
|
|
2916
|
+
├─ Reading by area (#1393) — a kind whose source lists index.area({ x, y, width, height, at: "centre", cell }): a window at a time, the rows whose box meets the view and its margin, read from the index alone; past 5,000 rows in view their density from the index's counts, the footer saying how many it shades (`Density: 20,000 racks — zoom in to draw them`); its details by the runs the window's cells are of the index; a choice of the rail through an area index led by its fields, else the one led by none, the rest asked of the window's rows; its Backlog through the index's rows with no position. Refused as the kind is read, its read failing with Retry: area indexes all led by fields, or one boxing the rows otherwise than the kind's roles place them, the reason naming what to declare. A draft is kept by the row's key whatever the window, drawn where it places its row while the narrowing holds its stored row — the Backlog listing a row it unplaces, the Index a row it moves under the zone it now stands in — and Save writes it through the kind's patch door. A selected row stays selected off the window — Details reads it by its key, Del deletes it, `onSelect` hears no camera move — until a read shows it gone, a draft deletes or unplaces it, or a layer hides or locks it. The drawing's counts take the kind from its source, never its window
|
|
2917
|
+
├─ Narrow (#1394) — below 480px of main: one finger pans, two pinch, a tap selects, and nothing drags — no Move or Connect among the View's tools, no library card drags, no drop cell, no link handles. Details takes the gestures' place: an item's Connect to… (a Chooser of the items a link may end at, the link kind beside it while several editable kinds start at the item) and a Backlog card's Place (at the view's centre), beside x and y, a link's ends, a net's sides and Delete, every route that connects held to `canConnect`; a resize keeps the selection and the drafts
|
|
2918
|
+
└─ Factories:
|
|
2919
|
+
├─ Schematic.items / zones / links / nets(record, config) — the kinds · Schematic.details(record, { status?, meter?, metric?, sublabel?, fields?, inspector?, entries? }) — a kind's details · Schematic.templates(rows, { name, group?, icon?, values, source }) — a kind's templates record, `values` every field but what a drop places · Schematic.layers(rows, config) — the layers
|
|
2920
|
+
├─ Schematic.circle(r) / .polyline(verts, { width? }) / .polygon(verts) / .rect() — geometry about the entity's anchor, each vertex an offset from it: the row's own points, `{ x, y }` (no bulge) or `{ x, y, bulge }`; a radius and a width a Float of the row's
|
|
2921
|
+
├─ Schematic.outline() / .hatch() — zone patterns · Schematic.solid() / .dashed() — link and net styles
|
|
2922
|
+
└─ Schematic.patch(R, { … }) — a patch over a kind's row type, what an author's card dropped on an entity sets · Schematic.Types.* — the value types the accessors return, and what the host hears (Ref, Candidate)
|
|
2923
|
+
```
|
|
2924
|
+
|
|
2925
|
+
| Signature | Description | Example |
|
|
2926
|
+
| --- | --- | --- |
|
|
2927
|
+
| `items={{ racks: Schematic.items(Data.bind(racks), { name, icon, x, y, width?, height?, label, … }) }}` **❗** | An item kind over the app's record, read only and read whole; its card's second line and bar from accessors. A role naming a field the row lacks, or one of another type, is refused, naming the role, the field and its type. | `schematicFloor` |
|
|
2928
|
+
| `Schematic.items(Record.bind(r), { …, entries: Data.bindPaged(r) })` + a source of `slice` named after its slot **❗** | Editable, read complete through its source. `Data.bind` there, or a `Record.bind` without `entries`, is refused, naming the binding to use. | `schematicDataCentre` |
|
|
2929
|
+
| `x` / `y` Options of Float | The rows with no position are the kind's backlog: drawn nowhere, and listed by `Schematic.library.backlog()`. | `schematicDataCentre` |
|
|
2930
|
+
| `slice={{ slice: Slice.bindRecords(key, { racks: { source: Data.bindPaged(racks, { indexes }) }, … }) }}` · `narrowed="dim"` **❗** | One rail (#1392): each source narrows its kind everywhere it appears — the canvas, the Backlog, the Index, the templates it names, the inspector's Choosers and the drawing's counts — a link or a net only while its ends draw. Hidden by default, the kind reading only its narrowing; `dim` reads every row and draws what the narrowing leaves out as context, faded and greyed, never selected. `emphasis`, `frame` and `fit` take the matches whatever `show` is. | `schematicDataCentre` |
|
|
2931
|
+
| a kind's source listing `index.area(…)`, beside area indexes led by the rail's fields **❗** | Read by area, a window at a time from the index alone; past 5,000 rows in view their density, the footer saying how many; drafts kept by key wherever the window goes, and Save read back through the index; a selected row kept off the window, read by its key; the drawing's counts its source's. Area indexes all led by fields, or one boxing the rows otherwise than the kind's roles, are refused, naming what to declare. | `schematicRecordIndexes` |
|
|
2932
|
+
| below 480px of main | Nothing drags; Details' Connect to… and the Backlog card's Place take the gestures' place, beside an item's `x` and `y`, a link's ends and a net's sides, each held to `canConnect`. | `schematicRecordIndexesMobile`, `schematicDataCentre` |
|
|
2933
|
+
| an editable kind, and no `library` or `inspector` | The frame is its toolbar — the tools, Move among them, and the history — its canvas and its footer; every gesture still one step Save writes. | `schematicHandover` |
|
|
2934
|
+
| `footprint: (u) => some(Schematic.circle(u.radius))` + `width` / `height`; `Schematic.polygon` / `.circle` / `.polyline` **❗** | An accessor over the row's own fields, returning an `Option<Schematic.Types.Geometry>`: geometry about the item's centre, or a zone's `geometry` about its box's centre, each vertex an offset from it — the row's own `{ x, y }` points, or `{ x, y, bulge }`. A footprint without the box, and a shape role naming a field, are refused, naming the accessor to write. | `schematicShapes`, `schematicDataCentre` |
|
|
2935
|
+
| `zones={{ areas: Schematic.zones(…, { x, y, width, height, label, pattern?, geometry? }) }}`, `pattern: (a) => a.kind.match({ … })`, `Schematic.outline()` / `.hatch()` | Zones in their boxes under the items, outlined or hatched by what the row says it is. | `schematicShapes`, `schematicDataCentre` |
|
|
2936
|
+
| `links={{ fibre: Schematic.links(…, { from: { field, of }, to: { field, of }, via? }) }}`, `Schematic.solid()` / `.dashed()` | Links between items, routed orthogonally, two of one pair fanning into lanes, in the shared links look: each from its `from` item's edge to its `to` item's, its head there, in the links' muted ink or its style's tone; an end of a variant field takes its slot from its case. | `schematicDataCentre` |
|
|
2937
|
+
| `nets={{ feeds: Schematic.nets(…, { sources: { field, of }, destinations: { field, of } }) }}` | A bus joining many items to many. | `schematicDataCentre` |
|
|
2938
|
+
| a kind bound with its patch door — `Record.bind(record)` **❗** | Every gesture one step of one history over a session per record (#1388): Move, Connect, Shift growing a net of the first editable net kind that takes its items, an end retargeted, a net extended, Del; Undo, Redo and Discard; Save sending each record's changes through its patch door, one refused leaving the others' commits standing. A gesture writes only the fields its roles map, a new row under a minted key. | `schematicDataCentre` |
|
|
2939
|
+
| `details: Schematic.details(record, { status?, meter?, metric?, sublabel?, fields?, inspector?, entries? })` **❗** | A record keyed as its kind is, its roles drawn on the cards; a role given by the kind too is refused. Bound with its patch door — `entries` beside it where the kind is read through its source — the inspector edits it beside the kind's row. | `schematicDataCentre` |
|
|
2940
|
+
| `templates: Schematic.templates(rows, { name, group?, values, source })` **❗** | A templates record, its `source` one of the slice's; `values` gives every field but what a drop places — an item's `x` and `y`, a link's ends, a net's sides. A list written in the code is refused. | `schematicDataCentre` |
|
|
2941
|
+
| `library={[Schematic.library.templates(), .backlog(), .index(), .layers(), .tab(rows, {…})]}` **❗** | The library pane's tabs, each over every kind as the Calendar's are, each checked against the kinds: Templates and Backlog are refused when no kind has any, naming the change. The frame's start pane draws them, each with its count, each card tab following its source of the rail (#1389). | `schematicDataCentre` |
|
|
2942
|
+
| the Layers tab's Isolate, Lock, Show or Hide and Show all | The viewer's layer choices, kept under the Schematic's `id` over the author's defaults: a hidden layer's entities never draw, a locked one's never select. | `schematicDataCentre` |
|
|
2943
|
+
| a template's or a backlog row's card dropped on the canvas **❗** | One step (#1390): an item template adds an item at the point, its key minted, selected; a backlog row is placed there, selected; a link or net template gives a link or a net of its kind its values, never its ends, sides or key. The caption says where (`Cage A · 12.00, 1.00 m`, `onto A03 → ODF-1`), or why not (`Outside the drawing`, `Drop onto a fibre run`, `Racks are read only`). | `schematicDataCentre` |
|
|
2944
|
+
| `Schematic.library.tab(owners.read(), { name, label, meta?, drop: (o) => Schematic.patch(DcRack, { owner: o.name }) })` **❗** | An author's card dropped on an entity of the patch's kind sets those fields, one step; ⏎ — on a touch screen a tap on the selected card — sets them on every selected entity of that kind. A tab without `drop` doesn't drag. | `schematicDataCentre` |
|
|
2945
|
+
| `layers={Schematic.layers(rows, { label, order?, tone?, color?, visible?, locked?, opacity? })}` **❗** | The layers, the app's own rows in their `order`, then key order, each keyed by its row's key; an accessor of another type, and a list written in the code, are refused, naming them. | `schematicShapes`, `schematicDataCentre` |
|
|
2946
|
+
| `color` / `tone` / `fillOpacity` / `weight` | Colour overrides on an item's footprint, a zone and a layer — a theme colour token or a tone. | `schematicShapes` |
|
|
2947
|
+
| `inspector` · a kind's `fields`, `ready` and own `inspector` **❗** | The inspector pane, opted into as the Plan's is (#1391): Details — an item's form and its details', its position and Unplace, what it is joined to; a zone's form; a link's ends as Choosers; a net's sides as tags, a leg's Remove; several and Delete; with nothing selected the drawing's counts and saves — each edit one step; and Issues, each selecting what it names. `fields` hints a kind's form, `ready` checks a drafted row, and a kind's own `inspector` is its Details in place of its form. `inspector={{ <slot>: … }}` is refused, naming the kind's `inspector`. | `schematicDataCentre` |
|
|
2948
|
+
| `onSelect={fn(Array<Schematic.Types.Ref>) => Null}` | The whole selection after each change, items, zones, links and nets alike, each `{ kind, key }`; `[]` once cleared. | `schematicDataCentre` |
|
|
2949
|
+
| `canConnect={fn(Schematic.Types.Candidate) => Option<String>}` | A connect drag's candidate, `{ kind, from, to }`: `none` lets it snap, and words refuse it — the draft says them in its caption, and letting go records nothing; a veto that throws allows the pair. | `schematicDataCentre` |
|
|
2950
|
+
| `id="floor"` | Keeps the Schematic's panes, layers and tools apart when one surface holds two. | every example |
|
|
2951
|
+
|
|
2952
|
+
Removed with #1401, each still written refused at build, naming its
|
|
2953
|
+
replacement: `name` (`id`); `inspector={{ <slot>: Details }}` (the kind's own
|
|
2954
|
+
`inspector`); `Schematic.library.templates(slot)` and `unplaced(slot)`
|
|
2955
|
+
(`templates()` and `backlog()`, over every kind); `onSelectionChange`,
|
|
2956
|
+
`onZoneSelectionChange` and `onSelectLink` (`onSelect`, the whole selection);
|
|
2957
|
+
`onViewportChange` (the canvas reads its own window); `onItemOpen` (Enter or a
|
|
2958
|
+
double-click opens the selection in the inspector's Details); `selectZoomFocus`
|
|
2959
|
+
(a tap selects in place; Find, the Index tab and the arrow keys fly); a
|
|
2960
|
+
`layers` list written in the code (`Schematic.layers` over the app's rows);
|
|
2961
|
+
and a `footprint` or a `geometry` naming a field that holds
|
|
2962
|
+
`Schematic.Types.Geometry` (an accessor over the row's own fields). The
|
|
2963
|
+
selection, zone selection and viewport event types are gone, and
|
|
2964
|
+
`canConnect` hears a `Schematic.Types.Candidate` where it heard two
|
|
2965
|
+
references.
|
|
2966
|
+
|
|
2967
|
+
Removed with #1386, each still written refused at build, naming its
|
|
2968
|
+
replacement: rows and their mappers — `items` as rows with `item`, and `zone`,
|
|
2969
|
+
`link` and `net` — (kinds over the app's records, by slot); `onMoveItem`,
|
|
2970
|
+
`onCreateLink`, `onEditLink`, `onDeleteLink` and `onEditNet` (the editing
|
|
2971
|
+
session's gestures: bind a kind with its patch door to edit it); `linkMode`;
|
|
2972
|
+
`readOnlyLinks` and `readOnlyItems` (a kind bound read only); the hover cards
|
|
2973
|
+
`itemHover`, `zoneHover` and `linkHover` (the inspector); `affordances`
|
|
2974
|
+
outside `slice`; the slice's effect props `sliceHidden`, `sliceOpacity`,
|
|
2975
|
+
`sliceDesaturate`, `sliceDot`, `sliceEmphasis`, `sliceFrame` and
|
|
2976
|
+
`sliceFrameFit` (`narrowed`); `sliceSelectField`; `selectionMode` (a tap
|
|
2977
|
+
selects, Shift extends it, and the marquee boxes); `navigator` (the library's
|
|
2978
|
+
Index tab); `height` and `maxHeight` (the Schematic fills the box it is given);
|
|
2979
|
+
and `onSelectZone` (`onSelect`, over the whole selection since #1401). A prop
|
|
2980
|
+
the Schematic doesn't take is refused, naming those it does.
|
|
2981
|
+
|
|
2108
2982
|
## Rendering surfaces in an app — `<E3Provider>`
|
|
2109
2983
|
|
|
2110
2984
|
A deployed `ui()` task renders in a React app through
|
|
@@ -2228,8 +3102,8 @@ Tested examples live in `test/*.examples.tsx`:
|
|
|
2228
3102
|
folds, its weeks source holding the window; `planRowDrop` edits nested
|
|
2229
3103
|
record entries through one session; `planVariants`, `planNumberAxis` (its
|
|
2230
3104
|
orders source on the integer arm), `planOrdinalAxis` and `slicePlanChrome`
|
|
2231
|
-
(the `rows` source of its rail
|
|
2232
|
-
and Slice examples.
|
|
3105
|
+
(the `rows` source of its rail, the canvas holding its own window) retain
|
|
3106
|
+
their distinct axis, configuration and Slice examples.
|
|
2233
3107
|
- `plan/plan-events.examples.tsx` — `planEvents` is the smallest builder,
|
|
2234
3108
|
with a bound backlog, a customer-card tab and an Unassigned row.
|
|
2235
3109
|
`planPrintWorks` covers grouped presses and crews, nested bindery resources,
|
|
@@ -2241,18 +3115,26 @@ Tested examples live in `test/*.examples.tsx`:
|
|
|
2241
3115
|
`planEventLinks` keeps the individual link-routing cases.
|
|
2242
3116
|
- `plan/plan-windows.examples.tsx` — `<Plan>` over records too large to read
|
|
2243
3117
|
whole (`planWindows`): 2,000 presses a task generates, paged as the canvas
|
|
2244
|
-
scrolls, and the jobs narrowed through the rail's `job` source
|
|
2245
|
-
|
|
2246
|
-
|
|
3118
|
+
scrolls, and the jobs on them narrowed through the rail's `job` source —
|
|
3119
|
+
read a cell at a time through their series index, shaded from their summary
|
|
3120
|
+
zoomed out, their backlog through their interval index — and read by key
|
|
3121
|
+
through their own entries, edited and saved without the record read whole;
|
|
3122
|
+
and its presses a record the rail narrows by hall through an index of it,
|
|
3123
|
+
the canvas paging the narrowing's presses and reading the cells of those in
|
|
3124
|
+
view alone (`planSeriesByHall`).
|
|
2247
3125
|
- `plan/plan-records.examples.tsx` — the Sheet's 50,000 work orders on a Plan
|
|
2248
|
-
(`planRecordIndexes`, `planRecordIndexesMobile`): the
|
|
2249
|
-
|
|
2250
|
-
|
|
3126
|
+
(`planRecordIndexes`, `planRecordIndexesMobile`): the work orders a paged
|
|
3127
|
+
resource kind its rail's `orders` source narrows through their indexes —
|
|
3128
|
+
their Due a field of the rail, so a pan keeps the rows where they are — and
|
|
3129
|
+
their 50,000 operations a record of their own placed on them, the source
|
|
3130
|
+
holding the window, read a cell at a time through its series index and
|
|
3131
|
+
shaded from its summary zoomed out; each move saved through the operations'
|
|
3132
|
+
patch door by the operation's key.
|
|
2251
3133
|
- `sheet/sheet.examples.tsx` — `<Sheet>`, each in its frame, its rows bound
|
|
2252
3134
|
from e3: the smallest sheet over a record; the configurator over the host's
|
|
2253
3135
|
rows, committed through `onApply`; two thousand jobs of a record, read only,
|
|
2254
|
-
|
|
2255
|
-
library of the author's own cards; the workshop's orders, the flagship
|
|
3136
|
+
the narrowing its rail makes through the record's indexes drawn over them as
|
|
3137
|
+
the lens, with saved views; a library of the author's own cards; the workshop's orders, the flagship
|
|
2256
3138
|
(groups, the driver and registers, the link grammar, the copilot's fills and
|
|
2257
3139
|
proposers, readiness, one rail over the orders and two templates records,
|
|
2258
3140
|
the library, the inspector and views); one week's rows with every column
|
|
@@ -2262,6 +3144,11 @@ Tested examples live in `test/*.examples.tsx`:
|
|
|
2262
3144
|
every pane combination: none, a library, an inspector, and both. A sheet
|
|
2263
3145
|
paged from a dataset is `data.examples.tsx`'s, and one over a record's own
|
|
2264
3146
|
entries through `Record.onApply` `record.examples.tsx`'s `recordSheetApply`.
|
|
3147
|
+
- `sheet/sheet-records.examples.tsx` — the lens over 50,000 work orders
|
|
3148
|
+
(`sheetRecordIndexes`, `sheetRecordIndexesMobile`): the `rows` source of the
|
|
3149
|
+
sheet's rail narrows them through the record's indexes, and the sheet draws
|
|
3150
|
+
only the orders the narrowing keeps, each at its row number, read a segment
|
|
3151
|
+
at a time near the view, each Save through `Record.onApply` by the order's key.
|
|
2265
3152
|
- `sheet/sheet-link.examples.ts` — the link grammar: `Sheet.link.parse` and
|
|
2266
3153
|
`Sheet.link.print`.
|
|
2267
3154
|
- `sheet/sheet-transactions.examples.ts` — `Sheet.apply` over rows, and over
|
|
@@ -2286,6 +3173,36 @@ Tested examples live in `test/*.examples.tsx`:
|
|
|
2286
3173
|
commit, a `canConnect` veto — with a state's and a transition's own Details
|
|
2287
3174
|
(`flowchartDetail`). Between them, every pane combination: none, a library,
|
|
2288
3175
|
an inspector, and both.
|
|
3176
|
+
- `schematic/schematic.examples.tsx` — `<Schematic>` over the app's records:
|
|
3177
|
+
one hall's racks, read only and read whole, each card's second line and
|
|
3178
|
+
power meter from its row (`schematicFloor`); the same hall handed over to
|
|
3179
|
+
its install crew, its racks moved and deleted, with no pane — the toolbar,
|
|
3180
|
+
the canvas and the footer (`schematicHandover`); footprints and zone shapes
|
|
3181
|
+
built by accessors from the rows' own fields and drawn about their anchors
|
|
3182
|
+
— a unit's outline corners and a tower's radius about each unit's centre,
|
|
3183
|
+
a hall's corners and a duct bank's arcing band about each area's box centre
|
|
3184
|
+
— with theme colour tokens, tones and layers from a systems record
|
|
3185
|
+
(`schematicShapes`); and a data centre floor over several records — racks
|
|
3186
|
+
editable and read through their source of the rail, their loads as details
|
|
3187
|
+
on each card, edited in the inspector, network frames, round cooling units with their own Details,
|
|
3188
|
+
power distribution units, halls, cages and aisles outlined or hatched by
|
|
3189
|
+
what they are, fibre runs between racks and frames, solid or dashed by
|
|
3190
|
+
their cable, a run between two frames refused in the veto's words, power
|
|
3191
|
+
feeds and fibre trunks as nets — no trunk until a Shift session grows one —
|
|
3192
|
+
every gesture a step of one history and Save per record, rack and fibre
|
|
3193
|
+
templates records, the racks with no position, the whole selection kept by
|
|
3194
|
+
the host, the inspector, and the library's tabs over every kind
|
|
3195
|
+
(`schematicDataCentre`).
|
|
3196
|
+
- `schematic/schematic-records.examples.tsx` — a floor of 20,000 racks in ten
|
|
3197
|
+
halls read a window at a time (`schematicRecordIndexes`): the racks bound
|
|
3198
|
+
with their patch door and read through their source of the rail by area, an
|
|
3199
|
+
area index at a 1 cm cell beside one led by the hall and an index of their
|
|
3200
|
+
models; their density past 5,000 racks in view; their loads read by the runs
|
|
3201
|
+
of the window's cells; a rack moved, a draft kept wherever the window goes
|
|
3202
|
+
and saved through the door. The same floor in a phone-width frame with its
|
|
3203
|
+
inspector, a rack found by its key and moved by its `x` and `y`
|
|
3204
|
+
(`schematicRecordIndexesMobile`). Its records are seeded by the pure
|
|
3205
|
+
generator that also runs as an e3 task.
|
|
2289
3206
|
- `query/query.examples.tsx` — queries as a solution writes them: the shared
|
|
2290
3207
|
fixture's datasets; the saved queries record, its value seven queries from
|
|
2291
3208
|
`Query.value`; the builder open on three of them and over an empty record; an
|
|
@@ -2329,16 +3246,16 @@ index, with `range: "start"`, and the kind gives `entries`, the record's own
|
|
|
2329
3246
|
entries, beside it:
|
|
2330
3247
|
|
|
2331
3248
|
```tsx
|
|
2332
|
-
// Package side
|
|
2333
|
-
//
|
|
3249
|
+
// Package side, among each record's indexes: jobs_by_time: index.interval({ from: "start", to: "end" }),
|
|
3250
|
+
// jobs_by_status_time the same with `on: ["status"]`, machines_by_area: index.on(["area"]), job_templates_by_group: index.on(["group"]).
|
|
2334
3251
|
// Inside Reactive: jobs has optional start/end and a machine reference.
|
|
2335
3252
|
const machines = $.let(Data.bind(d.machines));
|
|
2336
3253
|
const templates = $.let(Data.bind(d.jobTemplates));
|
|
2337
|
-
const jobs = $.let(Record.bind(d.jobs
|
|
2338
|
-
const jobIndex = $.let(Data.bindPaged(d.jobs, { indexes: [
|
|
3254
|
+
const jobs = $.let(Record.bind(d.jobs));
|
|
3255
|
+
const jobIndex = $.let(Data.bindPaged(d.jobs, { indexes: ["jobs_by_time", "jobs_by_status_time"] }));
|
|
2339
3256
|
const jobEntries = $.let(Data.bindPaged(d.jobs));
|
|
2340
|
-
const machineIndex = $.let(Data.bindPaged(d.machines, { indexes: [
|
|
2341
|
-
const templateIndex = $.let(Data.bindPaged(d.jobTemplates, { indexes: [
|
|
3257
|
+
const machineIndex = $.let(Data.bindPaged(d.machines, { indexes: ["machines_by_area"] }));
|
|
3258
|
+
const templateIndex = $.let(Data.bindPaged(d.jobTemplates, { indexes: ["job_templates_by_group"] }));
|
|
2342
3259
|
// One rail, a source per record, each named after what it narrows.
|
|
2343
3260
|
const slice = $.let(Slice.bindRecords("ops.calendar", {
|
|
2344
3261
|
job: { source: jobIndex, fields: { status: { label: "Status" }, start: { label: "Dates" } }, range: "start" },
|
|
@@ -2365,21 +3282,54 @@ return <Calendar slice={{ slice }}
|
|
|
2365
3282
|
|
|
2366
3283
|
The runnable examples `calendarQuickstart`, `calendarOperations`,
|
|
2367
3284
|
`calendarMinimal`, `calendarMobile`, `calendarWindowed`, `calendarReadOnly`,
|
|
2368
|
-
`calendarCustomInspector`, `calendarRecordIndexes
|
|
2369
|
-
`calendarRecordIndexesMobile`
|
|
2370
|
-
|
|
3285
|
+
`calendarCustomInspector`, `calendarRecordIndexes`,
|
|
3286
|
+
`calendarRecordIndexesMobile`, `calendarWindows`, `calendarWindowsByHall` and
|
|
3287
|
+
`calendarWindowsMobile` include their record declarations and seeds. Search
|
|
3288
|
+
those names in the example index. Their bindings become the UI task's
|
|
2371
3289
|
read/write manifest.
|
|
2372
3290
|
|
|
3291
|
+
A record too large to read whole is read the Plan's way (#1424): a resource
|
|
3292
|
+
kind may be paged — `Calendar.resources(Data.bindPaged(record), …)` — and an
|
|
3293
|
+
event kind placed on it is read a cell at a time through a series index and
|
|
3294
|
+
shaded from a summary index, its source listing both beside its interval
|
|
3295
|
+
index; every other kind is counted before one of its events is read, and none
|
|
3296
|
+
past 10,000 over the window is read or drawn. Its Backlog is paged through
|
|
3297
|
+
each kind's source, a page at a time; nothing reads a record whole:
|
|
3298
|
+
|
|
3299
|
+
```tsx
|
|
3300
|
+
// Package side, among the jobs record's indexes, beside its interval index (the e3 skill):
|
|
3301
|
+
// jobs_by_press_week: index.series({ key: "press", from: "start", to: "end", tile: "week" }),
|
|
3302
|
+
// jobs_by_press_month: index.summary({ key: "press", from: "start", to: "end", tile: "month" }),
|
|
3303
|
+
// Inside Reactive: 2,000 presses a task generates, paged 200 to a window; the jobs a cell at a time.
|
|
3304
|
+
const presses = $.let(Data.bindPaged(pressesTask));
|
|
3305
|
+
const jobs = $.let(Record.bind(jobsRecord));
|
|
3306
|
+
const entries = $.let(Data.bindPaged(jobsRecord));
|
|
3307
|
+
const slice = $.let(Slice.bindRecords("ops.calendar", {
|
|
3308
|
+
job: { source: Data.bindPaged(jobsRecord, { indexes: ["jobs_by_time", "jobs_by_press_week", "jobs_by_press_month"] }), range: "start" },
|
|
3309
|
+
}));
|
|
3310
|
+
return <Calendar slice={{ slice }} inspector library={[Calendar.library.backlog()]}
|
|
3311
|
+
view={{ layout: "timeline", period: "week", date: new Date("2026-10-05T00:00:00Z") }}
|
|
3312
|
+
resources={{ presses: Calendar.resources(presses, { name: "Presses", icon: "print", label: p => p.name, meta: p => some(p.hall) }) }}
|
|
3313
|
+
events={{ job: Calendar.events(jobs, {
|
|
3314
|
+
name: "Print job", icon: "file-lines", title: "title", start: "start", end: "end",
|
|
3315
|
+
resource: { field: "press", of: "presses" }, entries,
|
|
3316
|
+
backlog: { duration: j => variant("hours", j.sheets.divide(8000.0)), due: j => j.due },
|
|
3317
|
+
}) }} />;
|
|
3318
|
+
```
|
|
3319
|
+
|
|
2373
3320
|
| Signature | Description | Example |
|
|
2374
3321
|
| --- | --- | --- |
|
|
2375
|
-
| `<Calendar events resources? slice? library? inspector? view? hours? week? density? now? readOnly? canDrop? onSelect? id? />` **❗** | One shared frame: a toolbar with the time navigator, the rail over its `Slice.bindRecords` and its summary, and the history; the schedule; the shared footer rail. Calendar day/week/month, Resources day, Timeline day/week/month. Panes are optional. Empty event maps, invalid working hours and duplicate library tab names are refused. | `calendarOperations`, `calendarMinimal` |
|
|
3322
|
+
| `<Calendar events resources? slice? library? inspector? view? hours? week? density? now? readOnly? canDrop? optimise? onSelect? id? />` **❗** | One shared frame: a toolbar with the time navigator, the rail over its `Slice.bindRecords` and its summary, and the history; the schedule; the shared footer rail. Calendar day/week/month, Resources day, Timeline day/week/month. Panes are optional. Empty event maps, invalid working hours and duplicate library tab names are refused. | `calendarOperations`, `calendarMinimal` |
|
|
2376
3323
|
| `Calendar.events(record, config)` **❗** | `Schedule.events`: span fields, resource references, status cases, shared `field` editors, `ready`, custom `inspector`, overlap policy and record patch writes. Narrowed through its source of `slice`, named after its slot, it gives `entries: Data.bindPaged(record)` beside it for its original-key reads. A kind's own `slice` or `filter`, and `window` / `backlogWindow` beside a source, are refused, naming the change. Whole-row edits use one EditSession per kind and one shared EditHistory. | `calendarOperations` |
|
|
2377
|
-
| `slice={{ slice: Slice.bindRecords(key, { [kind]: { source, fields, range: "start" }, [resources]: …, [templates]: … }), affordances?, persist? }}` **❗** | One rail, a source per record (#1343). An event kind's reads its events in the window and its backlog through
|
|
2378
|
-
| `Calendar.resources(rows, config)` | `Schedule.resources` over a dictionary, usually a record bound with `Data.bind
|
|
3324
|
+
| `slice={{ slice: Slice.bindRecords(key, { [kind]: { source, fields, range: "start" }, [resources]: …, [templates]: … }), affordances?, persist? }}` **❗** | One rail, a source per record (#1343). An event kind's reads its events in the window through an interval index — the window's rows alone — and the Backlog tab pages its backlog through it, its start Unset, never with the window (#1424); a draft draws where it places its event, so a backlog event scheduled shows where it lands, and the draft of an event the kind's narrowing does not hold draws nowhere. A resource kind's narrows its columns, the timeline's rows and the resource Choosers, which keep a placement's own resource and the resources an event's fields name; a paged kind's pages the narrowing's resources in its index's order. A templates list's and an app tab's narrow their cards. One date window: the first event source with a range leads, its range the time navigator's Range panel, never a chip of the rail, and navigation writes the window into every event source. Overlaps, checks and commands read every event and every resource. `Slice.bind`, a single `Slice.bindRecord` and a source nothing names are refused. | `calendarOperations`, `calendarRecordIndexes` |
|
|
3325
|
+
| `Calendar.resources(rows, config)` | `Schedule.resources` over a dictionary, usually a record bound with `Data.bind`, read whole. | `calendarOperations` |
|
|
3326
|
+
| `Calendar.resources(Data.bindPaged(record), { name, icon, label, meta? })` + a kind's source listing `index.series` and `index.summary` beside its interval index **❗** | A resource kind paged a window at a time (#1424): 200 resources to a window, the timeline's rows and the Resources view's columns the pages around the view — never every resource read — and each Chooser seeking a resource by its key through the paged read (`Press 1460 · P-1460 · Hall 4`). Where a view lays its resources out, an event kind placed on it is read a cell at a time — the events of the blocks of fifty resources around those in view, over the tiles the window touches, a range a resource a level (#1433), two requests a change — and past 10,000 events in the cells wanted shades each resource from the summary, the share of each period booked; the time grids and the agenda count it over the window first. A source named after the paged kind's slot (`Data.bindPaged(record, { indexes })` over its own record) pages the narrowing's resources in its index's order, the last view standing, retained, until its windows land. A kind on it whose source lists its interval index alone, or a summary and no series, one read whole or through its day index, and one placed beside another resource kind, are refused, naming both declarations over the kind's own fields. | `calendarWindows`, `calendarWindowsByHall`, `calendarWindowsMobile` |
|
|
3327
|
+
| every other event kind, past 10,000 events over the window | Counted before one of its events is read — through its source's interval index, through its day index (the days its events run on, an upper bound) or in memory — and none of them read or drawn but its drafts: a resource's row or column shades from its source's summary at the view's period, else is hatched, the banner says how many events there are and where they show, and every figure that would count them says they are not counted, never 0. | `calendarRecordIndexes`, `calendarWindowed` |
|
|
2379
3328
|
| `Calendar.templates(rows, { name, group?, at?, duration, values, source })` **❗** | A templates record, bound with `Data.bind`, its `source` the rail's source over it, whose shown keys filter the Templates tab; typed `values` supplies exactly the event fields other than its scheduling roles. A list written in the code, one naming no source of the slice, and templates on a Calendar with no slice are refused, naming the change. Template changes do not rewrite existing event rows. | `calendarOperations` |
|
|
2380
|
-
| `Calendar.library.templates()` / `.backlog()` / `.tab(rows, { …, source? })` **❗** | Shared Library cards and search, each tab following its sources: it counts what it shows (`5 of 6`) and, when the rail leaves it none, says so with Clear filters for them. The Backlog counts `N of M`, what its kinds' narrowing shows beside the stored events their choices hide
|
|
3329
|
+
| `Calendar.library.templates()` / `.backlog()` / `.tab(rows, { …, source? })` **❗** | Shared Library cards and search, each tab following its sources: it counts what it shows (`5 of 6`) and, when the rail leaves it none, says so with Clear filters for them. The Backlog counts `N of M`, what its kinds' narrowing shows beside the stored events their choices hide — `…` until its counts are read, never a bare `Backlog` — and its Clear filters keeps each window. A kind narrowed through its source has its backlog paged through it (#1424), a page a request, its count the index's: never every unscheduled event read, no step of the dates reads it again, and a new narrowing keeps its cards and count standing, retained, until its own land. That paged list has no search of its own, as the Schematic's Backlog has none: the rail's choices narrow it. An author's tab names the source it follows (`source`) and may apply `Calendar.patch(RowType, fields)` to a compatible selected event. Invalid or ambiguous patch row types, and a `source` the slice lacks, are refused. | `calendarOperations` |
|
|
2381
3330
|
| the summary and the footer | The rail's summary names each source a narrowing hides rows of, in its own words (`6 of 10 production events`), and folded, the Filters popover heads each source's fields with its count (`4`, `6 of 10`, #1358); the window's own event count shows only with no slice. After a Save, a notice per event kind says, in its words, what left its narrowing. | `calendarOperations` |
|
|
2382
3331
|
| `Calendar.days(start, end)` / `.unscheduled(start, due)` | UTC half-open day keys and the unscheduled index key, for a kind with no source of `slice`: supply paged `window`, `backlogWindow` when applicable, and `entries` for selection and draft baselines by key. | `calendarWindowed` |
|
|
3332
|
+
| `optimise={Func.bind(fn: FunctionDef<[Calendar.Types.OptimiseRequest(E, S)], Calendar.Types.OptimiseResult(E)>)}` **❗** | The app's optimiser (#1409): `E` and `S` the event and resource kinds' row types by slot (`Calendar.Types.OptimiseRequest({ job: Job }, { machines: Machine })`). The Elara eye in the toolbar, immediately before the history item, names the window (`Optimise Sep 28 – Oct 4`): one call per run with the window, the run's number, every event kind's rows in the window and the backlog by key as the session holds them — base plus every unapplied draft, every row whatever the rail shows — and every resource kind's rows read whole; the history and every gesture locked while it is out, a change of period stopping it. Its answer — the events complete — is diffed by key and recorded as ONE step: Undo in one, Redo replays the landing, Save commits it with everything else. A backlog event given a time is an add on the canvas, its Backlog count flashing; a moved event a change with a ghost at its old slot; one taken back a remove; each marked until Apply with its provenance on hover. A result equal to the request records nothing and says so; a failed call is a banner until the next run. | `calendarOptimise` |
|
|
2383
3333
|
| `readOnly` | Same layouts, filters, selection and overlaps, without sessions, mutation actions or editing callbacks. | `calendarReadOnly` |
|
|
2384
3334
|
| Narrow layout | Below 480px of main-panel width, chronological agenda cards replace the grid. Create, Schedule, Move, Resize, Duplicate, Delete, Return to backlog and author-card actions use shared anchored forms and the same commands as desktop gestures. No dragging. Desktop view, date, filters, selection and drafts survive resizing. | `calendarMobile` |
|
|
2385
3335
|
|
|
@@ -2412,10 +3362,14 @@ times; teams:
|
|
|
2412
3362
|
```tsx
|
|
2413
3363
|
// Module level: the app's own duties; its workers (name, team), shift times (name, code, starts, hours, order) and teams likewise.
|
|
2414
3364
|
export const Duty = StructType({ person: StringType, start: DateTimeType, end: DateTimeType, shift: StringType, team: StringType });
|
|
2415
|
-
export const duties = e3.record("duties", DictType(StringType, Duty),
|
|
2416
|
-
|
|
3365
|
+
export const duties = e3.record("duties", DictType(StringType, Duty), {
|
|
3366
|
+
initial: new Map(),
|
|
3367
|
+
mutations: (mutation) => ({
|
|
3368
|
+
patch: mutation.patch(),
|
|
3369
|
+
}),
|
|
3370
|
+
});
|
|
2417
3371
|
// Inside Reactive:
|
|
2418
|
-
const rota = $.let(Record.bind(duties
|
|
3372
|
+
const rota = $.let(Record.bind(duties));
|
|
2419
3373
|
const staff = $.let(Data.bind(workers));
|
|
2420
3374
|
const times = $.let(Data.bind(shiftTimes));
|
|
2421
3375
|
const groups = $.let(Data.bind(teams));
|
|
@@ -2436,18 +3390,18 @@ its check, Publish, one rail and its fit (`rosterWarehouse`):
|
|
|
2436
3390
|
|
|
2437
3391
|
```tsx
|
|
2438
3392
|
// Module level: the warehouse's duty, WarehouseDuty — who (a staff key, or an agency's), start, end, shift, team, role, task,
|
|
2439
|
-
// unpaidBreak, agreed, costCentre and note — and its records: rosterDuties (
|
|
3393
|
+
// unpaidBreak, agreed, costCentre and note — and its records: rosterDuties (its interval index roster_duties_by_time), rosterWeeks, rosterCover, ….
|
|
2440
3394
|
// Inside Reactive: what the Roster writes, each through its patch door.
|
|
2441
|
-
const rota = $.let(Record.bind(rosterDuties
|
|
3395
|
+
const rota = $.let(Record.bind(rosterDuties));
|
|
2442
3396
|
const entries = $.let(Data.bindPaged(rosterDuties)); // one duty by its key
|
|
2443
|
-
const byTime = $.let(Data.bindPaged(rosterDuties, { indexes: [
|
|
2444
|
-
const weeks = $.let(Record.bind(rosterWeeks
|
|
2445
|
-
const cover = $.let(Record.bind(rosterCover
|
|
3397
|
+
const byTime = $.let(Data.bindPaged(rosterDuties, { indexes: ["roster_duties_by_time"] })); // index.interval({ from: "start", to: "end" })
|
|
3398
|
+
const weeks = $.let(Record.bind(rosterWeeks));
|
|
3399
|
+
const cover = $.let(Record.bind(rosterCover));
|
|
2446
3400
|
// What it reads whole — every person and activity is the calculation scope — and their indexes for the rail.
|
|
2447
3401
|
const people = $.let(Data.bind(rosterStaff));
|
|
2448
3402
|
const acts = $.let(Data.bind(rosterActivities));
|
|
2449
|
-
const staffIndex = $.let(Data.bindPaged(rosterStaff, { indexes: [
|
|
2450
|
-
const actIndex = $.let(Data.bindPaged(rosterActivities, { indexes: [
|
|
3403
|
+
const staffIndex = $.let(Data.bindPaged(rosterStaff, { indexes: ["roster_staff_by_team_name", "roster_staff_by_skill"] }));
|
|
3404
|
+
const actIndex = $.let(Data.bindPaged(rosterActivities, { indexes: ["roster_activities_by_group"] }));
|
|
2451
3405
|
const slice = $.let(Slice.bindRecords("ops.roster", {
|
|
2452
3406
|
// The week's duties: the Roster's navigation writes the range; no chip picks the week.
|
|
2453
3407
|
duty: { source: byTime, range: "start" },
|
|
@@ -2569,25 +3523,28 @@ export const rosterStaffFit = e3.function("roster_staff_fit", East.function([Ros
|
|
|
2569
3523
|
| Signature | Description | Example |
|
|
2570
3524
|
| --- | --- | --- |
|
|
2571
3525
|
| **Builder and records** | | |
|
|
2572
|
-
| `<Roster events resources shifts groups periods? targets? positions? activities? proposals? pay? currency? rules? check? fit? slice? library? inspector? view? week? density? chipSkills? requirements? readOnly? id? />` **❗** | Shifts day
|
|
2573
|
-
| `events={{ duty: Schedule.events(Record.bind(duties
|
|
3526
|
+
| `<Roster events resources shifts groups periods? targets? positions? activities? proposals? patterns? pay? currency? rules? check? fit? optimise? slice? library? inspector? view? tileTimes? letters? week? density? chipSkills? requirements? readOnly? id? />` **❗** | Shifts by day or week, People by week or month (#1406). Refused at build, each naming the change: a removed prop (below); `events` holding other than one kind; `resources` other than `people` and `agencies`; a duty kind whose resource names no `people`; a resource kind read a page at a time; a `people` kind with no `group`; `positions` beside a duty kind with no `position` role; `pay` without `currency`; a `pay`, a `check` or a `fit` over another row type; a `view.period` its layout does not take (Shifts takes `"day"` or `"week"`, People `"week"` or `"month"`); `Roster.library.patterns()` without `patterns`; and a library tab listed twice. The renderer validates live configuration. | `rosterWarehouse`, `rosterMinimal`, `rosterPeopleMonth` |
|
|
3527
|
+
| `events={{ duty: Schedule.events(Record.bind(duties), { start, end, resource, shift, group, position?, activity?, hours?, fields?, inspector?, ready?, entries? }) }}` **❗** | Exactly one kind, the duties: a span kind of plain DateTime times, keyed by String. `resource` names a `people` key, or a variant whose cases name `people` and `agencies`: a person, or an agency asked to send someone. `hours` is what a duty counts for coverage, skill hours, contract hours, rest and pay; its length when omitted. The inspector shows a duty's times, position and activity, then the kind's form over the fields no role writes (`fields` hints) or the kind's own `inspector`; `ready` is the app's check on a draft. | `rosterWarehouse`, `rosterCustomInspector` |
|
|
2574
3528
|
| `resources={{ people: Schedule.resources(rows, { name, icon, label, group, position?, skills?, contract?, agency?, trainer?, trainee?, usual? }), agencies?: Schedule.resources(rows, { name, icon, label }) }}` | Every person and agency, read whole: the calculation scope. A person's key is what a duty's resource holds; `group` is their home group. | `rosterWarehouse` |
|
|
2575
|
-
| `periods={Schedule.periods(Record.bind(weeks
|
|
2576
|
-
| `targets={Roster.targets(Record.bind(cover
|
|
2577
|
-
| `Roster.shifts(rows, { label, code, start, hours, order? })` · `Roster.groups(rows, { label, order? })` · `Roster.positions(rows, { label, code, lead, order? })` **❗** | The lists the Roster reads over the app's rows, each a `Dict` keyed by String, in its `order`, then key order. A shift's `start` is a `Schedule.Types.Clock` and its `hours` what a new duty runs and a skill holder's capacity;
|
|
3529
|
+
| `periods={Schedule.periods(Record.bind(weeks), { every: "week", status, dismissed?, entries? })}` | The app's weeks, each keyed by its first day by the Roster's week start: the status chip, Publish and the picker's Published · Draft · Not started. `dismissed` keeps the proposals a week rejected; without it, a proposal offers Accept only. Without `periods`, no Publish. | `rosterWarehouse`, `rosterPublished` |
|
|
3530
|
+
| `targets={Roster.targets(Record.bind(cover), { day, group, shift, positions, hours, inspector?, entries? })}` **❗** | The app's cover targets, a row per slot, which its forecast fills: `day` a DateTime (a UTC midnight), `group` and `shift` Strings, `positions` an Integer, `hours` a `Dict<String, Float>` by activity. A slot with no row has no target; a target edit on one inserts a row under a minted key, its other fields their type's default. `inspector` is the app's own form for one target. | `rosterWarehouse`, `rosterCustomInspector` |
|
|
3531
|
+
| `Roster.shifts(rows, { label, code, start, hours, order?, color? })` · `Roster.groups(rows, { label, order? })` · `Roster.positions(rows, { label, code, lead, order? })` **❗** | The lists the Roster reads over the app's rows, each a `Dict` keyed by String, in its `order`, then key order. A shift's `start` is a `Schedule.Types.Clock` and its `hours` what a new duty runs and a skill holder's capacity; its `color` (#1406) returns an `Option<ColorToken>` — east-ui's `ColorTokenType`, `Style.ColorToken("teal", "500")` — the People week's tiles' and the month's marks' fill, resolved through the theme, the same in both colour modes, the words on it in the ink the shade takes; `none`, neutral. A position that leads satisfies the lead rule, which needs the duty kind's `position` role. An accessor returning another type is refused, naming what it returns (`color`, naming `ColorTokenType`). | `rosterWarehouse`, `rosterMinimal`, `rosterPeople` |
|
|
2578
3532
|
| `Roster.activities(rows, { label, code?, group?, order? })` | Every activity from a keyed activities record (`Roster.Types.Activity` rows, or the app's own through the accessors): a group's skills are the activities of that group, and an activity of no group is a duty every group may assign. Coverage and checks read every one. | `rosterWarehouse` |
|
|
2579
3533
|
| `Roster.proposals(rows, { person, day, group, shift, reason })` | The app's suggestions, a person for a slot, each keyed by its row's key: Accept places the duty and keeps the key in the week's `dismissed`, one step; Reject keeps it there alone. A stale or already rostered proposal is hidden. | `rosterWarehouse` |
|
|
3534
|
+
| `patterns={Roster.patterns(rows, { name, icon?, label, days, order? })}` **❗** | The app's week patterns (#1406), a keyed record read through accessors as the other lists are: `label` the card's name and the drop's words; `days` seven `Option<String>` shift keys from the week's first day, `none` a day off; `order` where it shows. `name` titles the tab `Roster.library.patterns()` lists them under — required, as `Schedule.events`' `name` is — and `icon` is its cards' Font Awesome solid icon (`calendar-week` by default). A pattern dropped on a person's week — a row's day in Week, a cell in Month, or chosen through a phone card's `Apply pattern…` — replaces its seven days as one `Apply pattern` step: a day it changes runs the pattern's shift in the person's home group at the shift's own times, position kept, activity kept within the group and cleared across one; a day off removes the duties, struck through until Save; a day it leaves as it is keeps its duty. The caption names the pattern, the person and the first warning (`Early · Mon–Fri → A. Alder · 6 h rest`), `Already matches …` where nothing changes, or the refusal: a published or read-only week, the Agency requests row (`Drop a pattern on a person's week.`), a shift not configured. A pattern with other than seven days or naming a shift the Roster has not configured is listed greyed with the reason and dragged by no one. Without `patterns` the People layout is as it is; the slice may narrow the tab through a `patterns` source. | `rosterPeople`, `rosterPeopleMonth` |
|
|
3535
|
+
| `view={{ layout?, period?, date? }}` · `tileTimes="always" \| "different"` · `letters` | The first view (#1406): Shifts takes `"day"` (default) or `"week"`, People `"week"` (default) or `"month"`; each layout keeps its own period per viewer, and ‹ › and `[` `]` step it. A month is the four to six weeks touching it, a column each, from the configured week start; a week cell holds seven marks in the shifts' colours, lettered where a day is 13px or wider while `letters` is on (`true` by default); a column's header opens its week in Week. `tileTimes` says whether a tile shows the shift's hours (`06–14`) always (default) or only a duty's own times where they differ. Below 480px of main People offers Week alone, and a persisted month opens on its first week. | `rosterPeople`, `rosterPeopleMonth` |
|
|
2580
3536
|
| `slice={{ slice: Slice.bindRecords(key, { <duty slot>?, targets?, people?, activities?, <a tab's source>? }) }}` **❗** | One rail. The duty kind's source reads the week's duties through an interval index from `start` to `end`, `range: "start"`, the range the Roster's own navigation writes — no chip picks the week — the kind given `entries`; `targets` reads the week's targets the same way, `Roster.targets` given `entries`; `people` narrows the grid's and the People layout's people, the People tab and the person choices; `activities` the Activities tab and the activity choices. Without the duty source the duties are read whole. Coverage, pay and rules read every duty of the week and every person; filters never drop hidden drafts from Save. | `rosterWarehouse`, `rosterWindowed`, `rosterRecordIndexes` |
|
|
2581
3537
|
| **Pay, checks and fits** | | |
|
|
2582
3538
|
| `pay={fn(Roster.Types.PayRequest(R)) => Roster.Types.Pay}` + `currency` **❗** | The app's pay in its own codes: asked once per person with that person's duties for the week — `{ who, start, duties, context }` — and once per agency with its requests, each duty's cost by code back; asked again only for those whose duties, or whose duties' rows, changed. The Roster sums the costs by duty, slot, day, week, person and code, printed in `currency`; a call that throws is a warning banner naming the person, whose duties show no cost. Without `pay`, no costs show. | `rosterWarehouse` |
|
|
2583
3539
|
| `check={fn(Roster.Types.CheckContext(R)) => Array<Roster.Types.Issue>}` | The app's issues after the built-in ones, over `{ start, week, context, coverage, pay }`: each assignment carries its duty's own row (`a.row`), so a check reads the app's own fields. | `rosterWarehouse` |
|
|
2584
3540
|
| `fit={Func.bind(fn: FunctionDef<[Roster.Types.FitRequest(R)], Roster.Types.FitResult>)}` **❗** | The app's fit, beside `check`. A Find fix asks it who fits its issue's need on its slot, over the drafted week and the complete staff, whatever the rail shows; the People tab shows its fits among what the rail shows, best first, each card with the fit's reason, under a `Find · Leads` chip that clears it. One question is in flight at a time, answers are kept by question and drafts, and a find asks again when the drafts change. While asked, the fix reads `Finding…`; a failure is a warning banner, `Fits couldn't be checked`, with Retry. Omitted, no Find fix is offered, though `Roster.check` still reports the issue. A fit bound over another row type is refused, naming both. | `rosterWarehouse`, `rosterRecordIndexes` |
|
|
3541
|
+
| `optimise={Func.bind(fn: FunctionDef<[Roster.Types.OptimiseRequest(R)], Roster.Types.OptimiseResult(R)>)}` **❗** | The app's optimiser (#1409). The Elara eye in the toolbar — between the issues item and the history item, as tall as the row's other controls, and at a phone's width, where the row's floor needs its room once the rail's icon has dropped its caret, the View's entry beside the issues, never out of the toolbar: `Stop optimising` while a run is out, off and saying why (`Read-only`) while the week's reads are in flight; listed only where the week can be optimised, never in Month — names the week (`Optimise W11`): one call per run with the week (`from`, `to`), the run's number, the week's duties by key as the session holds them — base plus every unapplied draft, every row whatever the rail shows — its targets by slot and `Roster.Types.Context`, the complete staff and configuration; whatever the layout. The history and every gesture are locked while it is out; a change of week stops it. Its answer — the duties complete — is diffed by key and recorded as ONE step: Undo in one, Redo replays the landing, Save commits it with everything else. Off → shift is an add, shift → another a change, shift → Off a remove struck on its tombstone for the highlight, each marked on the People tiles and the Shifts chips alike with its provenance on hover (`Was Mon Early · Elara run #7`); the changed people's hours and meter and the issues item flash. A result equal to the request records nothing and says so. | `rosterOptimise` |
|
|
2585
3542
|
| **Library and inspector** | | |
|
|
2586
|
-
| `Roster.library.people()` / `.activities()` | Shared Library cards, avatars, search and explicit placement, each tab following its source of the rail: it counts what it shows and, emptied, says so with Clear filters. Neither has a filter of its own: leads, trainers, agency and skill are rail fields of the staff record, and a Find fix's result — the app's fit's answer — is shown in the People tab, with a chip that clears it. Named staff and unnamed agency requests stay distinct. | `rosterWarehouse`, `rosterMobile` |
|
|
3543
|
+
| `Roster.library.people()` / `.activities()` / `.patterns()` | Shared Library cards, avatars, search and explicit placement, each tab following its source of the rail: it counts what it shows and, emptied, says so with Clear filters. Neither People nor Activities has a filter of its own: leads, trainers, agency and skill are rail fields of the staff record, and a Find fix's result — the app's fit's answer — is shown in the People tab, with a chip that clears it. Named staff and unnamed agency requests stay distinct. `patterns()` (#1406) lists the app's week patterns under the declaration's `name`, each card its name, `5 shifts · 40 h` and a strip of seven marks in the shifts' colours under a header of the days' initials, dragged onto a person's week; refused without `patterns`. | `rosterWarehouse`, `rosterMobile`, `rosterPeople` |
|
|
2587
3544
|
| `Roster.library.tab(rows, { name, icon?, label, meta?, group?, drop?, source? })` **❗** | Reified typed card accessors over a bound dictionary, following the rail's `source` it names. `drop(row, key)` returns `Schedule.patch(DutyRowType, { … })`: the duty's own fields a card dropped on it sets, as Plan's tabs do. A card never replaces a duty's person. A `source` the slice lacks is refused. | `rosterWarehouse` |
|
|
2588
3545
|
| `inspector` | The end pane: a duty's times, position and activity, its kind's form or own `inspector`, and its costs by code; a slot's coverage and its targets, or `Roster.targets`' `inspector`. Every edit is a command of the same session as a drag or a card's action. | `rosterWarehouse`, `rosterCustomInspector` |
|
|
2589
3546
|
| **Domain functions and values** | | |
|
|
2590
|
-
| `Roster.Types.{Week(R), Assignment(R), PayRequest(R), CheckContext(R), FitRequest(R), Pay, Status, Slot, Who, Requirements, Requirement, Person, Activity, Group, Skill, Shift, Position, Agency, Rules, Context, Coverage, CoverageLine, Proposal, Issue, Fix, Need, FitResult, Fit, Refusal, Clock}` | The Roster's reading of the app's rows, carried in requests and stored nowhere. Each `(R)` is a constructor of the app's duty row type: the same `R`, the same East type. An `Assignment(R)` is `{ slot, who, position, activity, start, end, hours, row }`, `row` the duty's own; a `Week(R)` `{ status, assignments, requirements, dismissed }`, its status Schedule's period status (`draft`, `published { at }` or `other`). `Pay` is duty key → pay code → cost. A `Need` (leads, trainers, a skill) is a Find fix's; a `FitResult` `{ need, slot, people: Dict<String, Fit> }` echoes its question. `Fit` and `Refusal` are `Schedule.Types`', the Allocation's too. | `rosterWarehouse`, `rosterCoverage` |
|
|
3547
|
+
| `Roster.Types.{Week(R), Assignment(R), PayRequest(R), CheckContext(R), FitRequest(R), OptimiseRequest(R), OptimiseResult(R), Pay, Status, Slot, Who, Requirements, Requirement, Person, Activity, Group, Skill, Shift, Position, Agency, Rules, Context, Coverage, CoverageLine, Proposal, Issue, Fix, Need, FitResult, Fit, Refusal, Clock}` | The Roster's reading of the app's rows, carried in requests and stored nowhere. Each `(R)` is a constructor of the app's duty row type: the same `R`, the same East type. An `Assignment(R)` is `{ slot, who, position, activity, start, end, hours, row }`, `row` the duty's own; a `Week(R)` `{ status, assignments, requirements, dismissed }`, its status Schedule's period status (`draft`, `published { at }` or `other`). `Pay` is duty key → pay code → cost. A `Need` (leads, trainers, a skill) is a Find fix's; a `FitResult` `{ need, slot, people: Dict<String, Fit> }` echoes its question. `Fit` and `Refusal` are `Schedule.Types`', the Allocation's too. | `rosterWarehouse`, `rosterCoverage` |
|
|
2591
3548
|
| `Roster.coverage(R): (week, context, pay) => Dict<Slot, Coverage>` | Positions, named people, permanent, agency and requested hours — each duty's own hours — skill capacity and assigned hours, leads and trainers for every configured slot; its cost the app's pay summed, zero without it. Requests fill positions but never supply skills or a lead. | `rosterCoverage` |
|
|
2592
3549
|
| `Roster.check(R): (week, context) => Array<Issue>` | The built-in checks: missing lead, double booking, within-week rest, trainee without trainer, skill compatibility, open positions and skill gaps, each with its fix — a rest breach's moves the duty to another shift of its day, or to a later start. | `rosterChecks` |
|
|
2593
3550
|
| `Roster.fit(R): (request, person) => Fit` | The ready-made rule: refused `Not a lead` / `Not a trainer` / `Not skilled` without the need, `Rostered` that day, `Rest too short` (its measure the rest); else it fits, scored by spare contract hours this week and a week's 168 more in the slot's group, its reason `Dispatch · 14 h spare` or `from Inbound · 2 h over contract`. | `rosterFitTrainer` |
|
|
@@ -2615,10 +3572,18 @@ and a lost response retries the same request. Drafts of every week stay in
|
|
|
2615
3572
|
their sessions, and Save includes them. Publish saves first, then writes the
|
|
2616
3573
|
week's status once every record reads back as saved; a failed save leaves the
|
|
2617
3574
|
week a draft, its drafts kept. A published week is read-only in every layout.
|
|
2618
|
-
|
|
3575
|
+
The People week (#1406) is a row a person under their group's band: the name
|
|
3576
|
+
cell with the week's hours against the contract (and the cost, with `pay`)
|
|
3577
|
+
over a meter, then a tile a duty in each day cell — the shift's label, its
|
|
3578
|
+
hours by `tileTimes`, a duty's own times where they differ or the group where
|
|
3579
|
+
it is not the person's, a warning mark where an issue names it — `+1` for a
|
|
3580
|
+
second duty the same day, and `+ Assign` on a day off, a menu of the shifts
|
|
3581
|
+
whose choice places the person as one gesture; a name selects the person-week,
|
|
3582
|
+
which the inspector's Details read. Below 480px of **main** width, Shifts and
|
|
3583
|
+
People use cards with explicit
|
|
2619
3584
|
Assign/Move/Edit/Remove/Restore/Request/Accept/Reject/target actions and no
|
|
2620
|
-
drag
|
|
2621
|
-
pattern.
|
|
3585
|
+
drag; a People card gains `Apply pattern…` where `patterns` are given. The
|
|
3586
|
+
`rosterMobile` example demonstrates the complete inline authoring pattern.
|
|
2622
3587
|
|
|
2623
3588
|
Removed with #1368, each still written refused at build, naming its
|
|
2624
3589
|
replacement: `weeks` and `window` (`events={{ <slot>: Schedule.events(…) }}`
|
|
@@ -2639,8 +3604,9 @@ fields, the app's `pay` and its `check`). `Roster.Types.Weeks` and
|
|
|
2639
3604
|
weeks and targets records. `visiblePeople` and `duties` stay refused (#1342).
|
|
2640
3605
|
|
|
2641
3606
|
The runnable examples `rosterMinimal`, `rosterWarehouse`, `rosterMobile`,
|
|
2642
|
-
`rosterPeople`, `
|
|
2643
|
-
`rosterCustomInspector`, `rosterRecordIndexes` and
|
|
3607
|
+
`rosterPeople`, `rosterPeopleMonth`, `rosterWeek`, `rosterWindowed`,
|
|
3608
|
+
`rosterPublished`, `rosterCustomInspector`, `rosterRecordIndexes` and
|
|
3609
|
+
`rosterRecordIndexesMobile`
|
|
2644
3610
|
include their record declarations and seeds; `rosterCoverage`, `rosterChecks`,
|
|
2645
3611
|
`rosterWeeklyHours`, `rosterRest`, `rosterFitTrainer` and `rosterAnswerLeads`
|
|
2646
3612
|
run the pure functions over a small staff. Search those names in the example
|
|
@@ -2672,10 +3638,14 @@ export const Bin = StructType({
|
|
|
2672
3638
|
bay: StringType, at: DateTimeType, load: OptionType(StringType),
|
|
2673
3639
|
m3: FloatType, kilns: DictType(StringType, FloatType), dryFor: TimeStepType,
|
|
2674
3640
|
});
|
|
2675
|
-
export const bins = e3.record("bins", DictType(StringType, Bin),
|
|
2676
|
-
|
|
3641
|
+
export const bins = e3.record("bins", DictType(StringType, Bin), {
|
|
3642
|
+
initial: new Map(),
|
|
3643
|
+
mutations: (mutation) => ({
|
|
3644
|
+
patch: mutation.patch(),
|
|
3645
|
+
}),
|
|
3646
|
+
});
|
|
2677
3647
|
// Inside Reactive:
|
|
2678
|
-
const rows = $.let(Record.bind(bins
|
|
3648
|
+
const rows = $.let(Record.bind(bins));
|
|
2679
3649
|
const loads = $.let(Data.bind(arrivals));
|
|
2680
3650
|
const yard = $.let(Data.bind(kilns));
|
|
2681
3651
|
const bays = $.let(Data.bind(yardBays));
|
|
@@ -2699,17 +3669,17 @@ own fields, one rail and its library (`allocationKilnYard`):
|
|
|
2699
3669
|
|
|
2700
3670
|
```tsx
|
|
2701
3671
|
// Inside Reactive: what the board writes, each through its patch door.
|
|
2702
|
-
const bins = $.let(Record.bind(kilnBins
|
|
3672
|
+
const bins = $.let(Record.bind(kilnBins));
|
|
2703
3673
|
const entries = $.let(Data.bindPaged(kilnBins)); // one bin by its key
|
|
2704
|
-
const byTime = $.let(Data.bindPaged(kilnBins, { indexes: [
|
|
2705
|
-
const days = $.let(Record.bind(yardDays
|
|
3674
|
+
const byTime = $.let(Data.bindPaged(kilnBins, { indexes: ["kiln_bins_by_time"] })); // index.interval({ from: "at", to: "at" })
|
|
3675
|
+
const days = $.let(Record.bind(yardDays));
|
|
2706
3676
|
// What it reads whole — every bay, kiln and load is the calculation scope — and their indexes for the rail.
|
|
2707
3677
|
const yard = $.let(Data.bind(kilns));
|
|
2708
3678
|
const loads = $.let(Data.bind(arrivals));
|
|
2709
3679
|
const bays = $.let(Data.bind(yardBays));
|
|
2710
|
-
const bayIndex = $.let(Data.bindPaged(yardBays, { indexes: [
|
|
2711
|
-
const kilnIndex = $.let(Data.bindPaged(kilns, { indexes: [
|
|
2712
|
-
const loadIndex = $.let(Data.bindPaged(arrivals, { indexes: [
|
|
3680
|
+
const bayIndex = $.let(Data.bindPaged(yardBays, { indexes: ["yard_bays_by_area"] }));
|
|
3681
|
+
const kilnIndex = $.let(Data.bindPaged(kilns, { indexes: ["kilns_by_group"] }));
|
|
3682
|
+
const loadIndex = $.let(Data.bindPaged(arrivals, { indexes: ["arrivals_by_spec"] }));
|
|
2713
3683
|
// One rail, a source named after each slot it narrows.
|
|
2714
3684
|
const slice = $.let(Slice.bindRecords("ops.allocation", {
|
|
2715
3685
|
bin: { source: byTime, range: "at" }, // the day's bins: the board's navigation writes the range
|
|
@@ -2783,11 +3753,11 @@ export const yardFit = e3.function("yard_fit", East.function([Allocation.Types.F
|
|
|
2783
3753
|
| Signature | Description | Example |
|
|
2784
3754
|
| --- | --- | --- |
|
|
2785
3755
|
| **Builder and records** | | |
|
|
2786
|
-
| `<Allocation events resources arrivals fit periods? every? check? columns? slice? rules? library? inspector? view? horizon? title? readOnly? labels? table? id? />` **❗** | One toolbar row at every width — the day's navigator, its picker marking each day Published · Draft · Not started; Table · Timeline; the status chip; the focus; the rail and its count; the issues; the history and Publish — folding into one View in the narrowest frame; then the Timeline or the Table, the panes the props ask for, and the footer's legend. Refused at build, each naming the change: a removed prop (below); `events` holding other than one kind; a bin kind with no lane or no parts; a resource kind that is neither the lanes' nor the resources'; resources read a page at a time or keyed by anything but String; a resources kind with no `min` or `max`; `arrivals` not from `Allocation.arrivals`, and its `values` of another struct than the bin kind's own fields; `periods` not from `Schedule.periods`; a `fit` bound over another row type, naming both; a `check` of another type; a library tab or a column given twice; an empty label; and Table bands that don't start at 0 and rise within the day. | `allocationMinimal`, `allocationKilnYard`, `allocationTable` |
|
|
2787
|
-
| `events={{ bin: Schedule.events(Record.bind(bins
|
|
3756
|
+
| `<Allocation events resources arrivals fit optimise? periods? every? check? columns? slice? rules? library? inspector? view? horizon? title? readOnly? labels? table? id? />` **❗** | One toolbar row at every width — the day's navigator, its picker marking each day Published · Draft · Not started; Table · Timeline; the status chip; the focus; the rail and its count; the issues; with `optimise`, the Elara eye; the history and Publish — folding into one View in the narrowest frame, and below that, where the row is still too wide, the rail's icon dropping its caret and then the eye going into the View as its entry, the history and Save always in the row; then the Timeline or the Table, the panes the props ask for, and the footer's legend. Refused at build, each naming the change: a removed prop (below); `events` holding other than one kind; a bin kind with no lane or no parts; a resource kind that is neither the lanes' nor the resources'; resources read a page at a time or keyed by anything but String; a resources kind with no `min` or `max`; `arrivals` not from `Allocation.arrivals`, and its `values` of another struct than the bin kind's own fields; `periods` not from `Schedule.periods`; a `fit` bound over another row type, naming both; a `check` of another type; a library tab or a column given twice; an empty label; and Table bands that don't start at 0 and rise within the day. | `allocationMinimal`, `allocationKilnYard`, `allocationTable` |
|
|
3757
|
+
| `events={{ bin: Schedule.events(Record.bind(bins), { at, resource: { field, of }, arrival, quantity: { field, unit? }, parts: { field, of }, hold, kind?, fields?, inspector?, ready?, entries? }) }}` **❗** | Exactly one kind, the bins: an instant kind keyed by String. A bin's UTC day is its plan day, and a batch's bins follow one minute apart. `resource` names its lane, a String key of the lanes' kind; `arrival` the arrival it books, `none` a reservation; `quantity`'s unit shows everywhere a quantity does; `parts` holds its quantity on each resource of the slot `of` names, empty while it is not linked; `hold` and `kind` are kept from booking. The inspector shows the kind's form over the fields no role writes (`fields` hints), or the kind's own `inspector`. | `allocationKilnYard`, `allocationCustomBin` |
|
|
2788
3758
|
| `resources={{ <lanes>: Schedule.resources(rows, { name, icon, label, offset? }), <resources>: Schedule.resources(rows, { name, icon, label, min, max, group?, meta?, note?, hold?, jobs? }) }}` **❗** | The lanes' kind under the slot the bin kind's `resource.of` names, each lane taking a bin each `every` at its `offset` past it, in key order; the resources' kind under the slot its `parts.of` names, each with its range in the quantity's unit. `jobs` is a resource's other work: one covering the day's start makes it busy, the first after it is its next job; `hold` is how long a reservation on it is checked for. Both are read whole and keyed by String. | `allocationKilnYard` |
|
|
2789
3759
|
| `arrivals={Allocation.arrivals(rows: Dict<String, R>, { label, quantity, hold, kind?, meta?, due?, window?, details?, values? })}` **❗** | Every arrival the day may book, each accessor `(row, key) => …`. Booking one writes the bin's roles — `arrival` its key, `quantity`, `kind` and `hold` from these accessors, `parts` empty, `at` and the lane where it is booked — and `values`' struct into the row's other fields: the bin kind's fields no role maps, held to them at build. Without `values` they take their type's default, as a reservation's always do. `due` before the day makes it overdue. | `allocationKilnYard` |
|
|
2790
|
-
| `periods={Schedule.periods(Record.bind(days
|
|
3760
|
+
| `periods={Schedule.periods(Record.bind(days), { every: "day", status, entries? })}` | The app's days, each keyed by its UTC midnight: the status chip, Publish and the picker's Published · Draft · Not started. A published day is read only under its banner; navigation, the rail, the focus, links and the inspector still work. Without `periods`, no status chip and no Publish. | `allocationKilnYard`, `allocationPublished`, `allocationNoPublish` |
|
|
2791
3761
|
| `slice={{ slice: Slice.bindRecords(key, { <bin slot>?, <lanes>?, <resources>?, arrivals? }) }}` **❗** | One rail in both layouts. The bin kind's source reads the day's bins through an interval index from `at` to `at`, `range: "at"`, the range the board's own navigation writes — no chip picks the day — the kind given `entries`; without it the bins are read whole. The lanes', the resources' and `arrivals` each narrow every place their rows appear — the Timeline's lanes and rows, the Table's rows, the Choosers and the Library's tabs (`Resources 10 of 33`) — and the board reads each one's keys itself. The summary counts the day's bins too (`26 of 33 bin events`). The bare handle, `Slice.bind`, a single `Slice.bindRecord`, a source of another name and one keyed otherwise than by String are refused. | `allocationKilnYard`, `allocationTable`, `allocationWindowed` |
|
|
2792
3762
|
| `check={fn(Allocation.Types.CheckContext(R)) => Array<Allocation.Types.Issue>}` **❗** | The app's issues, after the built-in ones wherever issues show, over `{ day, plan, context, loads }`: each bin carries its own row (`bin.row`), so a check reads the app's own fields. An issue's `flag` marks its bin's card (`≠ spec`); a neutral or info issue is listed, not counted. | `allocationKilnYard` |
|
|
2793
3763
|
| `columns={[{ key, label, text: fn(R) => String, tone?: fn(R) => Option<StatusToken>, table? }]}` **❗** | The app's bin fields as Table columns after the quantity, and as the inspector's rows, each over the bin's own row; `table: false` shows one in the inspector only. | `allocationKilnYard` |
|
|
@@ -2804,11 +3774,12 @@ export const yardFit = e3.function("yard_fit", East.function([Allocation.Types.F
|
|
|
2804
3774
|
| `Allocation.candidates(R): (FitRequest(R)) => Array<Candidate>` | A place's subject on every resource, in key order; a take's every bin (its unallocated quantity, with its part there), every arrival the day has not booked, then a reservation. | `allocationRules` |
|
|
2805
3775
|
| `Allocation.lookahead(R): (FitRequest(R), Candidate, fit: fn(Candidate) => Fit) => Fit` | A greedy look-ahead: it places the candidate, then the day's unbooked arrivals largest first, each on its best fit, and lowers the score by one for each left with nowhere to go (`· leaves 1 load with no fit`); a refusal passes through. | `allocationLookahead` |
|
|
2806
3776
|
| a rule over the ready-made fit | An `e3.function` that refuses what the app refuses, then gives `Allocation.fit`'s verdict: the board's ranks and reasons are its. | `allocationFitRule` |
|
|
3777
|
+
| `optimise={Func.bind(fn: FunctionDef<[Allocation.Types.OptimiseRequest(R)], Allocation.Types.OptimiseResult(R)>)}` **❗** | The app's optimiser (#1409). The Elara eye in the toolbar, after the issues item and immediately before the history item — at a phone's width, where the row's floor needs its room once the rail's icon has dropped its caret, the View's entry beside the issues, never out of the toolbar: `Stop optimising` while a run is out, off and saying why (`Read-only`) while the day's reads are in flight — names the day (`Optimise Tue, May 11, 2027`): one call per run with the day, the run's number, the day's bins by key as the session holds them — base plus every unapplied draft, every row whatever the rail shows — every arrival as the board reads them and `Allocation.Types.Context`. The history, the drags and every gesture are locked while it is out; a step to another day stops it. Its answer — the bins complete — is diffed by key and recorded as ONE step: Undo in one, Redo replays the landing, Save commits it with everything else. A bin added is an add (its arrival leaves the Arrivals to book), one moved a change with a ghost card at its old lane slot, one dropped a remove with its ghost (its arrival back in Arrivals), one linked or relinked a change with no ghost; each marked on the Timeline's cards, the Table's chips and the narrow cards with its provenance on hover; the lane heads' figures, the Arrivals count and the issues chip flash; for the highlight the Timeline draws the links the run added or changed with nothing selected, outlined in the Elara tone, and the loads of the kilns they land on glow with the cards. A result equal to the request records nothing and says so. The optimisation is joint — a result decides both a bin's slot and its links — and the yard's example optimiser is: a kiln over its maximum gives the excess of its latest bin to the kilns `Allocation.fit` ranks best, a bin not linked or partly allocated is linked, and every load not yet booked is booked into the day's first free bay slot and onto the kilns the fit ranks best, whole on the best fit or split when no one kiln takes it; what no kiln has room for is booked on none. | `allocationOptimise` |
|
|
2807
3778
|
| **Domain functions and values** | | |
|
|
2808
3779
|
| `Allocation.loads(R): (day, Day(R), Context) => Dict<String, Load>` | Every resource's fill, bins, hold and state on the day: free, loading or busy. | `allocationDayLoads` |
|
|
2809
3780
|
| `Allocation.check(R): (day, Day(R), Context) => Array<Issue>` | The built-in checks — Not linked, Partly allocated, Overlaps the next job, Over max, and Under minimum when `rules.under` — each with its fix; the app's `check` follows them wherever issues show. | `allocationChecks` |
|
|
2810
3781
|
| `Allocation.slots(R): (day, Day(R), Context) => Array<Slot>` | The day's lane slots, one per lane per `every`, in time then lane order, each free, booked or reserved, with its bins; a booking takes the first free one. | `allocationSlots` |
|
|
2811
|
-
| `Allocation.Types.{Lane, Status, Bin(R), Day(R), Interval, Job, Detail, Arrival, Resource, Rules, Context, LoadState, Load, Candidate, Refusal, Fit, FitAsk, FitRequest(R), FitResult, Fix, IssueKind, Issue, CheckContext(R), SlotStatus, Slot, Layout}` | The board's reading of the app's rows, carried in requests and stored nowhere. A type over a bin is a constructor of the app's bin row type: the same `R`, the same East type. A bin is `{ lane, at, arrival, quantity, kind, hold, parts, row }`, its roles beside the app's row, `parts` its quantity on each resource, empty while it is not linked; a reservation books no arrival. A day is `{ status, bins }`, its status Schedule's period status. `FitAsk` is `place { subject: bin \| arrival, quantity }` or `take { resource }`, and `FitResult` `{ ask, resources, bins, arrivals, reserve, basis }`. `Fit` and `Refusal` are `Schedule.Types`', the Roster's too. | `allocationKilnYard`, `allocationAnswerTake` |
|
|
3782
|
+
| `Allocation.Types.{Lane, Status, Bin(R), Day(R), Interval, Job, Detail, Arrival, Resource, Rules, Context, LoadState, Load, Candidate, Refusal, Fit, FitAsk, FitRequest(R), FitResult, OptimiseRequest(R), OptimiseResult(R), Fix, IssueKind, Issue, CheckContext(R), SlotStatus, Slot, Layout}` | The board's reading of the app's rows, carried in requests and stored nowhere. A type over a bin is a constructor of the app's bin row type: the same `R`, the same East type. A bin is `{ lane, at, arrival, quantity, kind, hold, parts, row }`, its roles beside the app's row, `parts` its quantity on each resource, empty while it is not linked; a reservation books no arrival. A day is `{ status, bins }`, its status Schedule's period status. `FitAsk` is `place { subject: bin \| arrival, quantity }` or `take { resource }`, and `FitResult` `{ ask, resources, bins, arrivals, reserve, basis }`. `Fit` and `Refusal` are `Schedule.Types`', the Roster's too. | `allocationKilnYard`, `allocationAnswerTake` |
|
|
2812
3783
|
|
|
2813
3784
|
Every gesture — a drop, a Chooser's choice, a lane or a time step, a quantity,
|
|
2814
3785
|
a split, a fix, Back to Arrivals, Revert — is one step of one history over the
|