shutro-erp 0.3.2 → 0.3.4

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 (156) hide show
  1. package/dist/account_portal/.kosh/bindings.js +1 -1
  2. package/dist/account_portal/.kosh/schema.d.ts +156 -1
  3. package/dist/accounting/.kosh/bindings.js +1 -1
  4. package/dist/accounting/.kosh/schema.d.ts +156 -1
  5. package/dist/assistant/index.js +1 -1
  6. package/dist/contacts/.kosh/bindings.js +1 -1
  7. package/dist/contacts/.kosh/schema.d.ts +156 -1
  8. package/dist/integrations/index.js +12 -0
  9. package/dist/integrations/web/OAuthAppsPage.d.ts +9 -0
  10. package/dist/integrations/web/OAuthAppsPage.js +79 -0
  11. package/dist/integrations/web/index.d.ts +1 -0
  12. package/dist/integrations/web/index.js +3 -0
  13. package/dist/integrations/web/messages.js +58 -0
  14. package/dist/l10n_bd/.kosh/bindings.js +1 -1
  15. package/dist/l10n_bd/.kosh/schema.d.ts +156 -1
  16. package/dist/l10n_in/.kosh/bindings.js +1 -1
  17. package/dist/l10n_in/.kosh/schema.d.ts +156 -4
  18. package/dist/l10n_in/module.d.ts +0 -62
  19. package/dist/l10n_in/module.js +2 -4
  20. package/dist/messaging/web/index.js +1 -1
  21. package/dist/notify/web/index.js +1 -1
  22. package/dist/payment/.kosh/bindings.js +1 -1
  23. package/dist/payment/.kosh/schema.d.ts +157 -2
  24. package/dist/payment/bkash.d.ts +28 -0
  25. package/dist/payment/bkash.js +169 -0
  26. package/dist/payment/index.d.ts +4 -0
  27. package/dist/payment/index.js +80 -24
  28. package/dist/payment/module.d.ts +7 -4
  29. package/dist/payment/module.js +3 -0
  30. package/dist/payment/money.d.ts +7 -0
  31. package/dist/payment/money.js +37 -0
  32. package/dist/payment/providers.d.ts +13 -2
  33. package/dist/payment/razorpay.d.ts +12 -0
  34. package/dist/payment/razorpay.js +149 -0
  35. package/dist/payment/sslcommerz.js +1 -1
  36. package/dist/payment/stripe.d.ts +15 -0
  37. package/dist/payment/stripe.js +182 -0
  38. package/dist/permissions/admin.d.ts +3 -0
  39. package/dist/permissions/admin.js +108 -10
  40. package/dist/permissions/index.d.ts +1 -0
  41. package/dist/permissions/index.js +3 -0
  42. package/dist/permissions/roles.d.ts +2 -0
  43. package/dist/permissions/roles.js +236 -0
  44. package/dist/portal/.kosh/bindings.js +1 -1
  45. package/dist/portal/.kosh/schema.d.ts +156 -1
  46. package/dist/product/.kosh/bindings.js +1 -1
  47. package/dist/product/.kosh/schema.d.ts +156 -1
  48. package/dist/project/.kosh/bindings.js +1 -1
  49. package/dist/project/.kosh/schema.d.ts +156 -1
  50. package/dist/project_portal/.kosh/bindings.js +1 -1
  51. package/dist/project_portal/.kosh/schema.d.ts +156 -1
  52. package/dist/purchase/.kosh/bindings.js +1 -1
  53. package/dist/purchase/.kosh/schema.d.ts +156 -1
  54. package/dist/purchase_account/.kosh/bindings.js +1 -1
  55. package/dist/purchase_account/.kosh/schema.d.ts +156 -1
  56. package/dist/purchase_portal/.kosh/bindings.js +1 -1
  57. package/dist/purchase_portal/.kosh/schema.d.ts +156 -1
  58. package/dist/sale_account/.kosh/bindings.js +1 -1
  59. package/dist/sale_account/.kosh/schema.d.ts +156 -1
  60. package/dist/sale_portal/.kosh/bindings.js +1 -1
  61. package/dist/sale_portal/.kosh/schema.d.ts +156 -1
  62. package/dist/sale_project/.kosh/bindings.js +1 -1
  63. package/dist/sale_project/.kosh/schema.d.ts +156 -1
  64. package/dist/sales/.kosh/bindings.js +1 -1
  65. package/dist/sales/.kosh/schema.d.ts +156 -1
  66. package/dist/server/auth/base.js +98 -0
  67. package/dist/server/auth/oauth.d.ts +21 -0
  68. package/dist/server/auth/oauth.js +579 -0
  69. package/dist/server/auth/routes.d.ts +3 -0
  70. package/dist/server/auth/routes.js +29 -0
  71. package/dist/server/auth/service.d.ts +56 -0
  72. package/dist/server/auth/service.js +185 -3
  73. package/dist/server/identity.d.ts +4 -0
  74. package/dist/server/images.d.ts +12 -0
  75. package/dist/server/images.js +139 -0
  76. package/dist/server/index.d.ts +2 -1
  77. package/dist/server/index.js +1 -0
  78. package/dist/server/server.d.ts +4 -0
  79. package/dist/server/server.js +20 -4
  80. package/dist/settings/company.d.ts +34 -0
  81. package/dist/settings/company.js +189 -0
  82. package/dist/settings/index.d.ts +10 -10
  83. package/dist/settings/index.js +159 -30
  84. package/dist/settings/people.d.ts +16 -0
  85. package/dist/settings/people.js +507 -0
  86. package/dist/settings/security.d.ts +13 -0
  87. package/dist/settings/security.js +158 -0
  88. package/dist/settings/web/AuditPage.d.ts +8 -0
  89. package/dist/settings/web/AuditPage.js +74 -0
  90. package/dist/settings/web/CompaniesPage.d.ts +8 -0
  91. package/dist/settings/web/CompaniesPage.js +52 -0
  92. package/dist/settings/web/CompanyPage.d.ts +9 -0
  93. package/dist/settings/web/CompanyPage.js +105 -0
  94. package/dist/settings/web/ConnectedAppsPage.d.ts +7 -0
  95. package/dist/settings/web/ConnectedAppsPage.js +33 -0
  96. package/dist/settings/web/OverviewPage.d.ts +9 -0
  97. package/dist/settings/web/OverviewPage.js +77 -0
  98. package/dist/settings/web/PeoplePage.d.ts +9 -0
  99. package/dist/settings/web/PeoplePage.js +154 -0
  100. package/dist/settings/web/ProfilePage.d.ts +14 -0
  101. package/dist/settings/web/ProfilePage.js +196 -0
  102. package/dist/settings/web/RolesEditor.d.ts +35 -0
  103. package/dist/settings/web/RolesEditor.js +49 -0
  104. package/dist/settings/web/RolesPage.d.ts +9 -0
  105. package/dist/settings/web/RolesPage.js +296 -0
  106. package/dist/settings/web/SecurityPage.d.ts +8 -0
  107. package/dist/settings/web/SecurityPage.js +81 -0
  108. package/dist/settings/web/index.d.ts +10 -0
  109. package/dist/settings/web/index.js +31 -1
  110. package/dist/settings/web/messages.js +594 -0
  111. package/dist/ui/api.d.ts +8 -0
  112. package/dist/ui/app.d.ts +5 -0
  113. package/dist/ui/app.js +143 -33
  114. package/dist/ui/components/AppIcon.js +11 -1
  115. package/dist/ui/components/Avatar.d.ts +21 -0
  116. package/dist/ui/components/Avatar.js +71 -0
  117. package/dist/ui/components/primitives.d.ts +9 -1
  118. package/dist/ui/components/primitives.js +12 -4
  119. package/dist/ui/context.d.ts +10 -1
  120. package/dist/ui/context.js +60 -2
  121. package/dist/ui/i18n/messages.d.ts +76 -0
  122. package/dist/ui/i18n/messages.js +152 -0
  123. package/dist/ui/index.d.ts +3 -1
  124. package/dist/ui/index.js +3 -1
  125. package/dist/ui/lib/accent.d.ts +13 -0
  126. package/dist/ui/lib/accent.js +36 -0
  127. package/dist/ui/registry.d.ts +7 -1
  128. package/dist/ui/registry.js +10 -1
  129. package/dist/ui/search/ViewTabs.d.ts +14 -0
  130. package/dist/ui/search/ViewTabs.js +42 -0
  131. package/dist/ui/shell/Activate.d.ts +6 -0
  132. package/dist/ui/shell/Activate.js +39 -0
  133. package/dist/ui/shell/CommandPalette.d.ts +7 -1
  134. package/dist/ui/shell/CommandPalette.js +16 -2
  135. package/dist/ui/shell/Consent.d.ts +5 -0
  136. package/dist/ui/shell/Consent.js +35 -0
  137. package/dist/ui/shell/Home.js +85 -16
  138. package/dist/ui/shell/Sidebar.d.ts +27 -0
  139. package/dist/ui/shell/Sidebar.js +77 -0
  140. package/dist/ui/shell/TwoFactorSetup.d.ts +5 -0
  141. package/dist/ui/shell/TwoFactorSetup.js +32 -0
  142. package/dist/ui/shell/shortcuts.d.ts +3 -0
  143. package/dist/ui/shell/shortcuts.js +13 -4
  144. package/dist/ui/styles.css +70 -28
  145. package/dist/ui/views/DateField.d.ts +10 -0
  146. package/dist/ui/views/DateField.js +129 -0
  147. package/dist/ui/views/FormView.js +161 -21
  148. package/dist/ui/views/ListView.d.ts +2 -0
  149. package/dist/ui/views/ListView.js +57 -25
  150. package/dist/ui/views/StatusChip.d.ts +10 -0
  151. package/dist/ui/views/StatusChip.js +22 -0
  152. package/dist/ui/views/widgets.d.ts +3 -0
  153. package/dist/ui/views/widgets.js +26 -8
  154. package/dist/views/types.d.ts +7 -0
  155. package/dist/views/validate.js +4 -0
  156. package/package.json +2 -2
@@ -1,5 +1,6 @@
1
1
  /*
2
- * Kosh design tokens (RFC 0007): colors, radius and typography as CSS variables, light and dark themes, Tailwind v4
2
+ * Kosh design tokens (RFC 0007, refreshed by RFC 0018): colors, radius, typography and density as CSS variables, light
3
+ * and dark themes with the company's accent, Tailwind v4
3
4
  * utilities mapped onto them. Components use logical properties only (ms/me, ps/pe, start/end), so RTL needs no
4
5
  * overrides. Import once from the app: `@import './styles.css';` after `@import 'tailwindcss';`.
5
6
  */
@@ -30,23 +31,33 @@
30
31
  --font-sans: var(--font-family);
31
32
  }
32
33
 
33
- :root {
34
- --radius: 0.5rem;
34
+ /* `.light` (and `.dark`) re-declare the tokens on a part of the page: a preview in the other theme, or with another
35
+ accent — custom properties are computed where they are declared, so the accent recomputes there. */
36
+ :root,
37
+ .light {
38
+ /* 6 px controls; cards and popovers use --radius-lg (10 px). */
39
+ --radius: 0.375rem;
35
40
  --font-family: "Inter", "Noto Sans Bengali", "Noto Sans Arabic", system-ui, sans-serif;
36
- /* Light: contrast ratios ≥ 4.5:1 for text on background and surface (WCAG AA). */
37
- --background: oklch(0.985 0.002 250);
38
- --foreground: oklch(0.21 0.02 260);
41
+ /* The accent (RFC 0018): a company's brand hue and chroma, indigo by default; each theme fixes its lightness. */
42
+ --accent-h: 277;
43
+ --accent-c: 0.2;
44
+ /* Density (RFC 0018): comfortable by default, compact as a user setting. */
45
+ --control-h: 2.25rem;
46
+ --cell-py: 0.375rem;
47
+ /* Light: neutral (zinc) surfaces; text contrast ≥ 4.5:1 on background and surface (WCAG AA). */
48
+ --background: oklch(0.985 0.002 286);
49
+ --foreground: oklch(0.21 0.006 286);
39
50
  --surface: oklch(1 0 0);
40
- --surface-foreground: oklch(0.21 0.02 260);
41
- --muted: oklch(0.955 0.005 250);
42
- --muted-foreground: oklch(0.45 0.02 260);
43
- --border: oklch(0.9 0.008 250);
44
- --input: oklch(0.86 0.01 250);
45
- --ring: oklch(0.55 0.17 255);
46
- --primary: oklch(0.5 0.17 255);
51
+ --surface-foreground: oklch(0.21 0.006 286);
52
+ --muted: oklch(0.965 0.003 286);
53
+ --muted-foreground: oklch(0.47 0.014 286);
54
+ --border: oklch(0.92 0.004 286);
55
+ --input: oklch(0.87 0.007 286);
56
+ --ring: oklch(0.55 var(--accent-c) var(--accent-h));
57
+ --primary: oklch(0.5 var(--accent-c) var(--accent-h));
47
58
  --primary-foreground: oklch(0.99 0 0);
48
- --accent: oklch(0.94 0.03 255);
49
- --accent-foreground: oklch(0.3 0.1 255);
59
+ --accent: oklch(0.96 calc(var(--accent-c) * 0.16) var(--accent-h));
60
+ --accent-foreground: oklch(0.38 var(--accent-c) var(--accent-h));
50
61
  --danger: oklch(0.52 0.2 25);
51
62
  --danger-foreground: oklch(0.99 0 0);
52
63
  --success: oklch(0.5 0.13 150);
@@ -56,20 +67,25 @@
56
67
  color-scheme: light;
57
68
  }
58
69
 
70
+ [data-density="compact"] {
71
+ --control-h: 2rem;
72
+ --cell-py: 0.125rem;
73
+ }
74
+
59
75
  .dark {
60
- --background: oklch(0.17 0.015 260);
61
- --foreground: oklch(0.94 0.01 250);
62
- --surface: oklch(0.21 0.017 260);
63
- --surface-foreground: oklch(0.94 0.01 250);
64
- --muted: oklch(0.25 0.017 260);
65
- --muted-foreground: oklch(0.72 0.02 255);
66
- --border: oklch(0.3 0.02 260);
67
- --input: oklch(0.36 0.02 260);
68
- --ring: oklch(0.7 0.14 255);
69
- --primary: oklch(0.68 0.15 255);
70
- --primary-foreground: oklch(0.16 0.02 260);
71
- --accent: oklch(0.3 0.05 255);
72
- --accent-foreground: oklch(0.92 0.04 255);
76
+ --background: oklch(0.145 0.004 286);
77
+ --foreground: oklch(0.95 0.003 286);
78
+ --surface: oklch(0.185 0.005 286);
79
+ --surface-foreground: oklch(0.95 0.003 286);
80
+ --muted: oklch(0.24 0.006 286);
81
+ --muted-foreground: oklch(0.72 0.012 286);
82
+ --border: oklch(0.29 0.006 286);
83
+ --input: oklch(0.35 0.008 286);
84
+ --ring: oklch(0.7 min(var(--accent-c), 0.16) var(--accent-h));
85
+ --primary: oklch(0.7 min(var(--accent-c), 0.16) var(--accent-h));
86
+ --primary-foreground: oklch(0.16 0.01 var(--accent-h));
87
+ --accent: oklch(0.28 calc(var(--accent-c) * 0.3) var(--accent-h));
88
+ --accent-foreground: oklch(0.9 calc(var(--accent-c) * 0.35) var(--accent-h));
73
89
  --danger: oklch(0.65 0.19 25);
74
90
  --danger-foreground: oklch(0.16 0.02 260);
75
91
  --success: oklch(0.7 0.14 150);
@@ -87,6 +103,21 @@
87
103
  background: var(--background);
88
104
  color: var(--foreground);
89
105
  font-family: var(--font-family);
106
+ font-size: 0.875rem;
107
+ line-height: 1.5;
108
+ /* Inter: the single-storey a and open digits; numbers line up in columns. */
109
+ font-feature-settings: "cv11", "ss01";
110
+ -webkit-font-smoothing: antialiased;
111
+ }
112
+ h1,
113
+ h2,
114
+ h3 {
115
+ letter-spacing: -0.011em;
116
+ }
117
+ td,
118
+ .tabular-nums,
119
+ input[inputmode="decimal"] {
120
+ font-variant-numeric: tabular-nums;
90
121
  }
91
122
  :focus-visible {
92
123
  outline: 2px solid var(--ring);
@@ -103,3 +134,14 @@
103
134
  }
104
135
  }
105
136
  }
137
+
138
+ /* Outside the layers on purpose: unlayered styles win over Tailwind's utilities (the inputs' border classes). */
139
+ /* Saved records read first (RFC 0018 §4.2): their inputs show no box until hovered or focused. */
140
+ .kosh-quiet
141
+ :is(input:not([type="checkbox"]):not([type="color"]), textarea, button[role="combobox"]):not(
142
+ :hover
143
+ ):not(:focus-visible):not(:focus):not([aria-invalid="true"]) {
144
+ border-color: transparent;
145
+ background-color: transparent;
146
+ box-shadow: none;
147
+ }
@@ -0,0 +1,10 @@
1
+ import { type ReactElement } from 'react';
2
+ /** A typed date as ISO, or null: `2026-10-28`, or `28/10/2026` (`10/28/2026` where months come first). */
3
+ export declare function parseTypedDate(text: string, lang: string): string | null;
4
+ export declare function DateField({ id, value, onChange, invalid, required, }: {
5
+ readonly id?: string;
6
+ readonly value: string | null;
7
+ readonly onChange: (iso: string | null) => void;
8
+ readonly invalid?: boolean;
9
+ readonly required?: boolean;
10
+ }): ReactElement;
@@ -0,0 +1,129 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The date field (RFC 0018 §4.2): the date as the user's language writes it (`28 Oct 2026`, `২৮ অক্টো, ২০২৬`), typed
4
+ * (ISO `2026-10-28`, or day, month and year in the language's order) or picked on a calendar — arrows move by
5
+ * day and week, Page Up / Page Down by month, Home / End to the week's ends, Enter picks. Values stay ISO dates.
6
+ */
7
+ import { CalendarDays, ChevronLeft, ChevronRight } from 'lucide-react';
8
+ import { useEffect, useMemo, useRef, useState } from 'react';
9
+ import { useTranslation } from 'react-i18next';
10
+ import { Button, Input, Popover, PopoverContent, PopoverTrigger, } from '../components/primitives.js';
11
+ import { useKosh } from '../context.js';
12
+ import { cn } from '../lib/cn.js';
13
+ const pad = (n) => String(n).padStart(2, '0');
14
+ const iso = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
15
+ const fromIso = (value) => {
16
+ const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(value);
17
+ if (m === null)
18
+ return null;
19
+ const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
20
+ return Number.isNaN(d.getTime()) ? null : d;
21
+ };
22
+ const addDays = (d, n) => new Date(d.getFullYear(), d.getMonth(), d.getDate() + n);
23
+ const addMonths = (d, n) => {
24
+ const target = new Date(d.getFullYear(), d.getMonth() + n, 1);
25
+ const last = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate();
26
+ return new Date(target.getFullYear(), target.getMonth(), Math.min(d.getDate(), last));
27
+ };
28
+ /** The order of day, month and year in `lang`'s numeric dates (`dmy`, `mdy`, `ymd`). */
29
+ function orderOf(lang) {
30
+ try {
31
+ return new Intl.DateTimeFormat(lang, { day: 'numeric', month: 'numeric', year: 'numeric' })
32
+ .formatToParts(new Date(2026, 9, 28))
33
+ .map((p) => p.type[0])
34
+ .filter((c) => c === 'd' || c === 'm' || c === 'y')
35
+ .join('');
36
+ }
37
+ catch {
38
+ return 'dmy';
39
+ }
40
+ }
41
+ /** A typed date as ISO, or null: `2026-10-28`, or `28/10/2026` (`10/28/2026` where months come first). */
42
+ export function parseTypedDate(text, lang) {
43
+ const raw = text.trim();
44
+ if (raw === '')
45
+ return null;
46
+ const isoDate = fromIso(raw);
47
+ if (isoDate !== null && /^\d{4}-\d{2}-\d{2}$/.test(raw))
48
+ return iso(isoDate);
49
+ // Any digits (Bengali ones too) separated by / . - or spaces.
50
+ const digits = raw.replace(/[০-৯]/g, (c) => String(c.charCodeAt(0) - 0x09e6));
51
+ const parts = digits.split(/[\s/.-]+/).filter((p) => p !== '');
52
+ if (parts.length !== 3 || parts.some((p) => !/^\d+$/.test(p)))
53
+ return null;
54
+ const order = orderOf(lang);
55
+ const at = (c) => Number(parts[order.indexOf(c)]);
56
+ let year = at('y');
57
+ if (year < 100)
58
+ year += 2000;
59
+ const d = new Date(year, at('m') - 1, at('d'));
60
+ return d.getMonth() === at('m') - 1 && d.getDate() === at('d') ? iso(d) : null;
61
+ }
62
+ /** The first day of the week in `lang` (0 Sunday … 6 Saturday). */
63
+ function weekStart(lang) {
64
+ try {
65
+ const locale = new Intl.Locale(lang);
66
+ const first = (locale.getWeekInfo?.() ?? locale.weekInfo)?.firstDay;
67
+ return first === undefined ? 1 : first % 7;
68
+ }
69
+ catch {
70
+ return 1;
71
+ }
72
+ }
73
+ function Calendar({ value, onPick, lang, }) {
74
+ const { t } = useTranslation();
75
+ const { format } = useKosh();
76
+ const today = useMemo(() => new Date(), []);
77
+ const [focus, setFocus] = useState(() => (value === null ? null : fromIso(value)) ?? today);
78
+ const grid = useRef(null);
79
+ const first = weekStart(lang);
80
+ const month = new Date(focus.getFullYear(), focus.getMonth(), 1);
81
+ const start = addDays(month, -((month.getDay() - first + 7) % 7));
82
+ const days = Array.from({ length: 42 }, (_, i) => addDays(start, i));
83
+ const weekday = new Intl.DateTimeFormat(lang, { weekday: 'narrow' });
84
+ const title = new Intl.DateTimeFormat(lang, { month: 'long', year: 'numeric' }).format(month);
85
+ useEffect(() => {
86
+ grid.current?.querySelector(`[data-day="${iso(focus)}"]`)?.focus();
87
+ }, [focus]);
88
+ const onKey = (e) => {
89
+ const moves = {
90
+ ArrowLeft: () => addDays(focus, document.dir === 'rtl' ? 1 : -1),
91
+ ArrowRight: () => addDays(focus, document.dir === 'rtl' ? -1 : 1),
92
+ ArrowUp: () => addDays(focus, -7),
93
+ ArrowDown: () => addDays(focus, 7),
94
+ PageUp: () => addMonths(focus, -1),
95
+ PageDown: () => addMonths(focus, 1),
96
+ Home: () => addDays(focus, -((focus.getDay() - first + 7) % 7)),
97
+ End: () => addDays(focus, 6 - ((focus.getDay() - first + 7) % 7)),
98
+ };
99
+ const move = moves[e.key];
100
+ if (move === undefined)
101
+ return;
102
+ e.preventDefault();
103
+ setFocus(move());
104
+ };
105
+ return (_jsxs("div", { className: "grid gap-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx(Button, { type: "button", variant: "ghost", size: "icon", "aria-label": t('views.previousMonth'), onClick: () => setFocus(addMonths(focus, -1)), children: _jsx(ChevronLeft, { className: "rtl:rotate-180", "aria-hidden": true }) }), _jsx("p", { className: "text-sm font-medium", "aria-live": "polite", children: title }), _jsx(Button, { type: "button", variant: "ghost", size: "icon", "aria-label": t('views.nextMonth'), onClick: () => setFocus(addMonths(focus, 1)), children: _jsx(ChevronRight, { className: "rtl:rotate-180", "aria-hidden": true }) })] }), _jsxs("table", { ref: grid, "aria-label": title, className: "border-separate border-spacing-0.5", onKeyDown: onKey, children: [_jsx("thead", { children: _jsx("tr", { children: days.slice(0, 7).map((d) => (_jsx("th", { scope: "col", className: "py-1 text-center text-xs font-normal text-muted-foreground", children: weekday.format(d) }, iso(d)))) }) }), _jsx("tbody", { children: [0, 1, 2, 3, 4, 5].map((week) => (_jsx("tr", { children: days.slice(week * 7, week * 7 + 7).map((d) => {
106
+ const key = iso(d);
107
+ const selected = key === value;
108
+ const inMonth = d.getMonth() === month.getMonth();
109
+ return (_jsx("td", { className: "p-0", children: _jsx("button", { type: "button", "data-day": key, tabIndex: key === iso(focus) ? 0 : -1, "aria-pressed": selected, "aria-label": format.date(key), onClick: () => onPick(key), className: cn('size-8 rounded-md text-sm tabular-nums hover:bg-muted', !inMonth && 'text-muted-foreground/60', key === iso(today) && 'font-semibold underline underline-offset-4', selected && 'bg-primary text-primary-foreground hover:bg-primary'), children: new Intl.NumberFormat(lang).format(d.getDate()) }) }, key));
110
+ }) }, week))) })] }), _jsxs("div", { className: "flex justify-between", children: [_jsx(Button, { type: "button", size: "sm", variant: "ghost", onClick: () => onPick(iso(today)), children: t('views.today') }), _jsx(Button, { type: "button", size: "sm", variant: "ghost", onClick: () => onPick(null), children: t('views.clear') })] })] }));
111
+ }
112
+ export function DateField({ id, value, onChange, invalid, required, }) {
113
+ const { t, i18n } = useTranslation();
114
+ const { format } = useKosh();
115
+ const lang = i18n.resolvedLanguage ?? i18n.language ?? 'en';
116
+ const [typing, setTyping] = useState(null);
117
+ const [open, setOpen] = useState(false);
118
+ const shown = typing ?? (value === null ? '' : format.date(value));
119
+ const bad = typing !== null && typing.trim() !== '' && parseTypedDate(typing, lang) === null;
120
+ return (_jsxs("div", { className: "relative", children: [_jsx(Input, { id: id, value: shown, required: required, placeholder: t('views.datePlaceholder'), "aria-invalid": invalid === true || bad ? true : undefined, className: "pe-10", onFocus: () => setTyping(value ?? ''), onChange: (e) => {
121
+ setTyping(e.target.value);
122
+ const parsed = parseTypedDate(e.target.value, lang);
123
+ if (parsed !== null || e.target.value.trim() === '')
124
+ onChange(parsed);
125
+ }, onBlur: () => setTyping(null) }), _jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "absolute inset-y-0 end-0 h-full", "aria-label": t('views.pickDate'), children: _jsx(CalendarDays, { "aria-hidden": true }) }) }), _jsx(PopoverContent, { align: "end", className: "w-auto", children: _jsx(Calendar, { value: value, lang: lang, onPick: (picked) => {
126
+ onChange(picked);
127
+ setOpen(false);
128
+ } }) })] })] }));
129
+ }
@@ -8,7 +8,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
8
8
  import { evaluate, holds, } from '../../views/index.js';
9
9
  import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
10
10
  import { useBlocker, useParams } from '@tanstack/react-router';
11
- import { ArrowLeft, Check, ChevronLeft, ChevronRight, CircleDot, MoreHorizontal, Plus, Trash2, } from 'lucide-react';
11
+ import { ArrowLeft, Check, ChevronLeft, ChevronRight, CircleDot, MoreHorizontal, PanelRight, Plus, Trash2, } from 'lucide-react';
12
12
  import { useEffect, useId, useMemo, useRef, useState } from 'react';
13
13
  import { useTranslation } from 'react-i18next';
14
14
  import { ApiError, NetworkError, } from '../api.js';
@@ -18,7 +18,9 @@ import { useDesign } from '../design.js';
18
18
  import { cn } from '../lib/cn.js';
19
19
  import { useLiveRecord } from '../live.js';
20
20
  import { useToast } from '../shell/toast.js';
21
+ import { StatusChip } from './StatusChip.js';
21
22
  import { PrefetchedNames, useRowNames } from './widgets.js';
23
+ const pick = (row, keys) => Object.fromEntries(keys.filter((k) => k in row).map((k) => [k, row[k]]));
22
24
  /** A line's values as the server takes them: many2many as `{ set }` (a line's taxes). */
23
25
  function lineValues(values, fields) {
24
26
  const out = {};
@@ -26,6 +28,58 @@ function lineValues(values, fields) {
26
28
  out[key] = fields[key]?.kind === 'many2many' && Array.isArray(value) ? { set: value } : value;
27
29
  return out;
28
30
  }
31
+ /**
32
+ * The one2many and many2many fields a form shows (its lines, its tags): a record's default fields leave them out,
33
+ * so the form asks for them by name.
34
+ */
35
+ const RAIL_KEY = 'kosh.rail';
36
+ /** Whether the record pages' right rail is open (RFC 0018: open by default), remembered per browser. */
37
+ function useRail() {
38
+ const [open, setOpen] = useState(() => {
39
+ try {
40
+ return localStorage.getItem(RAIL_KEY) !== 'closed';
41
+ }
42
+ catch {
43
+ return true;
44
+ }
45
+ });
46
+ return [
47
+ open,
48
+ (next) => {
49
+ setOpen(next);
50
+ try {
51
+ if (next)
52
+ localStorage.removeItem(RAIL_KEY);
53
+ else
54
+ localStorage.setItem(RAIL_KEY, 'closed');
55
+ }
56
+ catch {
57
+ // storage unavailable: the choice lasts for this page
58
+ }
59
+ },
60
+ ];
61
+ }
62
+ function relationFieldsOf(sheet, fields) {
63
+ const out = new Set();
64
+ const walk = (nodes) => {
65
+ for (const node of nodes) {
66
+ if (node.kind === 'lines')
67
+ out.add(node.field);
68
+ else if (node.kind === 'field') {
69
+ const kind = fields[node.field]?.kind;
70
+ if (kind === 'one2many' || kind === 'many2many')
71
+ out.add(node.field);
72
+ }
73
+ else if (node.kind === 'group')
74
+ walk(node.children);
75
+ else if (node.kind === 'notebook')
76
+ for (const page of node.pages)
77
+ walk(page.children);
78
+ }
79
+ };
80
+ walk(sheet);
81
+ return [...out];
82
+ }
29
83
  function commandsOf(lines, fields) {
30
84
  return {
31
85
  ...(lines.rows.some((r) => r.id === undefined)
@@ -108,6 +162,7 @@ export function FormView({ served, recordId, defaults, onSaved, onBack, crumbs,
108
162
  const toast = useToast();
109
163
  const queryClient = useQueryClient();
110
164
  const formId = useId();
165
+ const [railOpen, setRailOpen] = useRail();
111
166
  const route = useParams({ strict: false });
112
167
  const view = served.view;
113
168
  if (view.type !== 'form')
@@ -130,7 +185,24 @@ export function FormView({ served, recordId, defaults, onSaved, onBack, crumbs,
130
185
  },
131
186
  etag: null,
132
187
  }
133
- : api.get(model, recordId),
188
+ : (async () => {
189
+ // The defaults (what conditions and the header read), then the lines and tags the form shows.
190
+ const relations = relationFieldsOf(view.sheet, fields);
191
+ const [record, related] = await Promise.all([
192
+ api.get(model, recordId),
193
+ relations.length === 0 ? null : api.get(model, recordId, relations),
194
+ ]);
195
+ return related === null
196
+ ? record
197
+ : {
198
+ ...record,
199
+ record: {
200
+ ...related.record,
201
+ ...record.record,
202
+ ...pick(related.record, relations),
203
+ },
204
+ };
205
+ })(),
134
206
  });
135
207
  // Proposed values (from a document) are unsaved changes: Save, Discard and the leave guard apply to them.
136
208
  // Proposed lines of a one2many are new rows of its line editor.
@@ -374,13 +446,84 @@ export function FormView({ served, recordId, defaults, onSaved, onBack, crumbs,
374
446
  (state === undefined || spec.from.includes(String(state))) &&
375
447
  visible(b.visibleWhen));
376
448
  });
449
+ // The rail (RFC 0018 §4.2): the summary (the view's `summary` fields, else its `totals` group), the record's
450
+ // details, and the timeline.
451
+ const totalsGroup = view.summary === undefined
452
+ ? view.sheet.find((n) => n.kind === 'group' && n.id === 'totals')
453
+ : undefined;
454
+ const summaryNodes = view.summary !== undefined
455
+ ? view.summary.map((key) => ({ kind: 'field', id: `summary-${key}`, field: key }))
456
+ : totalsGroup?.kind === 'group'
457
+ ? totalsGroup.children.filter((n) => n.kind === 'field')
458
+ : [];
459
+ const row = (node) => {
460
+ const field = fields[node.field];
461
+ if (field === undefined || !visible(node.visibleWhen))
462
+ return null;
463
+ const Widget = registry.widget(node, field);
464
+ return (_jsxs("div", { className: "flex items-baseline justify-between gap-3 py-1 text-sm", children: [_jsx("dt", { className: "text-muted-foreground", children: node.label ?? field.label }), _jsx("dd", { className: "text-end font-medium tabular-nums", children: _jsx(Widget, { id: `${formId}-rail-${node.id}`, node: node, field: field, value: values[node.field], record: values, readonly: true }) })] }, node.id));
465
+ };
466
+ const detailNodes = ['createdBy', 'createdAt', 'updatedBy', 'updatedAt', 'company']
467
+ .filter((key) => fields[key] !== undefined && values[key] !== undefined && values[key] !== null)
468
+ .map((key) => ({ kind: 'field', id: `detail-${key}`, field: key }));
469
+ const chatter = view.chatter === true && registry.component('chatter') !== undefined && !isNew
470
+ ? registry.component('chatter')
471
+ : undefined;
472
+ const rail = (_jsxs(_Fragment, { children: [summaryNodes.length === 0 ? null : (_jsxs("section", { className: "rounded-lg border bg-surface p-4", "aria-labelledby": `${formId}-summary`, children: [_jsx("h2", { id: `${formId}-summary`, className: "mb-1 text-sm font-semibold", children: t('views.summary') }), _jsx("dl", { className: "divide-y", children: summaryNodes.map(row) })] })), isNew || detailNodes.length === 0 ? null : (_jsxs("section", { className: "rounded-lg border bg-surface p-4", "aria-labelledby": `${formId}-details`, children: [_jsx("h2", { id: `${formId}-details`, className: "mb-1 text-sm font-semibold", children: t('views.details') }), _jsx("dl", { children: detailNodes.map(row) })] })), chatter === undefined
473
+ ? null
474
+ : (() => {
475
+ const Chatter = chatter;
476
+ return (_jsx(Chatter, { node: { kind: 'component', id: 'chatter', component: 'chatter' }, record: record ?? null, model: model, served: served, api: api, version: loaded.data?.etag ?? null, reload: () => void loaded.refetch() }));
477
+ })()] }));
478
+ const showRail = railOpen;
479
+ // The main column: each group, notebook and table of lines a card; loose fields together.
480
+ const sections = [];
481
+ for (const node of view.sheet) {
482
+ if (node === totalsGroup)
483
+ continue;
484
+ const loose = node.kind === 'field' || node.kind === 'component';
485
+ const last = sections[sections.length - 1];
486
+ if (loose && last?.key.startsWith('loose-') === true)
487
+ last.nodes.push(node);
488
+ else
489
+ sections.push({ key: loose ? `loose-${node.id}` : node.id, nodes: [node] });
490
+ }
491
+ // The key facts under the title: the view's `subtitle`, else the first many2one, date and monetary fields.
492
+ const flatFields = (nodes) => nodes.flatMap((n) => n.kind === 'field'
493
+ ? [n]
494
+ : n.kind === 'group'
495
+ ? flatFields(n.children)
496
+ : n.kind === 'notebook'
497
+ ? n.pages.flatMap((p) => flatFields(p.children))
498
+ : []);
499
+ const facts = isNew
500
+ ? []
501
+ : view.subtitle !== undefined
502
+ ? view.subtitle
503
+ .filter((key) => fields[key] !== undefined)
504
+ .map((key) => ({ kind: 'field', id: `fact-${key}`, field: key }))
505
+ : (() => {
506
+ const candidates = flatFields(view.sheet).filter((n) => n.field !== statusbar &&
507
+ values[n.field] !== null &&
508
+ values[n.field] !== undefined &&
509
+ values[n.field] !== '');
510
+ const first = (kinds) => candidates.find((n) => kinds.includes(fields[n.field]?.kind ?? ''));
511
+ // The total, when the summary has one; else its first amount.
512
+ const totals = summaryNodes.find((n) => n.field === 'amountTotal') ??
513
+ summaryNodes.find((n) => fields[n.field]?.kind === 'monetary');
514
+ return [first(['many2one']), first(['date']), totals ?? first(['monetary'])].filter((n) => n !== undefined);
515
+ })();
377
516
  const named = [values['displayName'], values['name']].find((v) => typeof v === 'string' && v.trim() !== '');
378
517
  const title = named === undefined ? (isNew ? t('views.new') : `#${recordId}`) : String(named);
379
- return (_jsxs(PrefetchedNames.Provider, { value: names, children: [_jsxs("form", { "aria-label": title, "data-kosh-form": true, className: "grid gap-4", onSubmit: (e) => {
518
+ return (_jsxs(PrefetchedNames.Provider, { value: names, children: [_jsxs("form", { "aria-label": title, "data-kosh-form": true, className: cn('grid gap-4', !isNew && 'kosh-quiet'), onSubmit: (e) => {
380
519
  e.preventDefault();
381
520
  if (dirty && !locked)
382
521
  save.mutate();
383
- }, children: [_jsxs("div", { className: "sticky top-12 z-20 -mx-4 flex flex-wrap items-center justify-between gap-2 border-b bg-background/95 px-4 py-2 backdrop-blur", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx(Button, { type: "button", variant: "ghost", size: "icon", "aria-label": t('views.back'), "data-kosh-back": true, onClick: onBack, children: _jsx(ArrowLeft, { className: "rtl:-scale-x-100", "aria-hidden": true }) }), _jsxs("div", { className: "min-w-0", children: [crumbs === undefined || crumbs.length === 0 ? null : (_jsx("nav", { "aria-label": t('views.breadcrumbs'), children: _jsx("ol", { className: "flex flex-wrap items-center gap-1 text-xs text-muted-foreground", children: crumbs.map((c, i) => (_jsxs("li", { className: "flex items-center gap-1", children: [c.onClick === undefined ? (_jsx("span", { children: c.label })) : (_jsx("button", { type: "button", className: "hover:text-foreground hover:underline", onClick: c.onClick, children: c.label })), _jsx(ChevronRight, { className: "size-3 rtl:-scale-x-100", "aria-hidden": true })] }, i))) }) })), _jsx("h1", { className: "truncate text-lg font-semibold", children: title })] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [live.others.length === 0 ? null : (_jsx("ul", { className: "flex items-center -space-x-1", "aria-label": t('views.viewing', {
522
+ }, children: [_jsxs("div", { className: "sticky top-12 z-20 -mx-4 flex flex-wrap items-center justify-between gap-2 border-b bg-background/95 px-4 py-2 backdrop-blur", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx(Button, { type: "button", variant: "ghost", size: "icon", "aria-label": t('views.back'), "data-kosh-back": true, onClick: onBack, children: _jsx(ArrowLeft, { className: "rtl:-scale-x-100", "aria-hidden": true }) }), _jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-2.5", children: [_jsx("h1", { className: "truncate text-xl font-semibold", children: title }), statusbar === undefined || fields[statusbar] === undefined || isNew ? null : (_jsx(StatusChip, { field: fields[statusbar], value: state, transitions: served.transitions.filter((tr) => tr.field === statusbar) })), locked && !isNew ? (_jsx("span", { className: "rounded-full border px-2 py-0.5 text-xs text-muted-foreground", children: t('views.readonly') })) : null] }), facts.length === 0 ? null : (_jsx("p", { className: "mt-0.5 flex flex-wrap items-center gap-x-2 text-sm text-muted-foreground", children: facts.map((node, i) => {
523
+ const field = fields[node.field];
524
+ const Widget = registry.widget(node, field);
525
+ return (_jsxs("span", { className: "flex items-center gap-2", title: field.label, children: [i === 0 ? null : _jsx("span", { "aria-hidden": true, children: "\u00B7" }), _jsxs("span", { className: "sr-only", children: [field.label, ": "] }), _jsx(Widget, { id: `${formId}-fact-${node.id}`, node: node, field: field, value: values[node.field], record: values, readonly: true })] }, node.id));
526
+ }) }))] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [live.others.length === 0 ? null : (_jsx("ul", { className: "flex items-center -space-x-1", "aria-label": t('views.viewing', {
384
527
  names: live.others.map((u) => u.name).join(', '),
385
528
  }), title: t('views.viewing', { names: live.others.map((u) => u.name).join(', ') }), children: live.others.slice(0, 4).map((u) => (_jsx("li", { className: "flex size-6 items-center justify-center rounded-full border-2 border-surface bg-accent text-[10px] font-semibold text-accent-foreground", children: u.name
386
529
  .split(/\s+/)
@@ -390,23 +533,20 @@ export function FormView({ served, recordId, defaults, onSaved, onBack, crumbs,
390
533
  setChanges({});
391
534
  setLines(new Map());
392
535
  setProblem(null);
393
- }, children: t('views.discard') }), _jsx(Button, { type: "submit", variant: "primary", busy: save.isPending, "data-kosh-save": true, children: t('views.save') })] })) : null, !isNew && served.operations.unlink ? (_jsxs(Menu, { children: [_jsx(MenuTrigger, { asChild: true, children: _jsx(Button, { type: "button", variant: "ghost", size: "icon", "aria-label": "Actions", children: _jsx(MoreHorizontal, { "aria-hidden": true }) }) }), _jsx(MenuContent, { children: _jsxs(MenuItem, { onSelect: () => setDeleting(true), children: [_jsx(Trash2, { className: "size-4", "aria-hidden": true }), t('views.delete')] }) })] })) : null] })] }), live.changedByOther ? (_jsx(Alert, { tone: "warning", action: _jsx(Button, { type: "button", size: "sm", onClick: () => {
394
- setChanges({});
395
- setLines(new Map());
396
- live.dismiss();
397
- void loaded.refetch();
398
- }, children: t('views.reload') }), children: t('views.changedByOther') })) : null, problem === null ? null : (_jsx(Alert, { tone: problem.kind === 'stale-record' ? 'warning' : 'danger', action: problem.kind === 'stale-record' ? (_jsx(Button, { type: "button", size: "sm", onClick: () => {
399
- setChanges({});
400
- setProblem(null);
401
- void loaded.refetch();
402
- }, children: t('views.reload') })) : undefined, children: problem.kind === 'stale-record' ? t('views.stale') : problem.message })), warnings.map((w) => (_jsx(Alert, { tone: "info", children: w }, w))), statusbar === undefined && buttons.length === 0 ? null : (_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3 rounded-lg border bg-surface px-3 py-2", children: [_jsx("div", { className: "flex flex-wrap gap-2", children: buttons.map((b) => (_jsx(Button, { type: "button", size: "sm", variant: b.primary === true ? 'primary' : 'secondary', busy: transition.isPending && transition.variables === b.transition, onClick: () => b.confirm === undefined ? transition.mutate(b.transition) : setConfirming(b), children: b.label ?? b.transition }, b.id))) }), statusbar === undefined ? null : (_jsx("ol", { "aria-label": fields[statusbar]?.label ?? statusbar, className: "flex flex-wrap items-center gap-1 text-xs", children: (fields[statusbar]?.options ?? []).map((o) => (_jsx("li", { "aria-current": o.value === state ? 'step' : undefined, className: cn('rounded-full px-2.5 py-1', o.value === state
403
- ? 'bg-primary text-primary-foreground'
404
- : 'bg-muted text-muted-foreground'), children: o.label }, o.value))) }))] })), _jsxs("div", { className: "grid gap-6 rounded-lg border bg-surface p-5", children: [locked && !isNew ? (_jsx("p", { className: "text-xs text-muted-foreground", children: t('views.readonly') })) : null, _jsx("div", { className: "grid gap-x-6 gap-y-4 sm:grid-cols-2", children: renderNodes(view.sheet) })] }), view.chatter === true && registry.component('chatter') !== undefined && !isNew
405
- ? (() => {
406
- const Chatter = registry.component('chatter');
407
- return (_jsx(Chatter, { node: { kind: 'component', id: 'chatter', component: 'chatter' }, record: record ?? null, model: model, served: served, api: api, version: loaded.data?.etag ?? null, reload: () => void loaded.refetch() }));
408
- })()
409
- : null] }), _jsx(Dialog, { open: confirming !== null, onOpenChange: (open) => !open && setConfirming(null), title: confirming?.label ?? confirming?.transition ?? '', description: confirming?.confirm ?? '', footer: _jsxs(_Fragment, { children: [_jsx(Button, { onClick: () => setConfirming(null), children: t('views.no') }), _jsx(Button, { variant: "primary", onClick: () => {
536
+ }, children: t('views.discard') }), _jsx(Button, { type: "submit", variant: "primary", busy: save.isPending, "data-kosh-save": true, children: t('views.save') })] })) : null, _jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "hidden xl:inline-flex", "aria-pressed": railOpen, "aria-label": railOpen ? t('views.hidePanel') : t('views.showPanel'), title: railOpen ? t('views.hidePanel') : t('views.showPanel'), onClick: () => setRailOpen(!railOpen), children: _jsx(PanelRight, { className: "rtl:-scale-x-100", "aria-hidden": true }) }), !isNew && served.operations.unlink ? (_jsxs(Menu, { children: [_jsx(MenuTrigger, { asChild: true, children: _jsx(Button, { type: "button", variant: "ghost", size: "icon", "aria-label": "Actions", children: _jsx(MoreHorizontal, { "aria-hidden": true }) }) }), _jsx(MenuContent, { children: _jsxs(MenuItem, { onSelect: () => setDeleting(true), children: [_jsx(Trash2, { className: "size-4", "aria-hidden": true }), t('views.delete')] }) })] })) : null] })] }), _jsxs("div", { className: cn('grid gap-4', showRail && 'xl:grid-cols-[minmax(0,1fr)_20rem] xl:items-start'), children: [_jsxs("div", { className: "grid min-w-0 content-start gap-4", children: [live.changedByOther ? (_jsx(Alert, { tone: "warning", action: _jsx(Button, { type: "button", size: "sm", onClick: () => {
537
+ setChanges({});
538
+ setLines(new Map());
539
+ live.dismiss();
540
+ void loaded.refetch();
541
+ }, children: t('views.reload') }), children: t('views.changedByOther') })) : null, problem === null ? null : (_jsx(Alert, { tone: problem.kind === 'stale-record' ? 'warning' : 'danger', action: problem.kind === 'stale-record' ? (_jsx(Button, { type: "button", size: "sm", onClick: () => {
542
+ setChanges({});
543
+ setProblem(null);
544
+ void loaded.refetch();
545
+ }, children: t('views.reload') })) : undefined, children: problem.kind === 'stale-record' ? t('views.stale') : problem.message })), warnings.map((w) => (_jsx(Alert, { tone: "info", children: w }, w))), buttons.length === 0 ? null : (_jsx("div", { role: "toolbar", "aria-label": t('views.recordActions'), className: "flex flex-wrap gap-2", children: _jsx("div", { className: "contents", children: buttons.map((b, i) => (_jsx(Button, { type: "button", size: "sm", variant: b.primary === true || (i === 0 && !buttons.some((x) => x.primary === true))
546
+ ? 'primary'
547
+ : 'secondary', busy: transition.isPending && transition.variables === b.transition, onClick: () => b.confirm === undefined ? transition.mutate(b.transition) : setConfirming(b), children: b.label ?? b.transition }, b.id))) }) })), sections.map((section) => (_jsx("section", { className: "rounded-lg border bg-surface p-5", children: _jsx("div", { className: "grid gap-x-6 gap-y-4 sm:grid-cols-2", children: renderNodes(section.nodes) }) }, section.key))), showRail ? null : rail] }), showRail ? (_jsx("aside", { "aria-label": t('views.recordPanel'),
548
+ // Beside the record when wide; under it otherwise (one grid column).
549
+ className: "grid content-start gap-4 xl:sticky xl:top-28", children: rail })) : null] })] }), _jsx(Dialog, { open: confirming !== null, onOpenChange: (open) => !open && setConfirming(null), title: confirming?.label ?? confirming?.transition ?? '', description: confirming?.confirm ?? '', footer: _jsxs(_Fragment, { children: [_jsx(Button, { onClick: () => setConfirming(null), children: t('views.no') }), _jsx(Button, { variant: "primary", onClick: () => {
410
550
  if (confirming !== null)
411
551
  transition.mutate(confirming.transition);
412
552
  setConfirming(null);
@@ -1,4 +1,6 @@
1
1
  import { type ReactElement } from 'react';
2
2
  import type { ViewProps } from '../registry.js';
3
3
  /** The default list renderer. */
4
+ /** A sum's label: "Total Credit limit", but "Total" alone for a column already called "Total" (no "Total Total"). */
5
+ export declare function totalLabel(word: string, label: string): string;
4
6
  export declare function ListView(props: ViewProps): ReactElement;