@columenlabs/lumen-ui 0.0.0-stage → 0.2.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 (297) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +12 -0
  3. package/README.md +4 -2
  4. package/dist/add-column.d.ts +23 -0
  5. package/dist/add-column.js +62 -0
  6. package/dist/api-fetch.d.ts +9 -0
  7. package/dist/api-fetch.js +60 -0
  8. package/dist/app-switcher.d.ts +21 -0
  9. package/dist/app-switcher.js +12 -0
  10. package/dist/app-views.d.ts +18 -0
  11. package/dist/app-views.js +41 -0
  12. package/dist/assistant-context.d.ts +72 -0
  13. package/dist/assistant-context.js +30 -0
  14. package/dist/assistant-panel.d.ts +58 -0
  15. package/dist/assistant-panel.js +244 -0
  16. package/dist/assistant-screen.d.ts +124 -0
  17. package/dist/assistant-screen.js +252 -0
  18. package/dist/card-switch.d.ts +16 -0
  19. package/dist/card-switch.js +16 -0
  20. package/dist/color-dot.d.ts +14 -0
  21. package/dist/color-dot.js +19 -0
  22. package/dist/confirm.d.ts +18 -0
  23. package/dist/confirm.js +5 -0
  24. package/dist/connections.d.ts +45 -0
  25. package/dist/connections.js +275 -0
  26. package/dist/coverage-calendar-map.d.ts +27 -0
  27. package/dist/coverage-calendar-map.js +84 -0
  28. package/dist/coverage-cells.d.ts +55 -0
  29. package/dist/coverage-cells.js +85 -0
  30. package/dist/coverage-map.d.ts +112 -0
  31. package/dist/coverage-map.js +428 -0
  32. package/dist/details-table.d.ts +20 -0
  33. package/dist/details-table.js +13 -0
  34. package/dist/drawer-section.d.ts +27 -0
  35. package/dist/drawer-section.js +23 -0
  36. package/dist/floating-head.d.ts +2 -0
  37. package/dist/floating-head.js +57 -0
  38. package/dist/fold.d.ts +16 -0
  39. package/dist/fold.js +70 -0
  40. package/dist/form-issues.d.ts +32 -0
  41. package/dist/form-issues.js +60 -0
  42. package/dist/formula-input.d.ts +17 -0
  43. package/dist/formula-input.js +104 -0
  44. package/dist/frame-script.d.ts +7 -0
  45. package/dist/frame-script.js +103 -0
  46. package/dist/freshness.d.ts +24 -0
  47. package/dist/freshness.js +99 -0
  48. package/dist/group-button.d.ts +33 -0
  49. package/dist/group-button.js +67 -0
  50. package/dist/growth-chart.d.ts +77 -0
  51. package/dist/growth-chart.js +195 -0
  52. package/dist/hooks/use-keyboard-fit.d.ts +15 -0
  53. package/dist/hooks/use-keyboard-fit.js +29 -0
  54. package/dist/hooks/use-mobile.d.ts +1 -0
  55. package/dist/hooks/use-mobile.js +15 -0
  56. package/dist/icon-button.d.ts +21 -0
  57. package/dist/icon-button.js +22 -0
  58. package/dist/icon-picker.d.ts +14 -0
  59. package/dist/icon-picker.js +36 -0
  60. package/dist/index.d.ts +83 -0
  61. package/dist/index.js +90 -0
  62. package/dist/integrations.d.ts +57 -0
  63. package/dist/integrations.js +73 -0
  64. package/dist/joins.d.ts +128 -0
  65. package/dist/joins.js +767 -0
  66. package/dist/kit-config.d.ts +13 -0
  67. package/dist/kit-config.js +15 -0
  68. package/dist/lib/utils.d.ts +2 -0
  69. package/dist/lib/utils.js +5 -0
  70. package/dist/lumen-boundary.d.ts +7 -0
  71. package/dist/lumen-boundary.js +10 -0
  72. package/dist/lumen-code-editor-impl.d.ts +2 -0
  73. package/dist/lumen-code-editor-impl.js +16 -0
  74. package/dist/lumen-code-editor.d.ts +11 -0
  75. package/dist/lumen-code-editor.js +11 -0
  76. package/dist/lumen-code.d.ts +22 -0
  77. package/dist/lumen-code.js +75 -0
  78. package/dist/lumen-drawer.d.ts +30 -0
  79. package/dist/lumen-drawer.js +27 -0
  80. package/dist/lumen-echart.d.ts +58 -0
  81. package/dist/lumen-echart.js +129 -0
  82. package/dist/lumen-filter.d.ts +75 -0
  83. package/dist/lumen-filter.js +546 -0
  84. package/dist/lumen-finder.d.ts +62 -0
  85. package/dist/lumen-finder.js +275 -0
  86. package/dist/lumen-form.d.ts +34 -0
  87. package/dist/lumen-form.js +81 -0
  88. package/dist/lumen-geo-map.d.ts +9 -0
  89. package/dist/lumen-geo-map.js +135 -0
  90. package/dist/lumen-grid.d.ts +49 -0
  91. package/dist/lumen-grid.js +203 -0
  92. package/dist/lumen-layout.d.ts +150 -0
  93. package/dist/lumen-layout.js +658 -0
  94. package/dist/lumen-links.d.ts +96 -0
  95. package/dist/lumen-links.js +213 -0
  96. package/dist/lumen-page-state.d.ts +20 -0
  97. package/dist/lumen-page-state.js +15 -0
  98. package/dist/lumen-selector.d.ts +139 -0
  99. package/dist/lumen-selector.js +308 -0
  100. package/dist/lumen-source.d.ts +137 -0
  101. package/dist/lumen-source.js +327 -0
  102. package/dist/lumen-tab-drawer.d.ts +27 -0
  103. package/dist/lumen-tab-drawer.js +26 -0
  104. package/dist/lumen-tabs.d.ts +21 -0
  105. package/dist/lumen-tabs.js +6 -0
  106. package/dist/lumen-toast.d.ts +11 -0
  107. package/dist/lumen-toast.js +16 -0
  108. package/dist/lumen-trend.d.ts +18 -0
  109. package/dist/lumen-trend.js +157 -0
  110. package/dist/lumen-view-card.d.ts +35 -0
  111. package/dist/lumen-view-card.js +50 -0
  112. package/dist/lumen-view.d.ts +172 -0
  113. package/dist/lumen-view.js +645 -0
  114. package/dist/lumen-widget-charts.d.ts +57 -0
  115. package/dist/lumen-widget-charts.js +69 -0
  116. package/dist/lumen-widget.d.ts +614 -0
  117. package/dist/lumen-widget.js +594 -0
  118. package/dist/market/compare-chart.d.ts +34 -0
  119. package/dist/market/compare-chart.js +292 -0
  120. package/dist/market/indicator-picker.d.ts +29 -0
  121. package/dist/market/indicator-picker.js +125 -0
  122. package/dist/market/indicators.d.ts +63 -0
  123. package/dist/market/indicators.js +314 -0
  124. package/dist/market/live.d.ts +120 -0
  125. package/dist/market/live.js +444 -0
  126. package/dist/market/lumen-chart.d.ts +88 -0
  127. package/dist/market/lumen-chart.js +645 -0
  128. package/dist/market/market-widget.d.ts +45 -0
  129. package/dist/market/market-widget.js +346 -0
  130. package/dist/market/option-chain.d.ts +19 -0
  131. package/dist/market/option-chain.js +53 -0
  132. package/dist/market/price-chart.d.ts +128 -0
  133. package/dist/market/price-chart.js +619 -0
  134. package/dist/mcp-settings.d.ts +20 -0
  135. package/dist/mcp-settings.js +65 -0
  136. package/dist/nav.d.ts +77 -0
  137. package/dist/nav.js +73 -0
  138. package/dist/note-line.d.ts +13 -0
  139. package/dist/note-line.js +5 -0
  140. package/dist/notifications.d.ts +9 -0
  141. package/dist/notifications.js +166 -0
  142. package/dist/origin-legend.d.ts +14 -0
  143. package/dist/origin-legend.js +38 -0
  144. package/dist/overview.d.ts +84 -0
  145. package/dist/overview.js +105 -0
  146. package/dist/page-inputs.d.ts +14 -0
  147. package/dist/page-inputs.js +16 -0
  148. package/dist/page.d.ts +32 -0
  149. package/dist/page.js +45 -0
  150. package/dist/pager.d.ts +10 -0
  151. package/dist/pager.js +14 -0
  152. package/dist/prefs.d.ts +71 -0
  153. package/dist/prefs.js +126 -0
  154. package/dist/query-client.d.ts +7 -0
  155. package/dist/query-client.js +19 -0
  156. package/dist/refresh.d.ts +4 -0
  157. package/dist/refresh.js +19 -0
  158. package/dist/rename-field.d.ts +11 -0
  159. package/dist/rename-field.js +26 -0
  160. package/dist/reset-button.d.ts +9 -0
  161. package/dist/reset-button.js +10 -0
  162. package/dist/route-error.d.ts +42 -0
  163. package/dist/route-error.js +229 -0
  164. package/dist/schedules.d.ts +88 -0
  165. package/dist/schedules.js +221 -0
  166. package/dist/settings.d.ts +51 -0
  167. package/dist/settings.js +181 -0
  168. package/dist/shell.d.ts +99 -0
  169. package/dist/shell.js +137 -0
  170. package/dist/source-health.d.ts +12 -0
  171. package/dist/source-health.js +58 -0
  172. package/dist/spotlight.d.ts +23 -0
  173. package/dist/spotlight.js +138 -0
  174. package/dist/status-health.d.ts +16 -0
  175. package/dist/status-health.js +41 -0
  176. package/dist/status.d.ts +9 -0
  177. package/dist/status.js +12 -0
  178. package/dist/summary-cards.d.ts +56 -0
  179. package/dist/summary-cards.js +270 -0
  180. package/dist/symbol-sheet-context.d.ts +4 -0
  181. package/dist/symbol-sheet-context.js +5 -0
  182. package/dist/symbol-sheet.d.ts +9 -0
  183. package/dist/symbol-sheet.js +24 -0
  184. package/dist/tasks.d.ts +29 -0
  185. package/dist/tasks.js +278 -0
  186. package/dist/theme.css +606 -0
  187. package/dist/tile.d.ts +37 -0
  188. package/dist/tile.js +23 -0
  189. package/dist/ui/alert-dialog.d.ts +18 -0
  190. package/dist/ui/alert-dialog.js +41 -0
  191. package/dist/ui/alert.d.ts +10 -0
  192. package/dist/ui/alert.js +27 -0
  193. package/dist/ui/avatar.d.ts +11 -0
  194. package/dist/ui/avatar.js +22 -0
  195. package/dist/ui/badge.d.ts +9 -0
  196. package/dist/ui/badge.js +27 -0
  197. package/dist/ui/breadcrumb.d.ts +11 -0
  198. package/dist/ui/breadcrumb.js +27 -0
  199. package/dist/ui/button-group.d.ts +11 -0
  200. package/dist/ui/button-group.js +27 -0
  201. package/dist/ui/button.d.ts +10 -0
  202. package/dist/ui/button.js +38 -0
  203. package/dist/ui/card.d.ts +11 -0
  204. package/dist/ui/card.js +24 -0
  205. package/dist/ui/chart.d.ts +44 -0
  206. package/dist/ui/chart.js +117 -0
  207. package/dist/ui/checkbox.d.ts +4 -0
  208. package/dist/ui/checkbox.js +8 -0
  209. package/dist/ui/collapsible.d.ts +5 -0
  210. package/dist/ui/collapsible.js +12 -0
  211. package/dist/ui/combobox.d.ts +24 -0
  212. package/dist/ui/combobox.js +60 -0
  213. package/dist/ui/command.d.ts +18 -0
  214. package/dist/ui/command.js +34 -0
  215. package/dist/ui/dialog.d.ts +23 -0
  216. package/dist/ui/dialog.js +50 -0
  217. package/dist/ui/dropdown-menu.d.ts +29 -0
  218. package/dist/ui/dropdown-menu.js +50 -0
  219. package/dist/ui/emoji-picker.d.ts +6 -0
  220. package/dist/ui/emoji-picker.js +28 -0
  221. package/dist/ui/empty.d.ts +11 -0
  222. package/dist/ui/empty.js +33 -0
  223. package/dist/ui/field.d.ts +24 -0
  224. package/dist/ui/field.js +65 -0
  225. package/dist/ui/hover-card.d.ts +6 -0
  226. package/dist/ui/hover-card.js +13 -0
  227. package/dist/ui/input-group.d.ts +16 -0
  228. package/dist/ui/input-group.js +56 -0
  229. package/dist/ui/input.d.ts +3 -0
  230. package/dist/ui/input.js +6 -0
  231. package/dist/ui/item.d.ts +23 -0
  232. package/dist/ui/item.js +67 -0
  233. package/dist/ui/kbd.d.ts +3 -0
  234. package/dist/ui/kbd.js +9 -0
  235. package/dist/ui/label.d.ts +4 -0
  236. package/dist/ui/label.js +7 -0
  237. package/dist/ui/popover.d.ts +10 -0
  238. package/dist/ui/popover.js +25 -0
  239. package/dist/ui/progress.d.ts +4 -0
  240. package/dist/ui/progress.js +9 -0
  241. package/dist/ui/resizable.d.ts +7 -0
  242. package/dist/ui/resizable.js +13 -0
  243. package/dist/ui/scroll-area.d.ts +5 -0
  244. package/dist/ui/scroll-area.js +10 -0
  245. package/dist/ui/select.d.ts +15 -0
  246. package/dist/ui/select.js +36 -0
  247. package/dist/ui/separator.d.ts +4 -0
  248. package/dist/ui/separator.js +7 -0
  249. package/dist/ui/sheet.d.ts +14 -0
  250. package/dist/ui/sheet.js +54 -0
  251. package/dist/ui/sidebar.d.ts +69 -0
  252. package/dist/ui/sidebar.js +206 -0
  253. package/dist/ui/skeleton.d.ts +2 -0
  254. package/dist/ui/skeleton.js +6 -0
  255. package/dist/ui/slider.d.ts +4 -0
  256. package/dist/ui/slider.js +9 -0
  257. package/dist/ui/sonner.d.ts +3 -0
  258. package/dist/ui/sonner.js +24 -0
  259. package/dist/ui/spinner.d.ts +2 -0
  260. package/dist/ui/spinner.js +7 -0
  261. package/dist/ui/switch.d.ts +6 -0
  262. package/dist/ui/switch.js +7 -0
  263. package/dist/ui/table.d.ts +10 -0
  264. package/dist/ui/table.js +31 -0
  265. package/dist/ui/tabs.d.ts +7 -0
  266. package/dist/ui/tabs.js +22 -0
  267. package/dist/ui/textarea.d.ts +3 -0
  268. package/dist/ui/textarea.js +6 -0
  269. package/dist/ui/toggle-group.d.ts +10 -0
  270. package/dist/ui/toggle-group.js +22 -0
  271. package/dist/ui/toggle.d.ts +9 -0
  272. package/dist/ui/toggle.js +25 -0
  273. package/dist/ui/tooltip.d.ts +7 -0
  274. package/dist/ui/tooltip.js +16 -0
  275. package/dist/view-edit-cell.d.ts +49 -0
  276. package/dist/view-edit-cell.js +198 -0
  277. package/dist/view-editor.d.ts +52 -0
  278. package/dist/view-editor.js +940 -0
  279. package/dist/view-formulas.d.ts +61 -0
  280. package/dist/view-formulas.js +247 -0
  281. package/dist/view-icons.d.ts +20 -0
  282. package/dist/view-icons.js +70 -0
  283. package/dist/view-model.d.ts +390 -0
  284. package/dist/view-model.js +316 -0
  285. package/dist/whats-new.d.ts +3 -0
  286. package/dist/whats-new.js +53 -0
  287. package/dist/widget-controls.d.ts +9 -0
  288. package/dist/widget-controls.js +26 -0
  289. package/dist/widget-settings.d.ts +436 -0
  290. package/dist/widget-settings.js +290 -0
  291. package/dist/widget-thumbs.d.ts +3 -0
  292. package/dist/widget-thumbs.js +29 -0
  293. package/dist/workspace-keys.d.ts +12 -0
  294. package/dist/workspace-keys.js +13 -0
  295. package/dist/workspaces.d.ts +57 -0
  296. package/dist/workspaces.js +473 -0
  297. package/package.json +82 -4
@@ -0,0 +1,61 @@
1
+ import { type ViewFormat, type ViewFormula, type ViewOwnTile } from '@columenlabs/lumen-ui-types/views';
2
+ import { type LucideIcon } from 'lucide-react';
3
+ import { type LumenOption } from './lumen-selector.js';
4
+ import { type ViewDomain, type ViewField } from './view-model.js';
5
+ export declare const MAX_FORMULAS = 20;
6
+ /**
7
+ * A format's short name, short enough for the field list's Format column, and how it reads
8
+ *. ± marks the signed, gain/loss coloured ones.
9
+ */
10
+ export declare const FORMAT_LABELS: Record<ViewFormat, string>;
11
+ /** A field's type as a short chip, its full name on hover. */
12
+ export declare function FormatChip({ format, icon }: {
13
+ format: ViewFormat;
14
+ icon?: LucideIcon;
15
+ }): import("react").JSX.Element;
16
+ /** The number formats as picker options, each with how it reads. */
17
+ export declare const formatOptions: () => LumenOption<ViewFormula['format']>[];
18
+ /**
19
+ * A formula as it shows: a Text one has no sum or average of words, so its
20
+ * group and Total rows are blank unless they count, or show the first or last value. Its format is
21
+ * the person's pick; a number format on words just shows the words.
22
+ */
23
+ export declare function shownFormula(f: ViewFormula): ViewFormula;
24
+ /**
25
+ * Fields as LumenSelector options, grouped by the domain's field groups (Holding, Symbol, Account,
26
+ * Formulas) when it has them; without a domain or groups they are one list.
27
+ */
28
+ export declare function fieldOptions<T>(fields: ViewField<T>[], domain?: ViewDomain<T>): LumenOption[];
29
+ /** A view's formulas as fields: shown, sorted and rolled up like the domain's own. */
30
+ export declare function formulaFields<T>(formulas: ViewFormula[]): ViewField<T>[];
31
+ /**
32
+ * What is wrong with each formula on a domain (its fields, not its formulas); null where nothing.
33
+ * A formula reads fields by name and any other formula by its key, never in a loop.
34
+ */
35
+ export declare function formulaIssues<T>(formulas: ViewFormula[], domain: ViewDomain<T>): (string | null)[];
36
+ /**
37
+ * The fields and formulas a formula can read, as picker options: each field's name as the value
38
+ * (`symbol.price`), grouped as in the Columns tree; the formulas by key, but `except`.
39
+ */
40
+ export declare function formulaOptions<T>(domain: ViewDomain<T>, formulas: ViewFormula[], except?: string): LumenOption[];
41
+ /** The DOM id of the FormulaForm field an issue from `formulaIssues` is about. */
42
+ export declare const formulaIssueTarget: (key: string, issue: string) => string;
43
+ export declare const AGG_LABELS: Record<ViewOwnTile['agg'], string>;
44
+ export declare const MAX_TILES = 8;
45
+ /**
46
+ * One formula's form, under its row in the Fields list: label and key, the expression (fields
47
+ * offered as you type), its format and how group and Total rows roll it up.
48
+ */
49
+ export declare function FormulaForm({ formula: f, issue, blank, fields, onChange, onRemove, onDone, }: {
50
+ formula: ViewFormula;
51
+ issue: string | null;
52
+ /** Why it is blank on some of the page's rows (`blank on 3 of 24 rows: x is empty`). */
53
+ blank?: string | null;
54
+ /** The fields and formulas it can read, by name, as picker options (`formulaOptions`). */
55
+ fields: LumenOption[];
56
+ onChange: (p: Partial<ViewFormula>) => void;
57
+ /** Remove and Done, for a formula opened in a list. Left out (a dialog with its own footer), the
58
+ * form is bare: no row actions, no row tint or indent. */
59
+ onRemove?: () => void;
60
+ onDone?: () => void;
61
+ }): import("react").JSX.Element;
@@ -0,0 +1,247 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Formula columns and tiles on a custom view: a view computes its own columns
3
+ // from the domain's fields (`config.formulas`, Jexl, `@columenlabs/lumen-ui-types/views`) and rolls fields up
4
+ // into its own tiles (`config.tiles`). Each formula is a row of the Edit dialog's Fields list; its
5
+ // pencil opens this form under the row.
6
+ import { checkViewFormula, compileViewFormula, formulaLoops, formulaReads, viewAggs, viewFormulaIsText, viewNumberFormats, } from '@columenlabs/lumen-ui-types/views';
7
+ import { CalendarCheckIcon, CalendarClockIcon, CalendarDaysIcon, CalendarFoldIcon, CalendarIcon, CalendarRangeIcon, CheckIcon, CircleAlertIcon, SigmaIcon, Trash2Icon, WandSparklesIcon, } from 'lucide-react';
8
+ import { LumenConfirm } from './confirm.js';
9
+ import { FormulaInput, guessField, swapWord } from './formula-input.js';
10
+ import { IconButton } from './icon-button.js';
11
+ import { LumenSelector } from './lumen-selector.js';
12
+ import { Badge } from './ui/badge.js';
13
+ import { Button } from './ui/button.js';
14
+ import { Input } from './ui/input.js';
15
+ import { Label } from './ui/label.js';
16
+ import { FORMULAS_GROUP, fieldName, groupOf } from './view-model.js';
17
+ export const MAX_FORMULAS = 20;
18
+ /**
19
+ * A format's short name, short enough for the field list's Format column, and how it reads
20
+ *. ± marks the signed, gain/loss coloured ones.
21
+ */
22
+ export const FORMAT_LABELS = {
23
+ number: 'Number',
24
+ rounded: 'Rounded',
25
+ integer: 'Integer',
26
+ compact: 'Compact',
27
+ signed: 'Number ±',
28
+ price: 'Currency',
29
+ price_rounded: 'Rounded $',
30
+ price_signed: 'Currency ±',
31
+ weight: 'Percent',
32
+ percent: 'Percent ±',
33
+ text: 'Text',
34
+ };
35
+ /**
36
+ * A format as a chip glyph for the field list: $ the currency (USD, the only
37
+ * one the views draw), # a number, % a percent, Aa text, ± the signed ones.
38
+ */
39
+ const FORMAT_GLYPHS = {
40
+ number: '#',
41
+ rounded: '#',
42
+ integer: '#',
43
+ compact: '#',
44
+ signed: '±#',
45
+ price: '$',
46
+ price_rounded: '$',
47
+ price_signed: '±$',
48
+ weight: '%',
49
+ percent: '±%',
50
+ text: 'Aa',
51
+ };
52
+ /** Text fields whose icon is a calendar are dates (no date format exists; the icon says it). */
53
+ const DATE_ICONS = new Set([
54
+ CalendarIcon,
55
+ CalendarDaysIcon,
56
+ CalendarRangeIcon,
57
+ CalendarClockIcon,
58
+ CalendarCheckIcon,
59
+ CalendarFoldIcon,
60
+ ]);
61
+ /** A field's type as a short chip, its full name on hover. */
62
+ export function FormatChip({ format, icon }) {
63
+ const date = format === 'text' && !!icon && DATE_ICONS.has(icon);
64
+ return (_jsx(Badge, { variant: "outline", className: "h-5 min-w-7 justify-center px-1.5 font-mono text-[0.7rem] font-normal text-muted-foreground", title: date ? 'Date' : FORMAT_LABELS[format], "aria-label": date ? 'Date' : FORMAT_LABELS[format], children: date ? _jsx(CalendarDaysIcon, { className: "size-3" }) : FORMAT_GLYPHS[format] }));
65
+ }
66
+ const FORMAT_EXAMPLES = {
67
+ number: '1,234.56',
68
+ rounded: '1,235',
69
+ integer: '2026',
70
+ compact: '1.2K · 10B',
71
+ signed: '+1,234.56',
72
+ price: '$1,234.56',
73
+ price_rounded: '$1,235',
74
+ price_signed: '+$1,234.56',
75
+ weight: '12.3%',
76
+ percent: '+12.3%',
77
+ text: 'Abc',
78
+ };
79
+ /** The number formats as picker options, each with how it reads. */
80
+ export const formatOptions = () => viewNumberFormats.map((v) => ({
81
+ value: v,
82
+ label: FORMAT_LABELS[v],
83
+ detail: FORMAT_EXAMPLES[v],
84
+ }));
85
+ const NO_ROLLUP = '_none';
86
+ /** What a text formula's group and Total rows can show: no sum or average of words. */
87
+ const TEXT_AGGS = ['count', 'distinct', 'first', 'last'];
88
+ const isTextAgg = (a) => TEXT_AGGS.includes(a);
89
+ /**
90
+ * A formula as it shows: a Text one has no sum or average of words, so its
91
+ * group and Total rows are blank unless they count, or show the first or last value. Its format is
92
+ * the person's pick; a number format on words just shows the words.
93
+ */
94
+ export function shownFormula(f) {
95
+ if (f.format !== 'text')
96
+ return f;
97
+ return { ...f, agg: isTextAgg(f.agg) ? f.agg : 'none' };
98
+ }
99
+ /**
100
+ * Fields as LumenSelector options, grouped by the domain's field groups (Holding, Symbol, Account,
101
+ * Formulas) when it has them; without a domain or groups they are one list.
102
+ */
103
+ export function fieldOptions(fields, domain) {
104
+ const labels = new Map((domain?.groups ?? []).map((g) => [g.id, g.label]));
105
+ const grouped = !!domain && (labels.size > 1 || fields.some((f) => f.group === FORMULAS_GROUP));
106
+ return fields.map((f) => {
107
+ const g = domain ? groupOf(domain, f) : f.group;
108
+ return {
109
+ value: f.key,
110
+ label: f.label,
111
+ icon: f.icon,
112
+ group: grouped
113
+ ? g === FORMULAS_GROUP
114
+ ? 'Formulas'
115
+ : (labels.get(g ?? '') ?? undefined)
116
+ : undefined,
117
+ };
118
+ });
119
+ }
120
+ /** A view's formulas as fields: shown, sorted and rolled up like the domain's own. */
121
+ export function formulaFields(formulas) {
122
+ return formulas.map(shownFormula).map((f) => {
123
+ let run;
124
+ try {
125
+ run = compileViewFormula(f.expr);
126
+ }
127
+ catch {
128
+ run = null;
129
+ }
130
+ return {
131
+ key: f.key,
132
+ label: f.label,
133
+ group: FORMULAS_GROUP,
134
+ icon: SigmaIcon,
135
+ value: () => null,
136
+ format: f.format,
137
+ formula: f.expr,
138
+ // A formula can work out dollars from amounts; only a share (%) stays under Hide amounts.
139
+ amount: f.format !== 'percent' && f.format !== 'weight' && f.format !== 'text',
140
+ // `none` leaves group and Total rows blank: neither a roll-up nor the formula again.
141
+ aggregate: f.agg && f.agg !== 'none' ? f.agg : undefined,
142
+ rollFormula: f.agg === null,
143
+ compute: (_, scope) => (run ? run(scope) : null),
144
+ };
145
+ });
146
+ }
147
+ /**
148
+ * What is wrong with each formula on a domain (its fields, not its formulas); null where nothing.
149
+ * A formula reads fields by name and any other formula by its key, never in a loop.
150
+ */
151
+ export function formulaIssues(formulas, domain) {
152
+ const keys = domain.fields.map((f) => f.key);
153
+ const names = [...domain.fields.map((f) => fieldName(domain, f)), ...formulas.map((f) => f.key)];
154
+ const loops = formulaLoops(formulas);
155
+ return formulas.map((f, i) => {
156
+ if (!/^[a-z][a-z0-9_]{0,31}$/.test(f.key))
157
+ return 'Key: lowercase letters, digits and _';
158
+ if (keys.includes(f.key) || formulas.slice(0, i).some((x) => x.key === f.key))
159
+ return `Key ${f.key} is taken`;
160
+ if (!f.label.trim())
161
+ return 'Give it a label';
162
+ if (!f.expr.trim())
163
+ return 'Write an expression';
164
+ const issue = checkViewFormula(f.expr, names);
165
+ if (issue)
166
+ return issue;
167
+ if (!loops.has(f.key))
168
+ return null;
169
+ const via = formulaReads(f.expr).filter((k) => loops.has(k) && k !== f.key);
170
+ return `${f.key} loops back to itself${via.length ? ` through ${via.join(', ')}` : ''}`;
171
+ });
172
+ }
173
+ /**
174
+ * The fields and formulas a formula can read, as picker options: each field's name as the value
175
+ * (`symbol.price`), grouped as in the Columns tree; the formulas by key, but `except`.
176
+ */
177
+ export function formulaOptions(domain, formulas, except) {
178
+ const fields = domain.fields.filter((f) => groupOf(domain, f) !== FORMULAS_GROUP);
179
+ return [
180
+ ...fieldOptions(fields, domain).map((o, i) => {
181
+ const f = fields[i];
182
+ return { ...o, value: fieldName(domain, f) };
183
+ }),
184
+ ...formulas
185
+ .filter((f) => f.key !== except)
186
+ .map((f) => ({ value: f.key, label: f.label || f.key, group: 'Formulas' })),
187
+ ];
188
+ }
189
+ /** The DOM id of the FormulaForm field an issue from `formulaIssues` is about. */
190
+ export const formulaIssueTarget = (key, issue) => `formula-${key}-${issue.startsWith('Key') ? 'key' : issue === 'Give it a label' ? 'label' : 'expr'}`;
191
+ export const AGG_LABELS = {
192
+ sum: 'Sum',
193
+ mean: 'Average',
194
+ min: 'Min',
195
+ max: 'Max',
196
+ count: 'Count',
197
+ distinct: 'Distinct count',
198
+ first: 'First',
199
+ last: 'Last',
200
+ };
201
+ export const MAX_TILES = 8;
202
+ /**
203
+ * One formula's form, under its row in the Fields list: label and key, the expression (fields
204
+ * offered as you type), its format and how group and Total rows roll it up.
205
+ */
206
+ export function FormulaForm({ formula: f, issue, blank, fields, onChange, onRemove, onDone, }) {
207
+ const id = `formula-${f.key}`;
208
+ const row = !!(onRemove && onDone);
209
+ const shown = shownFormula(f);
210
+ const text = shown.format === 'text';
211
+ const words = viewFormulaIsText(f.expr);
212
+ // Text is the default for words, never a lock: the format moves only when
213
+ // the expression starts or stops giving words, and the picker can change it any time.
214
+ const setExpr = (expr) => {
215
+ const now = viewFormulaIsText(expr);
216
+ if (now === words)
217
+ return onChange({ expr });
218
+ onChange(now
219
+ ? { expr, format: 'text', agg: shownFormula({ ...f, format: 'text' }).agg }
220
+ : { expr, format: 'number', agg: null });
221
+ };
222
+ // `unknown field Price`: the field each name most likely means, offered as a one-tap fix.
223
+ const unknown = issue?.startsWith('unknown field ')
224
+ ? issue.slice('unknown field '.length).split(', ')
225
+ : [];
226
+ const fixes = unknown.flatMap((name) => {
227
+ const to = guessField(name, fields);
228
+ return to ? [{ name, to }] : [];
229
+ });
230
+ return (_jsxs("div", { className: row ? 'col-span-full grid gap-3 border-t bg-muted/30 px-3 py-3 md:pl-10' : 'grid gap-3 p-4', children: [_jsxs("div", { className: "grid gap-3 sm:grid-cols-2", children: [_jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: `${id}-label`, children: "Label" }), _jsx(Input, { id: `${id}-label`, value: f.label, onChange: (e) => onChange({ label: e.target.value }) })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: `${id}-key`, children: "Key" }), _jsx(Input, { id: `${id}-key`, className: "font-mono text-xs", value: f.key, onChange: (e) => onChange({ key: e.target.value }) })] })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: `${id}-expr`, children: "Expression" }), _jsx(FormulaInput, { id: `${id}-expr`, invalid: !!issue, fields: fields, placeholder: 'Type a field: market_value / group_sum("market_value") * 100', value: f.expr, onChange: setExpr }), issue ? (_jsxs("p", { role: "alert", className: "flex flex-wrap items-center gap-1.5 text-xs text-destructive", children: [_jsx(CircleAlertIcon, { className: "size-3.5" }), fixes.length
231
+ ? fixes.map(({ name, to }) => (_jsxs("span", { className: "flex items-center gap-1.5", children: ["No field ", name, ". Did you mean ", _jsx("code", { className: "font-mono", children: to.value }), " (", to.label, ")?", _jsxs(Button, { variant: "outline", size: "xs", onClick: () => setExpr(swapWord(f.expr, name, to.value)), children: [_jsx(WandSparklesIcon, {}), "Fix"] })] }, name)))
232
+ : issue] })) : blank ? (_jsxs("p", { className: "flex items-center gap-1.5 text-xs text-warning", children: [_jsx(CircleAlertIcon, { className: "size-3.5" }), "Reads fine, but ", blank] })) : (_jsxs("p", { className: "flex items-center gap-1.5 text-xs text-gain", children: [_jsx(CheckIcon, { className: "size-3.5" }), "Reads fine"] }))] }), _jsxs("div", { className: "flex flex-wrap items-end gap-3", children: [_jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { children: "Format" }), _jsx(LumenSelector, { label: "Format", size: "sm", className: "w-40", options: [
233
+ ...formatOptions(),
234
+ { value: 'text', label: FORMAT_LABELS.text, detail: FORMAT_EXAMPLES.text },
235
+ ], value: shown.format, onChange: (v) => onChange(v === 'text'
236
+ ? { format: v, agg: shownFormula({ ...f, format: v }).agg }
237
+ : { format: v, agg: text ? null : f.agg }) })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { children: "Group and Total rows" }), _jsx(LumenSelector, { label: "Group and Total rows", size: "sm", className: "w-40", options: text
238
+ ? [
239
+ { value: 'none', label: 'None' },
240
+ ...TEXT_AGGS.map((a) => ({ value: a, label: AGG_LABELS[a] })),
241
+ ]
242
+ : [
243
+ { value: NO_ROLLUP, label: 'Same formula' },
244
+ ...viewAggs.map((a) => ({ value: a, label: AGG_LABELS[a] })),
245
+ { value: 'none', label: 'None' },
246
+ ], value: shown.agg ?? NO_ROLLUP, onChange: (v) => onChange({ agg: v === NO_ROLLUP ? null : v }) })] }), onRemove && onDone && (_jsxs("div", { className: "ml-auto flex gap-2", children: [_jsx(LumenConfirm, { title: `Remove formula ${f.label || f.key}?`, description: "It leaves this view when you save.", action: "Remove", onConfirm: onRemove, children: _jsx(IconButton, { icon: Trash2Icon, label: "Remove formula", variant: "destructive" }) }), _jsxs(Button, { variant: "outline", size: "sm", onClick: onDone, children: [_jsx(CheckIcon, {}), "Done"] })] }))] })] }));
247
+ }
@@ -0,0 +1,20 @@
1
+ import type { ViewIcon, ViewIconName, ViewShape } from '@columenlabs/lumen-ui-types/views';
2
+ import { type LucideIcon } from 'lucide-react';
3
+ import { iconNames } from 'lucide-react/dynamic';
4
+ import type { NavIcon } from './nav.js';
5
+ export declare const VIEW_ICONS: Record<ViewIconName, LucideIcon>;
6
+ /** A view's sidebar icon: its own pick (a lucide name or an emoji), else its shape's. */
7
+ export declare const viewIcon: (config: {
8
+ icon?: ViewIcon | null;
9
+ shape: ViewShape;
10
+ }) => NavIcon;
11
+ /** A picked icon (a lucide name or an emoji) as a component; `fallback` when none or unknown. */
12
+ export declare const iconOf: (icon: ViewIcon | null | undefined, fallback: NavIcon) => NavIcon;
13
+ /** Any lucide icon by name, loaded on first use (lucide's own DynamicIcon). */
14
+ export declare const lucideIcon: (name: string) => NavIcon;
15
+ /** An emoji drawn where an icon goes. */
16
+ export declare const emojiIcon: (emoji: string) => NavIcon;
17
+ /** Every lucide icon name, for the icon picker's search. */
18
+ export { iconNames as lucideIconNames };
19
+ /** A shape's own icon (the Edit dialog's Shape picker). */
20
+ export declare const shapeIcon: (shape: ViewShape) => LucideIcon;
@@ -0,0 +1,70 @@
1
+ import { BanknoteIcon, BriefcaseIcon, CalendarIcon, ChartBarIcon, ChartLineIcon, ChartPieIcon, ClockIcon, CoinsIcon, FlameIcon, Grid3x3Icon, LandmarkIcon, LayersIcon, ListTreeIcon, PiggyBankIcon, ReceiptIcon, ShieldIcon, StarIcon, TableIcon, TagIcon, TargetIcon, TrendingDownIcon, TrendingUpIcon, WalletIcon, ZapIcon, } from 'lucide-react';
2
+ import { DynamicIcon, iconNames } from 'lucide-react/dynamic';
3
+ import { createElement } from 'react';
4
+ import { cn } from './lib/utils.js';
5
+ export const VIEW_ICONS = {
6
+ table: TableIcon,
7
+ 'list-tree': ListTreeIcon,
8
+ grid: Grid3x3Icon,
9
+ wallet: WalletIcon,
10
+ receipt: ReceiptIcon,
11
+ briefcase: BriefcaseIcon,
12
+ landmark: LandmarkIcon,
13
+ 'piggy-bank': PiggyBankIcon,
14
+ banknote: BanknoteIcon,
15
+ coins: CoinsIcon,
16
+ 'chart-line': ChartLineIcon,
17
+ 'chart-bar': ChartBarIcon,
18
+ 'chart-pie': ChartPieIcon,
19
+ 'trending-up': TrendingUpIcon,
20
+ 'trending-down': TrendingDownIcon,
21
+ target: TargetIcon,
22
+ shield: ShieldIcon,
23
+ star: StarIcon,
24
+ zap: ZapIcon,
25
+ flame: FlameIcon,
26
+ calendar: CalendarIcon,
27
+ clock: ClockIcon,
28
+ layers: LayersIcon,
29
+ tag: TagIcon,
30
+ };
31
+ const SHAPE_ICONS = {
32
+ table: TableIcon,
33
+ group: ListTreeIcon,
34
+ pivot: Grid3x3Icon,
35
+ };
36
+ /** A view's sidebar icon: its own pick (a lucide name or an emoji), else its shape's. */
37
+ export const viewIcon = (config) => iconOf(config.icon, SHAPE_ICONS[config.shape]);
38
+ /** A picked icon (a lucide name or an emoji) as a component; `fallback` when none or unknown. */
39
+ export const iconOf = (icon, fallback) => {
40
+ if (!icon)
41
+ return fallback;
42
+ if (icon in VIEW_ICONS)
43
+ return VIEW_ICONS[icon];
44
+ if (/^[a-z]/.test(icon))
45
+ return lucideNames.has(icon) ? lucideIcon(icon) : fallback;
46
+ return emojiIcon(icon);
47
+ };
48
+ const lucideNames = new Set(iconNames);
49
+ const made = new Map();
50
+ /** One component per pick, so React keeps it between renders. */
51
+ const once = (key, make) => {
52
+ let Icon = made.get(key);
53
+ if (!Icon) {
54
+ Icon = make();
55
+ made.set(key, Icon);
56
+ }
57
+ return Icon;
58
+ };
59
+ /** Any lucide icon by name, loaded on first use (lucide's own DynamicIcon). */
60
+ export const lucideIcon = (name) => once(`lucide:${name}`, () => (props) => createElement(DynamicIcon, { name: name, ...props }));
61
+ /** An emoji drawn where an icon goes. */
62
+ export const emojiIcon = (emoji) => once(`emoji:${emoji}`, () => ({ className }) => createElement('span', {
63
+ 'aria-hidden': true,
64
+ 'data-slot': 'view-emoji',
65
+ className: cn('inline-grid size-4 shrink-0 place-items-center text-sm leading-none', className),
66
+ }, emoji));
67
+ /** Every lucide icon name, for the icon picker's search. */
68
+ export { iconNames as lucideIconNames };
69
+ /** A shape's own icon (the Edit dialog's Shape picker). */
70
+ export const shapeIcon = (shape) => SHAPE_ICONS[shape];