@dxos/plugin-sheet 0.13.0 → 0.14.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.
package/PLUGIN.mdl CHANGED
@@ -398,6 +398,32 @@ test QA-1: Create a sheet, set values, evaluate a formula, read back
398
398
  default to that same collection rather than a different one.
399
399
  ```
400
400
 
401
+ ```mdl
402
+ test QA-2: Build a budget and watch its totals recompute
403
+ desc: The demo path — create a sheet, type a small table, total it with SUM, and change a value.
404
+ actors: human
405
+ covers: [F-1.1, F-2.1, F-2.3, T-1, T-2]
406
+ status: passing # 2026-10-09, recorded by autocue/budget.mjs
407
+ given:
408
+ - an identity exists, on a fresh profile
409
+ - space: the default space is open on its Home
410
+ steps:
411
+ - name: Create a sheet
412
+ do: Click "Add to space" and choose Sheet.
413
+ expect: an empty grid opens in a new plank
414
+ - name: Enter a header row and four rows of costs
415
+ do: |
416
+ From A1, type "Item", "Q1", "Q2" with Tab between cells and Enter at the end, then four rows:
417
+ Hosting 120 140, Design 80 95, Marketing 60 75, Support 40 45.
418
+ expect: the values fill A1:C5, numbers right-aligned
419
+ - name: Total each quarter with SUM
420
+ do: In A6 type "Total", then "=SUM(B2:B5)" in B6 and "=SUM(C2:C5)" in C6.
421
+ expect: B6 shows 300 and C6 shows 355
422
+ - name: Change a cost and watch the total update
423
+ do: Click B2, type "200" and press Enter.
424
+ expect: B6 updates to 380 without any other action
425
+ ```
426
+
401
427
  ---
402
428
 
403
429
  ## Appendix: Extension Definitions
@@ -1,5 +1,5 @@
1
1
  //#region PLUGIN.mdl?raw
2
- var PLUGIN_default = "---\nid: org.dxos.plugin.sheet\nname: SheetPlugin\nversion: 0.1.0\n---\n\nSpreadsheet plugin for `DXOS` Composer — a full-featured grid backed by ECHO for local-first, real-time collaborative data entry, formula evaluation, and structured data display.\n\n## Extensions\n\nThe following extension dialects are used in this document.\nEach extension is defined in the Appendix or resolved via its URI.\n\n| Term | URI |\n|-------------|--------------------------------|\n| `type` | `org.dxos.mdl.type@1.0` |\n| `feat` | `org.dxos.mdl.feat@1.0` |\n| `scenario` | `org.dxos.mdl.scenario@1.0` |\n| `component` | `org.dxos.mdl.component@1.0` |\n| `op` | `org.dxos.mdl.op@1.1` |\n| `test` | `org.dxos.mdl.test@1.0` |\n\n## Types\n\n```mdl\ntype CellValue\n fields:\n value: any # scalar: string | number | boolean | formula string\n```\n\n```mdl\ntype RowColumnMeta\n fields:\n size?: number # pixel width/height; absent means default\n```\n\n```mdl\ntype Range\n fields:\n range: string # A1-notation range (e.g. \"A1:C3\")\n key: string # formatting key: alignment | comment | style\n value: string # formatting value (e.g. \"center\", \"highlight\")\n```\n\n```mdl\ntype Axis\n literals: row | col\n```\n\n```mdl\ntype Sheet\n typename: org.dxos.type.sheet\n fields:\n name?: string\n cells: Map<string, CellValue> # sparse map keyed by internal cell index\n rows: string[] # ordered row indices\n columns: string[] # ordered column indices\n rowMeta: Map<string, RowColumnMeta>\n columnMeta: Map<string, RowColumnMeta>\n ranges: Range[] # range-based formatting annotations\n```\n\n## Components\n\n```mdl\ncomponent SheetArticle\n desc: Primary spreadsheet view rendered inside an article or section role surface.\n props:\n subject: Sheet\n attendableId: string\n space: Space\n registry: ComputeGraphRegistry\n state:\n selected?: CellAddress\n activeRefs?: GridContentProps['activeRefs']\n slots:\n toolbar?: ReactNode\n actions:\n selectCell(address: CellAddress)\n editCell(address: CellAddress, value: any)\n insertRow(index: number)\n insertColumn(index: number)\n dropRow(axisIndex: string)\n dropColumn(axisIndex: string)\n layout: |\n ┌───────────────────────────────────┐\n │ [toolbar slot] │\n ├───┬───┬───┬───┬───────────────────┤\n │ │ A │ B │ C │ … column header │\n ├───┼───┼───┼───┤ │\n │ 1 │ │ │ │ cell grid │\n ├───┼───┼───┼───┤ │\n │ 2 │ │ │ │ │\n │ … │ │ │ │ │\n └───┴───┴───┴───┴───────────────────┘\n```\n\n```mdl\ncomponent RangeList\n desc: Object-properties panel listing named ranges and formatting annotations on a Sheet.\n props:\n sheet: Sheet\n state:\n selected?: Range\n actions:\n addRange(range: Range)\n removeRange(range: Range)\n updateRange(range: Range)\n```\n\n## Operations\n\n```mdl\nop insertAxis\n key: org.dxos.operation.sheet.insertAxis\n desc: Inserts one or more rows or columns at the given index in the sheet model.\n input:\n model: SheetModel\n axis: Axis\n index: number\n count?: number # defaults to 1\n output: void\n effects: [echo:write]\n```\n\n```mdl\nop dropAxis\n key: org.dxos.operation.sheet.dropAxis\n desc: Removes a row or column by axis index, returning data needed to restore it (undo).\n input:\n model: SheetModel\n axis: Axis\n axisIndex: string\n output: DropAxisOutput # axis, axisIndex, index, axisMeta, values\n effects: [echo:write]\n```\n\n```mdl\nop restoreAxis\n key: org.dxos.operation.sheet.restoreAxis\n desc: Restores a previously dropped row or column (undo of dropAxis).\n input:\n model: SheetModel\n axis: Axis\n axisIndex: string # original axis index to restore\n index: number # original position\n axisMeta: RowColumnMeta\n values: any[] # cell values to re-insert\n output: void\n effects: [echo:write]\n```\n\n```mdl\nop scrollToAnchor\n key: org.dxos.operation.sheet.scrollToAnchor\n desc: Programmatically scrolls and highlights a cell range in an active grid instance.\n input:\n subject: string # attendable ID of the target sheet\n cursor: string # A1-notation cell or range\n ref?: any # active refs for highlight overlay\n output: void\n effects: [ui:scroll]\n```\n\nAgent-facing skill operations (not previously documented here — found in `src/types/SheetOperation.ts`):\n\n```mdl\nop create\n key: org.dxos.operation.sheet.create\n desc: Creates a new Sheet and adds it to the space.\n input:\n name?: string\n rows?: number # default 50\n columns?: number # default 26\n output: { id: string } # DXN of the created sheet\n effects: [echo:write]\n requires: [Database.Service]\n\nop getRange\n key: org.dxos.operation.sheet.getRange\n desc: Reads cell values from a sheet as a 2D array. Omit `range` for the entire occupied area.\n input:\n sheet: Ref<Sheet>\n range?: string # A1 notation, e.g. \"A1:C5\"\n output: { values: any[][], range: string }\n effects: [echo:read]\n requires: [Database.Service]\n\nop setRange\n key: org.dxos.operation.sheet.setRange\n desc: Sets multiple cell values in a sheet at once, keyed by A1 address.\n input:\n sheet: Ref<Sheet>\n cells: Record<string, any> # e.g. { \"A1\": \"Name\", \"B1\": 42, \"C1\": \"=A1+B1\" }\n output: void\n effects: [echo:write]\n requires: [Database.Service]\n```\n\n## Features\n\n```mdl\nfeat F-1: Sheet Object\n\n req F-1.1: A Sheet is an ECHO object with typename org.dxos.type.sheet persisted in the user's space.\n req F-1.2: Cells are stored in a sparse map keyed by internal row/column indices.\n req F-1.3: Row and column order is maintained via ordered index arrays; insertion/deletion preserves existing cell references.\n req F-1.4: Range-based formatting annotations are stored as a flat array on the Sheet object.\n```\n\n```mdl\nfeat F-2: Formula Evaluation\n\n req F-2.1: Formulas are prefixed with \"=\" and evaluated by the HyperFormula engine via the ComputeGraph.\n req F-2.2: Over 400 built-in functions are available (arithmetic, statistical, text, date/time, financial, logical).\n req F-2.3:\n when: a cell value changes\n then: all dependent formula cells are recomputed reactively\n req F-2.4: Cell references in stored formulas are translated to internal indices; A1-notation is converted on read/write.\n```\n\n```mdl\nfeat F-3: Row and Column Management\n\n req F-3.1:\n when: op:insertAxis is called\n then: a new row or column is inserted at the specified index; existing cells shift accordingly\n\n req F-3.2:\n when: op:dropAxis is called\n then: the row or column is removed and undo data returned; op:restoreAxis reverses the operation\n\n req F-3.3: Row and column sizes are persisted in rowMeta/columnMeta; absent size means default grid size.\n```\n\n```mdl\nfeat F-4: Cell Formatting\n\n req F-4.1: Ranges support alignment values: start | center | end.\n req F-4.2: Ranges support comment annotations displayed as a visual overlay on the cell.\n req F-4.3: Ranges support style values: highlight | softwrap.\n req F-4.4: RangeList in the object-properties surface allows users to view and manage range annotations.\n```\n\n```mdl\nfeat F-5: Collaborative Editing\n\n req F-5.1: All cell edits are written through ECHO and replicated to peers in real time.\n req F-5.2:\n when: a peer writes a cell value\n then: the local grid re-renders with the updated value without full reload\n\n req F-5.3: ComputeGraphRegistry manages per-space HyperFormula instances so all peers share the same formula graph.\n```\n\n```mdl\nfeat F-6: Markdown Integration\n\n req F-6.1: SheetPlugin contributes a CodeMirror extension to plugin-markdown via MarkdownCapabilities.ExtensionProvider.\n req F-6.2:\n when: the extension is active in a document that belongs to a space\n then: formula references inside Markdown fenced code blocks resolve against the space's ComputeGraph\n```\n\n```mdl\nfeat F-7: Anchor Navigation\n\n req F-7.1:\n when: op:scrollToAnchor is invoked with a sheet attendable ID and cursor\n then: the corresponding SheetArticle scrolls to the target cell and applies the active-ref highlight\n\n req F-7.2: AnchorSort contributes a sort comparator so sheet anchors appear in canonical order in navigation menus.\n```\n\n## Acceptance\n\n```mdl\nscenario T-1: Create sheet and enter a value\n given: a new Sheet is created in a space\n when: user types \"42\" into cell A1\n then:\n - Sheet.cells contains an entry for the A1 index with value 42\n - grid displays \"42\" in the A1 cell\n```\n\n```mdl\nscenario T-2: Formula evaluation\n given: A1 = 10, A2 = 20\n when: user enters \"=A1+A2\" in A3\n then:\n - A3 displays 30\n - changing A1 to 15 causes A3 to update to 35\n```\n\n```mdl\nscenario T-3: Insert and drop row\n given: a Sheet with values in rows 1 and 2\n when: op:insertAxis runs with axis=row, index=1\n then:\n - a blank row is inserted between existing rows 1 and 2\n - existing cell references remain valid\n\n when: op:dropAxis runs on the inserted row\n then:\n - the row is removed\n - op:restoreAxis re-inserts the row with its original values\n```\n\n```mdl\nscenario T-4: Range formatting\n given: a Sheet open in SheetArticle\n when: user applies \"highlight\" style to range B2:C4\n then:\n - Sheet.ranges contains an entry with range=\"B2:C4\", key=\"style\", value=\"highlight\"\n - cells in B2:C4 render with the highlight background class\n```\n\n```mdl\nscenario T-5: Real-time collaboration\n given: two peers have the same Sheet open\n when: peer A sets cell D5 to \"hello\"\n then:\n - peer B's grid displays \"hello\" in D5 without reload\n```\n\n```mdl\nscenario T-6: Scroll to anchor\n given: a SheetArticle is mounted with attendableId \"sheet-1\"\n when: op:scrollToAnchor runs with subject=\"sheet-1\", cursor=\"E10\"\n then:\n - the grid scrolls so that E10 is visible\n - E10 is highlighted via activeRefs\n```\n\n---\n\n## QA\n\nExecutable tests. Each step carries a human instruction (`do`) and, where the step maps to an\ninvocable operation, an agent binding (`invoke`). Report failures by test and step number.\n\n```mdl\ntest QA-1: Create a sheet, set values, evaluate a formula, read back\n desc: The core spreadsheet round-trip — create, set cells (including a formula), read the\n evaluated result back.\n actors: agent\n covers: [F-1.1, F-1.2, F-2.1, F-2.3, T-1, T-2]\n status: unverified\n given:\n - an identity exists\n - space: a space is open\n - root: the space's root collection — [op:create] takes no target and files the Sheet\n there via its default parent (`DefaultParent.add`), so the test cannot choose a different one\n - no Sheet named \"QA Sheet\" exists — ABORT rather than reusing one this test did not create\n steps:\n - name: Create the sheet\n do: In the navtree, click + on a collection and choose Sheet. Name it \"QA Sheet\".\n invoke: [op:create] { name: \"QA Sheet\" }\n capture: created\n expect: a Sheet named \"QA Sheet\" now exists\n assert: return !!$created.id\n\n - name: Place it in the root collection\n do: (no separate gesture — the navtree create action places it)\n expect: \"QA Sheet\" appears as a row under the space's root collection\n note: |\n NO `invoke` — [op:create] places the sheet in the space itself, so a following\n `space.addObject` is redundant. `$created.id` is a DXN string, not an object;\n resolve it (`db.getObjectById` on the DXN's last segment) before any step that\n needs the live Sheet.\n\n - name: Resolve the sheet and set values\n do: Click cell A1 and type \"10\", Enter. Click A2 and type \"20\", Enter. Click A3 and type\n \"=A1+A2\", Enter.\n invoke: [op:setRange] { sheet: space.db.makeRef($created.id), cells: { A1: 10, A2: 20, A3: \"=A1+A2\" } }\n expect: A1 shows 10, A2 shows 20, and A3 shows 30 (evaluated by HyperFormula)\n note: |\n `sheet` is a `Ref` and `op:create` returns only `{ id }` (a DXN string) — `db.makeRef`\n builds the `Ref` straight from that DXN, no load needed; `Ref.make` needs a live object,\n which nothing captured here has.\n\n - name: Read the range back\n do: (agent-only — confirm the evaluated values)\n invoke: [op:getRange] { sheet: space.db.makeRef($created.id), range: \"A1:A3\" }\n expect: A3 renders 30 in the grid\n assert: |\n // getRange returns STORED cell content, not the evaluated result — A3 comes back as\n // the formula string. Judge the evaluated value in the UI (`expect`), not here.\n return $result.values.flat().slice(0, 2).join(',') === '10,20';\n after:\n - name: Delete the sheet\n do: Right-click \"QA Sheet\" in the navtree and choose Delete.\n invoke: org.dxos.operation.space.removeObjects { objects: [space.db.getObjectById($created.id.split('/').pop())] }\n expect: the row leaves the navtree and its plank closes if it was open\n note: |\n No `target` — `op:create` calls `DefaultParent.add({ object })` with no target, so the\n Sheet lands in the space's root collection; omitting `target` here lets `removeObjects`\n default to that same collection rather than a different one.\n```\n\n---\n\n## Appendix: Extension Definitions\n\n```mdl\next type\n uri: org.dxos.mdl.type@1.0\n desc: A named data structure with typed fields and optional literals.\n fields:\n desc?: Prose\n fields?: FieldMap\n literals?: UnionList\n extends?: TypeRef[]\n```\n\n```mdl\next feat\n uri: org.dxos.mdl.feat@1.0\n desc: A named feature grouping one or more requirements.\n fields:\n desc?: Prose\n req: RequirementList\n nesting: self\n```\n\n```mdl\next scenario\n uri: org.dxos.mdl.scenario@1.0\n desc: An acceptance scenario expressed as given / when / then steps.\n fields:\n given?: Step | Step[]\n when?: Step | Step[]\n then: Step | Step[]\n tags?: TagList\n```\n\n```mdl\next component\n uri: org.dxos.mdl.component@1.0\n desc: A UI component with props, internal state, slots, actions, and events.\n fields:\n desc?: Prose\n props?: FieldMap\n state?: FieldMap\n slots?: FieldMap\n actions?: ActionMap\n emits?: EventMap\n layout?: CodeBlock\n```\n\n```mdl\next op\n uri: org.dxos.mdl.op@1.0\n desc: |\n A named operation with typed inputs, outputs, and declared errors.\n Pure ops have no effects or requires. Effectful ops declare both.\n fields:\n desc?: Prose\n input?: FieldMap\n output?: TypeExpr\n errors?: ErrorMap\n effects?: EffectList\n requires?: ServiceList\n note?: Prose\n```\n";
2
+ var PLUGIN_default = "---\nid: org.dxos.plugin.sheet\nname: SheetPlugin\nversion: 0.1.0\n---\n\nSpreadsheet plugin for `DXOS` Composer — a full-featured grid backed by ECHO for local-first, real-time collaborative data entry, formula evaluation, and structured data display.\n\n## Extensions\n\nThe following extension dialects are used in this document.\nEach extension is defined in the Appendix or resolved via its URI.\n\n| Term | URI |\n|-------------|--------------------------------|\n| `type` | `org.dxos.mdl.type@1.0` |\n| `feat` | `org.dxos.mdl.feat@1.0` |\n| `scenario` | `org.dxos.mdl.scenario@1.0` |\n| `component` | `org.dxos.mdl.component@1.0` |\n| `op` | `org.dxos.mdl.op@1.1` |\n| `test` | `org.dxos.mdl.test@1.0` |\n\n## Types\n\n```mdl\ntype CellValue\n fields:\n value: any # scalar: string | number | boolean | formula string\n```\n\n```mdl\ntype RowColumnMeta\n fields:\n size?: number # pixel width/height; absent means default\n```\n\n```mdl\ntype Range\n fields:\n range: string # A1-notation range (e.g. \"A1:C3\")\n key: string # formatting key: alignment | comment | style\n value: string # formatting value (e.g. \"center\", \"highlight\")\n```\n\n```mdl\ntype Axis\n literals: row | col\n```\n\n```mdl\ntype Sheet\n typename: org.dxos.type.sheet\n fields:\n name?: string\n cells: Map<string, CellValue> # sparse map keyed by internal cell index\n rows: string[] # ordered row indices\n columns: string[] # ordered column indices\n rowMeta: Map<string, RowColumnMeta>\n columnMeta: Map<string, RowColumnMeta>\n ranges: Range[] # range-based formatting annotations\n```\n\n## Components\n\n```mdl\ncomponent SheetArticle\n desc: Primary spreadsheet view rendered inside an article or section role surface.\n props:\n subject: Sheet\n attendableId: string\n space: Space\n registry: ComputeGraphRegistry\n state:\n selected?: CellAddress\n activeRefs?: GridContentProps['activeRefs']\n slots:\n toolbar?: ReactNode\n actions:\n selectCell(address: CellAddress)\n editCell(address: CellAddress, value: any)\n insertRow(index: number)\n insertColumn(index: number)\n dropRow(axisIndex: string)\n dropColumn(axisIndex: string)\n layout: |\n ┌───────────────────────────────────┐\n │ [toolbar slot] │\n ├───┬───┬───┬───┬───────────────────┤\n │ │ A │ B │ C │ … column header │\n ├───┼───┼───┼───┤ │\n │ 1 │ │ │ │ cell grid │\n ├───┼───┼───┼───┤ │\n │ 2 │ │ │ │ │\n │ … │ │ │ │ │\n └───┴───┴───┴───┴───────────────────┘\n```\n\n```mdl\ncomponent RangeList\n desc: Object-properties panel listing named ranges and formatting annotations on a Sheet.\n props:\n sheet: Sheet\n state:\n selected?: Range\n actions:\n addRange(range: Range)\n removeRange(range: Range)\n updateRange(range: Range)\n```\n\n## Operations\n\n```mdl\nop insertAxis\n key: org.dxos.operation.sheet.insertAxis\n desc: Inserts one or more rows or columns at the given index in the sheet model.\n input:\n model: SheetModel\n axis: Axis\n index: number\n count?: number # defaults to 1\n output: void\n effects: [echo:write]\n```\n\n```mdl\nop dropAxis\n key: org.dxos.operation.sheet.dropAxis\n desc: Removes a row or column by axis index, returning data needed to restore it (undo).\n input:\n model: SheetModel\n axis: Axis\n axisIndex: string\n output: DropAxisOutput # axis, axisIndex, index, axisMeta, values\n effects: [echo:write]\n```\n\n```mdl\nop restoreAxis\n key: org.dxos.operation.sheet.restoreAxis\n desc: Restores a previously dropped row or column (undo of dropAxis).\n input:\n model: SheetModel\n axis: Axis\n axisIndex: string # original axis index to restore\n index: number # original position\n axisMeta: RowColumnMeta\n values: any[] # cell values to re-insert\n output: void\n effects: [echo:write]\n```\n\n```mdl\nop scrollToAnchor\n key: org.dxos.operation.sheet.scrollToAnchor\n desc: Programmatically scrolls and highlights a cell range in an active grid instance.\n input:\n subject: string # attendable ID of the target sheet\n cursor: string # A1-notation cell or range\n ref?: any # active refs for highlight overlay\n output: void\n effects: [ui:scroll]\n```\n\nAgent-facing skill operations (not previously documented here — found in `src/types/SheetOperation.ts`):\n\n```mdl\nop create\n key: org.dxos.operation.sheet.create\n desc: Creates a new Sheet and adds it to the space.\n input:\n name?: string\n rows?: number # default 50\n columns?: number # default 26\n output: { id: string } # DXN of the created sheet\n effects: [echo:write]\n requires: [Database.Service]\n\nop getRange\n key: org.dxos.operation.sheet.getRange\n desc: Reads cell values from a sheet as a 2D array. Omit `range` for the entire occupied area.\n input:\n sheet: Ref<Sheet>\n range?: string # A1 notation, e.g. \"A1:C5\"\n output: { values: any[][], range: string }\n effects: [echo:read]\n requires: [Database.Service]\n\nop setRange\n key: org.dxos.operation.sheet.setRange\n desc: Sets multiple cell values in a sheet at once, keyed by A1 address.\n input:\n sheet: Ref<Sheet>\n cells: Record<string, any> # e.g. { \"A1\": \"Name\", \"B1\": 42, \"C1\": \"=A1+B1\" }\n output: void\n effects: [echo:write]\n requires: [Database.Service]\n```\n\n## Features\n\n```mdl\nfeat F-1: Sheet Object\n\n req F-1.1: A Sheet is an ECHO object with typename org.dxos.type.sheet persisted in the user's space.\n req F-1.2: Cells are stored in a sparse map keyed by internal row/column indices.\n req F-1.3: Row and column order is maintained via ordered index arrays; insertion/deletion preserves existing cell references.\n req F-1.4: Range-based formatting annotations are stored as a flat array on the Sheet object.\n```\n\n```mdl\nfeat F-2: Formula Evaluation\n\n req F-2.1: Formulas are prefixed with \"=\" and evaluated by the HyperFormula engine via the ComputeGraph.\n req F-2.2: Over 400 built-in functions are available (arithmetic, statistical, text, date/time, financial, logical).\n req F-2.3:\n when: a cell value changes\n then: all dependent formula cells are recomputed reactively\n req F-2.4: Cell references in stored formulas are translated to internal indices; A1-notation is converted on read/write.\n```\n\n```mdl\nfeat F-3: Row and Column Management\n\n req F-3.1:\n when: op:insertAxis is called\n then: a new row or column is inserted at the specified index; existing cells shift accordingly\n\n req F-3.2:\n when: op:dropAxis is called\n then: the row or column is removed and undo data returned; op:restoreAxis reverses the operation\n\n req F-3.3: Row and column sizes are persisted in rowMeta/columnMeta; absent size means default grid size.\n```\n\n```mdl\nfeat F-4: Cell Formatting\n\n req F-4.1: Ranges support alignment values: start | center | end.\n req F-4.2: Ranges support comment annotations displayed as a visual overlay on the cell.\n req F-4.3: Ranges support style values: highlight | softwrap.\n req F-4.4: RangeList in the object-properties surface allows users to view and manage range annotations.\n```\n\n```mdl\nfeat F-5: Collaborative Editing\n\n req F-5.1: All cell edits are written through ECHO and replicated to peers in real time.\n req F-5.2:\n when: a peer writes a cell value\n then: the local grid re-renders with the updated value without full reload\n\n req F-5.3: ComputeGraphRegistry manages per-space HyperFormula instances so all peers share the same formula graph.\n```\n\n```mdl\nfeat F-6: Markdown Integration\n\n req F-6.1: SheetPlugin contributes a CodeMirror extension to plugin-markdown via MarkdownCapabilities.ExtensionProvider.\n req F-6.2:\n when: the extension is active in a document that belongs to a space\n then: formula references inside Markdown fenced code blocks resolve against the space's ComputeGraph\n```\n\n```mdl\nfeat F-7: Anchor Navigation\n\n req F-7.1:\n when: op:scrollToAnchor is invoked with a sheet attendable ID and cursor\n then: the corresponding SheetArticle scrolls to the target cell and applies the active-ref highlight\n\n req F-7.2: AnchorSort contributes a sort comparator so sheet anchors appear in canonical order in navigation menus.\n```\n\n## Acceptance\n\n```mdl\nscenario T-1: Create sheet and enter a value\n given: a new Sheet is created in a space\n when: user types \"42\" into cell A1\n then:\n - Sheet.cells contains an entry for the A1 index with value 42\n - grid displays \"42\" in the A1 cell\n```\n\n```mdl\nscenario T-2: Formula evaluation\n given: A1 = 10, A2 = 20\n when: user enters \"=A1+A2\" in A3\n then:\n - A3 displays 30\n - changing A1 to 15 causes A3 to update to 35\n```\n\n```mdl\nscenario T-3: Insert and drop row\n given: a Sheet with values in rows 1 and 2\n when: op:insertAxis runs with axis=row, index=1\n then:\n - a blank row is inserted between existing rows 1 and 2\n - existing cell references remain valid\n\n when: op:dropAxis runs on the inserted row\n then:\n - the row is removed\n - op:restoreAxis re-inserts the row with its original values\n```\n\n```mdl\nscenario T-4: Range formatting\n given: a Sheet open in SheetArticle\n when: user applies \"highlight\" style to range B2:C4\n then:\n - Sheet.ranges contains an entry with range=\"B2:C4\", key=\"style\", value=\"highlight\"\n - cells in B2:C4 render with the highlight background class\n```\n\n```mdl\nscenario T-5: Real-time collaboration\n given: two peers have the same Sheet open\n when: peer A sets cell D5 to \"hello\"\n then:\n - peer B's grid displays \"hello\" in D5 without reload\n```\n\n```mdl\nscenario T-6: Scroll to anchor\n given: a SheetArticle is mounted with attendableId \"sheet-1\"\n when: op:scrollToAnchor runs with subject=\"sheet-1\", cursor=\"E10\"\n then:\n - the grid scrolls so that E10 is visible\n - E10 is highlighted via activeRefs\n```\n\n---\n\n## QA\n\nExecutable tests. Each step carries a human instruction (`do`) and, where the step maps to an\ninvocable operation, an agent binding (`invoke`). Report failures by test and step number.\n\n```mdl\ntest QA-1: Create a sheet, set values, evaluate a formula, read back\n desc: The core spreadsheet round-trip — create, set cells (including a formula), read the\n evaluated result back.\n actors: agent\n covers: [F-1.1, F-1.2, F-2.1, F-2.3, T-1, T-2]\n status: unverified\n given:\n - an identity exists\n - space: a space is open\n - root: the space's root collection — [op:create] takes no target and files the Sheet\n there via its default parent (`DefaultParent.add`), so the test cannot choose a different one\n - no Sheet named \"QA Sheet\" exists — ABORT rather than reusing one this test did not create\n steps:\n - name: Create the sheet\n do: In the navtree, click + on a collection and choose Sheet. Name it \"QA Sheet\".\n invoke: [op:create] { name: \"QA Sheet\" }\n capture: created\n expect: a Sheet named \"QA Sheet\" now exists\n assert: return !!$created.id\n\n - name: Place it in the root collection\n do: (no separate gesture — the navtree create action places it)\n expect: \"QA Sheet\" appears as a row under the space's root collection\n note: |\n NO `invoke` — [op:create] places the sheet in the space itself, so a following\n `space.addObject` is redundant. `$created.id` is a DXN string, not an object;\n resolve it (`db.getObjectById` on the DXN's last segment) before any step that\n needs the live Sheet.\n\n - name: Resolve the sheet and set values\n do: Click cell A1 and type \"10\", Enter. Click A2 and type \"20\", Enter. Click A3 and type\n \"=A1+A2\", Enter.\n invoke: [op:setRange] { sheet: space.db.makeRef($created.id), cells: { A1: 10, A2: 20, A3: \"=A1+A2\" } }\n expect: A1 shows 10, A2 shows 20, and A3 shows 30 (evaluated by HyperFormula)\n note: |\n `sheet` is a `Ref` and `op:create` returns only `{ id }` (a DXN string) — `db.makeRef`\n builds the `Ref` straight from that DXN, no load needed; `Ref.make` needs a live object,\n which nothing captured here has.\n\n - name: Read the range back\n do: (agent-only — confirm the evaluated values)\n invoke: [op:getRange] { sheet: space.db.makeRef($created.id), range: \"A1:A3\" }\n expect: A3 renders 30 in the grid\n assert: |\n // getRange returns STORED cell content, not the evaluated result — A3 comes back as\n // the formula string. Judge the evaluated value in the UI (`expect`), not here.\n return $result.values.flat().slice(0, 2).join(',') === '10,20';\n after:\n - name: Delete the sheet\n do: Right-click \"QA Sheet\" in the navtree and choose Delete.\n invoke: org.dxos.operation.space.removeObjects { objects: [space.db.getObjectById($created.id.split('/').pop())] }\n expect: the row leaves the navtree and its plank closes if it was open\n note: |\n No `target` — `op:create` calls `DefaultParent.add({ object })` with no target, so the\n Sheet lands in the space's root collection; omitting `target` here lets `removeObjects`\n default to that same collection rather than a different one.\n```\n\n```mdl\ntest QA-2: Build a budget and watch its totals recompute\n desc: The demo path — create a sheet, type a small table, total it with SUM, and change a value.\n actors: human\n covers: [F-1.1, F-2.1, F-2.3, T-1, T-2]\n status: passing # 2026-10-09, recorded by autocue/budget.mjs\n given:\n - an identity exists, on a fresh profile\n - space: the default space is open on its Home\n steps:\n - name: Create a sheet\n do: Click \"Add to space\" and choose Sheet.\n expect: an empty grid opens in a new plank\n - name: Enter a header row and four rows of costs\n do: |\n From A1, type \"Item\", \"Q1\", \"Q2\" with Tab between cells and Enter at the end, then four rows:\n Hosting 120 140, Design 80 95, Marketing 60 75, Support 40 45.\n expect: the values fill A1:C5, numbers right-aligned\n - name: Total each quarter with SUM\n do: In A6 type \"Total\", then \"=SUM(B2:B5)\" in B6 and \"=SUM(C2:C5)\" in C6.\n expect: B6 shows 300 and C6 shows 355\n - name: Change a cost and watch the total update\n do: Click B2, type \"200\" and press Enter.\n expect: B6 updates to 380 without any other action\n```\n\n---\n\n## Appendix: Extension Definitions\n\n```mdl\next type\n uri: org.dxos.mdl.type@1.0\n desc: A named data structure with typed fields and optional literals.\n fields:\n desc?: Prose\n fields?: FieldMap\n literals?: UnionList\n extends?: TypeRef[]\n```\n\n```mdl\next feat\n uri: org.dxos.mdl.feat@1.0\n desc: A named feature grouping one or more requirements.\n fields:\n desc?: Prose\n req: RequirementList\n nesting: self\n```\n\n```mdl\next scenario\n uri: org.dxos.mdl.scenario@1.0\n desc: An acceptance scenario expressed as given / when / then steps.\n fields:\n given?: Step | Step[]\n when?: Step | Step[]\n then: Step | Step[]\n tags?: TagList\n```\n\n```mdl\next component\n uri: org.dxos.mdl.component@1.0\n desc: A UI component with props, internal state, slots, actions, and events.\n fields:\n desc?: Prose\n props?: FieldMap\n state?: FieldMap\n slots?: FieldMap\n actions?: ActionMap\n emits?: EventMap\n layout?: CodeBlock\n```\n\n```mdl\next op\n uri: org.dxos.mdl.op@1.0\n desc: |\n A named operation with typed inputs, outputs, and declared errors.\n Pure ops have no effects or requires. Effectful ops declare both.\n fields:\n desc?: Prose\n input?: FieldMap\n output?: TypeExpr\n errors?: ErrorMap\n effects?: EffectList\n requires?: ServiceList\n note?: Prose\n```\n";
3
3
  //#endregion
4
4
  export { PLUGIN_default as t };
5
5
 
@@ -31,6 +31,7 @@ import { mx } from "@dxos/ui-theme";
31
31
  import { Doc } from "@dxos/echo-doc";
32
32
  import { cellClassesForFieldType, parseValue } from "@dxos/react-ui-form";
33
33
  import * as Icon from "@dxos/react-ui/Icon";
34
+ import * as Layout from "@dxos/react-ui/Layout";
34
35
  import { RegistryContext } from "@effect/atom-react/RegistryContext";
35
36
  import * as Atom from "effect/reactivity/Atom";
36
37
  import * as Hooks from "@dxos/app-toolkit/Hooks";
@@ -1330,7 +1331,6 @@ var SheetContent = Util.composable((props, forwardedRef) => {
1330
1331
  //#endregion
1331
1332
  //#region src/components/SheetStatusbar/SheetStatusbar.tsx
1332
1333
  var SheetStatusbar = Util.composable((props, forwardedRef) => {
1333
- const { className, ...rest } = Util.composableProps(props);
1334
1334
  const { model, cursor, range } = useSheetContext();
1335
1335
  let value;
1336
1336
  let formula = false;
@@ -1341,17 +1341,21 @@ var SheetStatusbar = Util.composable((props, forwardedRef) => {
1341
1341
  formula = true;
1342
1342
  } else if (value != null) value = String(value);
1343
1343
  }
1344
- return /* @__PURE__ */ jsx("div", {
1344
+ return /* @__PURE__ */ jsx(Layout.Flex, {
1345
+ justify: "between",
1346
+ align: "center",
1347
+ ...Util.composableProps(props, { classNames: "shrink-0 px-4 py-1 text-sm dx-toolbar-surface border-y !border-separator-subtle" }),
1345
1348
  ref: forwardedRef,
1346
- ...rest,
1347
- className: mx("flex shrink-0 justify-between items-center px-4 py-1 text-sm dx-toolbar-surface border-y !border-separator-subtle", className),
1348
- children: /* @__PURE__ */ jsxs("div", {
1349
- className: "flex gap-4 items-center",
1350
- children: [/* @__PURE__ */ jsx("div", {
1351
- className: "flex w-16 items-center font-mono",
1349
+ children: /* @__PURE__ */ jsxs(Layout.Flex, {
1350
+ gap: "lg",
1351
+ align: "center",
1352
+ children: [/* @__PURE__ */ jsx(Layout.Flex, {
1353
+ align: "center",
1354
+ classNames: "w-16 font-mono",
1352
1355
  children: range && rangeToA1Notation(range) || cursor && addressToA1Notation(cursor)
1353
- }), /* @__PURE__ */ jsxs("div", {
1354
- className: "flex gap-2 items-center",
1356
+ }), /* @__PURE__ */ jsxs(Layout.Flex, {
1357
+ gap: "sm",
1358
+ align: "center",
1355
1359
  children: [/* @__PURE__ */ jsx(Icon.Icon, {
1356
1360
  icon: "ph--function--regular",
1357
1361
  classNames: ["text-green-text", formula ? "visible" : "invisible"]