@elaraai/east-ui 1.0.74 → 1.0.76
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/collections/index.d.ts +1 -0
- package/dist/src/collections/index.d.ts.map +1 -1
- package/dist/src/collections/index.js +1 -0
- package/dist/src/collections/index.js.map +1 -1
- package/dist/src/collections/sheet/bridge.d.ts +265 -0
- package/dist/src/collections/sheet/bridge.d.ts.map +1 -0
- package/dist/src/collections/sheet/bridge.js +630 -0
- package/dist/src/collections/sheet/bridge.js.map +1 -0
- package/dist/src/collections/sheet/columns.d.ts +360 -0
- package/dist/src/collections/sheet/columns.d.ts.map +1 -0
- package/dist/src/collections/sheet/columns.js +99 -0
- package/dist/src/collections/sheet/columns.js.map +1 -0
- package/dist/src/collections/sheet/index.d.ts +207 -0
- package/dist/src/collections/sheet/index.d.ts.map +1 -0
- package/dist/src/collections/sheet/index.js +126 -0
- package/dist/src/collections/sheet/index.js.map +1 -0
- package/dist/src/collections/sheet/link.d.ts +392 -0
- package/dist/src/collections/sheet/link.d.ts.map +1 -0
- package/dist/src/collections/sheet/link.js +222 -0
- package/dist/src/collections/sheet/link.js.map +1 -0
- package/dist/src/collections/sheet/registers.d.ts +131 -0
- package/dist/src/collections/sheet/registers.d.ts.map +1 -0
- package/dist/src/collections/sheet/registers.js +119 -0
- package/dist/src/collections/sheet/registers.js.map +1 -0
- package/dist/src/collections/sheet/root.d.ts +179 -0
- package/dist/src/collections/sheet/root.d.ts.map +1 -0
- package/dist/src/collections/sheet/root.js +308 -0
- package/dist/src/collections/sheet/root.js.map +1 -0
- package/dist/src/collections/sheet/types.d.ts +6601 -0
- package/dist/src/collections/sheet/types.d.ts.map +1 -0
- package/dist/src/collections/sheet/types.js +744 -0
- package/dist/src/collections/sheet/types.js.map +1 -0
- package/dist/src/component.d.ts +2500 -0
- package/dist/src/component.d.ts.map +1 -1
- package/dist/src/component.js +6 -0
- package/dist/src/component.js.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/internal.d.ts +4 -2
- package/dist/src/internal.d.ts.map +1 -1
- package/dist/src/internal.js +5 -2
- package/dist/src/internal.js.map +1 -1
- package/dist/src/platform/index.d.ts +1 -1
- package/dist/src/platform/index.d.ts.map +1 -1
- package/dist/src/platform/index.js +1 -1
- package/dist/src/platform/index.js.map +1 -1
- package/dist/src/platform/slice/impl.d.ts +17 -1
- package/dist/src/platform/slice/impl.d.ts.map +1 -1
- package/dist/src/platform/slice/impl.js +60 -12
- package/dist/src/platform/slice/impl.js.map +1 -1
- package/dist/src/platform/slice/index.d.ts +155 -10
- package/dist/src/platform/slice/index.d.ts.map +1 -1
- package/dist/src/platform/slice/index.js +37 -17
- package/dist/src/platform/slice/index.js.map +1 -1
- package/dist/src/runtime/collections/index.d.ts +1 -0
- package/dist/src/runtime/collections/index.d.ts.map +1 -1
- package/dist/src/runtime/collections/index.js +1 -0
- package/dist/src/runtime/collections/index.js.map +1 -1
- package/dist/src/runtime/collections/sheet.d.ts +89 -0
- package/dist/src/runtime/collections/sheet.d.ts.map +1 -0
- package/dist/src/runtime/collections/sheet.js +65 -0
- package/dist/src/runtime/collections/sheet.js.map +1 -0
- package/dist/src/slice/index.d.ts +97 -1
- package/dist/src/slice/index.d.ts.map +1 -1
- package/dist/test/collections/sheet.examples.d.ts +56 -0
- package/dist/test/collections/sheet.examples.d.ts.map +1 -0
- package/dist/test/collections/sheet.examples.js +725 -0
- package/dist/test/collections/sheet.examples.js.map +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@elaraai/east-ui/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2025 Elara AI Pty Ltd
|
|
4
|
+
* Dual-licensed under AGPL-3.0 and commercial license. See LICENSE for details.
|
|
5
|
+
*/
|
|
6
|
+
/** @jsxImportSource @elaraai/east-ui */
|
|
7
|
+
import { East, ArrayType, BooleanType, DateTimeType, DictType, FloatType, IntegerType, NullType, OptionType, StringType, StructType, example, none, some, variant, } from "@elaraai/east";
|
|
8
|
+
import { Badge, Box, Configurator, Format, HStack, Input, Paged, Reactive, SegmentGroup, Sheet, Slice, State, Status, Style, Switch, Text, UIComponentType, VStack, } from "@elaraai/east-ui";
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// The Sheet corpus — the five slots of EXAMPLES_AUTHORING.md §8 (Sheet Spec.md §8 P1):
|
|
11
|
+
// `sheetBasic` · `sheetVariants` (THE configurator) · `sheetPlan` (the flagship)
|
|
12
|
+
// · the behavioural isolates `sheetCopilot` / `sheetLens` / `sheetWriteBack` /
|
|
13
|
+
// `sheetPaged` · `sheetStress`. Every example is self-contained — types,
|
|
14
|
+
// fixtures and constructors inside the body, bulk data derived East-side —
|
|
15
|
+
// and the fixtures are the prototype's synthetic registers and rows: a
|
|
16
|
+
// discrete manufacturing plant (machines on lines, work orders moving parts
|
|
17
|
+
// between them), no customer, site or product names.
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// The one thing at module scope: a platform declaration the async proposer
|
|
20
|
+
// of `sheetCopilot` / `sheetPlan` awaits. The row and context types it is
|
|
21
|
+
// typed over must be declared beside it, so they are declared once here and
|
|
22
|
+
// re-declared inside the example bodies that use them (the examples rule).
|
|
23
|
+
const PLAN_ROW_TYPE = StructType({
|
|
24
|
+
id: StringType, start: OptionType(DateTimeType), end: OptionType(DateTimeType), activity: StringType,
|
|
25
|
+
qty: OptionType(FloatType), notes: StringType, stations: Sheet.Types.Link, setups: OptionType(IntegerType),
|
|
26
|
+
fromSite: StringType, toSite: StringType, orderCode: StringType, status: StringType,
|
|
27
|
+
});
|
|
28
|
+
const ACTIVITY_TYPE = StructType({
|
|
29
|
+
name: StringType, uom: StringType, rate: FloatType, fte: IntegerType, days: IntegerType, sides: Sheet.Types.Sides,
|
|
30
|
+
});
|
|
31
|
+
/** The model behind an async proposer — an e3 function, a service, a notebook; the Sheet only needs the types. */
|
|
32
|
+
const planRecommend = East.asyncPlatform("sheet_plan_recommend", [Sheet.Types.Context(PLAN_ROW_TYPE, ACTIVITY_TYPE)], ArrayType(Sheet.Types.Proposal(PLAN_ROW_TYPE)), { optional: true });
|
|
33
|
+
/**
|
|
34
|
+
* The smallest sheet (§3.1) — three typed columns over the host's structs,
|
|
35
|
+
* `onUpdate` receiving the whole collection with the edit applied (the
|
|
36
|
+
* `ValueTree` idiom), so a `State.bind` is the entire persistence story.
|
|
37
|
+
*/
|
|
38
|
+
export const sheetBasic = example({
|
|
39
|
+
keywords: ["Sheet", "Root", "basic", "column", "date", "text", "quantity", "onUpdate", "State", "Reactive", "id"],
|
|
40
|
+
description: "The smallest sheet — three typed columns over the host's structs, the whole collection written back on every edit",
|
|
41
|
+
fn: East.function([], UIComponentType, (_$) => (_jsx(Reactive, { children: $ => {
|
|
42
|
+
const JobType = StructType({
|
|
43
|
+
id: StringType,
|
|
44
|
+
start: OptionType(DateTimeType), // none = blank cell
|
|
45
|
+
task: StringType, // "" = blank cell
|
|
46
|
+
qty: OptionType(FloatType),
|
|
47
|
+
});
|
|
48
|
+
const jobs = $.let(State.bind([ArrayType(JobType)], "sheet_basic_jobs", [
|
|
49
|
+
{ id: "j1", start: none, task: "Machining", qty: none },
|
|
50
|
+
]));
|
|
51
|
+
return (_jsx(Sheet, { data: jobs.read(), id: "id", columns: {
|
|
52
|
+
start: Sheet.column.date(JobType, { header: "Start", sub: "d/m · fri · +3d" }),
|
|
53
|
+
task: Sheet.column.text(JobType, { header: "Task" }),
|
|
54
|
+
qty: Sheet.column.quantity(JobType, { header: "Qty" }), // no driver on this sheet — the two-argument form
|
|
55
|
+
}, onUpdate: jobs.write }));
|
|
56
|
+
} }))),
|
|
57
|
+
inputs: [],
|
|
58
|
+
});
|
|
59
|
+
/**
|
|
60
|
+
* THE Sheet configurator — ONE live sheet; every axis is an expression-fed
|
|
61
|
+
* prop on that single instance: density, the blank tail, read-only, the size
|
|
62
|
+
* mode (auto / scroll / fill) and the copilot switch (a fill provider that
|
|
63
|
+
* reads the switch through its captured bind handle). Selection and edits
|
|
64
|
+
* log to the reactive aside.
|
|
65
|
+
*/
|
|
66
|
+
export const sheetVariants = example({
|
|
67
|
+
keywords: ["Sheet", "Root", "configurator", "density", "blanks", "readOnly", "height", "fill", "scroll", "#320", "copilot", "fill", "provider", "onSelect", "onEdit", "Configurator", "SegmentGroup", "Switch", "Input", "Reactive", "State"],
|
|
68
|
+
description: "Sheet configurator — density, the blank tail, read-only, size mode (auto / scroll / fill) and the copilot switch all expression-fed into one live sheet; selection and edits log to the aside",
|
|
69
|
+
fn: East.function([], UIComponentType, (_$) => (_jsx(Reactive, { children: $ => {
|
|
70
|
+
const JobType = StructType({ id: StringType, start: OptionType(DateTimeType), task: StringType, qty: OptionType(FloatType), notes: StringType });
|
|
71
|
+
const Ctx = Sheet.Types.Context(JobType);
|
|
72
|
+
const rows = $.let(State.bind([ArrayType(JobType)], "sheet_variants_rows", [
|
|
73
|
+
{ id: "j1", start: some(new Date("2026-02-16T00:00:00Z")), task: "Machining", qty: some(1200.0), notes: "Rough the P-40 blanks" },
|
|
74
|
+
{ id: "j2", start: some(new Date("2026-03-09T00:00:00Z")), task: "Painting", qty: some(250.0), notes: "" },
|
|
75
|
+
{ id: "j3", start: none, task: "Packaging", qty: none, notes: "" },
|
|
76
|
+
]));
|
|
77
|
+
const densities = $.const([
|
|
78
|
+
variant("condensed", null), variant("compact", null), variant("comfortable", null),
|
|
79
|
+
], ArrayType(Style.Types.Density));
|
|
80
|
+
const sizeModes = $.const(["auto", "scroll", "fill"], ArrayType(StringType));
|
|
81
|
+
const densityBind = $.let(State.bind([StringType], "sheet_variants_density", "compact"));
|
|
82
|
+
const blanksBind = $.let(State.bind([IntegerType], "sheet_variants_blanks", 18n));
|
|
83
|
+
const readOnlyBind = $.let(State.bind([BooleanType], "sheet_variants_readonly", false));
|
|
84
|
+
const sizeBind = $.let(State.bind([StringType], "sheet_variants_size", "scroll"));
|
|
85
|
+
const copilotBind = $.let(State.bind([BooleanType], "sheet_variants_copilot", true));
|
|
86
|
+
const lastEventBind = $.let(State.bind([StringType], "sheet_variants_last_event", ""));
|
|
87
|
+
const dKey = $.let(densityBind.read());
|
|
88
|
+
const blanks = $.let(blanksBind.read());
|
|
89
|
+
const readOnly = $.let(readOnlyBind.read());
|
|
90
|
+
const sKey = $.let(sizeBind.read());
|
|
91
|
+
const copilotOn = $.let(copilotBind.read());
|
|
92
|
+
const lastEvent = $.let(lastEventBind.read());
|
|
93
|
+
const onDensity = $.const(East.function([StringType], NullType, ($, next) => { $(densityBind.write(next)); }));
|
|
94
|
+
const onBlanks = $.const(East.function([IntegerType], NullType, ($, next) => { $(blanksBind.write(next)); }));
|
|
95
|
+
const onReadOnly = $.const(East.function([BooleanType], NullType, ($, next) => { $(readOnlyBind.write(next)); }));
|
|
96
|
+
const onSize = $.const(East.function([StringType], NullType, ($, next) => { $(sizeBind.write(next)); }));
|
|
97
|
+
const onCopilot = $.const(East.function([BooleanType], NullType, ($, next) => { $(copilotBind.write(next)); }));
|
|
98
|
+
// The copilot axis — a fill that reads the switch through the
|
|
99
|
+
// captured bind handle, so the one instance keeps its providers.
|
|
100
|
+
const NoteFill = OptionType(Sheet.Types.Fill(StringType));
|
|
101
|
+
const phrase = $.const(East.function([Ctx], NoteFill, ($, ctx) => {
|
|
102
|
+
const noFill = $.const(none, NoteFill);
|
|
103
|
+
return copilotBind.read().and(() => ctx.row.task.length().greater(0n)).ifElse((_$) => East.value(some({ value: East.str `${ctx.row.task} as planned`, meta: "phrasing from the task" }), NoteFill), (_$) => noFill);
|
|
104
|
+
}));
|
|
105
|
+
const onSelect = $.const(East.function([Sheet.Types.Selection], NullType, ($, sel) => {
|
|
106
|
+
$(lastEventBind.write(East.str `onSelect: row ${sel.rowId.match({ some: (_$, id) => id, none: (_$) => "—" })} · ${sel.key.match({ some: (_$, k) => k, none: (_$) => "—" })}`));
|
|
107
|
+
}));
|
|
108
|
+
const onEdit = $.const(East.function([Sheet.Types.Edit(JobType)], NullType, ($, e) => {
|
|
109
|
+
$(lastEventBind.write(e.match({
|
|
110
|
+
commit: (_$, c) => East.str `commit: ${c.rowId} · ${c.key} · ${c.source.getTag()}`,
|
|
111
|
+
insert: (_$, i) => East.str `insert: ${i.row.id}`,
|
|
112
|
+
remove: (_$, r) => East.str `remove: ${r.rowIds.length()} rows`,
|
|
113
|
+
})));
|
|
114
|
+
}));
|
|
115
|
+
const densitySel = $.let(densities.filter((_$, v) => v.getTag().equal(dKey)).get(0n));
|
|
116
|
+
// Size mode — an empty height reads as unbounded, so ONE sheet
|
|
117
|
+
// covers auto / scroll / fill; the wrapper Box bounds fill mode only.
|
|
118
|
+
const boxHeight = $.let(sKey.equal("fill").ifElse((_$) => "320px", (_$) => ""));
|
|
119
|
+
const sheetHeight = $.let(sKey.equal("scroll").ifElse((_$) => "360px", (_$) => sKey.equal("fill").ifElse((_$) => "fill", (_$) => "")));
|
|
120
|
+
return (_jsx(Configurator, { controls: [
|
|
121
|
+
Configurator.Control("Density", dKey, _jsx(SegmentGroup, { value: dKey, onChange: onDensity, size: "sm", items: densities.map((_$, v) => SegmentGroup.Item(v.getTag(), _jsx(Text, { children: v.getTag().upperCase() }))) })),
|
|
122
|
+
Configurator.Control("Size", sKey, _jsx(SegmentGroup, { value: sKey, onChange: onSize, size: "sm", items: sizeModes.map((_$, m) => SegmentGroup.Item(m, _jsx(Text, { children: m.upperCase() }))) })),
|
|
123
|
+
Configurator.Control("Blank tail", East.print(blanks), _jsx(Input.Integer, { value: blanks, min: 0n, max: 40n, step: 2n, size: "sm", onChange: onBlanks })),
|
|
124
|
+
Configurator.Slot("Chrome", _jsxs(HStack, { gap: "5", align: "center", wrap: "wrap", children: [_jsx(Switch, { checked: readOnly, label: "Read-only", onChange: onReadOnly }), _jsx(Switch, { checked: copilotOn, label: "Copilot", onChange: onCopilot })] })),
|
|
125
|
+
], preview: _jsx(Box, { width: "100%", height: boxHeight, overflow: "hidden", children: _jsx(Sheet, { data: rows.read(), id: "id", columns: {
|
|
126
|
+
start: Sheet.column.date(JobType, { header: "Start", sub: "d/m · fri · +3d", width: "96px" }),
|
|
127
|
+
task: Sheet.column.text(JobType, { header: "Task", width: "180px" }),
|
|
128
|
+
qty: Sheet.column.quantity(JobType, { header: "Qty", sub: "1,200 · 1.2k · pcs", width: "112px", format: Format.Number({ maximumFractionDigits: 0n }) }),
|
|
129
|
+
notes: Sheet.column.text(JobType, { header: "Notes", sub: "free text", fill: [phrase] }),
|
|
130
|
+
}, density: densitySel, blanks: blanks, readOnly: readOnly, onSelect: onSelect, onEdit: onEdit, onUpdate: rows.write, style: { height: sheetHeight } }) }), aside: {
|
|
131
|
+
label: "Events · Reactive",
|
|
132
|
+
body: (_jsx(Badge, { colorPalette: "brand", variant: "outline", children: East.equal(lastEvent.length(), 0n).ifElse((_$) => "Interact with the sheet", (_$) => lastEvent) })),
|
|
133
|
+
}, spec: [
|
|
134
|
+
Configurator.Spec("Copilot", copilotOn.ifElse((_$) => "on · notes phrase", (_$) => "off")),
|
|
135
|
+
Configurator.Spec("Rows", East.str `${rows.read().length()} real`),
|
|
136
|
+
] }));
|
|
137
|
+
} }))),
|
|
138
|
+
inputs: [],
|
|
139
|
+
});
|
|
140
|
+
/**
|
|
141
|
+
* The flagship (§3.11) — every column kind, three registers and a driver,
|
|
142
|
+
* the copilot's fills and proposers as author functions, the slice lens with
|
|
143
|
+
* saved views, a footer, and `onUpdate` write-back, on one sheet.
|
|
144
|
+
*/
|
|
145
|
+
export const sheetPlan = example({
|
|
146
|
+
keywords: ["Sheet", "Root", "plan", "flagship", "driver", "register", "lookup", "reference", "enum", "link", "stamped", "quantity", "uom", "sides", "arity", "check", "fill", "propose", "suggest", "asyncFunction", "asyncPlatform", "slice", "search", "filter", "lens", "views", "text", "Sheet.link.print", "footer", "owned", "onUpdate", "Reactive", "State"],
|
|
147
|
+
description: "The flagship production plan — every column kind over one raw source, registers and a driver, the copilot's fills and proposers as author functions (one async), the slice lens with saved views, a footer and whole-collection write-back",
|
|
148
|
+
fn: East.function([], UIComponentType, (_$) => (_jsx(Reactive, { children: $ => {
|
|
149
|
+
const PlanRowType = StructType({
|
|
150
|
+
id: StringType, start: OptionType(DateTimeType), end: OptionType(DateTimeType), activity: StringType,
|
|
151
|
+
qty: OptionType(FloatType), notes: StringType, stations: Sheet.Types.Link, setups: OptionType(IntegerType),
|
|
152
|
+
fromSite: StringType, toSite: StringType, orderCode: StringType, status: StringType,
|
|
153
|
+
});
|
|
154
|
+
const ActivityType = StructType({ name: StringType, uom: StringType, rate: FloatType, fte: IntegerType, days: IntegerType, sides: Sheet.Types.Sides });
|
|
155
|
+
const MachineType = StructType({ code: StringType, family: StringType, line: StringType, site: StringType });
|
|
156
|
+
const LineType = StructType({ code: StringType, name: StringType, machines: IntegerType, aliases: ArrayType(StringType) });
|
|
157
|
+
const FamilyType = StructType({ name: StringType, aliases: ArrayType(StringType) });
|
|
158
|
+
const StatusType = StructType({ word: StringType, tone: Status.Types.Value });
|
|
159
|
+
const Ctx = Sheet.Types.Context(PlanRowType, ActivityType);
|
|
160
|
+
const Proposals = ArrayType(Sheet.Types.Proposal(PlanRowType));
|
|
161
|
+
// The synthetic registers — a discrete manufacturing plant: machines on
|
|
162
|
+
// lines, work orders moving parts between them.
|
|
163
|
+
const activities = $.const([
|
|
164
|
+
{ name: "Machining", uom: "pcs", rate: 60.0, fte: 2n, days: 4n, sides: variant("both", null) },
|
|
165
|
+
{ name: "Machining - Roughing", uom: "pcs", rate: 80.0, fte: 2n, days: 4n, sides: variant("both", null) },
|
|
166
|
+
{ name: "Assembly", uom: "units", rate: 30.0, fte: 3n, days: 4n, sides: variant("both", null) },
|
|
167
|
+
{ name: "Sub-assembly", uom: "units", rate: 45.0, fte: 2n, days: 4n, sides: variant("both", null) },
|
|
168
|
+
{ name: "Painting", uom: "pcs", rate: 50.0, fte: 3n, days: 4n, sides: variant("both", null) },
|
|
169
|
+
{ name: "Painting - Primer", uom: "pcs", rate: 70.0, fte: 2n, days: 4n, sides: variant("both", null) },
|
|
170
|
+
{ name: "Packaging", uom: "cartons", rate: 120.0, fte: 1n, days: 3n, sides: variant("both", null) },
|
|
171
|
+
{ name: "Deburring", uom: "pcs", rate: 90.0, fte: 1n, days: 3n, sides: variant("both", null) },
|
|
172
|
+
{ name: "Heat treatment", uom: "pcs", rate: 40.0, fte: 2n, days: 3n, sides: variant("both", null) },
|
|
173
|
+
{ name: "Rework", uom: "pcs", rate: 20.0, fte: 2n, days: 3n, sides: variant("in", null) },
|
|
174
|
+
{ name: "Inspection", uom: "lots", rate: 4.0, fte: 2n, days: 4n, sides: variant("in", null) },
|
|
175
|
+
{ name: "Calibration", uom: "machines", rate: 2.0, fte: 2n, days: 4n, sides: variant("in", null) },
|
|
176
|
+
{ name: "Changeover", uom: "h", rate: 1.0, fte: 1n, days: 1n, sides: variant("in", null) },
|
|
177
|
+
{ name: "Receiving", uom: "pallets", rate: 20.0, fte: 1n, days: 1n, sides: variant("to", null) },
|
|
178
|
+
{ name: "Shipping", uom: "pallets", rate: 20.0, fte: 1n, days: 1n, sides: variant("from", null) },
|
|
179
|
+
{ name: "Maintenance", uom: "h", rate: 1.0, fte: 1n, days: 1n, sides: variant("in", null) },
|
|
180
|
+
], ArrayType(ActivityType));
|
|
181
|
+
const machines = $.const([
|
|
182
|
+
{ code: "M1104", family: "120 t press", line: "Line 1", site: "North plant" }, { code: "M1120", family: "120 t press", line: "Line 1", site: "North plant" },
|
|
183
|
+
{ code: "M2140", family: "CNC lathe", line: "Line 2", site: "South plant" }, { code: "M2141", family: "CNC lathe", line: "Line 2", site: "South plant" },
|
|
184
|
+
{ code: "M2142", family: "CNC lathe", line: "Line 2", site: "South plant" }, { code: "M2145", family: "CNC lathe", line: "Line 2", site: "South plant" },
|
|
185
|
+
{ code: "M2150", family: "CNC lathe", line: "Line 2", site: "South plant" }, { code: "M2151", family: "CNC lathe", line: "Line 2", site: "South plant" },
|
|
186
|
+
{ code: "M2160", family: "CNC lathe", line: "Line 2", site: "South plant" }, { code: "M2162", family: "CNC lathe", line: "Line 2", site: "South plant" },
|
|
187
|
+
{ code: "M3210", family: "5-axis mill", line: "Line 3", site: "North plant" }, { code: "M3215", family: "5-axis mill", line: "Line 3", site: "North plant" },
|
|
188
|
+
{ code: "M5010", family: "gantry mill", line: "Line 5", site: "East plant" }, { code: "M5011", family: "gantry mill", line: "Line 5", site: "East plant" },
|
|
189
|
+
{ code: "M7301", family: "assembly bench", line: "Line 7", site: "North plant" }, { code: "M7302", family: "assembly bench", line: "Line 7", site: "North plant" },
|
|
190
|
+
{ code: "M7305", family: "assembly bench", line: "Line 7", site: "North plant" }, { code: "M7310", family: "assembly bench", line: "Line 7", site: "West plant" },
|
|
191
|
+
{ code: "M7311", family: "assembly bench", line: "Line 7", site: "West plant" }, { code: "M7320", family: "assembly bench", line: "Line 7", site: "West plant" },
|
|
192
|
+
{ code: "M7322", family: "assembly bench", line: "Line 7", site: "West plant" }, { code: "M8001", family: "test rig", line: "Line 8", site: "East plant" },
|
|
193
|
+
], ArrayType(MachineType));
|
|
194
|
+
const lines = $.const([
|
|
195
|
+
{ code: "L2", name: "Line 2", machines: 96n, aliases: ["line 2", "l2", "the 2 line"] },
|
|
196
|
+
{ code: "L1", name: "Line 1", machines: 24n, aliases: ["line 1", "l1"] },
|
|
197
|
+
{ code: "L3", name: "Line 3", machines: 40n, aliases: ["line 3", "l3"] },
|
|
198
|
+
{ code: "L5", name: "Line 5", machines: 18n, aliases: ["line 5", "l5"] },
|
|
199
|
+
{ code: "L7", name: "Line 7", machines: 72n, aliases: ["line 7", "l7"] },
|
|
200
|
+
{ code: "L8", name: "Line 8", machines: 12n, aliases: ["line 8", "l8"] },
|
|
201
|
+
{ code: "CA", name: "Cell A", machines: 150n, aliases: ["cell a", "a cell", "the a cell"] },
|
|
202
|
+
{ code: "GIN", name: "Goods in", machines: 36n, aliases: ["goods in", "inbound"] },
|
|
203
|
+
{ code: "FH1", name: "Finishing hall", machines: 64n, aliases: ["finishing", "finishing hall"] },
|
|
204
|
+
{ code: "PKL", name: "Pack line", machines: 20n, aliases: ["pack", "packing"] },
|
|
205
|
+
{ code: "TB", name: "Test bay", machines: 9n, aliases: ["test bay", "the test bay", "bay"] },
|
|
206
|
+
], ArrayType(LineType));
|
|
207
|
+
// A countable-by-attribute kind: the machine family — a count of a kind, resolved to machines later.
|
|
208
|
+
const families = $.const([
|
|
209
|
+
{ name: "CNC lathe", aliases: ["lathe", "lathes", "cnc"] },
|
|
210
|
+
{ name: "5-axis mill", aliases: ["mill", "mills", "5 axis", "5axis"] },
|
|
211
|
+
{ name: "120 t press", aliases: ["press", "presses", "120t", "120 t"] },
|
|
212
|
+
{ name: "assembly bench", aliases: ["bench", "benches"] },
|
|
213
|
+
{ name: "gantry mill", aliases: ["gantry"] },
|
|
214
|
+
{ name: "test rig", aliases: ["rig", "rigs"] },
|
|
215
|
+
], ArrayType(FamilyType));
|
|
216
|
+
const sites = $.const(["North plant", "South plant", "East plant", "West plant", "Central store", "River depot", "Harbour bay", "Hill site"], ArrayType(StringType));
|
|
217
|
+
const statuses = $.const([
|
|
218
|
+
{ word: "PLANNED", tone: variant("neutral", null) }, { word: "RELEASED", tone: variant("info", null) },
|
|
219
|
+
{ word: "IN PROGRESS", tone: variant("warning", null) }, { word: "COMPLETE", tone: variant("success", null) },
|
|
220
|
+
{ word: "CANCELLED", tone: variant("danger", null) },
|
|
221
|
+
], ArrayType(StatusType));
|
|
222
|
+
const machineSites = $.const(machines.toDict((_$, m) => m.code, (_$, m) => m.site), DictType(StringType, StringType));
|
|
223
|
+
// The plan rows — the prototype's seed, as typed data: a Link is a value, never a string.
|
|
224
|
+
const plan = $.let(State.bind([ArrayType(PlanRowType)], "sheet_plan_rows", [
|
|
225
|
+
{ id: "1", start: some(new Date("2026-02-16T00:00:00Z")), end: some(new Date("2026-02-20T00:00:00Z")), activity: "Machining - Roughing", qty: some(1200.0), notes: "Rough 1,200 P-40 blanks on 4 lathes for the Q3 build", stations: { from: [], to: [variant("identified", { key: "M2140" }), variant("identified", { key: "M2141" }), variant("identified", { key: "M2145" }), variant("identified", { key: "M2150" })] }, setups: some(4n), fromSite: "", toSite: "South plant", orderCode: "WO-26001", status: "RELEASED" },
|
|
226
|
+
{ id: "2", start: some(new Date("2026-02-16T00:00:00Z")), end: some(new Date("2026-02-20T00:00:00Z")), activity: "Inspection", qty: some(4.0), notes: "Inspect the 4 roughing lots at the south plant", stations: { from: [], to: [variant("counted", { n: 4n, key: "CNC lathe" })] }, setups: none, fromSite: "", toSite: "", orderCode: "WO-26002", status: "RELEASED" },
|
|
227
|
+
{ id: "3", start: some(new Date("2026-03-09T00:00:00Z")), end: some(new Date("2026-03-13T00:00:00Z")), activity: "Assembly", qty: some(280.0), notes: "Assemble 280 units on line 7", stations: { from: [variant("identified", { key: "M2151" }), variant("identified", { key: "M2160" })], to: [variant("identified", { key: "M7301" }), variant("identified", { key: "M7302" })] }, setups: none, fromSite: "South plant", toSite: "North plant", orderCode: "WO-26003", status: "RELEASED" },
|
|
228
|
+
{ id: "4", start: some(new Date("2026-04-20T00:00:00Z")), end: some(new Date("2026-04-24T00:00:00Z")), activity: "Rework", qty: some(96.0), notes: "Rework 96 rejected housings", stations: { from: [], to: [variant("placeholder", null)] }, setups: none, fromSite: "", toSite: "", orderCode: "WO-26005", status: "RELEASED" },
|
|
229
|
+
{ id: "5", start: some(new Date("2026-06-22T00:00:00Z")), end: some(new Date("2026-06-26T00:00:00Z")), activity: "Sub-assembly", qty: some(180.0), notes: "sub-assemble at cell A", stations: { from: [], to: [] }, setups: none, fromSite: "", toSite: "", orderCode: "", status: "RELEASED" },
|
|
230
|
+
{ id: "6", start: some(new Date("2026-06-29T00:00:00Z")), end: some(new Date("2026-07-03T00:00:00Z")), activity: "Sub-assembly", qty: some(180.0), notes: "Sub-assemble the second lot", stations: { from: [], to: [] }, setups: none, fromSite: "", toSite: "", orderCode: "", status: "RELEASED" },
|
|
231
|
+
{ id: "7", start: some(new Date("2026-07-06T00:00:00Z")), end: some(new Date("2026-07-10T00:00:00Z")), activity: "Machining - Roughing", qty: some(1600.0), notes: "1,600 P-40 blanks", stations: { from: [], to: [] }, setups: none, fromSite: "", toSite: "", orderCode: "", status: "RELEASED" },
|
|
232
|
+
{ id: "8", start: some(new Date("2026-07-13T00:00:00Z")), end: some(new Date("2026-07-17T00:00:00Z")), activity: "Machining", qty: some(1600.0), notes: "Finish 1,600 P-40 blanks", stations: { from: [], to: [] }, setups: none, fromSite: "", toSite: "", orderCode: "", status: "RELEASED" },
|
|
233
|
+
{ id: "9", start: some(new Date("2026-07-13T00:00:00Z")), end: some(new Date("2026-07-17T00:00:00Z")), activity: "Painting", qty: some(250.0), notes: "paint 250 P-40 housings", stations: { from: [], to: [] }, setups: none, fromSite: "", toSite: "", orderCode: "", status: "CANCELLED" },
|
|
234
|
+
{ id: "10", start: some(new Date("2026-09-07T00:00:00Z")), end: some(new Date("2026-09-11T00:00:00Z")), activity: "Painting", qty: some(180.0), notes: "paint the sub-assemblies next week", stations: { from: [variant("identified", { key: "M1104" })], to: [variant("identified", { key: "Test bay" })] }, setups: none, fromSite: "North plant", toSite: "", orderCode: "WO-26008", status: "RELEASED" },
|
|
235
|
+
{ id: "11", start: some(new Date("2026-09-07T00:00:00Z")), end: some(new Date("2026-09-11T00:00:00Z")), activity: "Painting", qty: some(140.0), notes: "paint 140 housings", stations: { from: [], to: [] }, setups: none, fromSite: "", toSite: "", orderCode: "", status: "" },
|
|
236
|
+
]));
|
|
237
|
+
const views = $.let(State.bind([ArrayType(Sheet.Types.View)], "sheet_plan_views", []));
|
|
238
|
+
const rows = $.let(plan.read());
|
|
239
|
+
// The slice — search runs THROUGH it; the sheet draws the narrowing as a lens (§3.8).
|
|
240
|
+
// A Link is searched by its display form (`M2140 > 4 x CNC lathe`), never its `.east` text.
|
|
241
|
+
const cfg = $.const(Slice.config(PlanRowType, {
|
|
242
|
+
fields: { activity: { label: "Activity" }, notes: { label: "Notes" }, status: { label: "Status" },
|
|
243
|
+
stations: { label: "Work centres", text: r => Sheet.link.print(r.stations) } },
|
|
244
|
+
searchFieldIds: ["activity", "notes", "stations"],
|
|
245
|
+
}));
|
|
246
|
+
const slice = $.let(Slice.bind([PlanRowType], "sheet_plan_slice", cfg, Slice.state(), rows, none));
|
|
247
|
+
// The arity rule (§3.4) — how many machines the To half should hold, from the quantity.
|
|
248
|
+
const impliedStations = $.const(East.function([Ctx], OptionType(Sheet.Types.Counted), ($, ctx) => {
|
|
249
|
+
const noCount = $.const(none, OptionType(Sheet.Types.Counted));
|
|
250
|
+
const qty = $.let(ctx.row.qty.match({ some: (_$, v) => v, none: (_$) => 0.0 }));
|
|
251
|
+
const piecesPerMachine = $.const(300.0);
|
|
252
|
+
// ⌈qty ÷ 300⌉ and round(qty) by hand — `toInteger` refuses a fraction.
|
|
253
|
+
const share = $.let(qty.divide(piecesPerMachine));
|
|
254
|
+
const frac = $.let(share.remainder(1.0));
|
|
255
|
+
const needed = $.let(frac.equal(0.0).ifElse((_$) => share, (_$) => share.subtract(frac).add(1.0)).toInteger());
|
|
256
|
+
const half = $.let(qty.add(0.5));
|
|
257
|
+
const whole = $.let(half.subtract(half.remainder(1.0)).toInteger());
|
|
258
|
+
return ctx.driver.match({
|
|
259
|
+
none: (_$) => noCount,
|
|
260
|
+
some: (_$, d) => d.uom.equal("lots").ifElse((_$2) => East.value(some({ n: whole, key: "CNC lathe" }), OptionType(Sheet.Types.Counted)), // one lot per machine
|
|
261
|
+
(_$2) => d.uom.equal("pcs").or(() => d.uom.equal("units")).ifElse((_$3) => qty.greater(0.0).ifElse((_$4) => East.value(some({ n: needed, key: "CNC lathe" }), OptionType(Sheet.Types.Counted)), (_$4) => noCount), (_$3) => noCount)), // hours, cartons, pallets name no machines
|
|
262
|
+
});
|
|
263
|
+
}));
|
|
264
|
+
// A member check — a machine named on a half must sit at that half's site.
|
|
265
|
+
const CheckCtx = Sheet.Types.CheckContext(PlanRowType);
|
|
266
|
+
const siteMatches = $.const(East.function([CheckCtx], OptionType(StringType), ($, c) => {
|
|
267
|
+
const noFlag = $.const(none, OptionType(StringType));
|
|
268
|
+
const site = $.let(c.half.match({ from: (_$) => c.row.fromSite, to: (_$) => c.row.toSite }));
|
|
269
|
+
return c.member.match({
|
|
270
|
+
identified: (_$, m) => site.equal("").not()
|
|
271
|
+
.and(() => machineSites.has(m.key))
|
|
272
|
+
.and(() => machineSites.get(m.key).equal(site).not())
|
|
273
|
+
.ifElse((_$2) => East.value(some(East.str `${m.key} is not at ${site}`), OptionType(StringType)), (_$2) => noFlag),
|
|
274
|
+
}, (_$) => noFlag);
|
|
275
|
+
}));
|
|
276
|
+
// The fills (§3.5) — derive, history, sequence, default, phrase, capacity — as author functions.
|
|
277
|
+
const DateFill = OptionType(Sheet.Types.Fill(DateTimeType));
|
|
278
|
+
const FloatFill = OptionType(Sheet.Types.Fill(FloatType));
|
|
279
|
+
const TextFill = OptionType(Sheet.Types.Fill(StringType));
|
|
280
|
+
const LinkFill = OptionType(Sheet.Types.Fill(Sheet.Types.Link));
|
|
281
|
+
const endFromStart = $.const(East.function([Ctx], DateFill, ($, ctx) => {
|
|
282
|
+
const noFill = $.const(none, DateFill);
|
|
283
|
+
return ctx.row.start.match({
|
|
284
|
+
none: (_$) => noFill,
|
|
285
|
+
some: (_$, start) => ctx.driver.match({
|
|
286
|
+
none: (_$2) => noFill,
|
|
287
|
+
some: (_$2, d) => East.value(some({ value: start.addDays(d.days), meta: East.str `+${d.days}d · ${d.name}` }), DateFill),
|
|
288
|
+
}),
|
|
289
|
+
});
|
|
290
|
+
}));
|
|
291
|
+
const lastSimilar = $.const(East.function([Ctx], OptionType(PlanRowType), ($, ctx) => {
|
|
292
|
+
const similar = $.let(ctx.rows.slice(0n, ctx.rowIndex).filter((_$, r) => r.activity.equal(ctx.row.activity)));
|
|
293
|
+
return similar.length().equal(0n).ifElse((_$) => East.value(none, OptionType(PlanRowType)), (_$) => East.value(some(similar.get(similar.length().subtract(1n))), OptionType(PlanRowType)));
|
|
294
|
+
}));
|
|
295
|
+
const lastQuantity = $.const(East.function([Ctx], FloatFill, ($, ctx) => {
|
|
296
|
+
const noFill = $.const(none, FloatFill);
|
|
297
|
+
const similar = $.const(lastSimilar);
|
|
298
|
+
return similar(ctx).match({
|
|
299
|
+
none: (_$) => noFill,
|
|
300
|
+
some: (_$, r) => r.qty.match({ none: (_$2) => noFill, some: (_$2, v) => East.value(some({ value: v, meta: East.str `like ${r.id}` }), FloatFill) }),
|
|
301
|
+
});
|
|
302
|
+
}));
|
|
303
|
+
const lastStations = $.const(East.function([Ctx], LinkFill, ($, ctx) => {
|
|
304
|
+
const noFill = $.const(none, LinkFill);
|
|
305
|
+
const similar = $.const(lastSimilar);
|
|
306
|
+
return similar(ctx).match({
|
|
307
|
+
none: (_$) => noFill,
|
|
308
|
+
some: (_$, r) => r.stations.from.length().add(r.stations.to.length()).equal(0n).ifElse((_$2) => noFill, (_$2) => East.value(some({ value: r.stations, meta: East.str `same stations as ${r.id}` }), LinkFill)),
|
|
309
|
+
});
|
|
310
|
+
}));
|
|
311
|
+
const nextSlot = $.const(East.function([Ctx], DateFill, ($, ctx) => {
|
|
312
|
+
const dated = $.let(ctx.rows.slice(0n, ctx.rowIndex).filter((_$, r) => r.start.hasTag("some")));
|
|
313
|
+
return dated.length().greater(0n).ifElse(($2) => {
|
|
314
|
+
const last = $2.let(dated.get(dated.length().subtract(1n)));
|
|
315
|
+
return East.value(some({ value: last.start.unwrap("some").addDays(7n), meta: East.str `week after ${last.id}` }), DateFill);
|
|
316
|
+
}, ($2) => {
|
|
317
|
+
const daysToMonday = $2.let(East.value(8n, IntegerType).subtract(ctx.today.getDayOfWeek()).remainder(7n));
|
|
318
|
+
const monday = $2.let(ctx.today.addDays(daysToMonday.equal(0n).ifElse((_$) => 7n, (_$) => daysToMonday)));
|
|
319
|
+
return East.value(some({ value: monday, meta: "next Monday" }), DateFill);
|
|
320
|
+
});
|
|
321
|
+
}));
|
|
322
|
+
const shiftQuantity = $.const(East.function([Ctx], FloatFill, ($, ctx) => {
|
|
323
|
+
const noFill = $.const(none, FloatFill);
|
|
324
|
+
return ctx.driver.match({
|
|
325
|
+
none: (_$) => noFill,
|
|
326
|
+
some: (_$, d) => East.value(some({ value: d.rate.multiply(8.0), meta: East.str `${d.rate}/h × 8 h` }), FloatFill),
|
|
327
|
+
});
|
|
328
|
+
}));
|
|
329
|
+
const phrase = $.const(East.function([Ctx], TextFill, ($, ctx) => {
|
|
330
|
+
const noFill = $.const(none, TextFill);
|
|
331
|
+
const n = $.let(ctx.row.qty.match({
|
|
332
|
+
some: ($2, v) => { const k = $2.let(v.add(0.5)); return k.subtract(k.remainder(1.0)).toInteger(); }, // round by hand — `toInteger` refuses a fraction
|
|
333
|
+
none: (_$) => 0n,
|
|
334
|
+
}));
|
|
335
|
+
return ctx.row.activity.startsWith("Machining").ifElse((_$) => East.value(some({ value: East.str `Machine ${n} P-40 blanks`, meta: "phrasing from past machining runs" }), TextFill), (_$) => ctx.row.activity.startsWith("Painting").ifElse((_$2) => East.value(some({ value: East.str `Paint ${n} P-40 housings`, meta: "phrasing from past painting runs" }), TextFill), (_$2) => noFill));
|
|
336
|
+
}));
|
|
337
|
+
const countedByQuantity = $.const(East.function([Ctx], LinkFill, ($, ctx) => {
|
|
338
|
+
const noFill = $.const(none, LinkFill);
|
|
339
|
+
const noMembers = $.const([], ArrayType(Sheet.Types.Member));
|
|
340
|
+
const implied = $.const(impliedStations);
|
|
341
|
+
return implied(ctx).match({
|
|
342
|
+
none: (_$) => noFill,
|
|
343
|
+
some: (_$, c) => East.value(some({ value: { from: noMembers, to: [variant("counted", { n: c.n, key: c.key })] }, meta: "capacity · from the quantity" }), LinkFill),
|
|
344
|
+
});
|
|
345
|
+
}));
|
|
346
|
+
// The proposers (§3.6) — a domain pattern, a learned follower, and an ASYNC model call.
|
|
347
|
+
const roughingFollowUps = $.const(East.function([Ctx], Proposals, ($, ctx) => {
|
|
348
|
+
const empty = $.const([], Proposals);
|
|
349
|
+
const implied = $.const(impliedStations);
|
|
350
|
+
const n = $.let(implied(ctx).match({ some: (_$, c) => c.n, none: (_$) => 1n }));
|
|
351
|
+
return ctx.row.activity.equal("Machining - Roughing").and(() => ctx.row.end.hasTag("some")).ifElse(($2) => {
|
|
352
|
+
const endAt = $2.let(ctx.row.end.unwrap("some"));
|
|
353
|
+
return $2.const([
|
|
354
|
+
{ patch: Sheet.patch(PlanRowType, { activity: "Inspection", start: ctx.row.start, end: ctx.row.end, qty: some(n.toFloat()), notes: East.str `Inspect ${n} roughing lots` }),
|
|
355
|
+
meta: "inspection · same days" },
|
|
356
|
+
{ patch: Sheet.patch(PlanRowType, { activity: "Machining", start: some(endAt.addDays(3n)), end: some(endAt.addDays(7n)), qty: ctx.row.qty, notes: "Finish the roughed blanks" }),
|
|
357
|
+
meta: "finishing · end +3…+7 d" },
|
|
358
|
+
], Proposals);
|
|
359
|
+
}, (_$2) => empty);
|
|
360
|
+
}));
|
|
361
|
+
const lastFollower = $.const(East.function([Ctx], Proposals, ($, ctx) => {
|
|
362
|
+
const empty = $.const([], Proposals);
|
|
363
|
+
const dated = $.let(ctx.rows.filter((_$, r) => r.start.hasTag("some").and(() => r.activity.equal("").not())));
|
|
364
|
+
const n = $.let(dated.length());
|
|
365
|
+
const upper = $.let(n.greater(1n).ifElse((_$) => n.subtract(1n), (_$) => 0n));
|
|
366
|
+
const pairs = $.let(East.Array.range(0n, upper).filter((_$, i) => dated.get(i).activity.equal(ctx.row.activity).and(() => dated.get(i.add(1n)).activity.equal(ctx.row.activity).not())));
|
|
367
|
+
return pairs.length().equal(0n).or(() => ctx.row.activity.equal("")).or(() => ctx.row.start.hasTag("some").not()).ifElse((_$) => empty, ($2) => {
|
|
368
|
+
const i = $2.let(pairs.get(pairs.length().subtract(1n)));
|
|
369
|
+
const from = $2.let(dated.get(i));
|
|
370
|
+
const next = $2.let(dated.get(i.add(1n)));
|
|
371
|
+
const gapMs = $2.let(next.start.unwrap("some").toEpochMilliseconds().subtract(from.start.unwrap("some").toEpochMilliseconds()));
|
|
372
|
+
const at = $2.let(ctx.row.start.unwrap("some").addMilliseconds(gapMs));
|
|
373
|
+
return $2.const([{
|
|
374
|
+
patch: Sheet.patch(PlanRowType, { activity: next.activity, start: some(at), qty: next.qty, notes: next.notes }),
|
|
375
|
+
meta: East.str `${next.activity} followed ${ctx.row.activity} last time · +${gapMs.divide(86400000n)}d`,
|
|
376
|
+
}], Proposals);
|
|
377
|
+
});
|
|
378
|
+
}));
|
|
379
|
+
const modelProposals = $.const(East.asyncFunction([Ctx], Proposals, (_$, ctx) => planRecommend(ctx)));
|
|
380
|
+
const planned = $.let(rows.filter((_$, r) => r.activity.length().greater(0n)).length());
|
|
381
|
+
return (_jsx(Sheet, { data: rows, id: "id", owned: r => r.orderCode.length().greater(0n).or(() => r.status.equal("CANCELLED")), driver: Sheet.driver("activity", activities, { key: a => a.name, label: a => a.name }), registers: {
|
|
382
|
+
stations: Sheet.register.concat([
|
|
383
|
+
Sheet.register.members(machines, { kind: "machine", key: m => m.code, label: m => m.code,
|
|
384
|
+
meta: m => some(m.family), parent: m => some(m.line) }),
|
|
385
|
+
Sheet.register.members(lines, { kind: "line", key: l => l.name, label: l => l.name,
|
|
386
|
+
aliases: l => l.aliases, meta: l => some(East.str `line · ${l.machines}`) }),
|
|
387
|
+
// The countable-by-attribute kind: a family names a count of machines, resolved to codes later.
|
|
388
|
+
Sheet.register.members(families, { kind: "family", key: f => f.name, label: f => f.name,
|
|
389
|
+
aliases: f => f.aliases, meta: _f => some("family") }),
|
|
390
|
+
]),
|
|
391
|
+
sites: Sheet.register.members(sites, { kind: "site", key: s => s, label: s => s }),
|
|
392
|
+
statuses: Sheet.register.members(statuses, { kind: "status", key: s => s.word, label: s => s.word, tone: s => some(s.tone) }),
|
|
393
|
+
}, columns: {
|
|
394
|
+
start: Sheet.column.date(PlanRowType, { header: "Start", sub: "d/m · fri · +3d", width: "96px", fill: [nextSlot] }),
|
|
395
|
+
end: Sheet.column.date(PlanRowType, { header: "End", sub: "4d = start+4", width: "96px", base: "start", fill: [endFromStart] }),
|
|
396
|
+
activity: Sheet.column.lookup(PlanRowType, { header: "Activity", sub: "activity register", width: "214px" }),
|
|
397
|
+
qty: Sheet.column.quantity(PlanRowType, ActivityType, {
|
|
398
|
+
header: "Qty", sub: "uom per activity", width: "112px", uom: d => d.uom,
|
|
399
|
+
format: Format.Number({ maximumFractionDigits: 0n }), fill: [lastQuantity, shiftQuantity]
|
|
400
|
+
}),
|
|
401
|
+
notes: Sheet.column.text(PlanRowType, { header: "Notes", sub: "free text", width: "250px", fill: [phrase] }),
|
|
402
|
+
stations: Sheet.column.link(PlanRowType, ActivityType, "stations", {
|
|
403
|
+
header: "Work centres", sub: "from → to · 4 x lathe · machine · line", width: "352px",
|
|
404
|
+
members: [{ kind: "machine", identified: true }, { kind: "range", identified: true },
|
|
405
|
+
{ kind: "line", countable: true, resolvesTo: "machine" },
|
|
406
|
+
{ kind: "family", countable: true, resolvesTo: "machine" }],
|
|
407
|
+
multiple: { forms: ["N x kind", "kind x N"], ops: ["x", "X", "*", "×"], appliesTo: "countable" },
|
|
408
|
+
sides: { value: d => d.sides, locks: { from: { to: "external", in: "in place" }, to: { from: "external" } } },
|
|
409
|
+
arity: Sheet.link.arity("to", impliedStations),
|
|
410
|
+
check: [Sheet.link.check.exists(), siteMatches],
|
|
411
|
+
fill: [lastStations, countedByQuantity]
|
|
412
|
+
}),
|
|
413
|
+
setups: Sheet.column.integer(PlanRowType, { header: "Setups", sub: "n", width: "64px" }),
|
|
414
|
+
fromSite: Sheet.column.reference(PlanRowType, "sites", { header: "From site", sub: "site register", width: "112px" }),
|
|
415
|
+
toSite: Sheet.column.reference(PlanRowType, "sites", { header: "To site", sub: "site register", width: "112px" }),
|
|
416
|
+
orderCode: Sheet.column.stamped(PlanRowType, { header: "Order code", sub: "stamped on release", owner: "ERP", width: "104px" }),
|
|
417
|
+
status: Sheet.column.enum(PlanRowType, "statuses", { header: "Status", sub: "erp", width: "124px" }),
|
|
418
|
+
}, suggest: { ahead: 2n, triggers: ["activity", "start", "end", "qty", "notes", "stations"],
|
|
419
|
+
propose: [roughingFollowUps, modelProposals, lastFollower] }, slice: slice, affordances: ["search", "filter"], views: views.read(), onViewsChange: views.write, onUpdate: plan.write, footer: [{ text: East.str `${planned} planned` }], style: { height: "fill" } }));
|
|
420
|
+
} }))),
|
|
421
|
+
inputs: [],
|
|
422
|
+
});
|
|
423
|
+
/**
|
|
424
|
+
* The copilot in isolation (§3.5–§3.6) — the prototype's rules as author
|
|
425
|
+
* functions: derive (End from Start + the driver's days), history (the last
|
|
426
|
+
* similar row's quantity), sequence (a week after the last dated row), default
|
|
427
|
+
* (eight hours at the driver's rate), a domain pattern proposer, a learned
|
|
428
|
+
* follower, and an ASYNC proposer behind a platform function; every take
|
|
429
|
+
* logs its provenance through `onEdit`.
|
|
430
|
+
*/
|
|
431
|
+
export const sheetCopilot = example({
|
|
432
|
+
keywords: ["Sheet", "Root", "copilot", "fill", "provider", "propose", "proposer", "suggest", "derive", "history", "sequence", "default", "learned", "follower", "asyncFunction", "asyncPlatform", "pending", "latest wins", "provenance", "onEdit", "source", "Context", "Fill", "Patch", "Proposal", "Reactive", "State"],
|
|
433
|
+
description: "The copilot in isolation — derive, history, sequence and default fills, a pattern proposer, a learned follower and an async model proposer, all author East functions over the typed context; takes log their provenance",
|
|
434
|
+
fn: East.function([], UIComponentType, (_$) => (_jsx(Reactive, { children: $ => {
|
|
435
|
+
const PlanRowType = StructType({
|
|
436
|
+
id: StringType, start: OptionType(DateTimeType), end: OptionType(DateTimeType), activity: StringType,
|
|
437
|
+
qty: OptionType(FloatType), notes: StringType, stations: Sheet.Types.Link, setups: OptionType(IntegerType),
|
|
438
|
+
fromSite: StringType, toSite: StringType, orderCode: StringType, status: StringType,
|
|
439
|
+
});
|
|
440
|
+
const ActivityType = StructType({ name: StringType, uom: StringType, rate: FloatType, fte: IntegerType, days: IntegerType, sides: Sheet.Types.Sides });
|
|
441
|
+
const Ctx = Sheet.Types.Context(PlanRowType, ActivityType);
|
|
442
|
+
const Proposals = ArrayType(Sheet.Types.Proposal(PlanRowType));
|
|
443
|
+
const activities = $.const([
|
|
444
|
+
{ name: "Machining", uom: "pcs", rate: 60.0, fte: 2n, days: 4n, sides: variant("both", null) },
|
|
445
|
+
{ name: "Machining - Roughing", uom: "pcs", rate: 80.0, fte: 2n, days: 4n, sides: variant("both", null) },
|
|
446
|
+
{ name: "Painting", uom: "pcs", rate: 50.0, fte: 3n, days: 4n, sides: variant("both", null) },
|
|
447
|
+
{ name: "Inspection", uom: "lots", rate: 4.0, fte: 2n, days: 4n, sides: variant("in", null) },
|
|
448
|
+
{ name: "Packaging", uom: "cartons", rate: 120.0, fte: 1n, days: 3n, sides: variant("both", null) },
|
|
449
|
+
], ArrayType(ActivityType));
|
|
450
|
+
const plan = $.let(State.bind([ArrayType(PlanRowType)], "sheet_copilot_rows", [
|
|
451
|
+
{ id: "1", start: some(new Date("2026-02-16T00:00:00Z")), end: some(new Date("2026-02-20T00:00:00Z")), activity: "Machining - Roughing", qty: some(1200.0), notes: "Rough the P-40 blanks", stations: { from: [], to: [] }, setups: none, fromSite: "", toSite: "", orderCode: "", status: "PLANNED" },
|
|
452
|
+
{ id: "2", start: some(new Date("2026-02-16T00:00:00Z")), end: some(new Date("2026-02-20T00:00:00Z")), activity: "Inspection", qty: some(4.0), notes: "Inspect the 4 lots", stations: { from: [], to: [] }, setups: none, fromSite: "", toSite: "", orderCode: "", status: "PLANNED" },
|
|
453
|
+
{ id: "3", start: some(new Date("2026-03-09T00:00:00Z")), end: none, activity: "Painting", qty: some(250.0), notes: "", stations: { from: [], to: [] }, setups: none, fromSite: "", toSite: "", orderCode: "", status: "PLANNED" },
|
|
454
|
+
]));
|
|
455
|
+
const log = $.let(State.bind([ArrayType(StringType)], "sheet_copilot_log", []));
|
|
456
|
+
const DateFill = OptionType(Sheet.Types.Fill(DateTimeType));
|
|
457
|
+
const FloatFill = OptionType(Sheet.Types.Fill(FloatType));
|
|
458
|
+
// derive — End = Start + the driver's days.
|
|
459
|
+
const endFromStart = $.const(East.function([Ctx], DateFill, ($, ctx) => {
|
|
460
|
+
const noFill = $.const(none, DateFill);
|
|
461
|
+
return ctx.row.start.match({
|
|
462
|
+
none: (_$) => noFill,
|
|
463
|
+
some: (_$, start) => ctx.driver.match({
|
|
464
|
+
none: (_$2) => noFill,
|
|
465
|
+
some: (_$2, d) => East.value(some({ value: start.addDays(d.days), meta: East.str `+${d.days}d · ${d.name}` }), DateFill),
|
|
466
|
+
}),
|
|
467
|
+
});
|
|
468
|
+
}));
|
|
469
|
+
// history — the last row above with this activity.
|
|
470
|
+
const lastQuantity = $.const(East.function([Ctx], FloatFill, ($, ctx) => {
|
|
471
|
+
const noFill = $.const(none, FloatFill);
|
|
472
|
+
const similar = $.let(ctx.rows.slice(0n, ctx.rowIndex).filter((_$, r) => r.activity.equal(ctx.row.activity)));
|
|
473
|
+
return similar.length().equal(0n).ifElse((_$) => noFill, ($2) => {
|
|
474
|
+
const r = $2.let(similar.get(similar.length().subtract(1n)));
|
|
475
|
+
return r.qty.match({ none: (_$) => noFill, some: (_$, v) => East.value(some({ value: v, meta: East.str `like ${r.id}` }), FloatFill) });
|
|
476
|
+
});
|
|
477
|
+
}));
|
|
478
|
+
// sequence — a week after the nearest dated row above; else next Monday.
|
|
479
|
+
const nextSlot = $.const(East.function([Ctx], DateFill, ($, ctx) => {
|
|
480
|
+
const dated = $.let(ctx.rows.slice(0n, ctx.rowIndex).filter((_$, r) => r.start.hasTag("some")));
|
|
481
|
+
return dated.length().greater(0n).ifElse(($2) => {
|
|
482
|
+
const last = $2.let(dated.get(dated.length().subtract(1n)));
|
|
483
|
+
return East.value(some({ value: last.start.unwrap("some").addDays(7n), meta: East.str `week after ${last.id}` }), DateFill);
|
|
484
|
+
}, ($2) => {
|
|
485
|
+
const daysToMonday = $2.let(East.value(8n, IntegerType).subtract(ctx.today.getDayOfWeek()).remainder(7n));
|
|
486
|
+
const monday = $2.let(ctx.today.addDays(daysToMonday.equal(0n).ifElse((_$) => 7n, (_$) => daysToMonday)));
|
|
487
|
+
return East.value(some({ value: monday, meta: "next Monday" }), DateFill);
|
|
488
|
+
});
|
|
489
|
+
}));
|
|
490
|
+
// default — eight hours at the driver's rate when nothing similar exists.
|
|
491
|
+
const shiftQuantity = $.const(East.function([Ctx], FloatFill, ($, ctx) => {
|
|
492
|
+
const noFill = $.const(none, FloatFill);
|
|
493
|
+
return ctx.driver.match({
|
|
494
|
+
none: (_$) => noFill,
|
|
495
|
+
some: (_$, d) => East.value(some({ value: d.rate.multiply(8.0), meta: East.str `${d.rate}/h × 8 h` }), FloatFill),
|
|
496
|
+
});
|
|
497
|
+
}));
|
|
498
|
+
// A domain pattern — a roughing run is followed by an inspection and a finishing run.
|
|
499
|
+
const roughingFollowUps = $.const(East.function([Ctx], Proposals, ($, ctx) => {
|
|
500
|
+
const empty = $.const([], Proposals);
|
|
501
|
+
return ctx.row.activity.equal("Machining - Roughing").and(() => ctx.row.end.hasTag("some")).ifElse(($2) => {
|
|
502
|
+
const endAt = $2.let(ctx.row.end.unwrap("some"));
|
|
503
|
+
return $2.const([
|
|
504
|
+
{ patch: Sheet.patch(PlanRowType, { activity: "Inspection", start: ctx.row.start, end: ctx.row.end, qty: some(4.0), notes: "Inspect 4 lots" }),
|
|
505
|
+
meta: "inspection · same days" },
|
|
506
|
+
{ patch: Sheet.patch(PlanRowType, { activity: "Machining", start: some(endAt.addDays(3n)), end: some(endAt.addDays(7n)), qty: ctx.row.qty, notes: "Finish the roughed blanks" }),
|
|
507
|
+
meta: "finishing · end +3…+7 d" },
|
|
508
|
+
], Proposals);
|
|
509
|
+
}, (_$2) => empty);
|
|
510
|
+
}));
|
|
511
|
+
// Learned from the sheet — what followed this activity last time, after how long.
|
|
512
|
+
const lastFollower = $.const(East.function([Ctx], Proposals, ($, ctx) => {
|
|
513
|
+
const empty = $.const([], Proposals);
|
|
514
|
+
const dated = $.let(ctx.rows.filter((_$, r) => r.start.hasTag("some").and(() => r.activity.equal("").not())));
|
|
515
|
+
const n = $.let(dated.length());
|
|
516
|
+
const upper = $.let(n.greater(1n).ifElse((_$) => n.subtract(1n), (_$) => 0n));
|
|
517
|
+
const pairs = $.let(East.Array.range(0n, upper).filter((_$, i) => dated.get(i).activity.equal(ctx.row.activity).and(() => dated.get(i.add(1n)).activity.equal(ctx.row.activity).not())));
|
|
518
|
+
return pairs.length().equal(0n).or(() => ctx.row.activity.equal("")).or(() => ctx.row.start.hasTag("some").not()).ifElse((_$) => empty, ($2) => {
|
|
519
|
+
const i = $2.let(pairs.get(pairs.length().subtract(1n)));
|
|
520
|
+
const from = $2.let(dated.get(i));
|
|
521
|
+
const next = $2.let(dated.get(i.add(1n)));
|
|
522
|
+
const gapMs = $2.let(next.start.unwrap("some").toEpochMilliseconds().subtract(from.start.unwrap("some").toEpochMilliseconds()));
|
|
523
|
+
const at = $2.let(ctx.row.start.unwrap("some").addMilliseconds(gapMs));
|
|
524
|
+
return $2.const([{
|
|
525
|
+
patch: Sheet.patch(PlanRowType, { activity: next.activity, start: some(at), qty: next.qty, notes: next.notes }),
|
|
526
|
+
meta: East.str `${next.activity} followed ${ctx.row.activity} last time · +${gapMs.divide(86400000n)}d`,
|
|
527
|
+
}], Proposals);
|
|
528
|
+
});
|
|
529
|
+
}));
|
|
530
|
+
// A model — an ASYNC proposer; the strip shows a pending chip and a newer context cancels the wait.
|
|
531
|
+
const modelProposals = $.const(East.asyncFunction([Ctx], Proposals, (_$, ctx) => planRecommend(ctx)));
|
|
532
|
+
// Every take carries its provenance through `onEdit` — the log measures copilot uptake.
|
|
533
|
+
const onEdit = $.const(East.function([Sheet.Types.Edit(PlanRowType)], NullType, ($, e) => {
|
|
534
|
+
const line = $.let(e.match({
|
|
535
|
+
commit: (_$, c) => East.str `${c.source.getTag()} · ${c.rowId} · ${c.key}`,
|
|
536
|
+
insert: (_$, i) => East.str `${i.source.getTag()} · new row ${i.row.id}`,
|
|
537
|
+
remove: (_$, r) => East.str `removed ${r.rowIds.length()}`,
|
|
538
|
+
}));
|
|
539
|
+
$(log.write(log.read().concat([line])));
|
|
540
|
+
}));
|
|
541
|
+
const entries = $.let(log.read());
|
|
542
|
+
return (_jsxs(VStack, { gap: "3", align: "stretch", children: [_jsx(Sheet, { data: plan.read(), id: "id", driver: Sheet.driver("activity", activities, { key: a => a.name, label: a => a.name }), columns: {
|
|
543
|
+
start: Sheet.column.date(PlanRowType, { header: "Start", sub: "sequence", width: "96px", fill: [nextSlot] }),
|
|
544
|
+
end: Sheet.column.date(PlanRowType, { header: "End", sub: "derive", width: "96px", base: "start", fill: [endFromStart] }),
|
|
545
|
+
activity: Sheet.column.lookup(PlanRowType, { header: "Activity", sub: "activity register", width: "214px" }),
|
|
546
|
+
qty: Sheet.column.quantity(PlanRowType, ActivityType, { header: "Qty", sub: "history · default", width: "112px", uom: d => d.uom, fill: [lastQuantity, shiftQuantity] }),
|
|
547
|
+
notes: Sheet.column.text(PlanRowType, { header: "Notes", width: "260px" }),
|
|
548
|
+
}, suggest: { ahead: 2n, triggers: ["activity", "start", "end", "qty"], propose: [roughingFollowUps, modelProposals, lastFollower] }, onEdit: onEdit, onUpdate: plan.write, style: { height: "420px" } }), _jsx(Text.MonoLabel, { children: East.str `COPILOT LOG · ${entries.length()} edits · ${entries.filter((_$, l) => l.startsWith("typed").not()).length()} from the copilot` })] }));
|
|
549
|
+
} }))),
|
|
550
|
+
inputs: [],
|
|
551
|
+
});
|
|
552
|
+
/**
|
|
553
|
+
* The lens (§3.8) — search and filter run through the bound slice; the sheet
|
|
554
|
+
* never narrows, it draws non-matching rows as collapsed context bands
|
|
555
|
+
* while hits keep their row numbers; a Link column is searched through its
|
|
556
|
+
* display form (`text: r => Sheet.link.print(r.stations)`); views are
|
|
557
|
+
* slice-state snapshots evaluated live, with their context and reveals.
|
|
558
|
+
*/
|
|
559
|
+
export const sheetLens = example({
|
|
560
|
+
keywords: ["Sheet", "Root", "slice", "search", "filter", "lens", "bands", "context", "reveal", "views", "onViewsChange", "activeView", "text", "Sheet.link.print", "set", "Slice", "config", "bind", "state", "Reactive", "State"],
|
|
561
|
+
description: "The lens — search and filter through the bound slice drawn as context bands (hits keep their row numbers), a Link column searched through its display text, and saved views as slice-state snapshots with their context and reveals",
|
|
562
|
+
fn: East.function([], UIComponentType, (_$) => (_jsx(Reactive, { children: $ => {
|
|
563
|
+
const JobType = StructType({ id: StringType, start: OptionType(DateTimeType), activity: StringType, notes: StringType, stations: Sheet.Types.Link, status: StringType, qty: OptionType(FloatType) });
|
|
564
|
+
const MachineType = StructType({ code: StringType, family: StringType });
|
|
565
|
+
// Sixty rows derived East-side — enough for a narrowing to collapse into bands.
|
|
566
|
+
const activities = $.const(["Machining", "Painting", "Packaging", "Changeover", "Maintenance"], ArrayType(StringType));
|
|
567
|
+
const words = $.const(["PLANNED", "RELEASED", "COMPLETE"], ArrayType(StringType));
|
|
568
|
+
const machines = $.const([
|
|
569
|
+
{ code: "M2140", family: "CNC lathe" }, { code: "M2141", family: "CNC lathe" }, { code: "M2145", family: "CNC lathe" },
|
|
570
|
+
{ code: "M3210", family: "5-axis mill" }, { code: "M7301", family: "assembly bench" },
|
|
571
|
+
], ArrayType(MachineType));
|
|
572
|
+
const noMembers = $.const([], ArrayType(Sheet.Types.Member));
|
|
573
|
+
const rows = $.let(East.Array.generate(60n, JobType, (_$, i) => ({
|
|
574
|
+
id: East.str `j${i}`,
|
|
575
|
+
start: some(East.value(new Date("2026-02-02T00:00:00Z"), DateTimeType).addDays(i.multiply(3n))),
|
|
576
|
+
activity: activities.get(i.remainder(5n)),
|
|
577
|
+
notes: i.remainder(7n).equal(0n).ifElse((_$2) => "urgent — inspect before shipping", (_$2) => East.str `lot ${i.add(100n)}`),
|
|
578
|
+
// Machining runs name a count of lathes; the rest a machine by code.
|
|
579
|
+
stations: i.remainder(5n).equal(0n).ifElse((_$2) => East.value({ from: noMembers, to: [variant("counted", { n: i.remainder(3n).add(2n), key: "CNC lathe" })] }, Sheet.Types.Link), (_$2) => East.value({ from: noMembers, to: [variant("identified", { key: machines.get(i.remainder(5n)).code })] }, Sheet.Types.Link)),
|
|
580
|
+
status: words.get(i.remainder(3n)),
|
|
581
|
+
qty: some(i.multiply(40n).toFloat().add(180.0)),
|
|
582
|
+
})), ArrayType(JobType));
|
|
583
|
+
// The Link is searched through its display form — `3 x CNC lathe`, `M7301` — not its `.east` text.
|
|
584
|
+
const cfg = $.const(Slice.config(JobType, {
|
|
585
|
+
fields: {
|
|
586
|
+
activity: { label: "Activity", hints: ["Machining", "Painting", "Packaging", "Changeover", "Maintenance"] },
|
|
587
|
+
notes: { label: "Notes" },
|
|
588
|
+
stations: { label: "Work centres", text: r => Sheet.link.print(r.stations) },
|
|
589
|
+
status: { label: "Status" },
|
|
590
|
+
},
|
|
591
|
+
searchFieldIds: ["activity", "notes", "stations"],
|
|
592
|
+
}));
|
|
593
|
+
const slice = $.let(Slice.bind([JobType], "sheet_lens_slice", cfg, Slice.state(), rows, none));
|
|
594
|
+
const views = $.let(State.bind([ArrayType(Sheet.Types.View)], "sheet_lens_views", [
|
|
595
|
+
{ id: "painting", name: "PAINTING", narrowing: Slice.state({ search: some("painting") }), context: 1n, reveals: [] },
|
|
596
|
+
{ id: "lathes", name: "LATHES", narrowing: Slice.state({ search: some("lathe") }), context: 0n, reveals: [] },
|
|
597
|
+
{ id: "urgent", name: "URGENT", narrowing: Slice.state({ search: some("urgent") }), context: 0n, reveals: [] },
|
|
598
|
+
]));
|
|
599
|
+
return (_jsx(Sheet, { data: rows, id: "id", registers: {
|
|
600
|
+
stations: Sheet.register.concat([
|
|
601
|
+
Sheet.register.members(machines, { kind: "machine", key: m => m.code, label: m => m.code, meta: m => some(m.family) }),
|
|
602
|
+
Sheet.register.members(machines, { kind: "family", key: m => m.family, label: m => m.family, meta: _m => some("family") }),
|
|
603
|
+
]),
|
|
604
|
+
}, columns: {
|
|
605
|
+
start: Sheet.column.date(JobType, { header: "Start", width: "96px" }),
|
|
606
|
+
activity: Sheet.column.text(JobType, { header: "Activity", width: "160px" }),
|
|
607
|
+
notes: Sheet.column.text(JobType, { header: "Notes", sub: "free text", width: "240px" }),
|
|
608
|
+
stations: Sheet.column.set(JobType, "stations", { header: "Work centres", sub: "3 x lathe · machine", width: "220px",
|
|
609
|
+
members: [{ kind: "machine", identified: true }, { kind: "family", countable: true, resolvesTo: "machine" }] }),
|
|
610
|
+
status: Sheet.column.text(JobType, { header: "Status", width: "120px" }),
|
|
611
|
+
qty: Sheet.column.quantity(JobType, { header: "Qty", width: "112px", format: Format.Number({ maximumFractionDigits: 0n }) }),
|
|
612
|
+
}, slice: slice, affordances: ["search", "filter"], views: views.read(), onViewsChange: views.write, activeView: some("painting"), style: { height: "420px" } }));
|
|
613
|
+
} }))),
|
|
614
|
+
inputs: [],
|
|
615
|
+
});
|
|
616
|
+
/**
|
|
617
|
+
* Write-back (§3.7) — the raw `onEdit` channel observing every commit,
|
|
618
|
+
* insert and remove with its provenance, beside `onUpdate` writing the whole
|
|
619
|
+
* collection; a staged bind holds changes until they are applied.
|
|
620
|
+
*/
|
|
621
|
+
export const sheetWriteBack = example({
|
|
622
|
+
keywords: ["Sheet", "Root", "onEdit", "onUpdate", "write-back", "commit", "insert", "remove", "source", "provenance", "Edit", "staged", "newRowId", "Reactive", "State"],
|
|
623
|
+
description: "Write-back — the raw onEdit event channel logging commit / insert / remove with provenance, beside onUpdate writing the whole collection, and a host-minted row id",
|
|
624
|
+
fn: East.function([], UIComponentType, (_$) => (_jsx(Reactive, { children: $ => {
|
|
625
|
+
const JobType = StructType({ id: StringType, task: StringType, qty: OptionType(FloatType), createdBy: StringType });
|
|
626
|
+
const jobs = $.let(State.bind([ArrayType(JobType)], "sheet_writeback_rows", [
|
|
627
|
+
{ id: "j1", task: "Machining", qty: some(1200.0), createdBy: "planner" },
|
|
628
|
+
{ id: "j2", task: "Painting", qty: none, createdBy: "planner" },
|
|
629
|
+
]));
|
|
630
|
+
const log = $.let(State.bind([ArrayType(StringType)], "sheet_writeback_log", []));
|
|
631
|
+
const counter = $.let(State.bind([IntegerType], "sheet_writeback_counter", 3n));
|
|
632
|
+
// The raw channel — the row AFTER the commit, typed over the host's
|
|
633
|
+
// struct: `createdBy` has no column, and still reads its real value.
|
|
634
|
+
const onEdit = $.const(East.function([Sheet.Types.Edit(JobType)], NullType, ($, e) => {
|
|
635
|
+
const line = $.let(e.match({
|
|
636
|
+
commit: (_$, c) => East.str `commit ${c.rowId}.${c.key} (${c.source.getTag()}) · ${c.row.task} · by ${c.row.createdBy}`,
|
|
637
|
+
insert: (_$, i) => East.str `insert ${i.row.id} after ${i.afterRowId.match({ some: (_$2, a) => a, none: (_$2) => "end" })}`,
|
|
638
|
+
remove: (_$, r) => East.str `remove ${r.rowIds.stringJoin(", ")}`,
|
|
639
|
+
}));
|
|
640
|
+
$(log.write(log.read().concat([line])));
|
|
641
|
+
}));
|
|
642
|
+
// The host mints ids for inserted rows.
|
|
643
|
+
const newRowId = $.const(East.function([], StringType, ($) => {
|
|
644
|
+
const n = $.let(counter.read());
|
|
645
|
+
$(counter.write(n.add(1n)));
|
|
646
|
+
return East.str `j${n}`;
|
|
647
|
+
}));
|
|
648
|
+
const entries = $.let(log.read());
|
|
649
|
+
return (_jsxs(VStack, { gap: "3", align: "stretch", children: [_jsx(Sheet, { data: jobs.read(), id: "id", columns: {
|
|
650
|
+
task: Sheet.column.text(JobType, { header: "Task", width: "200px" }),
|
|
651
|
+
qty: Sheet.column.quantity(JobType, { header: "Qty", width: "112px" }),
|
|
652
|
+
}, onEdit: onEdit, onUpdate: jobs.write, newRowId: newRowId, blanks: 6n }), _jsx(Text.MonoLabel, { children: East.str `EDITS · ${entries.length()}` }), entries.map((_$, l) => _jsx(Text, { textStyle: "caption", color: "fg.muted", children: l }))] }));
|
|
653
|
+
} }))),
|
|
654
|
+
inputs: [],
|
|
655
|
+
});
|
|
656
|
+
/**
|
|
657
|
+
* The paged arm (§3.13) — the same tag over a windowed source: windows land
|
|
658
|
+
* as the planner scrolls, the footer carries the transport line, the blank
|
|
659
|
+
* tail appears once the source is exhausted, search is a key search over
|
|
660
|
+
* `seek`, and edits go through `onEdit` to the host's journal.
|
|
661
|
+
*/
|
|
662
|
+
export const sheetPaged = example({
|
|
663
|
+
keywords: ["Sheet", "Root", "paged", "Paged", "of", "window", "page", "seek", "key search", "transport", "partial", "exhausted", "onEdit", "journal", "row-source", "Reactive", "State"],
|
|
664
|
+
description: "A paged sheet — the same tag over a Paged.of source keyed by id: windows land on scroll, the footer counts elements, key search seeks the source, and edits go to the host's journal through onEdit",
|
|
665
|
+
fn: East.function([], UIComponentType, (_$) => (_jsx(Reactive, { children: $ => {
|
|
666
|
+
const JobType = StructType({ id: StringType, start: OptionType(DateTimeType), task: StringType, qty: OptionType(FloatType) });
|
|
667
|
+
// Six hundred rows behind a source that windows them — keyed by id,
|
|
668
|
+
// which sorts as the sheet reads, so `seek` addresses real rows.
|
|
669
|
+
const rows = $.const(East.Array.generate(600n, JobType, (_$, i) => ({
|
|
670
|
+
id: East.str `J${i.add(1000n)}`,
|
|
671
|
+
start: some(East.value(new Date("2026-01-05T00:00:00Z"), DateTimeType).addDays(i)),
|
|
672
|
+
task: i.remainder(3n).equal(0n).ifElse((_$2) => "Machining", (_$2) => i.remainder(3n).equal(1n).ifElse((_$3) => "Painting", (_$3) => "Packaging")),
|
|
673
|
+
qty: some(i.multiply(15n).toFloat().add(180.0)),
|
|
674
|
+
})), ArrayType(JobType));
|
|
675
|
+
const source = $.const(Paged.of("sheet_paged_jobs", rows, { key: r => r.id })); // Data.bindPaged(planInput) in e3-ui
|
|
676
|
+
const edits = $.let(State.bind([ArrayType(Sheet.Types.Edit(JobType))], "sheet_paged_edits", []));
|
|
677
|
+
const applyEdit = $.const(East.function([Sheet.Types.Edit(JobType)], NullType, ($, e) => {
|
|
678
|
+
$(edits.write(edits.read().concat([e]))); // the host's edit journal, replayed server-side
|
|
679
|
+
}));
|
|
680
|
+
const journal = $.let(edits.read());
|
|
681
|
+
return (_jsxs(VStack, { gap: "3", align: "stretch", children: [_jsx(Sheet, { data: source, id: "id", columns: {
|
|
682
|
+
start: Sheet.column.date(JobType, { header: "Start", width: "96px" }),
|
|
683
|
+
task: Sheet.column.text(JobType, { header: "Task", width: "180px" }),
|
|
684
|
+
qty: Sheet.column.quantity(JobType, { header: "Qty", width: "112px", format: Format.Number({ maximumFractionDigits: 0n }) }),
|
|
685
|
+
}, onEdit: applyEdit, style: { height: "420px" } }), _jsx(Text.MonoLabel, { children: East.str `JOURNAL · ${journal.length()} edits` })] }));
|
|
686
|
+
} }))),
|
|
687
|
+
inputs: [],
|
|
688
|
+
});
|
|
689
|
+
/**
|
|
690
|
+
* Stress — two thousand rows, every column virtualised, the copilot's
|
|
691
|
+
* history fill reasoning over the resident sheet.
|
|
692
|
+
*/
|
|
693
|
+
export const sheetStress = example({
|
|
694
|
+
keywords: ["Sheet", "Root", "stress", "virtualization", "2000", "rows", "performance", "fill", "history", "Reactive", "State"],
|
|
695
|
+
description: "Two thousand rows in one sheet — virtualised rows, typed columns, and a history fill that reads the whole resident sheet",
|
|
696
|
+
fn: East.function([], UIComponentType, (_$) => (_jsx(Reactive, { children: $ => {
|
|
697
|
+
const JobType = StructType({ id: StringType, start: OptionType(DateTimeType), task: StringType, qty: OptionType(FloatType), notes: StringType });
|
|
698
|
+
const Ctx = Sheet.Types.Context(JobType);
|
|
699
|
+
const tasks = $.const(["Machining", "Painting", "Packaging", "Changeover", "Maintenance", "Receiving", "Shipping"], ArrayType(StringType));
|
|
700
|
+
const rows = $.let(State.bind([ArrayType(JobType)], "sheet_stress_rows", East.Array.generate(2000n, JobType, (_$, i) => ({
|
|
701
|
+
id: East.str `S${i}`,
|
|
702
|
+
start: some(East.value(new Date("2026-01-05T00:00:00Z"), DateTimeType).addDays(i.divide(4n))),
|
|
703
|
+
task: tasks.get(i.remainder(7n)),
|
|
704
|
+
qty: i.remainder(11n).equal(0n).ifElse((_$2) => East.value(none, OptionType(FloatType)), (_$2) => East.value(some(i.multiply(7n).toFloat().add(50.0)), OptionType(FloatType))),
|
|
705
|
+
notes: "",
|
|
706
|
+
}))));
|
|
707
|
+
const FloatFill = OptionType(Sheet.Types.Fill(FloatType));
|
|
708
|
+
const lastQty = $.const(East.function([Ctx], FloatFill, ($, ctx) => {
|
|
709
|
+
const noFill = $.const(none, FloatFill);
|
|
710
|
+
const similar = $.let(ctx.rows.slice(0n, ctx.rowIndex).filter((_$, r) => r.task.equal(ctx.row.task).and(() => r.qty.hasTag("some"))));
|
|
711
|
+
return similar.length().equal(0n).ifElse((_$) => noFill, ($2) => {
|
|
712
|
+
const r = $2.let(similar.get(similar.length().subtract(1n)));
|
|
713
|
+
return East.value(some({ value: r.qty.unwrap("some"), meta: East.str `like ${r.id}` }), FloatFill);
|
|
714
|
+
});
|
|
715
|
+
}));
|
|
716
|
+
return (_jsx(Sheet, { data: rows.read(), id: "id", columns: {
|
|
717
|
+
start: Sheet.column.date(JobType, { header: "Start", width: "96px" }),
|
|
718
|
+
task: Sheet.column.text(JobType, { header: "Task", width: "160px" }),
|
|
719
|
+
qty: Sheet.column.quantity(JobType, { header: "Qty", width: "112px", format: Format.Number({ maximumFractionDigits: 0n }), fill: [lastQty] }),
|
|
720
|
+
notes: Sheet.column.text(JobType, { header: "Notes", width: "240px" }),
|
|
721
|
+
}, onUpdate: rows.write, footer: [{ text: "2000 rows · virtualised" }], style: { height: "480px" } }));
|
|
722
|
+
} }))),
|
|
723
|
+
inputs: [],
|
|
724
|
+
});
|
|
725
|
+
//# sourceMappingURL=sheet.examples.js.map
|