@elabs-ai/components-editor 4.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 (207) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -0
  3. package/dist/chunk-LBC5VJBD.js +3748 -0
  4. package/dist/chunk-LBC5VJBD.js.map +1 -0
  5. package/dist/index.css +233 -0
  6. package/dist/index.css.map +1 -0
  7. package/dist/index.d.ts +199 -0
  8. package/dist/index.js +362 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/lib/monaco-environment.d.ts +2 -0
  11. package/dist/lib/monaco-environment.js +44 -0
  12. package/dist/lib/monaco-environment.js.map +1 -0
  13. package/dist/markdown/frontmatter.d.ts +11 -0
  14. package/dist/markdown/frontmatter.js +33 -0
  15. package/dist/markdown/frontmatter.js.map +1 -0
  16. package/dist/markdown/index.css +233 -0
  17. package/dist/markdown/index.css.map +1 -0
  18. package/dist/markdown/index.d.ts +1486 -0
  19. package/dist/markdown/index.js +4882 -0
  20. package/dist/markdown/index.js.map +1 -0
  21. package/dist/markdown/parse.d.ts +25 -0
  22. package/dist/markdown/parse.js +14 -0
  23. package/dist/markdown/parse.js.map +1 -0
  24. package/dist/markdown-editor-DfBZibAn.d.ts +702 -0
  25. package/package.json +101 -0
  26. package/src/ai-objects/ai-objects-integration.test.tsx +128 -0
  27. package/src/ai-objects/ai-objects.stories.tsx +233 -0
  28. package/src/ai-objects/ai-objects.test.tsx +205 -0
  29. package/src/ai-objects/decision-card.tsx +178 -0
  30. package/src/ai-objects/directives.ts +136 -0
  31. package/src/ai-objects/entity.tsx +181 -0
  32. package/src/ai-objects/index.ts +34 -0
  33. package/src/ai-objects/knowledge-card.tsx +143 -0
  34. package/src/calc-block/calc-block.stories.tsx +297 -0
  35. package/src/calc-block/calc-block.test.tsx +164 -0
  36. package/src/calc-block/calc-block.tsx +398 -0
  37. package/src/calc-block/calc-editor-monaco.ts +214 -0
  38. package/src/calc-block/calc-editor-prose.ts +128 -0
  39. package/src/calc-block/calc-editor.css +80 -0
  40. package/src/calc-block/calc-editor.stories.tsx +272 -0
  41. package/src/calc-block/calc-editor.test.ts +228 -0
  42. package/src/calc-block/calc-editor.ts +270 -0
  43. package/src/calc-block/calc-inline.stories.tsx +44 -0
  44. package/src/calc-block/calc-inline.test.tsx +44 -0
  45. package/src/calc-block/calc-inline.tsx +75 -0
  46. package/src/calc-block/index.ts +20 -0
  47. package/src/calc-block/types.ts +164 -0
  48. package/src/code-editor/code-editor.stories.tsx +218 -0
  49. package/src/code-editor/code-editor.test.tsx +134 -0
  50. package/src/code-editor/code-editor.tsx +250 -0
  51. package/src/code-editor/index.ts +6 -0
  52. package/src/code-workspace/code-workspace.stories.tsx +51 -0
  53. package/src/code-workspace/code-workspace.test.tsx +146 -0
  54. package/src/code-workspace/code-workspace.tsx +253 -0
  55. package/src/code-workspace/index.ts +6 -0
  56. package/src/copy-button/copy-button.tsx +51 -0
  57. package/src/copy-button/index.ts +1 -0
  58. package/src/css.d.ts +3 -0
  59. package/src/diff-editor/diff-editor.stories.tsx +44 -0
  60. package/src/diff-editor/diff-editor.test.tsx +68 -0
  61. package/src/diff-editor/diff-editor.tsx +151 -0
  62. package/src/diff-editor/index.ts +1 -0
  63. package/src/editor-context-menu/editor-context-menu.tsx +120 -0
  64. package/src/editor-context-menu/index.ts +1 -0
  65. package/src/editor-toolbar/editor-toolbar.tsx +83 -0
  66. package/src/editor-toolbar/index.ts +1 -0
  67. package/src/index.ts +48 -0
  68. package/src/lib/editor-completions-monaco.test.ts +206 -0
  69. package/src/lib/editor-completions-monaco.ts +127 -0
  70. package/src/lib/editor-completions.test.ts +156 -0
  71. package/src/lib/editor-completions.ts +153 -0
  72. package/src/lib/editor-content-access-prose.test.ts +411 -0
  73. package/src/lib/editor-content-access-prose.ts +172 -0
  74. package/src/lib/editor-content-access.test.ts +182 -0
  75. package/src/lib/editor-content-access.ts +109 -0
  76. package/src/lib/languages.ts +32 -0
  77. package/src/lib/markdown/diff.test.ts +80 -0
  78. package/src/lib/markdown/diff.ts +194 -0
  79. package/src/lib/markdown/directives.ts +342 -0
  80. package/src/lib/markdown/frontmatter.ts +50 -0
  81. package/src/lib/markdown/markdown-scale.test.ts +65 -0
  82. package/src/lib/markdown/markdown-scale.ts +62 -0
  83. package/src/lib/markdown/merge.test.ts +64 -0
  84. package/src/lib/markdown/merge.ts +158 -0
  85. package/src/lib/markdown/slugify.test.ts +156 -0
  86. package/src/lib/markdown/slugify.ts +47 -0
  87. package/src/lib/monaco-environment.ts +103 -0
  88. package/src/lib/monaco-theme-bridge.ts +293 -0
  89. package/src/lib/use-data-theme.ts +61 -0
  90. package/src/markdown/frontmatter.ts +14 -0
  91. package/src/markdown/index.ts +323 -0
  92. package/src/markdown/parse.test.ts +60 -0
  93. package/src/markdown/parse.ts +38 -0
  94. package/src/markdown-academic/citations.test.ts +77 -0
  95. package/src/markdown-academic/citations.tsx +442 -0
  96. package/src/markdown-academic/footnotes.tsx +235 -0
  97. package/src/markdown-academic/index.ts +32 -0
  98. package/src/markdown-academic/math.tsx +163 -0
  99. package/src/markdown-academic/toc.tsx +88 -0
  100. package/src/markdown-editor/completions/completions-menu.tsx +96 -0
  101. package/src/markdown-editor/completions/completions-prose.test.ts +356 -0
  102. package/src/markdown-editor/completions/completions-prose.ts +313 -0
  103. package/src/markdown-editor/completions/completions-widget.tsx +75 -0
  104. package/src/markdown-editor/completions/index.ts +37 -0
  105. package/src/markdown-editor/directive-nodes.ts +151 -0
  106. package/src/markdown-editor/directive-views.test.tsx +189 -0
  107. package/src/markdown-editor/directive-views.tsx +795 -0
  108. package/src/markdown-editor/exit-keymap.test.ts +88 -0
  109. package/src/markdown-editor/exit-keymap.ts +87 -0
  110. package/src/markdown-editor/index.ts +6 -0
  111. package/src/markdown-editor/markdown-editor.css +250 -0
  112. package/src/markdown-editor/markdown-editor.directives.test.tsx +105 -0
  113. package/src/markdown-editor/markdown-editor.fill.test.ts +34 -0
  114. package/src/markdown-editor/markdown-editor.paste-embed.test.tsx +297 -0
  115. package/src/markdown-editor/markdown-editor.stories.tsx +560 -0
  116. package/src/markdown-editor/markdown-editor.strictmode.test.tsx +38 -0
  117. package/src/markdown-editor/markdown-editor.table.test.tsx +287 -0
  118. package/src/markdown-editor/markdown-editor.tsx +560 -0
  119. package/src/markdown-editor/milkdown-react/editor.tsx +33 -0
  120. package/src/markdown-editor/milkdown-react/index.ts +8 -0
  121. package/src/markdown-editor/milkdown-react/types.ts +27 -0
  122. package/src/markdown-editor/milkdown-react/use-editor.ts +27 -0
  123. package/src/markdown-editor/milkdown-react/use-get-editor.ts +56 -0
  124. package/src/markdown-editor/milkdown-react/use-instance.ts +23 -0
  125. package/src/markdown-editor/paste-embed.ts +355 -0
  126. package/src/markdown-editor/slash/brand-slash-commands.test.ts +194 -0
  127. package/src/markdown-editor/slash/brand-slash-commands.ts +324 -0
  128. package/src/markdown-editor/slash/brand-slash-plugin.test.ts +254 -0
  129. package/src/markdown-editor/slash/brand-slash-plugin.ts +385 -0
  130. package/src/markdown-editor/slash/index.ts +107 -0
  131. package/src/markdown-editor/slash/insert-directive.test.ts +263 -0
  132. package/src/markdown-editor/slash/insert-directive.ts +345 -0
  133. package/src/markdown-editor/slash/monaco-slash-menu.test.tsx +198 -0
  134. package/src/markdown-editor/slash/monaco-slash-menu.tsx +356 -0
  135. package/src/markdown-editor/slash/shortcut-monaco.ts +69 -0
  136. package/src/markdown-editor/slash/shortcut.test.ts +157 -0
  137. package/src/markdown-editor/slash/shortcut.ts +57 -0
  138. package/src/markdown-editor/slash/slash-menu.stories.tsx +139 -0
  139. package/src/markdown-editor/slash/slash-menu.tsx +115 -0
  140. package/src/markdown-editor/slash/slash-scroll.test.tsx +52 -0
  141. package/src/markdown-editor/slash/slash-widget.tsx +97 -0
  142. package/src/markdown-editor/slash/source-slash-trigger.test.ts +42 -0
  143. package/src/markdown-editor/slash/source-slash-trigger.ts +46 -0
  144. package/src/markdown-editor/table-view.tsx +249 -0
  145. package/src/markdown-iteration/directive.tsx +92 -0
  146. package/src/markdown-iteration/edit-context.ts +56 -0
  147. package/src/markdown-iteration/index.ts +24 -0
  148. package/src/markdown-iteration/iteration-block.test.tsx +138 -0
  149. package/src/markdown-iteration/iteration-builder-dialog.stories.tsx +293 -0
  150. package/src/markdown-iteration/iteration-builder-dialog.tsx +312 -0
  151. package/src/markdown-iteration/iteration-builder.test.ts +252 -0
  152. package/src/markdown-iteration/iteration-builder.ts +303 -0
  153. package/src/markdown-iteration/iteration.test.ts +56 -0
  154. package/src/markdown-iteration/iteration.tsx +333 -0
  155. package/src/markdown-iteration/template-dialog.stories.tsx +63 -0
  156. package/src/markdown-iteration/template-dialog.test.tsx +104 -0
  157. package/src/markdown-iteration/template-dialog.tsx +116 -0
  158. package/src/markdown-outline/document-outline.stories.tsx +76 -0
  159. package/src/markdown-outline/document-outline.tsx +83 -0
  160. package/src/markdown-outline/index.ts +2 -0
  161. package/src/markdown-outline/markdown-outline.test.ts +49 -0
  162. package/src/markdown-outline/markdown-outline.ts +60 -0
  163. package/src/markdown-preview/code-fence.tsx +193 -0
  164. package/src/markdown-preview/index.ts +8 -0
  165. package/src/markdown-preview/markdown-preview-academic.stories.tsx +203 -0
  166. package/src/markdown-preview/markdown-preview-academic.test.tsx +257 -0
  167. package/src/markdown-preview/markdown-preview-iteration-calc.test.tsx +71 -0
  168. package/src/markdown-preview/markdown-preview-iteration-nested.test.tsx +58 -0
  169. package/src/markdown-preview/markdown-preview-iteration.stories.tsx +187 -0
  170. package/src/markdown-preview/markdown-preview-iteration.test.tsx +90 -0
  171. package/src/markdown-preview/markdown-preview-linking.test.tsx +61 -0
  172. package/src/markdown-preview/markdown-preview-transclusion.test.tsx +33 -0
  173. package/src/markdown-preview/markdown-preview.stories.tsx +108 -0
  174. package/src/markdown-preview/markdown-preview.test.tsx +411 -0
  175. package/src/markdown-preview/markdown-preview.tsx +1541 -0
  176. package/src/markdown-toolbar/index.ts +8 -0
  177. package/src/markdown-toolbar/markdown-commands.test.ts +84 -0
  178. package/src/markdown-toolbar/markdown-commands.ts +104 -0
  179. package/src/markdown-toolbar/markdown-toolbar.stories.tsx +56 -0
  180. package/src/markdown-toolbar/markdown-toolbar.tsx +263 -0
  181. package/src/markdown-workspace/focus-writing.test.ts +52 -0
  182. package/src/markdown-workspace/focus-writing.ts +38 -0
  183. package/src/markdown-workspace/index.ts +6 -0
  184. package/src/markdown-workspace/markdown-workspace.stories.tsx +688 -0
  185. package/src/markdown-workspace/markdown-workspace.test.tsx +326 -0
  186. package/src/markdown-workspace/markdown-workspace.tsx +787 -0
  187. package/src/mermaid-diagram/index.ts +1 -0
  188. package/src/mermaid-diagram/mermaid-diagram.stories.tsx +42 -0
  189. package/src/mermaid-diagram/mermaid-diagram.test.tsx +86 -0
  190. package/src/mermaid-diagram/mermaid-diagram.tsx +318 -0
  191. package/src/mermaid-diagram/mermaid-viewer.test.tsx +89 -0
  192. package/src/mermaid-diagram/mermaid-viewer.tsx +367 -0
  193. package/src/mermaid-diagram/real-parse.test.ts +42 -0
  194. package/src/mermaid-diagram/remediate.test.ts +60 -0
  195. package/src/mermaid-diagram/remediate.ts +124 -0
  196. package/src/mermaid-workspace/index.ts +1 -0
  197. package/src/mermaid-workspace/mermaid-workspace.stories.tsx +28 -0
  198. package/src/mermaid-workspace/mermaid-workspace.tsx +71 -0
  199. package/src/metric-block/index.ts +1 -0
  200. package/src/metric-block/metric-block.stories.tsx +40 -0
  201. package/src/metric-block/metric-block.tsx +14 -0
  202. package/src/monaco-workers.d.ts +10 -0
  203. package/src/prose/index.ts +14 -0
  204. package/src/prose/prose.stories.tsx +44 -0
  205. package/src/prose/prose.test.tsx +44 -0
  206. package/src/prose/prose.tsx +21 -0
  207. package/src/timeline/index.ts +18 -0
@@ -0,0 +1,297 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+
3
+ import { CalcBlock } from "./calc-block";
4
+ import type { CalcSheet, CalcTokenKind, EvaluateCalc } from "./types";
5
+
6
+ const meta = {
7
+ title: "Editor/CalcBlock",
8
+ component: CalcBlock,
9
+ tags: ["autodocs"],
10
+ parameters: { layout: "padded" },
11
+ } satisfies Meta<typeof CalcBlock>;
12
+
13
+ export default meta;
14
+ type Story = StoryObj<typeof meta>;
15
+
16
+ function tok(start: number, end: number, kind: CalcTokenKind, resolved = true) {
17
+ return { start, end, kind, resolved };
18
+ }
19
+
20
+ // A hand-authored sheet stands in for the consumer's real evaluator (the math
21
+ // engine is app/domain logic and lives outside the design system).
22
+ const SOURCE = [
23
+ "# Lisbon trip",
24
+ "flights = 320 EUR",
25
+ "hotel = 95 EUR * 4",
26
+ "total = flights + hotel",
27
+ "tax = 25% of total // estimate",
28
+ "mystery = foo + 1",
29
+ ].join("\n");
30
+
31
+ const SHEET: CalcSheet = {
32
+ results: [
33
+ { line: 1, tokens: [tok(0, 13, "comment")] },
34
+ {
35
+ line: 2,
36
+ tokens: [tok(0, 7, "var-def"), tok(10, 13, "number"), tok(14, 17, "currency")],
37
+ value: { kind: "currency", raw: 320, unit: "EUR", display: "€320.00" },
38
+ },
39
+ {
40
+ line: 3,
41
+ tokens: [
42
+ tok(0, 5, "var-def"),
43
+ tok(8, 10, "number"),
44
+ tok(11, 14, "currency"),
45
+ tok(15, 16, "operator"),
46
+ tok(17, 18, "number"),
47
+ ],
48
+ value: { kind: "currency", raw: 380, unit: "EUR", display: "€380.00" },
49
+ },
50
+ {
51
+ line: 4,
52
+ tokens: [
53
+ tok(0, 5, "var-def"),
54
+ tok(8, 15, "var-ref"),
55
+ tok(16, 17, "operator"),
56
+ tok(18, 23, "var-ref"),
57
+ ],
58
+ value: { kind: "currency", raw: 700, unit: "EUR", display: "€700.00" },
59
+ },
60
+ {
61
+ line: 5,
62
+ tokens: [
63
+ tok(0, 3, "var-def"),
64
+ tok(6, 8, "number"),
65
+ tok(8, 9, "operator"),
66
+ tok(13, 18, "var-ref"),
67
+ tok(19, 29, "comment"),
68
+ ],
69
+ value: { kind: "currency", raw: 175, unit: "EUR", display: "€175.00" },
70
+ },
71
+ {
72
+ line: 6,
73
+ tokens: [
74
+ tok(0, 7, "var-def"),
75
+ tok(10, 13, "unknown", false),
76
+ tok(14, 15, "operator"),
77
+ tok(16, 17, "number"),
78
+ ],
79
+ error: { message: 'Undefined symbol "foo"' },
80
+ },
81
+ ],
82
+ total: { kind: "currency", raw: 1575, unit: "EUR", display: "€1,575.00" },
83
+ };
84
+
85
+ const evaluate: EvaluateCalc = () => SHEET;
86
+
87
+ /** The leading `# Lisbon trip` is lifted into the block's title bar. */
88
+ export const Default: Story = {
89
+ args: { source: SOURCE, evaluate },
90
+ };
91
+
92
+ /** With no `# Heading` line, the header falls back to the neutral `calc` label. */
93
+ const UNTITLED_SOURCE = ["flights = 320 EUR", "hotel = 95 EUR * 4", "total = flights + hotel"].join(
94
+ "\n",
95
+ );
96
+ const UNTITLED_SHEET: CalcSheet = {
97
+ results: [
98
+ {
99
+ line: 1,
100
+ tokens: [tok(0, 7, "var-def"), tok(10, 13, "number"), tok(14, 17, "currency")],
101
+ value: { kind: "currency", raw: 320, unit: "EUR", display: "€320.00" },
102
+ },
103
+ {
104
+ line: 2,
105
+ tokens: [tok(0, 5, "var-def"), tok(8, 10, "number"), tok(11, 14, "currency")],
106
+ value: { kind: "currency", raw: 380, unit: "EUR", display: "€380.00" },
107
+ },
108
+ {
109
+ line: 3,
110
+ tokens: [tok(0, 5, "var-def"), tok(8, 15, "var-ref"), tok(18, 23, "var-ref")],
111
+ value: { kind: "currency", raw: 700, unit: "EUR", display: "€700.00" },
112
+ },
113
+ ],
114
+ total: { kind: "currency", raw: 700, unit: "EUR", display: "€700.00" },
115
+ };
116
+
117
+ export const Untitled: Story = {
118
+ args: { source: UNTITLED_SOURCE, evaluate: () => UNTITLED_SHEET },
119
+ };
120
+
121
+ export const WithoutTotal: Story = {
122
+ args: { source: SOURCE, evaluate, showTotal: false },
123
+ };
124
+
125
+ // ── Row rules (dividers) ─────────────────────────────────────────────────────
126
+ // A line-item ledger: a single rule separates the items from the subtotal, a
127
+ // dotted rule sets off the adjustment, and a double rule precedes the total.
128
+ const RULES_SOURCE = [
129
+ "# Invoice 0042",
130
+ "design = 1,200 USD",
131
+ "build = 3,400 USD",
132
+ "subtotal = design + build",
133
+ "discount = 10% of subtotal",
134
+ "total = subtotal - discount",
135
+ ].join("\n");
136
+
137
+ const RULES_SHEET: CalcSheet = {
138
+ results: [
139
+ { line: 1, tokens: [tok(0, 14, "comment")] },
140
+ {
141
+ line: 2,
142
+ tokens: [tok(0, 6, "var-def"), tok(9, 14, "number"), tok(15, 18, "currency")],
143
+ value: { kind: "currency", raw: 1200, unit: "USD", display: "$1,200.00" },
144
+ },
145
+ {
146
+ line: 3,
147
+ tokens: [tok(0, 5, "var-def"), tok(8, 13, "number"), tok(14, 17, "currency")],
148
+ value: { kind: "currency", raw: 3400, unit: "USD", display: "$3,400.00" },
149
+ rule: "single",
150
+ },
151
+ {
152
+ line: 4,
153
+ tokens: [
154
+ tok(0, 8, "var-def"),
155
+ tok(11, 17, "var-ref"),
156
+ tok(18, 19, "operator"),
157
+ tok(20, 25, "var-ref"),
158
+ ],
159
+ value: { kind: "currency", raw: 4600, unit: "USD", display: "$4,600.00" },
160
+ },
161
+ {
162
+ line: 5,
163
+ tokens: [tok(0, 8, "var-def"), tok(11, 14, "number"), tok(18, 26, "var-ref")],
164
+ value: { kind: "currency", raw: -460, unit: "USD", display: "−$460.00" },
165
+ rule: "double",
166
+ },
167
+ {
168
+ line: 6,
169
+ tokens: [
170
+ tok(0, 5, "var-def"),
171
+ tok(8, 16, "var-ref"),
172
+ tok(17, 18, "operator"),
173
+ tok(19, 27, "var-ref"),
174
+ ],
175
+ value: { kind: "currency", raw: 4140, unit: "USD", display: "$4,140.00" },
176
+ },
177
+ ],
178
+ total: { kind: "currency", raw: 4140, unit: "USD", display: "$4,140.00" },
179
+ };
180
+
181
+ export const Rules: Story = {
182
+ args: { source: RULES_SOURCE, evaluate: () => RULES_SHEET, showTotal: false },
183
+ };
184
+
185
+ // ── Row tints ────────────────────────────────────────────────────────────────
186
+ // Semantic status washes flag rows at a glance: on-track (success), watch
187
+ // (warning), over budget (destructive), and a neutral (muted) note row.
188
+ const TINTS_SOURCE = [
189
+ "# Q3 budget",
190
+ "marketing = 12,000 USD",
191
+ "travel = 8,000 USD",
192
+ "overage = 3,000 USD",
193
+ "headcount = 5",
194
+ ].join("\n");
195
+
196
+ const TINTS_SHEET: CalcSheet = {
197
+ results: [
198
+ { line: 1, tokens: [tok(0, 11, "comment")] },
199
+ {
200
+ line: 2,
201
+ tokens: [tok(0, 9, "var-def"), tok(12, 18, "number"), tok(19, 22, "currency")],
202
+ value: { kind: "currency", raw: 12000, unit: "USD", display: "$12,000.00" },
203
+ tint: "success",
204
+ },
205
+ {
206
+ line: 3,
207
+ tokens: [tok(0, 6, "var-def"), tok(9, 14, "number"), tok(15, 18, "currency")],
208
+ value: { kind: "currency", raw: 8000, unit: "USD", display: "$8,000.00" },
209
+ tint: "warning",
210
+ },
211
+ {
212
+ line: 4,
213
+ tokens: [tok(0, 7, "var-def"), tok(10, 15, "number"), tok(16, 19, "currency")],
214
+ value: { kind: "currency", raw: 3000, unit: "USD", display: "$3,000.00" },
215
+ tint: "destructive",
216
+ },
217
+ {
218
+ line: 5,
219
+ tokens: [tok(0, 9, "var-def"), tok(12, 13, "number")],
220
+ value: { kind: "number", raw: 5, display: "5" },
221
+ tint: "muted",
222
+ },
223
+ ],
224
+ total: { kind: "currency", raw: 23000, unit: "USD", display: "$23,000.00" },
225
+ };
226
+
227
+ export const Tints: Story = {
228
+ args: { source: TINTS_SOURCE, evaluate: () => TINTS_SHEET },
229
+ };
230
+
231
+ // ── Author markers (what a writer types) ─────────────────────────────────────
232
+ // The same tints/rules as above, but driven by trailing `@markers` the WRITER
233
+ // types in the calc text — CalcBlock strips them before `evaluate` runs, so this
234
+ // evaluator is marker-agnostic and sets NO tint/rule fields itself.
235
+ const AUTHORED_SOURCE = [
236
+ "# Q3 budget",
237
+ "marketing = 12,000 USD @success",
238
+ "travel = 8,000 USD @warning",
239
+ "overage = 3,000 USD @danger",
240
+ "subtotal = marketing + travel + overage @line",
241
+ "note: review with finance @muted",
242
+ ].join("\n");
243
+
244
+ // Token columns are relative to the marker-stripped text CalcBlock passes here.
245
+ const AUTHORED_SHEET: CalcSheet = {
246
+ results: [
247
+ { line: 1, tokens: [tok(0, 11, "comment")] },
248
+ {
249
+ line: 2,
250
+ tokens: [tok(0, 9, "var-def"), tok(12, 18, "number"), tok(19, 22, "currency")],
251
+ value: { kind: "currency", raw: 12000, unit: "USD", display: "$12,000.00" },
252
+ },
253
+ {
254
+ line: 3,
255
+ tokens: [tok(0, 6, "var-def"), tok(9, 14, "number"), tok(15, 18, "currency")],
256
+ value: { kind: "currency", raw: 8000, unit: "USD", display: "$8,000.00" },
257
+ },
258
+ {
259
+ line: 4,
260
+ tokens: [tok(0, 7, "var-def"), tok(10, 15, "number"), tok(16, 19, "currency")],
261
+ value: { kind: "currency", raw: 3000, unit: "USD", display: "$3,000.00" },
262
+ },
263
+ {
264
+ line: 5,
265
+ tokens: [
266
+ tok(0, 8, "var-def"),
267
+ tok(11, 20, "var-ref"),
268
+ tok(23, 29, "var-ref"),
269
+ tok(32, 39, "var-ref"),
270
+ ],
271
+ value: { kind: "currency", raw: 23000, unit: "USD", display: "$23,000.00" },
272
+ },
273
+ { line: 6, tokens: [tok(0, 4, "var-def"), tok(6, 25, "comment")] },
274
+ ],
275
+ total: { kind: "currency", raw: 23000, unit: "USD", display: "$23,000.00" },
276
+ };
277
+
278
+ export const AuthoredMarkers: Story = {
279
+ args: { source: AUTHORED_SOURCE, evaluate: () => AUTHORED_SHEET },
280
+ };
281
+
282
+ /**
283
+ * Override the footer total and its label — e.g. show a budget remaining the
284
+ * evaluator surfaced separately, rather than the auto running total.
285
+ */
286
+ export const TotalOverride: Story = {
287
+ args: {
288
+ source: SOURCE,
289
+ evaluate,
290
+ totalLabel: "Budget left",
291
+ total: { kind: "currency", raw: 425, unit: "EUR", display: "€425.00" },
292
+ },
293
+ };
294
+
295
+ export const Empty: Story = {
296
+ args: { source: "", evaluate: () => ({ results: [] }) },
297
+ };
@@ -0,0 +1,164 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, expect, it } from "vitest";
3
+
4
+ import { CalcBlock } from "./calc-block";
5
+ import type { CalcSheet, EvaluateCalc } from "./types";
6
+
7
+ const SHEET: CalcSheet = {
8
+ results: [
9
+ {
10
+ line: 1,
11
+ tokens: [{ start: 0, end: 5, kind: "var-def", resolved: true }],
12
+ value: { kind: "number", raw: 800, display: "800" },
13
+ },
14
+ {
15
+ line: 2,
16
+ tokens: [{ start: 0, end: 4, kind: "unknown", resolved: false }],
17
+ error: { message: "unknown unit" },
18
+ },
19
+ ],
20
+ total: { kind: "number", raw: 1000, display: "1,000" },
21
+ };
22
+
23
+ // No TooltipProvider on purpose: CalcBlock must render standalone (it uses a
24
+ // native `title`, not a Radix Tooltip) — including its inline error marker —
25
+ // since it mounts inside MarkdownPreview / a story with no provider ancestor.
26
+ function renderBlock(
27
+ source: string,
28
+ evaluate: EvaluateCalc,
29
+ props?: Partial<{ showTotal: boolean }>,
30
+ ) {
31
+ return render(<CalcBlock source={source} evaluate={evaluate} showTotal {...props} />);
32
+ }
33
+
34
+ describe("CalcBlock", () => {
35
+ it("renders the header, the result value, the total, and a per-line error", () => {
36
+ renderBlock("gross = 800\noops", () => SHEET);
37
+ expect(screen.getByText("calc")).toBeInTheDocument();
38
+ expect(screen.getByText("800")).toBeInTheDocument(); // the value cell
39
+ expect(screen.getByText("Total")).toBeInTheDocument();
40
+ expect(screen.getByText("1,000")).toBeInTheDocument();
41
+ expect(screen.getByLabelText(/^Error:/)).toBeInTheDocument();
42
+ });
43
+
44
+ it("tags each token span with its brand-calc-tok role class (the #226 non-hue cue hook)", () => {
45
+ const { container } = renderBlock("gross = 800\noops", () => SHEET);
46
+ // The role classes are what the low-chroma cues (weight/underline) hang off,
47
+ // so both the editor decorations and the rendered block read identically.
48
+ expect(container.querySelector(".brand-calc-tok.brand-calc-tok--var-def")).toBeInTheDocument();
49
+ expect(container.querySelector(".brand-calc-tok.brand-calc-tok--unknown")).toBeInTheDocument();
50
+ });
51
+
52
+ it("does not bundle a math engine — values come only from the evaluate hook", () => {
53
+ let called = "";
54
+ renderBlock("anything", (src) => {
55
+ called = src;
56
+ return { results: [] };
57
+ });
58
+ expect(called).toBe("anything");
59
+ });
60
+
61
+ it("omits the total footer when showTotal is false", () => {
62
+ renderBlock("gross = 800", () => SHEET, { showTotal: false });
63
+ expect(screen.queryByText("Total")).not.toBeInTheDocument();
64
+ });
65
+
66
+ it("renders an empty calc block gracefully", () => {
67
+ renderBlock("", () => ({ results: [] }));
68
+ expect(screen.getByText("Empty calc block.")).toBeInTheDocument();
69
+ });
70
+
71
+ it("lifts a leading `# Heading` into the title bar and drops it from the body", () => {
72
+ const sheet: CalcSheet = {
73
+ results: [
74
+ { line: 1, tokens: [] },
75
+ { line: 2, tokens: [], value: { kind: "number", raw: 1, display: "1" } },
76
+ ],
77
+ };
78
+ const { container } = render(
79
+ <CalcBlock source={"# Lisbon trip\nx = 1"} evaluate={() => sheet} />,
80
+ );
81
+ // Title shown once (header), not duplicated as a body heading row, and the
82
+ // neutral `calc` label is gone.
83
+ expect(screen.getAllByText("Lisbon trip")).toHaveLength(1);
84
+ expect(screen.queryByText("calc")).not.toBeInTheDocument();
85
+ // The group is labelled by the title.
86
+ expect(container.querySelector('[role="group"]')).toHaveAccessibleName("Lisbon trip");
87
+ });
88
+
89
+ it("lets the `title` prop override the displayed title", () => {
90
+ const sheet: CalcSheet = { results: [{ line: 1, tokens: [] }] };
91
+ render(<CalcBlock source={"x = 1"} title="Receipt" evaluate={() => sheet} />);
92
+ expect(screen.getByText("Receipt")).toBeInTheDocument();
93
+ expect(screen.queryByText("calc")).not.toBeInTheDocument();
94
+ });
95
+
96
+ it("marks rows with a rule (divider) and a tint via data attributes", () => {
97
+ const sheet: CalcSheet = {
98
+ results: [
99
+ { line: 1, tokens: [], value: { kind: "number", raw: 1, display: "1" }, rule: "double" },
100
+ { line: 2, tokens: [], value: { kind: "number", raw: 2, display: "2" }, tint: "success" },
101
+ ],
102
+ };
103
+ const { container } = render(<CalcBlock source={"a = 1\nb = 2"} evaluate={() => sheet} />);
104
+ expect(container.querySelector('[data-rule="double"]')).toBeInTheDocument();
105
+ expect(container.querySelector('[data-tint="success"]')).toBeInTheDocument();
106
+ });
107
+
108
+ it("applies trailing author markers, strips them from the text, and hides them from evaluate", () => {
109
+ let seen = "";
110
+ const sheet: CalcSheet = {
111
+ results: [
112
+ { line: 1, tokens: [], value: { kind: "number", raw: 1, display: "1" } },
113
+ { line: 2, tokens: [], value: { kind: "number", raw: 2, display: "2" } },
114
+ ],
115
+ };
116
+ const { container } = render(
117
+ <CalcBlock
118
+ source={"a = 1 @success @line\nb = 2 @danger"}
119
+ evaluate={(src) => {
120
+ seen = src;
121
+ return sheet;
122
+ }}
123
+ />,
124
+ );
125
+ // The evaluator never sees the markers (cleaned source).
126
+ expect(seen).toBe("a = 1\nb = 2");
127
+ // Markers are stripped from the rendered rows…
128
+ expect(screen.queryByText(/@success|@line|@danger/)).not.toBeInTheDocument();
129
+ // …and applied as tint/rule (note `@danger` → destructive).
130
+ expect(container.querySelector('[data-rule="single"]')).toBeInTheDocument();
131
+ expect(container.querySelector('[data-tint="success"]')).toBeInTheDocument();
132
+ expect(container.querySelector('[data-tint="destructive"]')).toBeInTheDocument();
133
+ });
134
+
135
+ it("leaves an unrecognized trailing @word as literal text", () => {
136
+ const sheet: CalcSheet = { results: [{ line: 1, tokens: [] }] };
137
+ render(<CalcBlock source={"ping = host @bogus"} evaluate={() => sheet} />);
138
+ expect(screen.getByText(/@bogus/)).toBeInTheDocument();
139
+ });
140
+
141
+ it("does not parse markers when markers={false}", () => {
142
+ const sheet: CalcSheet = { results: [{ line: 1, tokens: [] }] };
143
+ const { container } = render(
144
+ <CalcBlock source={"a = 1 @success"} markers={false} evaluate={() => sheet} />,
145
+ );
146
+ expect(screen.getByText(/@success/)).toBeInTheDocument();
147
+ expect(container.querySelector("[data-tint]")).not.toBeInTheDocument();
148
+ });
149
+
150
+ it("overrides the footer total value and label", () => {
151
+ render(
152
+ <CalcBlock
153
+ source={"gross = 800"}
154
+ evaluate={() => SHEET}
155
+ totalLabel="Budget left"
156
+ total={{ kind: "number", raw: 425, display: "425" }}
157
+ />,
158
+ );
159
+ expect(screen.getByText("Budget left")).toBeInTheDocument();
160
+ expect(screen.getByText("425")).toBeInTheDocument();
161
+ // The overridden total replaces the sheet's computed total.
162
+ expect(screen.queryByText("1,000")).not.toBeInTheDocument();
163
+ });
164
+ });