@elaraai/e3-ui 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CLA.md +26 -0
  2. package/CONTRIBUTING.md +28 -0
  3. package/LICENSE.md +31 -0
  4. package/README.md +149 -0
  5. package/dist/src/buttons.d.ts +2 -0
  6. package/dist/src/buttons.d.ts.map +1 -0
  7. package/dist/src/buttons.js +2 -0
  8. package/dist/src/buttons.js.map +1 -0
  9. package/dist/src/data.d.ts +241 -0
  10. package/dist/src/data.d.ts.map +1 -0
  11. package/dist/src/data.js +195 -0
  12. package/dist/src/data.js.map +1 -0
  13. package/dist/src/derive.d.ts +33 -0
  14. package/dist/src/derive.d.ts.map +1 -0
  15. package/dist/src/derive.js +64 -0
  16. package/dist/src/derive.js.map +1 -0
  17. package/dist/src/diff.d.ts +335 -0
  18. package/dist/src/diff.d.ts.map +1 -0
  19. package/dist/src/diff.js +197 -0
  20. package/dist/src/diff.js.map +1 -0
  21. package/dist/src/index.d.ts +25 -0
  22. package/dist/src/index.d.ts.map +1 -0
  23. package/dist/src/index.js +25 -0
  24. package/dist/src/index.js.map +1 -0
  25. package/dist/src/manifest.d.ts +27 -0
  26. package/dist/src/manifest.d.ts.map +1 -0
  27. package/dist/src/manifest.js +29 -0
  28. package/dist/src/manifest.js.map +1 -0
  29. package/dist/src/ontology.d.ts +517 -0
  30. package/dist/src/ontology.d.ts.map +1 -0
  31. package/dist/src/ontology.js +311 -0
  32. package/dist/src/ontology.js.map +1 -0
  33. package/dist/src/ui.d.ts +57 -0
  34. package/dist/src/ui.d.ts.map +1 -0
  35. package/dist/src/ui.js +72 -0
  36. package/dist/src/ui.js.map +1 -0
  37. package/dist/src/utils.d.ts +21 -0
  38. package/dist/src/utils.d.ts.map +1 -0
  39. package/dist/src/utils.js +24 -0
  40. package/dist/src/utils.js.map +1 -0
  41. package/dist/test/data.examples.d.ts +20 -0
  42. package/dist/test/data.examples.d.ts.map +1 -0
  43. package/dist/test/data.examples.js +156 -0
  44. package/dist/test/data.examples.js.map +1 -0
  45. package/dist/test/diff.examples.d.ts +138 -0
  46. package/dist/test/diff.examples.d.ts.map +1 -0
  47. package/dist/test/diff.examples.js +964 -0
  48. package/dist/test/diff.examples.js.map +1 -0
  49. package/dist/test/ontology.examples.d.ts +412 -0
  50. package/dist/test/ontology.examples.d.ts.map +1 -0
  51. package/dist/test/ontology.examples.js +298 -0
  52. package/dist/test/ontology.examples.js.map +1 -0
  53. package/package.json +80 -0
@@ -0,0 +1,964 @@
1
+ /**
2
+ * Copyright (c) 2025 Elara AI Pty Ltd
3
+ * Dual-licensed under AGPL-3.0 and commercial license. See LICENSE for details.
4
+ */
5
+ /**
6
+ * Diff component examples — realistic editing scenes that combine
7
+ * `Data.bindStaged` with east-ui form components and a `Diff` panel
8
+ * surfacing the pending changes for sign-off.
9
+ *
10
+ * Coverage:
11
+ * 1. Slider-driven editor → workforcePolicyEditor
12
+ * 2. Heterogeneous inputs → serviceConfigForm
13
+ * 3. Editable Table cells → rosterTableEditor
14
+ * 4. Mixed sliders + inputs → pricingRulesEditor (side-by-side mode)
15
+ * 5. Minimal call site → diffDefaults (IR-roundtrip)
16
+ *
17
+ * Pattern:
18
+ * 1. Declare an `e3.input(name, type, default)` per editable scalar.
19
+ * 2. Inside `Reactive.Root`, bind each via `Data.bindStaged`.
20
+ * 3. Wire form components — `read()` for the current value,
21
+ * `write()` on change.
22
+ * 4. Close the scene with `Diff.Root({ staged: [...inputs.path] })` —
23
+ * Apply commits the merged batch; Discard drops the buffer.
24
+ */
25
+ import { East, FloatType, IntegerType, StringType, BooleanType, DateTimeType, ArrayType, StructType, SetType, DictType, VariantType, NullType, PatchType, diffFor, some, variant, example, } from "@elaraai/east";
26
+ import { Card, Stack, Slider, Input, Switch, Select, Text, Button, Reactive, Table, UIComponentType, } from "@elaraai/east-ui";
27
+ import { Data, Diff } from "@elaraai/e3-ui";
28
+ import * as e3 from "@elaraai/e3";
29
+ // ============================================================================
30
+ // Inputs — flat scalars for slider/input examples; one ArrayType binding for
31
+ // the table example so Diff can surface array-element patches end-to-end.
32
+ // ============================================================================
33
+ // Workforce-policy scenario (sliders)
34
+ export const maxWeeklyHoursInput = e3.input("max_weekly_hours", FloatType, 38.0);
35
+ export const overtimeThresholdInput = e3.input("overtime_threshold_hours", FloatType, 38.0);
36
+ export const restGapHoursInput = e3.input("mandatory_rest_gap_hours", FloatType, 12.0);
37
+ export const holidayPenaltyInput = e3.input("public_holiday_penalty", FloatType, 1.5);
38
+ // Service-config scenario (heterogeneous inputs)
39
+ export const serviceNameInput = e3.input("service_name", StringType, "auth-svc");
40
+ export const replicasInput = e3.input("replicas", IntegerType, 3n);
41
+ export const autoScaleInput = e3.input("auto_scale", BooleanType, false);
42
+ export const regionInput = e3.input("region", StringType, "ap-southeast-2");
43
+ export const deployAfterInput = e3.input("deploy_after", DateTimeType, new Date("2026-05-01T08:00:00Z"));
44
+ // Roster table scenario (single ArrayType<Struct> binding)
45
+ const RosterEntryType = StructType({
46
+ id: StringType,
47
+ name: StringType,
48
+ rate: FloatType,
49
+ shiftLength: IntegerType,
50
+ });
51
+ const RosterArrayType = ArrayType(RosterEntryType);
52
+ export const rosterInput = e3.input("roster", RosterArrayType, [
53
+ { id: "alice", name: "Alice Chen", rate: 32.50, shiftLength: 8n },
54
+ { id: "bob", name: "Bob Romero", rate: 28.00, shiftLength: 8n },
55
+ { id: "charlie", name: "Charlie Patel", rate: 35.75, shiftLength: 10n },
56
+ { id: "diana", name: "Diana Wallace", rate: 30.25, shiftLength: 8n },
57
+ ]);
58
+ // Merge-conflict demo scenario — single Float; both bindStaged (for the user's
59
+ // pending edit) and bindDirect (for the "simulate concurrent edit" button)
60
+ // point at the same path so writes via the button drift the server view away
61
+ // from the staged buffer's pinned snapshot.
62
+ export const mergeDemoHoursInput = e3.input("merge_demo_hours", FloatType, 38.0);
63
+ // Pricing-rules scenario (mixed slider + integer/string input)
64
+ export const listPriceInput = e3.input("list_price", FloatType, 49.95);
65
+ export const discountPctInput = e3.input("discount_pct", FloatType, 10.0);
66
+ export const minOrderQtyInput = e3.input("min_order_qty", IntegerType, 1n);
67
+ export const currencyCodeInput = e3.input("currency_code", StringType, "AUD");
68
+ // Feature flags scenario (Set<String>)
69
+ export const featureFlagsInput = e3.input("feature_flags", SetType(StringType), new Set(["dark_mode", "experiments"]));
70
+ // Regional pricing scenario (Dict<String, Float>)
71
+ export const regionalPricesInput = e3.input("regional_prices", DictType(StringType, FloatType), new Map([
72
+ ["AU", 49.95],
73
+ ["US", 39.95],
74
+ ["EU", 44.95],
75
+ ["JP", 5499.0],
76
+ ]));
77
+ // Deployment status scenario (Variant)
78
+ const DeploymentStatusType = VariantType({
79
+ pending: NullType,
80
+ in_progress: NullType,
81
+ complete: NullType,
82
+ failed: NullType,
83
+ });
84
+ export const deploymentStatusInput = e3.input("deployment_status", DeploymentStatusType, variant("pending", null));
85
+ // ============================================================================
86
+ // 1. Workforce-policy editor — slider-driven edits across 4 staged scalars
87
+ // ============================================================================
88
+ export const workforcePolicyEditor = example({
89
+ keywords: ["Diff", "Slider", "bindStaged", "workforce", "policy", "Card"],
90
+ description: "Slider-driven workforce-policy editor — 4 staged Float scalars; Diff surfaces pending edits",
91
+ fn: East.function([], UIComponentType, (_$) => {
92
+ return Reactive.Root(East.function([], UIComponentType, $ => {
93
+ const maxHours = $.let(Data.bind([FloatType], maxWeeklyHoursInput.path));
94
+ const otThresh = $.let(Data.bind([FloatType], overtimeThresholdInput.path));
95
+ const restGap = $.let(Data.bind([FloatType], restGapHoursInput.path));
96
+ const penalty = $.let(Data.bind([FloatType], holidayPenaltyInput.path));
97
+ return Card.Root([
98
+ Stack.VStack([
99
+ Text.Root("Workforce policy", { textStyle: "heading-md" }),
100
+ Text.Root("Drag the sliders to stage policy changes — review in the Diff card before applying."),
101
+ Stack.VStack([
102
+ Text.Root("Max weekly hours", { textStyle: "label-sm" }),
103
+ Slider.Root(maxHours.read(), {
104
+ min: 30.0, max: 60.0, step: 1.0,
105
+ onChangeEnd: ($, v) => $(maxHours.write(v)),
106
+ }),
107
+ ], { gap: "1" }),
108
+ Stack.VStack([
109
+ Text.Root("Overtime threshold (h)", { textStyle: "label-sm" }),
110
+ Slider.Root(otThresh.read(), {
111
+ min: 30.0, max: 60.0, step: 1.0,
112
+ onChangeEnd: ($, v) => $(otThresh.write(v)),
113
+ }),
114
+ ], { gap: "1" }),
115
+ Stack.VStack([
116
+ Text.Root("Mandatory rest gap (h)", { textStyle: "label-sm" }),
117
+ Slider.Root(restGap.read(), {
118
+ min: 8.0, max: 16.0, step: 1.0,
119
+ onChangeEnd: ($, v) => $(restGap.write(v)),
120
+ }),
121
+ ], { gap: "1" }),
122
+ Stack.VStack([
123
+ Text.Root("Public holiday penalty (×)", { textStyle: "label-sm" }),
124
+ Slider.Root(penalty.read(), {
125
+ min: 1.0, max: 3.0, step: 0.25,
126
+ onChangeEnd: ($, v) => $(penalty.write(v)),
127
+ }),
128
+ ], { gap: "1" }),
129
+ Diff.Root({
130
+ bindings: [maxHours.binding, otThresh.binding, restGap.binding, penalty.binding],
131
+ hideUnchanged: some(true),
132
+ }),
133
+ ], { gap: "5", align: "stretch" }),
134
+ ]);
135
+ }));
136
+ }),
137
+ inputs: [],
138
+ });
139
+ // ============================================================================
140
+ // 2. Service-config form — heterogeneous Inputs (String / Integer / DateTime),
141
+ // Switch, Select; commit fires a Toast.
142
+ // ============================================================================
143
+ export const serviceConfigForm = example({
144
+ keywords: ["Diff", "Input", "Switch", "Select", "DateTime", "Toast", "onCommitted"],
145
+ description: "Service-config form covering String / Integer / Bool / DateTime / Select; commit fires a success Toast",
146
+ fn: East.function([], UIComponentType, (_$) => {
147
+ return Reactive.Root(East.function([], UIComponentType, $ => {
148
+ const svcName = $.let(Data.bind([StringType], serviceNameInput.path));
149
+ const replicas = $.let(Data.bind([IntegerType], replicasInput.path));
150
+ const autoScale = $.let(Data.bind([BooleanType], autoScaleInput.path));
151
+ const region = $.let(Data.bind([StringType], regionInput.path));
152
+ const deployAfter = $.let(Data.bind([DateTimeType], deployAfterInput.path));
153
+ return Card.Root([
154
+ Stack.VStack([
155
+ Text.Root("Service configuration", { textStyle: "heading-md" }),
156
+ Text.Root("Stage edits across heterogeneous types; on apply you'll see a confirmation toast."),
157
+ Stack.VStack([
158
+ Text.Root("Service name", { textStyle: "label-sm" }),
159
+ Input.String(svcName.read(), {
160
+ placeholder: "service-name",
161
+ onChange: ($, v) => $(svcName.write(v)),
162
+ }),
163
+ ], { gap: "1" }),
164
+ Stack.VStack([
165
+ Text.Root("Replicas", { textStyle: "label-sm" }),
166
+ Input.Integer(replicas.read(), {
167
+ min: 1n, max: 50n,
168
+ onChange: ($, v) => $(replicas.write(v)),
169
+ }),
170
+ ], { gap: "1" }),
171
+ Stack.VStack([
172
+ Text.Root("Auto-scale", { textStyle: "label-sm" }),
173
+ Switch.Root(autoScale.read(), {
174
+ onChange: ($, v) => $(autoScale.write(v)),
175
+ }),
176
+ ], { gap: "1" }),
177
+ Stack.VStack([
178
+ Text.Root("Region", { textStyle: "label-sm" }),
179
+ Select.Root(region.read(), [
180
+ Select.Item("ap-southeast-2", "ap-southeast-2 (Sydney)"),
181
+ Select.Item("us-east-1", "us-east-1 (N. Virginia)"),
182
+ Select.Item("eu-west-1", "eu-west-1 (Ireland)"),
183
+ ], {
184
+ onChange: ($, v) => $(region.write(v)),
185
+ }),
186
+ ], { gap: "1" }),
187
+ Stack.VStack([
188
+ Text.Root("Deploy after", { textStyle: "label-sm" }),
189
+ Input.DateTime(deployAfter.read(), {
190
+ precision: "datetime",
191
+ onChange: ($, v) => $(deployAfter.write(v)),
192
+ }),
193
+ ], { gap: "1" }),
194
+ Diff.Root({
195
+ bindings: [svcName.binding, replicas.binding, autoScale.binding, region.binding, deployAfter.binding],
196
+ }),
197
+ ], { gap: "4", align: "stretch" }),
198
+ ]);
199
+ }));
200
+ }),
201
+ inputs: [],
202
+ });
203
+ // ============================================================================
204
+ // 3. Roster Table — editable cells (Float / Integer Inputs) writing back to
205
+ // a single ArrayType<Struct> staged binding. Diff visualises array
206
+ // patches per row.
207
+ // ============================================================================
208
+ export const rosterTableEditor = example({
209
+ keywords: ["Diff", "Table", "cell", "render", "Input", "Array", "bindStaged", "roster"],
210
+ description: "Editable Table — Input.Float / Input.Integer cells write back to a staged roster array; Diff shows per-row updates",
211
+ fn: East.function([], UIComponentType, (_$) => {
212
+ return Reactive.Root(East.function([], UIComponentType, $ => {
213
+ const roster = $.let(Data.bind([RosterArrayType], rosterInput.path));
214
+ // Get the roster array fresh inside the render closures to ensure edits to sibling
215
+ const rosterArray = $.let(roster.read(), RosterArrayType);
216
+ return Card.Root([
217
+ Stack.VStack([
218
+ Text.Root("Worker roster", { textStyle: "heading-md" }),
219
+ Text.Root("Edit hourly rates and default shift lengths inline; Diff card below tracks pending changes."),
220
+ Table.Root(rosterArray, {
221
+ name: { header: "Name" },
222
+ rate: {
223
+ header: "Hourly rate ($)",
224
+ render: East.function([Table.Types.CellRenderContext], UIComponentType, ($, ctx) => {
225
+ const idx = $.let(ctx.rowIndex, IntegerType);
226
+ const row = $.let(rosterArray.get(idx), RosterEntryType);
227
+ return Input.Float(row.rate, {
228
+ min: 15.0, max: 80.0, step: 0.25,
229
+ // Re-read fresh inside the handler so a sibling cell's
230
+ // edit on the same row isn't overwritten by a stale
231
+ // capture of `row`. Merge via the map iterator's `r`.
232
+ onChange: East.function([FloatType], NullType, ($, v) => {
233
+ const fresh = $.let(roster.read(), RosterArrayType);
234
+ const next = $.let(fresh.map(($, r, i) => i.equal(idx).ifElse(_$ => ({ ...r, rate: v }), () => r)), RosterArrayType);
235
+ $(roster.write(next));
236
+ }),
237
+ });
238
+ }),
239
+ },
240
+ shiftLength: {
241
+ header: "Shift length (h)",
242
+ render: East.function([Table.Types.CellRenderContext], UIComponentType, ($, ctx) => {
243
+ const idx = $.let(ctx.rowIndex, IntegerType);
244
+ const row = $.let(rosterArray.get(idx), RosterEntryType);
245
+ return Input.Integer(row.shiftLength, {
246
+ min: 4n, max: 12n,
247
+ onChange: East.function([IntegerType], NullType, ($, v) => {
248
+ const fresh = $.let(roster.read(), RosterArrayType);
249
+ $(roster.write(fresh.map(($, r, i) => i.equal(idx).ifElse(_$ => ({ ...r, shiftLength: v }), () => r))));
250
+ }),
251
+ });
252
+ }),
253
+ },
254
+ }, { variant: "line", striped: true }),
255
+ Diff.Root({
256
+ bindings: [roster.binding],
257
+ hideUnchanged: some(true),
258
+ }),
259
+ ], { gap: "5", align: "stretch" }),
260
+ ]);
261
+ }));
262
+ }),
263
+ inputs: [],
264
+ });
265
+ // ============================================================================
266
+ // 4. Pricing-rules editor — sliders + integer/string inputs together;
267
+ // side-by-side Diff mode for a wider review surface.
268
+ // ============================================================================
269
+ export const pricingRulesEditor = example({
270
+ keywords: ["Diff", "side-by-side", "Slider", "Input", "pricing", "discount", "mixed"],
271
+ description: "Pricing rules editor — sliders for list price/discount, IntegerInput for min order qty, StringInput for currency; Diff in side-by-side mode",
272
+ fn: East.function([], UIComponentType, (_$) => {
273
+ return Reactive.Root(East.function([], UIComponentType, $ => {
274
+ const listPrice = $.let(Data.bind([FloatType], listPriceInput.path));
275
+ const discountPct = $.let(Data.bind([FloatType], discountPctInput.path));
276
+ const minOrderQty = $.let(Data.bind([IntegerType], minOrderQtyInput.path));
277
+ const currency = $.let(Data.bind([StringType], currencyCodeInput.path));
278
+ return Card.Root([
279
+ Stack.VStack([
280
+ Text.Root("Pricing rules", { textStyle: "heading-md" }),
281
+ Text.Root("Stage pricing changes — review side-by-side before applying to the catalog."),
282
+ Stack.VStack([
283
+ Text.Root("List price", { textStyle: "label-sm" }),
284
+ Slider.Root(listPrice.read(), {
285
+ min: 0.0, max: 999.95, step: 0.05,
286
+ onChangeEnd: ($, v) => $(listPrice.write(v)),
287
+ }),
288
+ ], { gap: "1" }),
289
+ Stack.VStack([
290
+ Text.Root("Discount (%)", { textStyle: "label-sm" }),
291
+ Slider.Root(discountPct.read(), {
292
+ min: 0.0, max: 75.0, step: 0.5,
293
+ onChangeEnd: ($, v) => $(discountPct.write(v)),
294
+ }),
295
+ ], { gap: "1" }),
296
+ Stack.VStack([
297
+ Text.Root("Minimum order quantity", { textStyle: "label-sm" }),
298
+ Input.Integer(minOrderQty.read(), {
299
+ min: 1n, max: 1000n,
300
+ onChange: ($, v) => $(minOrderQty.write(v)),
301
+ }),
302
+ ], { gap: "1" }),
303
+ Stack.VStack([
304
+ Text.Root("Currency code", { textStyle: "label-sm" }),
305
+ Input.String(currency.read(), {
306
+ placeholder: "AUD",
307
+ onChange: ($, v) => $(currency.write(v)),
308
+ }),
309
+ ], { gap: "1" }),
310
+ Diff.Root({
311
+ bindings: [listPrice.binding, discountPct.binding, minOrderQty.binding, currency.binding],
312
+ hideUnchanged: some(true),
313
+ }),
314
+ ], { gap: "5", align: "stretch" }),
315
+ ]);
316
+ }));
317
+ }),
318
+ inputs: [],
319
+ });
320
+ // ============================================================================
321
+ // 5. Feature flags editor — Set<String> binding; Switch per known flag.
322
+ // Demonstrates Set insert/delete patches in the Diff.
323
+ // ============================================================================
324
+ export const featureFlagsEditor = example({
325
+ keywords: ["Diff", "Set", "Switch", "feature flags", "bindStaged"],
326
+ description: "Feature-flags editor — Switch per known flag toggles inclusion in a staged Set; Diff shows insertions / deletions",
327
+ fn: East.function([], UIComponentType, (_$) => {
328
+ return Reactive.Root(East.function([], UIComponentType, $ => {
329
+ const flags = $.let(Data.bind([SetType(StringType)], featureFlagsInput.path));
330
+ const flagsRead = $.let(flags.read(), SetType(StringType));
331
+ return Card.Root([
332
+ Stack.VStack([
333
+ Text.Root("Feature flags", { textStyle: "heading-md" }),
334
+ Text.Root("Toggle flags to stage changes; Diff card below shows the set delta."),
335
+ Stack.VStack([
336
+ Text.Root("dark_mode", { textStyle: "label-sm" }),
337
+ Switch.Root(flagsRead.has("dark_mode"), {
338
+ onChange: East.function([BooleanType], NullType, ($, isOn) => {
339
+ const next = $.let(flags.read(), SetType(StringType));
340
+ $.if(isOn, $ => { $(next.insert("dark_mode")); }).else($ => { $(next.delete("dark_mode")); });
341
+ $(flags.write(next));
342
+ }),
343
+ }),
344
+ ], { gap: "3", justify: "space-between" }),
345
+ Stack.VStack([
346
+ Text.Root("experiments", { textStyle: "label-sm" }),
347
+ Switch.Root(flagsRead.has("experiments"), {
348
+ onChange: East.function([BooleanType], NullType, ($, isOn) => {
349
+ const next = $.let(flags.read(), SetType(StringType));
350
+ $.if(isOn, $ => { $(next.insert("experiments")); }).else($ => { $(next.delete("experiments")); });
351
+ $(flags.write(next));
352
+ }),
353
+ }),
354
+ ], { gap: "3", justify: "space-between" }),
355
+ Stack.VStack([
356
+ Text.Root("notifications", { textStyle: "label-sm" }),
357
+ Switch.Root(flagsRead.has("notifications"), {
358
+ onChange: East.function([BooleanType], NullType, ($, isOn) => {
359
+ const next = $.let(flags.read(), SetType(StringType));
360
+ $.if(isOn, $ => { $(next.insert("notifications")); }).else($ => { $(next.delete("notifications")); });
361
+ $(flags.write(next));
362
+ }),
363
+ }),
364
+ ], { gap: "3", justify: "space-between" }),
365
+ Stack.VStack([
366
+ Text.Root("analytics", { textStyle: "label-sm" }),
367
+ Switch.Root(flagsRead.has("analytics"), {
368
+ onChange: East.function([BooleanType], NullType, ($, isOn) => {
369
+ const next = $.let(flags.read(), SetType(StringType));
370
+ $.if(isOn, $ => { $(next.insert("analytics")); }).else($ => { $(next.delete("analytics")); });
371
+ $(flags.write(next));
372
+ }),
373
+ }),
374
+ ], { gap: "3", justify: "space-between" }),
375
+ Stack.VStack([
376
+ Text.Root("ai_assist", { textStyle: "label-sm" }),
377
+ Switch.Root(flagsRead.has("ai_assist"), {
378
+ onChange: East.function([BooleanType], NullType, ($, isOn) => {
379
+ const next = $.let(flags.read(), SetType(StringType));
380
+ $.if(isOn, $ => { $(next.insert("ai_assist")); }).else($ => { $(next.delete("ai_assist")); });
381
+ $(flags.write(next));
382
+ }),
383
+ }),
384
+ ], { gap: "3", justify: "space-between" }),
385
+ Diff.Root({
386
+ bindings: [flags.binding],
387
+ hideUnchanged: some(true),
388
+ }),
389
+ ], { gap: "4", align: "stretch" }),
390
+ ]);
391
+ }));
392
+ }),
393
+ inputs: [],
394
+ });
395
+ // ============================================================================
396
+ // 6. Regional pricing editor — Dict<String, Float>; Input per known key.
397
+ // Demonstrates Dict update patches per key in the Diff.
398
+ // ============================================================================
399
+ export const regionalPricingEditor = example({
400
+ keywords: ["Diff", "Dict", "Input", "regional pricing", "bindStaged"],
401
+ description: "Regional-pricing editor — Input.Float per region writes back to a staged Dict; Diff shows per-key updates",
402
+ fn: East.function([], UIComponentType, (_$) => {
403
+ return Reactive.Root(East.function([], UIComponentType, $ => {
404
+ const prices = $.let(Data.bind([DictType(StringType, FloatType)], regionalPricesInput.path));
405
+ const pricesRead = $.let(prices.read(), DictType(StringType, FloatType));
406
+ return Card.Root([
407
+ Stack.VStack([
408
+ Text.Root("Regional pricing", { textStyle: "heading-md" }),
409
+ Text.Root("Edit per-region prices; Diff card below tracks pending updates."),
410
+ Stack.VStack([
411
+ Text.Root("AU", { textStyle: "label-sm" }),
412
+ Input.Float(pricesRead.get("AU", East.function([StringType], FloatType, _$ => 0.0)), {
413
+ min: 0.0, max: 99999.0, step: 0.05,
414
+ onChange: East.function([FloatType], NullType, ($, newPrice) => {
415
+ const next = $.let(prices.read(), DictType(StringType, FloatType));
416
+ $(next.insertOrUpdate("AU", newPrice));
417
+ $(prices.write(next));
418
+ }),
419
+ }),
420
+ ], { gap: "3", justify: "space-between" }),
421
+ Stack.VStack([
422
+ Text.Root("US", { textStyle: "label-sm" }),
423
+ Input.Float(pricesRead.get("US", East.function([StringType], FloatType, _$ => 0.0)), {
424
+ min: 0.0, max: 99999.0, step: 0.05,
425
+ onChange: East.function([FloatType], NullType, ($, newPrice) => {
426
+ const next = $.let(prices.read(), DictType(StringType, FloatType));
427
+ $(next.insertOrUpdate("US", newPrice));
428
+ $(prices.write(next));
429
+ }),
430
+ }),
431
+ ], { gap: "3", justify: "space-between" }),
432
+ Stack.VStack([
433
+ Text.Root("EU", { textStyle: "label-sm" }),
434
+ Input.Float(pricesRead.get("EU", East.function([StringType], FloatType, _$ => 0.0)), {
435
+ min: 0.0, max: 99999.0, step: 0.05,
436
+ onChange: East.function([FloatType], NullType, ($, newPrice) => {
437
+ const next = $.let(prices.read(), DictType(StringType, FloatType));
438
+ $(next.insertOrUpdate("EU", newPrice));
439
+ $(prices.write(next));
440
+ }),
441
+ }),
442
+ ], { gap: "3", justify: "space-between" }),
443
+ Stack.VStack([
444
+ Text.Root("JP", { textStyle: "label-sm" }),
445
+ Input.Float(pricesRead.get("JP", East.function([StringType], FloatType, _$ => 0.0)), {
446
+ min: 0.0, max: 99999.0, step: 0.05,
447
+ onChange: East.function([FloatType], NullType, ($, newPrice) => {
448
+ const next = $.let(prices.read(), DictType(StringType, FloatType));
449
+ $(next.insertOrUpdate("JP", newPrice));
450
+ $(prices.write(next));
451
+ }),
452
+ }),
453
+ ], { gap: "3", justify: "space-between" }),
454
+ Diff.Root({
455
+ bindings: [prices.binding],
456
+ hideUnchanged: some(true),
457
+ }),
458
+ ], { gap: "4", align: "stretch" }),
459
+ ]);
460
+ }));
461
+ }),
462
+ inputs: [],
463
+ });
464
+ // ============================================================================
465
+ // 7. Deployment status editor — Variant binding; one Button per status.
466
+ // Demonstrates Variant tag changes in the Diff.
467
+ // ============================================================================
468
+ export const deploymentStatusEditor = example({
469
+ keywords: ["Diff", "Variant", "Button", "deployment status", "bindStaged"],
470
+ description: "Deployment-status editor — buttons set a Variant binding; Diff shows the status tag change",
471
+ fn: East.function([], UIComponentType, (_$) => {
472
+ return Reactive.Root(East.function([], UIComponentType, $ => {
473
+ const status = $.let(Data.bind([DeploymentStatusType], deploymentStatusInput.path));
474
+ const statusRead = $.let(status.read(), DeploymentStatusType);
475
+ return Card.Root([
476
+ Stack.VStack([
477
+ Text.Root("Deployment status", { textStyle: "heading-md" }),
478
+ Text.Root("Current:"),
479
+ Text.Root(statusRead.getTag(), { textStyle: "label-md" }),
480
+ Stack.HStack([
481
+ Button.Root("Pending", {
482
+ onClick: East.function([], NullType, $ => $(status.write(variant("pending", null)))),
483
+ }),
484
+ Button.Root("In progress", {
485
+ onClick: East.function([], NullType, $ => $(status.write(variant("in_progress", null)))),
486
+ }),
487
+ Button.Root("Complete", {
488
+ onClick: East.function([], NullType, $ => $(status.write(variant("complete", null)))),
489
+ }),
490
+ Button.Root("Failed", {
491
+ onClick: East.function([], NullType, $ => $(status.write(variant("failed", null)))),
492
+ }),
493
+ ], { gap: "2" }),
494
+ Diff.Root({
495
+ bindings: [status.binding],
496
+ hideUnchanged: some(true),
497
+ }),
498
+ ], { gap: "4", align: "stretch" }),
499
+ ]);
500
+ }));
501
+ }),
502
+ inputs: [],
503
+ });
504
+ // ============================================================================
505
+ // 8. Minimal call site — defaults only (used by IR-roundtrip + smoke tests)
506
+ // ============================================================================
507
+ export const diffDefaults = example({
508
+ keywords: ["Diff", "Root", "defaults", "minimal"],
509
+ description: "Minimal Diff.Root — single binding, all options omitted (defaults from IR)",
510
+ fn: East.function([], UIComponentType, (_$) => {
511
+ return Reactive.Root(East.function([], UIComponentType, $ => {
512
+ const view = $.let(Data.bind([FloatType], maxWeeklyHoursInput.path));
513
+ return Diff.Root({ bindings: [view.binding] });
514
+ }));
515
+ }),
516
+ inputs: [],
517
+ });
518
+ // ============================================================================
519
+ // Merge-conflict demo — bindStaged for the user's edit + bindDirect on the
520
+ // SAME path for a "simulate concurrent edit" button. Clicking the button
521
+ // writes a different value through the live cache, so the staged buffer's
522
+ // pinned snapshot drifts away from the current server value. Hitting Apply
523
+ // on the Diff card then triggers the orange chooser flow.
524
+ // ============================================================================
525
+ /**
526
+ * Demonstrates the staged-mode merge tool / orange chooser. Workflow:
527
+ *
528
+ * 1. Drag the slider — your edit gets staged (snapshot pinned in StagedStore).
529
+ * 2. Click "Simulate concurrent edit" — `Data.bind.write(42.0)` writes 42 to
530
+ * the same path through the live cache, mimicking another session
531
+ * committing while you have edits open. Server view is now 42; your
532
+ * pinned snapshot is still 38; your buffer is whatever you dragged to.
533
+ * 3. Click Apply — `detectConflictsFor(userPatch, serverPatch)` finds both
534
+ * sides touching the root, switches the renderer to conflict mode, and
535
+ * shows the orange chooser row: keep yours (your dragged value), keep
536
+ * theirs (42), or manual (type a fresh number).
537
+ */
538
+ export const mergeConflictDemo = example({
539
+ keywords: ["Diff", "merge", "conflict", "chooser", "bindStaged", "Slider"],
540
+ description: "Demo of the merge tool's orange chooser — drag, click Simulate, then Apply",
541
+ fn: East.function([], UIComponentType, (_$) => {
542
+ return Reactive.Root(East.function([], UIComponentType, $ => {
543
+ const staged = $.let(Data.bind([FloatType], mergeDemoHoursInput.path));
544
+ const direct = $.let(Data.bind([FloatType], mergeDemoHoursInput.path, { mode: "direct" }));
545
+ const stagedValue = $.let(staged.read(), FloatType);
546
+ const serverValue = $.let(direct.read(), FloatType);
547
+ return Card.Root([
548
+ Stack.VStack([
549
+ Text.Root("Merge-conflict demo", { textStyle: "heading-md" }),
550
+ Text.Root("1. Drag the slider — your edit lands in the StagedStore (snapshot pinned). "
551
+ + "2. Click \"Simulate concurrent edit\" — writes 42 through the live cache, "
552
+ + "mimicking another session committing while you have edits open. "
553
+ + "3. Click Apply on the Diff card — the merge tool detects drift and fires "
554
+ + "the orange chooser row."),
555
+ Stack.VStack([
556
+ Text.Root("Server (live cache):", { textStyle: "label-sm" }),
557
+ Text.Root(East.str `${serverValue}`),
558
+ ], { gap: "2" }),
559
+ Stack.VStack([
560
+ Text.Root("Your staged value:", { textStyle: "label-sm" }),
561
+ Text.Root(East.str `${stagedValue}`),
562
+ ], { gap: "2" }),
563
+ Slider.Root(stagedValue, {
564
+ min: 30.0, max: 60.0, step: 1.0,
565
+ onChangeEnd: ($, v) => $(staged.write(v)),
566
+ }),
567
+ Stack.VStack([
568
+ Button.Root("Simulate concurrent edit (server ← 42)", {
569
+ onClick: $ => $(direct.write(42.0)),
570
+ }),
571
+ Button.Root("Reset server to 38", {
572
+ onClick: $ => $(direct.write(38.0)),
573
+ }),
574
+ ], { gap: "2" }),
575
+ Diff.Root({
576
+ bindings: [staged.binding],
577
+ hideUnchanged: some(true),
578
+ }),
579
+ ], { gap: "5", align: "stretch" }),
580
+ ]);
581
+ }));
582
+ }),
583
+ inputs: [],
584
+ });
585
+ // ============================================================================
586
+ // Density variants — same pricing-rules content rendered at the two
587
+ // non-default density presets, so the showcase can compare side-by-side
588
+ // against the default (`pricingRulesEditor` above).
589
+ // ============================================================================
590
+ /**
591
+ * Pricing-rules editor at `compact` density — tighter row padding and smaller
592
+ * type scale than the default `comfortable`. Suited to data-dense pages where
593
+ * the Diff card is one of several panels competing for vertical space.
594
+ */
595
+ export const pricingRulesEditorCompact = example({
596
+ keywords: ["Diff", "density", "compact", "Slider", "Input", "pricing"],
597
+ description: "Pricing-rules editor with `density: \"compact\"` — same content as pricingRulesEditor, denser rows",
598
+ fn: East.function([], UIComponentType, (_$) => {
599
+ return Reactive.Root(East.function([], UIComponentType, $ => {
600
+ const listPrice = $.let(Data.bind([FloatType], listPriceInput.path));
601
+ const discountPct = $.let(Data.bind([FloatType], discountPctInput.path));
602
+ const minOrderQty = $.let(Data.bind([IntegerType], minOrderQtyInput.path));
603
+ const currency = $.let(Data.bind([StringType], currencyCodeInput.path));
604
+ return Card.Root([
605
+ Stack.VStack([
606
+ Text.Root("Pricing rules — compact", { textStyle: "heading-md" }),
607
+ Slider.Root(listPrice.read(), {
608
+ min: 0.0, max: 999.95, step: 0.05,
609
+ onChangeEnd: ($, v) => $(listPrice.write(v)),
610
+ }),
611
+ Slider.Root(discountPct.read(), {
612
+ min: 0.0, max: 75.0, step: 0.5,
613
+ onChangeEnd: ($, v) => $(discountPct.write(v)),
614
+ }),
615
+ Input.Integer(minOrderQty.read(), {
616
+ min: 1n, max: 1000n,
617
+ onChange: ($, v) => $(minOrderQty.write(v)),
618
+ }),
619
+ Input.String(currency.read(), {
620
+ placeholder: "AUD",
621
+ onChange: ($, v) => $(currency.write(v)),
622
+ }),
623
+ Diff.Root({
624
+ bindings: [listPrice.binding, discountPct.binding, minOrderQty.binding, currency.binding],
625
+ density: "compact",
626
+ hideUnchanged: some(true),
627
+ }),
628
+ ], { gap: "5", align: "stretch" }),
629
+ ]);
630
+ }));
631
+ }),
632
+ inputs: [],
633
+ });
634
+ /**
635
+ * Pricing-rules editor at `condensed` density — mission-control sizing.
636
+ * Tightest row padding and smallest type scale; pairs with status dashboards
637
+ * and other read-heavy contexts where a diff list is reference data, not the
638
+ * main interaction surface.
639
+ */
640
+ export const pricingRulesEditorCondensed = example({
641
+ keywords: ["Diff", "density", "condensed", "Slider", "Input", "pricing"],
642
+ description: "Pricing-rules editor with `density: \"condensed\"` — mission-control sizing for read-heavy views",
643
+ fn: East.function([], UIComponentType, (_$) => {
644
+ return Reactive.Root(East.function([], UIComponentType, $ => {
645
+ const listPrice = $.let(Data.bind([FloatType], listPriceInput.path));
646
+ const discountPct = $.let(Data.bind([FloatType], discountPctInput.path));
647
+ const minOrderQty = $.let(Data.bind([IntegerType], minOrderQtyInput.path));
648
+ const currency = $.let(Data.bind([StringType], currencyCodeInput.path));
649
+ return Card.Root([
650
+ Stack.VStack([
651
+ Text.Root("Pricing rules — condensed", { textStyle: "heading-md" }),
652
+ Slider.Root(listPrice.read(), {
653
+ min: 0.0, max: 999.95, step: 0.05,
654
+ onChangeEnd: ($, v) => $(listPrice.write(v)),
655
+ }),
656
+ Slider.Root(discountPct.read(), {
657
+ min: 0.0, max: 75.0, step: 0.5,
658
+ onChangeEnd: ($, v) => $(discountPct.write(v)),
659
+ }),
660
+ Input.Integer(minOrderQty.read(), {
661
+ min: 1n, max: 1000n,
662
+ onChange: ($, v) => $(minOrderQty.write(v)),
663
+ }),
664
+ Input.String(currency.read(), {
665
+ placeholder: "AUD",
666
+ onChange: ($, v) => $(currency.write(v)),
667
+ }),
668
+ Diff.Root({
669
+ bindings: [listPrice.binding, discountPct.binding, minOrderQty.binding, currency.binding],
670
+ density: "condensed",
671
+ hideUnchanged: some(true),
672
+ }),
673
+ ], { gap: "5", align: "stretch" }),
674
+ ]);
675
+ }));
676
+ }),
677
+ inputs: [],
678
+ });
679
+ // ============================================================================
680
+ // Overlay-mode bindings — patches stored in a sibling `e3.input` typed
681
+ // `PatchType(T)`. The Diff component surfaces them via the `overlay` option
682
+ // instead of `staged`. See `Data.bindOverlay`.
683
+ // ============================================================================
684
+ // Patch inputs for overlay-mode examples. Each defaults to `unchanged`.
685
+ export const maxWeeklyHoursPatchInput = e3.input("max_weekly_hours_patch", PatchType(FloatType), variant("unchanged", null));
686
+ export const regionalPricesPatchInput = e3.input("regional_prices_patch", PatchType(DictType(StringType, FloatType)), variant("unchanged", null));
687
+ export const rosterPatchInput = e3.input("roster_patch", PatchType(RosterArrayType), variant("unchanged", null));
688
+ // Drift-laden patch input — defaults to a non-trivial patch whose ops are
689
+ // stale relative to the source dict `{AU: 49.95, US: 39.95, EU: 44.95, JP:
690
+ // 5499.0}`. Used by `regionalPricingOverlayDrift` to demonstrate what the
691
+ // Diff card surfaces when a server-stored patch was authored against an
692
+ // older version of its source. Conflicts (per `apply.ts` semantics):
693
+ // - `delete "MX"` — key not in source → stale delete
694
+ // - `insert "AU"` — key already exists in source → stale insert
695
+ // - `update "US"` with `replace(before=30 → 25)` — before mismatches source's 39.95
696
+ // - `update "EU"` with `replace(before=44.95 → 39.95)` — clean (for contrast)
697
+ export const regionalPricesDriftPatchInput = e3.input("regional_prices_drift_patch", PatchType(DictType(StringType, FloatType)), variant("patch", new Map([
698
+ ["MX", variant("delete", 99.0)],
699
+ ["AU", variant("insert", 100.0)],
700
+ ["US", variant("update", variant("replace", { before: 30.0, after: 25.0 }))],
701
+ ["EU", variant("update", variant("replace", { before: 44.95, after: 39.95 }))],
702
+ ])));
703
+ // Roster overlay drift — a patch over the array-of-structs source. Built with
704
+ // `diffFor` (base matches the bound source, so ops are clean) so the Diff card
705
+ // exercises its nested grouping: binding → array index → struct field. Only
706
+ // the `rate` of entries 0 and 1 change, producing `[0] → rate`, `[1] → rate`.
707
+ const rosterDriftBase = [
708
+ { id: "alice", name: "Alice Chen", rate: 32.50, shiftLength: 8n },
709
+ { id: "bob", name: "Bob Romero", rate: 28.00, shiftLength: 8n },
710
+ { id: "charlie", name: "Charlie Patel", rate: 35.75, shiftLength: 10n },
711
+ { id: "diana", name: "Diana Wallace", rate: 30.25, shiftLength: 8n },
712
+ ];
713
+ const rosterDriftEdited = [
714
+ { id: "alice", name: "Alice Chen", rate: 6.00, shiftLength: 8n },
715
+ { id: "bob", name: "Bob Romero", rate: 29.00, shiftLength: 8n },
716
+ { id: "charlie", name: "Charlie Patel", rate: 35.75, shiftLength: 10n },
717
+ { id: "diana", name: "Diana Wallace", rate: 30.25, shiftLength: 8n },
718
+ ];
719
+ export const rosterDriftPatchInput = e3.input("roster_drift_patch", PatchType(RosterArrayType), diffFor(RosterArrayType)(rosterDriftBase, rosterDriftEdited));
720
+ /**
721
+ * Single-Float overlay editor — Slider edits go to `max_weekly_hours_patch`,
722
+ * the Diff card surfaces the patch against the source.
723
+ */
724
+ export const policyOverlayEditor = example({
725
+ keywords: ["Diff", "bindOverlay", "Slider", "overlay", "policy", "patch"],
726
+ description: "Overlay-mode editor — Slider writes patches to a sibling e3.input; Diff shows them",
727
+ fn: East.function([], UIComponentType, (_$) => {
728
+ return Reactive.Root(East.function([], UIComponentType, $ => {
729
+ const view = $.let(Data.bind([FloatType], maxWeeklyHoursInput.path, { mode: "direct", patch: maxWeeklyHoursPatchInput.path }));
730
+ return Card.Root([
731
+ Stack.VStack([
732
+ Text.Root("Max weekly hours (overlay)", { textStyle: "heading-md" }),
733
+ Slider.Root(view.read(), {
734
+ min: 30.0, max: 60.0, step: 1.0,
735
+ onChangeEnd: ($, v) => $(view.write(v)),
736
+ }),
737
+ Diff.Root({
738
+ bindings: [view.binding],
739
+ hideUnchanged: some(true),
740
+ }),
741
+ ], { gap: "5", align: "stretch" }),
742
+ ]);
743
+ }));
744
+ }),
745
+ inputs: [],
746
+ });
747
+ /**
748
+ * Overlay binding whose patch input *starts* with a non-trivial patch that
749
+ * is stale relative to the source. Demonstrates what the Diff card displays
750
+ * when a server-stored patch was authored against an older revision of its
751
+ * source — stale delete, stale insert, stale replace.
752
+ *
753
+ * No form components: `view.read()` calls `apply(source, patch)`, which
754
+ * throws `ConflictError` on the stale ops. The Diff card walks the patch IR
755
+ * directly from cache bytes (no apply) so it renders cleanly. The
756
+ * `bindOverlay` call is kept so the renderer's overlay-type registry knows
757
+ * how to decode the patch bytes for this binding.
758
+ */
759
+ export const regionalPricingOverlayDrift = example({
760
+ keywords: ["Diff", "bindOverlay", "Dict", "conflict", "drift", "patch"],
761
+ description: "Overlay-mode example with a stale patch — Diff card surfaces ops that don't match the source",
762
+ fn: East.function([], UIComponentType, (_$) => {
763
+ return Reactive.Root(East.function([], UIComponentType, $ => {
764
+ const view = $.let(Data.bind([DictType(StringType, FloatType)], regionalPricesInput.path, { mode: "direct", patch: regionalPricesDriftPatchInput.path }));
765
+ return Card.Root([
766
+ Stack.VStack([
767
+ Text.Root("Regional pricing — overlay with stale patch", { textStyle: "heading-md" }),
768
+ Text.Root("The patch input was authored against an older source. The Diff card below "
769
+ + "shows: a stale delete (MX missing from source), a stale insert (AU already "
770
+ + "exists), a stale replace (US before=30 doesn't match current 39.95), and "
771
+ + "one clean replace (EU 44.95 → 39.95). Apply would throw ConflictError on "
772
+ + "the stale ops; this example exists to show what the Diff renderer surfaces "
773
+ + "when a server-stored patch has drifted from its source."),
774
+ Diff.Root({
775
+ bindings: [view.binding],
776
+ hideUnchanged: some(true),
777
+ }),
778
+ ], { gap: "5", align: "stretch" }),
779
+ ]);
780
+ }));
781
+ }),
782
+ inputs: [],
783
+ });
784
+ /**
785
+ * Roster overlay drift — array-of-structs patch surfaced by the Diff card,
786
+ * exercising nested grouping (binding → `[index]` → struct field). The patch
787
+ * touches only `rate` on the first two entries, so the card renders
788
+ * `ROSTER → [0] → rate`, `[1] → rate`.
789
+ */
790
+ export const rosterOverlayDrift = example({
791
+ keywords: ["Diff", "overlay", "roster", "array", "nested", "patch"],
792
+ description: "Overlay-mode roster patch — Diff card surfaces nested array-element field changes",
793
+ fn: East.function([], UIComponentType, (_$) => {
794
+ return Reactive.Root(East.function([], UIComponentType, $ => {
795
+ const view = $.let(Data.bind([RosterArrayType], rosterInput.path, { mode: "direct", patch: rosterDriftPatchInput.path }));
796
+ return Card.Root([
797
+ Stack.VStack([
798
+ Text.Root("Roster — overlay patch (nested)", { textStyle: "heading-md" }),
799
+ Text.Root("The patch changes the hourly rate of the first two roster entries. The "
800
+ + "Diff card groups each change under its array index, demonstrating the "
801
+ + "binding → [index] → field nesting."),
802
+ Diff.Root({
803
+ bindings: [view.binding],
804
+ hideUnchanged: some(true),
805
+ }),
806
+ ], { gap: "5", align: "stretch" }),
807
+ ]);
808
+ }));
809
+ }),
810
+ inputs: [],
811
+ });
812
+ /**
813
+ * Roster table editor in overlay mode — patches stored in `roster_patch`,
814
+ * survive page reload and are visible to other workspace sessions.
815
+ */
816
+ export const rosterTableEditorOverlay = example({
817
+ keywords: ["Diff", "Table", "bindOverlay", "patch", "overlay", "roster"],
818
+ description: "Editable table backed by a separate patch e3.input — patches persist across sessions",
819
+ fn: East.function([], UIComponentType, (_$) => {
820
+ return Reactive.Root(East.function([], UIComponentType, $ => {
821
+ const view = $.let(Data.bind([RosterArrayType], rosterInput.path, { mode: "direct", patch: rosterPatchInput.path }));
822
+ const rosterArray = $.let(view.read(), RosterArrayType);
823
+ return Card.Root([
824
+ Stack.VStack([
825
+ Text.Root("Worker roster (overlay-mode)", { textStyle: "heading-md" }),
826
+ Text.Root("Patches stored in a sibling dataset — survive reload, visible to other sessions."),
827
+ Table.Root(rosterArray, {
828
+ name: { header: "Name" },
829
+ rate: {
830
+ header: "Hourly rate ($)",
831
+ render: East.function([Table.Types.CellRenderContext], UIComponentType, ($, ctx) => {
832
+ const idx = $.let(ctx.rowIndex, IntegerType);
833
+ const row = $.let(rosterArray.get(idx), RosterEntryType);
834
+ return Input.Float(row.rate, {
835
+ min: 15.0, max: 80.0, step: 0.25,
836
+ onChange: East.function([FloatType], NullType, ($, v) => {
837
+ const fresh = $.let(view.read(), RosterArrayType);
838
+ const next = $.let(fresh.map(($, r, i) => i.equal(idx).ifElse(_$ => ({ ...r, rate: v }), _$ => r)), RosterArrayType);
839
+ $(view.write(next));
840
+ }),
841
+ });
842
+ }),
843
+ },
844
+ shiftLength: {
845
+ header: "Shift length (h)",
846
+ render: East.function([Table.Types.CellRenderContext], UIComponentType, ($, ctx) => {
847
+ const idx = $.let(ctx.rowIndex, IntegerType);
848
+ const row = $.let(rosterArray.get(idx), RosterEntryType);
849
+ return Input.Integer(row.shiftLength, {
850
+ min: 4n, max: 12n,
851
+ onChange: East.function([IntegerType], NullType, ($, v) => {
852
+ const fresh = $.let(view.read(), RosterArrayType);
853
+ const next = $.let(fresh.map(($, r, i) => i.equal(idx).ifElse(_$ => ({ ...r, shiftLength: v }), _$ => r)), RosterArrayType);
854
+ $(view.write(next));
855
+ }),
856
+ });
857
+ }),
858
+ },
859
+ }, { variant: "line", striped: true }),
860
+ Diff.Root({
861
+ bindings: [view.binding],
862
+ hideUnchanged: some(true),
863
+ }),
864
+ ], { gap: "5", align: "stretch" }),
865
+ ]);
866
+ }));
867
+ }),
868
+ inputs: [],
869
+ });
870
+ // ============================================================================
871
+ // Staged + patch dataset — the fourth mode. Edits buffer locally (IndexedDB)
872
+ // while the user iterates; commit publishes a fresh patch IR to the patch
873
+ // dataset (the source is not touched until a separate apply step). Pairs
874
+ // well with reviewer / approval workflows where the patch dataset becomes
875
+ // the unit of review.
876
+ // ============================================================================
877
+ /**
878
+ * Single-Float staged-with-patch editor — buffered Slider edits publish to
879
+ * `max_weekly_hours_patch` on Apply. The source `max_weekly_hours` stays
880
+ * untouched until a separate apply-patch-to-source step.
881
+ */
882
+ export const policyStagedPatchEditor = example({
883
+ keywords: ["Diff", "Slider", "staged", "patch", "publish", "policy"],
884
+ description: "Slider edits buffered locally; Apply publishes draft to a server-backed patch dataset",
885
+ fn: East.function([], UIComponentType, (_$) => {
886
+ return Reactive.Root(East.function([], UIComponentType, $ => {
887
+ const view = $.let(Data.bind([FloatType], maxWeeklyHoursInput.path, { mode: "staged", patch: maxWeeklyHoursPatchInput.path }));
888
+ return Card.Root([
889
+ Stack.VStack([
890
+ Text.Root("Max weekly hours (staged + patch)", { textStyle: "heading-md" }),
891
+ Text.Root("Drag to buffer locally; Apply publishes the draft as a patch to the patch dataset (source untouched)."),
892
+ Slider.Root(view.read(), {
893
+ min: 30.0, max: 60.0, step: 1.0,
894
+ onChange: ($, v) => $(view.write(v)),
895
+ }),
896
+ Diff.Root({
897
+ bindings: [view.binding],
898
+ hideUnchanged: some(true),
899
+ }),
900
+ ], { gap: "5", align: "stretch" }),
901
+ ]);
902
+ }));
903
+ }),
904
+ inputs: [],
905
+ });
906
+ /**
907
+ * Roster table editor in staged + patch mode. Cell edits buffer in
908
+ * IndexedDB until Apply, which publishes a fresh patch to `roster_patch`.
909
+ * Survives reload as a draft; multi-session reviewers see the patch only
910
+ * after the author commits.
911
+ */
912
+ export const rosterStagedPatchEditor = example({
913
+ keywords: ["Diff", "Table", "staged", "patch", "publish", "roster"],
914
+ description: "Editable roster — edits buffered locally; Apply publishes a fresh patch to a server-backed patch dataset",
915
+ fn: East.function([], UIComponentType, (_$) => {
916
+ return Reactive.Root(East.function([], UIComponentType, $ => {
917
+ const view = $.let(Data.bind([RosterArrayType], rosterInput.path, { mode: "staged", patch: rosterPatchInput.path }));
918
+ const rosterArray = $.let(view.read(), RosterArrayType);
919
+ return Card.Root([
920
+ Stack.VStack([
921
+ Text.Root("Worker roster (staged + patch)", { textStyle: "heading-md" }),
922
+ Text.Root("Edits buffer locally until Apply, which publishes a draft patch to the patch dataset for review."),
923
+ Table.Root(rosterArray, {
924
+ name: { header: "Name" },
925
+ rate: {
926
+ header: "Hourly rate ($)",
927
+ render: East.function([Table.Types.CellRenderContext], UIComponentType, ($, ctx) => {
928
+ const idx = $.let(ctx.rowIndex, IntegerType);
929
+ const row = $.let(rosterArray.get(idx), RosterEntryType);
930
+ return Input.Float(row.rate, {
931
+ min: 15.0, max: 80.0, step: 0.25,
932
+ onChange: East.function([FloatType], NullType, ($, v) => {
933
+ const fresh = $.let(view.read(), RosterArrayType);
934
+ $(view.write(fresh.map(($, r, i) => i.equal(idx).ifElse(_$ => ({ ...r, rate: v }), () => r))));
935
+ }),
936
+ });
937
+ }),
938
+ },
939
+ shiftLength: {
940
+ header: "Shift length (h)",
941
+ render: East.function([Table.Types.CellRenderContext], UIComponentType, ($, ctx) => {
942
+ const idx = $.let(ctx.rowIndex, IntegerType);
943
+ const row = $.let(rosterArray.get(idx), RosterEntryType);
944
+ return Input.Integer(row.shiftLength, {
945
+ min: 4n, max: 12n,
946
+ onChange: East.function([IntegerType], NullType, ($, v) => {
947
+ const fresh = $.let(view.read(), RosterArrayType);
948
+ $(view.write(fresh.map(($, r, i) => i.equal(idx).ifElse(_$ => ({ ...r, shiftLength: v }), () => r))));
949
+ }),
950
+ });
951
+ }),
952
+ },
953
+ }, { variant: "line", striped: true }),
954
+ Diff.Root({
955
+ bindings: [view.binding],
956
+ hideUnchanged: some(true),
957
+ }),
958
+ ], { gap: "5", align: "stretch" }),
959
+ ]);
960
+ }));
961
+ }),
962
+ inputs: [],
963
+ });
964
+ //# sourceMappingURL=diff.examples.js.map