@cosmicdrift/kumiko-renderer 0.384.0 → 0.386.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 (98) hide show
  1. package/dist/app/action-form-body.d.ts +50 -0
  2. package/dist/app/action-form-body.js +275 -0
  3. package/dist/app/client-actions.js +2 -2
  4. package/dist/app/config-edit-body.d.ts +9 -0
  5. package/dist/app/config-edit-body.js +340 -0
  6. package/dist/app/entity-edit-prefill.d.ts +27 -0
  7. package/dist/app/entity-edit-prefill.js +377 -0
  8. package/dist/app/entity-edit-screen.d.ts +11 -0
  9. package/dist/app/entity-edit-screen.js +395 -0
  10. package/dist/app/entity-list-screen.d.ts +10 -0
  11. package/dist/app/entity-list-screen.js +547 -0
  12. package/dist/app/kumiko-screen.d.ts +1 -36
  13. package/dist/app/kumiko-screen.js +16 -2855
  14. package/dist/app/projection-detail-body.d.ts +12 -0
  15. package/dist/app/projection-detail-body.js +464 -0
  16. package/dist/app/projection-detail-shim.d.ts +1 -1
  17. package/dist/app/projection-detail-shim.js +5 -1
  18. package/dist/app/projection-list-body.d.ts +10 -0
  19. package/dist/app/projection-list-body.js +245 -0
  20. package/dist/app/projection-list-shim.d.ts +2 -1
  21. package/dist/app/projection-list-shim.js +4 -0
  22. package/dist/app/reference-facet-bridge.js +2 -2
  23. package/dist/app/row-actions.js +3 -4
  24. package/dist/app/screen-shared.d.ts +72 -0
  25. package/dist/app/screen-shared.js +325 -0
  26. package/dist/app/secret-mint-body.js +3 -3
  27. package/dist/components/edit-mounts.d.ts +33 -0
  28. package/dist/components/edit-mounts.js +53 -0
  29. package/dist/components/error-boundary.js +2 -2
  30. package/dist/components/reference-create-dialog.js +1 -1
  31. package/dist/components/related-list-section.js +29 -6
  32. package/dist/components/render-edit-helpers.d.ts +29 -0
  33. package/dist/components/render-edit-helpers.js +121 -0
  34. package/dist/components/render-edit.d.ts +1 -4
  35. package/dist/components/render-edit.js +41 -443
  36. package/dist/components/render-field.js +3 -4
  37. package/dist/components/render-list.d.ts +1 -1
  38. package/dist/components/render-list.js +2 -2
  39. package/dist/components/use-wizard-form-draft.d.ts +51 -0
  40. package/dist/components/use-wizard-form-draft.js +291 -0
  41. package/dist/dev-console.d.ts +3 -0
  42. package/dist/dev-console.js +15 -0
  43. package/dist/headless/contracts/primitives.js +14 -17
  44. package/dist/headless/format/index.js +3 -3
  45. package/dist/headless/view-model/edit.d.ts +1 -1
  46. package/dist/headless/view-model/edit.js +9 -16
  47. package/dist/headless/view-model/list.d.ts +1 -1
  48. package/dist/headless/view-model/list.js +3 -8
  49. package/dist/index.d.ts +2 -1
  50. package/dist/index.js +5 -5
  51. package/dist/live/error-mapping.js +3 -4
  52. package/dist/web/app/create-app.js +5 -8
  53. package/dist/web/app/dashboard-body.js +4 -599
  54. package/dist/web/app/dashboard-panels.d.ts +53 -0
  55. package/dist/web/app/dashboard-panels.js +373 -0
  56. package/dist/web/app/dashboard-shared.d.ts +90 -0
  57. package/dist/web/app/dashboard-shared.js +171 -0
  58. package/dist/web/app/dashboard-text.d.ts +6 -0
  59. package/dist/web/app/dashboard-text.js +65 -0
  60. package/dist/web/layout/nav-tree.js +3 -4
  61. package/dist/web/layout/target-resolver-stub.js +2 -2
  62. package/dist/web/primitives/banner.d.ts +4 -0
  63. package/dist/web/primitives/banner.js +28 -0
  64. package/dist/web/primitives/button.d.ts +7 -0
  65. package/dist/web/primitives/button.js +48 -0
  66. package/dist/web/primitives/card-styles.d.ts +8 -0
  67. package/dist/web/primitives/card-styles.js +18 -0
  68. package/dist/web/primitives/card.d.ts +4 -0
  69. package/dist/web/primitives/card.js +41 -0
  70. package/dist/web/primitives/contexts.d.ts +22 -0
  71. package/dist/web/primitives/contexts.js +50 -0
  72. package/dist/web/primitives/data-table-cell.d.ts +76 -0
  73. package/dist/web/primitives/data-table-cell.js +403 -0
  74. package/dist/web/primitives/data-table.d.ts +4 -0
  75. package/dist/web/primitives/data-table.js +279 -0
  76. package/dist/web/primitives/default-form.d.ts +4 -0
  77. package/dist/web/primitives/default-form.js +79 -0
  78. package/dist/web/primitives/facet-filter.d.ts +14 -0
  79. package/dist/web/primitives/facet-filter.js +39 -0
  80. package/dist/web/primitives/field.d.ts +63 -0
  81. package/dist/web/primitives/field.js +181 -0
  82. package/dist/web/primitives/form.d.ts +53 -0
  83. package/dist/web/primitives/form.js +252 -0
  84. package/dist/web/primitives/index.d.ts +7 -30
  85. package/dist/web/primitives/index.js +27 -2006
  86. package/dist/web/primitives/input.d.ts +4 -0
  87. package/dist/web/primitives/input.js +166 -0
  88. package/dist/web/primitives/layout-elements.d.ts +14 -0
  89. package/dist/web/primitives/layout-elements.js +194 -0
  90. package/dist/web/primitives/modal.js +1 -1
  91. package/dist/web/primitives/pager.d.ts +22 -0
  92. package/dist/web/primitives/pager.js +89 -0
  93. package/dist/web/primitives/row-actions.d.ts +13 -0
  94. package/dist/web/primitives/row-actions.js +178 -0
  95. package/dist/web/primitives/secret-reveal.d.ts +4 -0
  96. package/dist/web/primitives/secret-reveal.js +67 -0
  97. package/package.json +5 -5
  98. package/src/changes.json +6 -0
@@ -0,0 +1,171 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { createContext, useCallback, useContext, useEffect, useRef, useState, } from "react";
3
+ import { currencyDecimals } from "../../headless/index.js";
4
+ import { dispatcherErrorText, useLocale, usePrimitives, useQuery, useTranslation, } from "../../index.js";
5
+ import { resolveSafeLocale } from "../primitives/locale-number.js";
6
+ import { formatMoney } from "../primitives/money-input.js";
7
+ import { Skeleton } from "../ui/skeleton.js";
8
+ import { EmptyState, LoadingState } from "../widgets/states.js";
9
+ import { useRelativeTimeFormatter } from "./relative-time.js";
10
+ const SKELETON_SEGMENT_WIDTHS = ["100%", "72%", "48%"];
11
+ export const WIDE_PANEL = "sm:col-span-2 lg:col-span-4";
12
+ export const HALF_PANEL = "sm:col-span-2 lg:col-span-2";
13
+ const CURRENCY_CODE_PATTERN = /^[A-Za-z]{3}$/;
14
+ export const HOUR_MS = 3_600_000;
15
+ export const DAY_MS = 24 * HOUR_MS;
16
+ const SKELETON_BAR_HEIGHTS = ["40%", "65%", "50%", "85%", "60%", "75%", "45%", "70%"];
17
+ export function panelPayload(panel, screenParams) {
18
+ return {
19
+ ...(panel.ignoreScreenFilter === true ? {} : screenParams.filterParams),
20
+ ...screenParams.rangeParams,
21
+ ...screenParams.recordParams,
22
+ ...panel.params,
23
+ };
24
+ }
25
+ export const DashboardQueryContext = createContext({
26
+ refreshIntervalMs: undefined,
27
+ register: () => () => { },
28
+ });
29
+ export function useRefetchRegistry() {
30
+ const registered = useRef(new Set());
31
+ const register = useCallback((refetch) => {
32
+ registered.current.add(refetch);
33
+ return () => {
34
+ registered.current.delete(refetch);
35
+ };
36
+ }, []);
37
+ const refetchRegistered = useCallback(async () => {
38
+ await Promise.all([...registered.current].map((refetch) => refetch()));
39
+ }, []);
40
+ return { register, refetchRegistered };
41
+ }
42
+ /** Live query of a panel or gate that takes part in the dashboard's refresh:
43
+ * polls at the screen's interval and is refetched by `refetchAll` after an
44
+ * action. A disabled query registers nothing. */
45
+ export function useDashboardQuery(type, payload, options = {}) {
46
+ const { refreshIntervalMs, register } = useContext(DashboardQueryContext);
47
+ const enabled = options.enabled ?? true;
48
+ const result = useQuery(type, payload, {
49
+ live: true,
50
+ enabled,
51
+ ...(refreshIntervalMs !== undefined && { refetchIntervalMs: refreshIntervalMs }),
52
+ });
53
+ const { refetch } = result;
54
+ useEffect(() => (enabled ? register(refetch) : undefined), [enabled, register, refetch]);
55
+ return result;
56
+ }
57
+ export function useDashboardFormats() {
58
+ const resolver = useLocale();
59
+ const locale = resolver.locale();
60
+ const timeZone = resolver.timeZone();
61
+ const formatRelative = useRelativeTimeFormatter(locale);
62
+ const number = new Intl.NumberFormat(locale, { maximumFractionDigits: 1 });
63
+ // Without the significant-digit cap, values below the locale's first
64
+ // abbreviation step keep a decimal ("298.989,9 €" in de); lessPrecision
65
+ // picks whichever rule rounds more, so that becomes "299.000 €" while
66
+ // "1,5 Mio. €" keeps its one decimal.
67
+ const compactOptions = {
68
+ notation: "compact",
69
+ maximumFractionDigits: 1,
70
+ maximumSignificantDigits: 3,
71
+ roundingPriority: "lessPrecision",
72
+ };
73
+ const compactNumber = new Intl.NumberFormat(locale, compactOptions);
74
+ const percent = new Intl.NumberFormat(locale, { style: "percent", maximumFractionDigits: 0 });
75
+ const day = new Intl.DateTimeFormat(locale, { day: "2-digit", month: "2-digit", timeZone });
76
+ const month = new Intl.DateTimeFormat(locale, { month: "2-digit", year: "numeric", timeZone });
77
+ const hour = new Intl.DateTimeFormat(locale, { hour: "2-digit", minute: "2-digit", timeZone });
78
+ const dateTime = new Intl.DateTimeFormat(locale, {
79
+ dateStyle: "medium",
80
+ timeStyle: "short",
81
+ timeZone,
82
+ });
83
+ const medium = new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeZone });
84
+ const formatMinor = (amountMinor, currency, fractionDigits) => formatMoney(amountMinor, currency, locale, fractionDigits);
85
+ return {
86
+ timeZone,
87
+ formatMedium: (atMs) => medium.format(atMs),
88
+ formatMinor,
89
+ formatRelative,
90
+ formatPanelValue: (valueFormat) => valueFormat === undefined
91
+ ? (value) => number.format(value)
92
+ : (value) => formatMinor(value, valueFormat.currency, valueFormat.fractionDigits),
93
+ formatPanelAxisValue: (valueFormat) => {
94
+ if (valueFormat === undefined || !CURRENCY_CODE_PATTERN.test(valueFormat.currency)) {
95
+ return (value) => compactNumber.format(value);
96
+ }
97
+ const compactMoney = new Intl.NumberFormat(resolveSafeLocale(locale), {
98
+ ...compactOptions,
99
+ style: "currency",
100
+ currency: valueFormat.currency,
101
+ });
102
+ const decimals = currencyDecimals(valueFormat.currency);
103
+ return (value) => compactMoney.format(value / 10 ** decimals);
104
+ },
105
+ formatNumber: (value) => number.format(value),
106
+ formatPercent: (fraction) => percent.format(fraction),
107
+ formatDay: (atMs) => day.format(atMs),
108
+ formatMonth: (atMs) => month.format(atMs),
109
+ formatHour: (atMs) => hour.format(atMs),
110
+ formatDateTime: (atMs) => dateTime.format(atMs),
111
+ };
112
+ }
113
+ export function PanelShell({ testId, title, subtitle, action, children, }) {
114
+ return (_jsxs("section", { "data-testid": testId, className: "flex h-full flex-col rounded-lg border border-border bg-card text-card-foreground", children: [_jsxs("header", { className: "flex min-h-11 shrink-0 flex-wrap items-center justify-between gap-x-3 gap-y-2 border-b border-border-row px-4 py-2", children: [_jsxs("div", { className: "flex min-w-0 flex-col gap-0.5", children: [_jsx("h2", { className: "text-sm font-semibold break-words", children: title }), subtitle !== undefined && _jsx("p", { className: "text-xs text-muted-foreground", children: subtitle })] }), action !== undefined && _jsx("div", { className: "shrink-0", children: action })] }), _jsx("div", { className: "grow p-4", children: children })] }));
115
+ }
116
+ export function PanelError({ label, error, onRetry, testId, }) {
117
+ const { Button } = usePrimitives();
118
+ const t = useTranslation();
119
+ return (_jsxs("div", { role: "alert", "data-testid": testId, className: "flex flex-col items-start gap-2 rounded-lg border border-dashed border-status-bad/40 px-4 py-6 text-sm", children: [_jsx("div", { className: "font-medium text-status-bad", children: t("kumiko.dashboard.panel.error.title", { label }) }), _jsx("div", { className: "text-muted-foreground", children: dispatcherErrorText(error, t) }), _jsx(Button, { variant: "secondary", onClick: onRetry, children: t("kumiko.dashboard.panel.error.retry") }), _jsx("div", { className: "text-xs text-muted-foreground", children: t("kumiko.dashboard.panel.error.isolated") })] }));
120
+ }
121
+ function PanelSkeleton({ shape }) {
122
+ const t = useTranslation();
123
+ if (shape === "rows")
124
+ return _jsx(LoadingState, { rows: 3 });
125
+ if (shape === "segments") {
126
+ return (_jsx("output", { "aria-label": t("kumiko.widget.loading"), className: "flex flex-col gap-4", children: SKELETON_SEGMENT_WIDTHS.map((width) => (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Skeleton, { className: "h-3 w-24" }), _jsx(Skeleton, { className: "h-2.5 rounded-full", style: { width } })] }, width))) }));
127
+ }
128
+ return (_jsx("output", { "aria-label": t("kumiko.widget.loading"), className: "flex h-40 items-end gap-1", children: SKELETON_BAR_HEIGHTS.map((height) => (_jsx(Skeleton, { className: "flex-1", style: { height } }, height))) }));
129
+ }
130
+ export function QueryPanel({ panel, label, subtitle, action, screenParams, translate, skeleton, isEmpty, children, }) {
131
+ const formats = useDashboardFormats();
132
+ const { data, error, loading, refetch } = useDashboardQuery(panel.query, panelPayload(panel, screenParams));
133
+ useReportPanelLoaded(data);
134
+ let body;
135
+ if (loading && data === null) {
136
+ body = _jsx(PanelSkeleton, { shape: skeleton });
137
+ }
138
+ else if (error !== null) {
139
+ body = _jsx(PanelError, { label: label, error: error, onRetry: () => void refetch() });
140
+ }
141
+ else if (data === null || isEmpty(data)) {
142
+ body = (_jsx(EmptyState, { title: translate(panel.emptyLabel ?? "kumiko.list.no-entries"), description: panel.emptyHint !== undefined ? translate(panel.emptyHint) : undefined }));
143
+ }
144
+ else {
145
+ body = children(data, formats);
146
+ }
147
+ return (_jsx(PanelShell, { testId: `dashboard-panel-${panel.id}`, title: label, subtitle: subtitle, action: action, children: body }));
148
+ }
149
+ // Starts at render time and restarts whenever the merged query params change;
150
+ // panels additionally report each successful (re)load, so live refetches and
151
+ // retries keep the stamp current.
152
+ export function useLoadedAt(screenParams) {
153
+ const paramsKey = JSON.stringify([
154
+ screenParams.filterParams,
155
+ screenParams.rangeParams,
156
+ screenParams.recordParams,
157
+ ]);
158
+ const [loaded, setLoaded] = useState(() => ({ paramsKey, atMs: Date.now() }));
159
+ if (loaded.paramsKey !== paramsKey)
160
+ setLoaded({ paramsKey, atMs: Date.now() });
161
+ const reportLoaded = useCallback((atMs) => setLoaded((prev) => (prev.atMs >= atMs ? prev : { ...prev, atMs })), []);
162
+ return { loadedAtMs: loaded.atMs, reportLoaded };
163
+ }
164
+ export const PanelLoadedContext = createContext(() => { });
165
+ export function useReportPanelLoaded(data) {
166
+ const reportLoaded = useContext(PanelLoadedContext);
167
+ useEffect(() => {
168
+ if (data !== null)
169
+ reportLoaded(Date.now());
170
+ }, [data, reportLoaded]);
171
+ }
@@ -0,0 +1,6 @@
1
+ import type { Translate } from "../../headless/index.ts";
2
+ import type { DashboardFormats } from "./dashboard-shared.tsx";
3
+ export declare function isRecord(value: unknown): value is Readonly<Record<string, unknown>>;
4
+ /** Plain strings pass through; DashboardI18nText is translated (nested params first).
5
+ * Anything else yields undefined so callers fall back to their own placeholder. */
6
+ export declare function resolveDashboardText(value: unknown, translate: Translate, formats: DashboardFormats): string | undefined;
@@ -0,0 +1,65 @@
1
+ const MAX_TEXT_PARAM_DEPTH = 4;
2
+ export function isRecord(value) {
3
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4
+ }
5
+ function isDashboardI18nText(value) {
6
+ return isRecord(value) && typeof value["i18nKey"] === "string";
7
+ }
8
+ function isMoneyParam(value) {
9
+ return (isRecord(value) &&
10
+ value["kind"] === "money" &&
11
+ typeof value["amountMinor"] === "number" &&
12
+ typeof value["currency"] === "string");
13
+ }
14
+ function isDateParam(value) {
15
+ return isRecord(value) && value["kind"] === "date" && typeof value["atMs"] === "number";
16
+ }
17
+ function isRelativeTimeParam(value) {
18
+ return isRecord(value) && value["kind"] === "relativeTime" && typeof value["atMs"] === "number";
19
+ }
20
+ function isTextParam(value) {
21
+ return (typeof value === "string" ||
22
+ typeof value === "number" ||
23
+ isDashboardI18nText(value) ||
24
+ isMoneyParam(value) ||
25
+ isDateParam(value) ||
26
+ isRelativeTimeParam(value));
27
+ }
28
+ function resolveTextParam(param, translate, formats, depth) {
29
+ if (typeof param === "string" || typeof param === "number")
30
+ return param;
31
+ if (isMoneyParam(param))
32
+ return formats.formatMinor(param.amountMinor, param.currency);
33
+ if (isDateParam(param))
34
+ return formats.formatMedium(param.atMs);
35
+ if (isRelativeTimeParam(param))
36
+ return formats.formatRelative(param.atMs);
37
+ if (isDashboardI18nText(param)) {
38
+ // Too deep: show the key instead of recursing further.
39
+ return depth >= MAX_TEXT_PARAM_DEPTH
40
+ ? param.i18nKey
41
+ : resolveI18nText(param, translate, formats, depth + 1);
42
+ }
43
+ return "";
44
+ }
45
+ function resolveI18nText(text, translate, formats, depth) {
46
+ if (text.i18nParams === undefined)
47
+ return translate(text.i18nKey);
48
+ const params = {};
49
+ for (const [name, param] of Object.entries(text.i18nParams)) {
50
+ if (isTextParam(param))
51
+ params[name] = resolveTextParam(param, translate, formats, depth);
52
+ }
53
+ return translate(text.i18nKey, params);
54
+ }
55
+ /** Plain strings pass through; DashboardI18nText is translated (nested params first).
56
+ * Anything else yields undefined so callers fall back to their own placeholder. */
57
+ export function resolveDashboardText(value, translate, formats) {
58
+ if (typeof value === "string")
59
+ return value;
60
+ if (typeof value === "number")
61
+ return String(value);
62
+ if (isDashboardI18nText(value))
63
+ return resolveI18nText(value, translate, formats, 1);
64
+ return undefined;
65
+ }
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { ChevronDown, ChevronRight, Plus, Search } from "lucide-react";
3
3
  import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react";
4
+ import { rendererWarn } from "../../dev-console.js";
4
5
  import { resolveNavigation } from "../../headless/index.js";
5
6
  import { createLiveRefetchScheduler } from "../../hooks/live-refetch-scheduler.js";
6
7
  import { lastSegment, toAppSchema, useLiveEvents, useNav, useTranslation } from "../../index.js";
@@ -140,8 +141,7 @@ function NavLeadingIcon({ node, active, expanded = false, label, }) {
140
141
  const NavIcon = iconKey !== undefined && isKnownIcon ? NAV_ICON_LOOKUP[iconKey] : undefined;
141
142
  useEffect(() => {
142
143
  if (iconKey !== undefined && !isKnownIcon) {
143
- // biome-ignore lint/suspicious/noConsole: diagnostic for unregistered nav icon keys
144
- console.warn(`[kumiko] Nav entry "${node.qualifiedName}" references icon "${iconKey}", which is not ` +
144
+ rendererWarn(`[kumiko] Nav entry "${node.qualifiedName}" references icon "${iconKey}", which is not ` +
145
145
  `registered in NAV_ICONS — falling back to the dot indicator. Check the icon key spelling.`);
146
146
  }
147
147
  }, [iconKey, isKnownIcon, node.qualifiedName]);
@@ -316,8 +316,7 @@ function warnTreeActionMisconfigured(label, problem) {
316
316
  if (warnedTreeActionLabels.has(key))
317
317
  return;
318
318
  warnedTreeActionLabels.add(key);
319
- // biome-ignore lint/suspicious/noConsole: dev-warning for a setup error
320
- console.warn(`[kumiko] Nav action "${label}" ${problem}`);
319
+ rendererWarn(`[kumiko] Nav action "${label}" ${problem}`);
321
320
  }
322
321
  // An action button carries screen XOR target (see TreeAction) — `screen`
323
322
  // renders a KumikoLink to the route, `target` a dispatch button. Same
@@ -16,6 +16,7 @@
16
16
  // die URL via nav.setSearchParams UND ruft den globalen dispatchTarget für
17
17
  // Test-Listener-Kompatibilität.
18
18
  import { useCallback } from "react";
19
+ import { rendererDebug } from "../../dev-console.js";
19
20
  import { requestLeave, useNav } from "../../index.js";
20
21
  import { serializeTarget } from "./target-url.js";
21
22
  let testListener;
@@ -31,8 +32,7 @@ export function dispatchTarget(target) {
31
32
  }
32
33
  return;
33
34
  }
34
- // biome-ignore lint/suspicious/noConsole: fallback wenn kein Subscriber registered
35
- console.debug("[NavTree] target dispatched (unhandled)", target);
35
+ rendererDebug("[NavTree] target dispatched (unhandled)", target);
36
36
  }
37
37
  /** Test-Hook: Exklusiver Spy. Returnt cleanup. */
38
38
  export function setDispatchListener(fn) {
@@ -0,0 +1,4 @@
1
+ import type { ReactNode } from "react";
2
+ import type { BannerProps } from "../../index.ts";
3
+ import "../react-css-custom-properties.ts";
4
+ export declare function DefaultBanner({ variant, title, titleTestId, children, actions, padded, testId, id, }: BannerProps): ReactNode;
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from "../lib/cn.js";
3
+ import "../react-css-custom-properties.js";
4
+ import { fieldLabelId } from "./field.js";
5
+ // ---- Banner (shadcn: Alert) ----
6
+ export function DefaultBanner({ variant = "info", title, titleTestId, children, actions, padded, testId, id, }) {
7
+ const isError = variant === "error";
8
+ const isWarning = variant === "warning";
9
+ const isPrimary = variant === "primary";
10
+ // `id` is set when a <Field> wraps this Banner as its control (see
11
+ // BannerProps.id). A <div> isn't labelable, so a <label htmlFor> pointing
12
+ // at it is inert for screen readers — role="group" + aria-labelledby is
13
+ // the actual mechanism, pointed at the label id Field emits (fieldLabelId).
14
+ const bannerRole = id !== undefined ? "group" : isError ? "alert" : undefined;
15
+ const banner = (_jsxs("div", { role: bannerRole, ...(id !== undefined && { "aria-labelledby": fieldLabelId(id) }), "data-testid": testId, "data-variant": variant, className: cn("relative w-full rounded-lg border px-4 py-3 text-sm flex items-center gap-3",
16
+ // Viewport breakpoint like the form footer bar; actions drop below the text on phones.
17
+ isPrimary && "flex-col items-start sm:flex-row sm:items-center", isError
18
+ ? "border-destructive/50 text-destructive bg-destructive/10 dark:border-destructive"
19
+ : isWarning
20
+ ? "border-status-warn/30 bg-status-warn/10 text-status-warn"
21
+ : isPrimary
22
+ ? "border-l-4 border-l-primary bg-card text-card-foreground"
23
+ : "bg-card text-card-foreground"), children: [_jsxs("div", { className: "flex-1", children: [title !== undefined && (_jsx("div", { "data-testid": titleTestId, className: "font-semibold", children: title })), children] }), actions !== undefined && _jsx("div", { "data-slot": "actions", children: actions })] }));
24
+ // Page state: the banner sits alone in main (which no longer has padding).
25
+ // The wrapper adds 24px outer spacing so the banner does not stick
26
+ // edge-to-edge to the sidebar/browser.
27
+ return padded === true ? _jsx("div", { className: "p-6", children: banner }) : banner;
28
+ }
@@ -0,0 +1,7 @@
1
+ import type { IconKey } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ import type { ReactNode } from "react";
3
+ import type { ButtonProps } from "../../index.ts";
4
+ import "../react-css-custom-properties.ts";
5
+ export declare function actionIconFor(icon: IconKey | undefined): IconKey | undefined;
6
+ export declare const SECONDARY_BUTTON_EXTRA_CLASSES = "border-input hover:bg-muted";
7
+ export declare function DefaultButton({ type, onClick, disabled, loading, variant, size, ariaLabel, title, pressed, expanded, width, children, testId, className, ref, icon, iconEnd, dataAttributes, }: ButtonProps): ReactNode;
@@ -0,0 +1,48 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Loader2 } from "lucide-react";
3
+ import { NAV_ICONS } from "../icons.js";
4
+ import { cn } from "../lib/cn.js";
5
+ import { Button as UiButton } from "../ui/button.js";
6
+ import "../react-css-custom-properties.js";
7
+ // ---- Button (vendored shadcn ui/button) ----
8
+ // Contract variant -> shadcn variant: secondary has always been the
9
+ // bordered bg-background look = shadcn's `outline`. primary->default,
10
+ // danger->destructive, link->link (no bg, underline on hover).
11
+ const BUTTON_VARIANT = {
12
+ primary: "default",
13
+ secondary: "outline",
14
+ danger: "destructive",
15
+ link: "link",
16
+ "danger-ghost": "ghost",
17
+ ghost: "ghost",
18
+ };
19
+ const BUTTON_SIZE = {
20
+ sm: "sm",
21
+ md: "default",
22
+ icon: "icon",
23
+ };
24
+ // Closed IconKey vocabulary, checked against the shared NAV_ICONS registry
25
+ // (icons.tsx) — an unknown key (schema data isn't statically typed against
26
+ // IconKey the way this prop is) renders no icon instead of crashing, same
27
+ // fallback shape as fieldIconFor above.
28
+ export function actionIconFor(icon) {
29
+ return icon !== undefined && Object.hasOwn(NAV_ICONS, icon) ? icon : undefined;
30
+ }
31
+ // Shared by DefaultButton's `secondary` variant and triggers that need Radix `asChild`
32
+ // prop forwarding (which DefaultButton does not support), so both stay visually identical.
33
+ export const SECONDARY_BUTTON_EXTRA_CLASSES = "border-input hover:bg-muted";
34
+ export function DefaultButton({ type = "button", onClick, disabled, loading, variant = "primary", size = "md", ariaLabel, title, pressed, expanded, width = "auto", children, testId, className, ref, icon, iconEnd, dataAttributes, }) {
35
+ // The link variant renders text-like (inline link in running text/banner),
36
+ // not as a padded surface; width="full" stretches CTA buttons in cards/panels.
37
+ const resolvedClassName = cn(variant === "link" ? "h-auto px-0 py-0" : "", variant === "danger-ghost"
38
+ ? "text-destructive hover:text-destructive hover:bg-destructive/10"
39
+ : "", variant === "ghost" ? "text-primary hover:text-primary hover:bg-primary/10" : "", variant === "secondary" ? SECONDARY_BUTTON_EXTRA_CLASSES : "", pressed === true
40
+ ? "border-primary/40 bg-accent text-accent-foreground ring-1 ring-primary/30"
41
+ : "", width === "full" ? "w-full" : "", className);
42
+ const IconStart = icon !== undefined ? NAV_ICONS[icon] : undefined;
43
+ const IconEnd = iconEnd !== undefined ? NAV_ICONS[iconEnd] : undefined;
44
+ // Loading swaps the leading icon slot for a spinner and keeps `children` —
45
+ // replacing the label would shift the button's width mid-submit.
46
+ const leading = loading === true ? (_jsx(Loader2, { className: "size-4 animate-spin", "aria-hidden": "true" })) : IconStart !== undefined ? (_jsx(IconStart, { className: "size-4", "aria-hidden": "true" })) : null;
47
+ return (_jsxs(UiButton, { ref: ref, type: type, onClick: onClick, disabled: disabled === true || loading === true, ...dataAttributes, "data-testid": testId, "data-loading": loading === true ? "true" : undefined, variant: BUTTON_VARIANT[variant], size: BUTTON_SIZE[size], "aria-label": ariaLabel, "aria-pressed": pressed, "aria-expanded": expanded, title: title ?? (size === "icon" ? ariaLabel : undefined), className: resolvedClassName, children: [leading, children, IconEnd !== undefined && _jsx(IconEnd, { className: "size-4", "aria-hidden": "true" })] }));
48
+ }
@@ -0,0 +1,8 @@
1
+ import "../react-css-custom-properties.ts";
2
+ export declare const cardSurface: (props?: ({
3
+ radius?: "lg" | "xl" | null | undefined;
4
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
5
+ export declare const cardFooter = "flex flex-wrap items-center justify-end gap-2 py-4";
6
+ export declare const cardFooterInset = "px-[var(--card-padding)]";
7
+ export declare const cardFooterBorder = "border-t bg-muted/50";
8
+ export declare const cardHeaderBorder = "border-b bg-muted/50";
@@ -0,0 +1,18 @@
1
+ import { cva } from "class-variance-authority";
2
+ import "../react-css-custom-properties.js";
3
+ // ---- Card chrome (one definition for form/section/card) ----
4
+ // The one card surface. Dimensions (padding/radius/shadow) come from --card-*
5
+ // CSS tokens (defaults in styles.css) so an app can override them once,
6
+ // centrally, in its styles.css, like the colors. Radius variant:
7
+ // xl = card/screen surface (default, token-driven), lg = "inner" surface.
8
+ export const cardSurface = cva("flex flex-col border bg-card text-card-foreground shadow-[var(--card-shadow)]", {
9
+ variants: { radius: { xl: "rounded-[var(--card-radius)]", lg: "rounded-lg" } },
10
+ defaultVariants: { radius: "xl" },
11
+ });
12
+ // Wraps instead of running off-screen when the row outgrows its container (fw#2528).
13
+ export const cardFooter = "flex flex-wrap items-center justify-end gap-2 py-4";
14
+ export const cardFooterInset = "px-[var(--card-padding)]";
15
+ // /30 read as nearly invisible against the light-theme card (white card,
16
+ // muted at 94% lightness) — /50 keeps the same token, just a stronger step.
17
+ export const cardFooterBorder = "border-t bg-muted/50";
18
+ export const cardHeaderBorder = "border-b bg-muted/50";
@@ -0,0 +1,4 @@
1
+ import type { ReactNode } from "react";
2
+ import type { CardProps } from "../../index.ts";
3
+ import "../react-css-custom-properties.ts";
4
+ export declare function DefaultCard({ slots, options, className, testId, dataAttributes, children, }: CardProps): ReactNode;
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from "../lib/cn.js";
3
+ import { InsideScreenFormColumnContext } from "./metric.js";
4
+ import "../react-css-custom-properties.js";
5
+ import { cardFooter, cardFooterBorder, cardFooterInset, cardSurface } from "./card-styles.js";
6
+ import { FormScreenShell } from "./form.js";
7
+ // Generic card chrome (rounded-xl like the entity card), slot- and
8
+ // options-based so the contract grows additively and consumers never have
9
+ // to migrate.
10
+ export function DefaultCard({ slots, options, className, testId, dataAttributes, children, }) {
11
+ const screenBody = options?.screenBody ?? false;
12
+ const padded = screenBody ? false : (options?.padded ?? true);
13
+ const radius = options?.radius ?? "xl";
14
+ const footerBordered = options?.footerBordered ?? true;
15
+ const footerInset = options?.footerInset ?? true;
16
+ const fillHeight = options?.fillHeight ?? false;
17
+ const framed = screenBody ? false : (options?.framed ?? true);
18
+ const s = slots ?? {};
19
+ const defaultHeader = s.title !== undefined ||
20
+ s.subtitle !== undefined ||
21
+ s.headerContent !== undefined ||
22
+ s.headerActions !== undefined ? (_jsxs("div", { className: cn("flex flex-col gap-3 px-[var(--card-padding)] pt-6 pb-4", fillHeight && "shrink-0"), children: [(s.title !== undefined || s.subtitle !== undefined || s.headerActions !== undefined) && (_jsxs("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [_jsxs("div", { className: "flex flex-col gap-1", children: [s.title !== undefined && (_jsx("h3", { className: "text-base font-semibold leading-none tracking-tight", children: s.title })), s.subtitle !== undefined && (_jsx("p", { className: "text-sm text-muted-foreground", children: s.subtitle }))] }), s.headerActions] })), s.headerContent] })) : null;
23
+ const header = s.header ?? defaultHeader;
24
+ const hasHeader = header !== null && header !== undefined;
25
+ const card = (_jsxs("div", { ...dataAttributes, "data-slot": "card", "data-testid": testId, className: cn(framed ? cardSurface({ radius }) : "flex flex-col",
26
+ // An unframed card has no radius to clip; overflow-hidden would also
27
+ // zero its flex-item min-height and cut off content inside a scroller.
28
+ (framed || fillHeight) && "overflow-hidden",
29
+ // Same "no flex-1" reasoning as DefaultForm's own fillHeight card
30
+ // (fw#2722/#2778): sizes to content and only shrinks (min-h-0) once
31
+ // an ancestor is itself height-constrained.
32
+ fillHeight && "min-h-0 flex flex-col", className), children: [header, children != null && (_jsx("div", { className: cn("grow", screenBody && "flex flex-col gap-6", padded &&
33
+ (hasHeader
34
+ ? "px-[var(--card-padding)] pb-[var(--card-padding)]"
35
+ : "p-[var(--card-padding)]"),
36
+ // The one region allowed to shrink and scroll internally — mirrors
37
+ // FormSections' fillHeight === true && "min-h-0" treatment of the
38
+ // section it wraps.
39
+ fillHeight && "min-h-0 flex flex-col"), children: children })), s.footer !== undefined && s.footer !== null && s.footer !== false && (_jsx("div", { className: cn(cardFooter, footerInset && cardFooterInset, footerBordered && cardFooterBorder, fillHeight && "shrink-0"), children: s.footer }))] }));
40
+ return (_jsx(InsideScreenFormColumnContext.Provider, { value: false, children: screenBody ? _jsx(FormScreenShell, { children: card }) : card }));
41
+ }
@@ -0,0 +1,22 @@
1
+ import { type ReactNode } from "react";
2
+ import type { FormWidth } from "../../index.ts";
3
+ import "../react-css-custom-properties.ts";
4
+ export declare const FieldLayoutContext: import("react").Context<"inline" | "row" | "stacked" | undefined>;
5
+ export declare const ScreenFormContext: import("react").Context<boolean>;
6
+ export declare const DrawerBodyContext: import("react").Context<boolean>;
7
+ export declare const FieldLabelledContext: import("react").Context<boolean>;
8
+ export declare const FieldDescriptionContext: import("react").Context<string | undefined>;
9
+ export declare const InsideFormContext: import("react").Context<boolean>;
10
+ export declare const BareFormContext: import("react").Context<boolean>;
11
+ export declare const EmbeddedFormContext: import("react").Context<boolean>;
12
+ export declare function EmbeddedFormProvider({ children }: {
13
+ children: ReactNode;
14
+ }): ReactNode;
15
+ export declare function BareFormProvider({ children }: {
16
+ children: ReactNode;
17
+ }): ReactNode;
18
+ export declare const ScreenWidthContext: import("react").Context<FormWidth | undefined>;
19
+ export declare function ScreenWidthProvider({ width, children, }: {
20
+ readonly width: FormWidth | undefined;
21
+ readonly children: ReactNode;
22
+ }): ReactNode;
@@ -0,0 +1,50 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext } from "react";
3
+ import "../react-css-custom-properties.js";
4
+ // Boolean optics depend on the surrounding Field layout: layout="inline"
5
+ // (checkbox lists like MultiSelectCheckboxes, the BooleanField widget) keeps
6
+ // the checkbox, the standard form field (layout="stacked"/default) gets the
7
+ // new switch. Pure web-renderer rendering decision, no contract field needed
8
+ // — Field hands its resolved layout down to the nested Input via context.
9
+ export const FieldLayoutContext = createContext("stacked");
10
+ // True inside the sections of a card-less screen form: sections then drop their
11
+ // own horizontal padding, the screen form's scroll surface provides it.
12
+ export const ScreenFormContext = createContext(false);
13
+ export const DrawerBodyContext = createContext(false);
14
+ // aria-labelledby into a Field label that doesn't exist leaves a radiogroup
15
+ // without any accessible name — fall back to the control's name instead.
16
+ export const FieldLabelledContext = createContext(false);
17
+ // The Field description is the control's accessible description; the control
18
+ // is a descendant (not a prop of Field), so the id travels via context. Always
19
+ // provided (undefined without a description) so a nested Field never inherits
20
+ // its parent's description id.
21
+ export const FieldDescriptionContext = createContext(undefined);
22
+ // ---- Form + Section + Grid + Text ----
23
+ // Puts DefaultSection into inner-region mode: the whole form is ONE card,
24
+ // sections are divider-separated parts inside it (shadcn pattern like
25
+ // Shipping/Invoice/Profile). Standalone (outside a form) a section stays its
26
+ // own card.
27
+ export const InsideFormContext = createContext(false);
28
+ // Embedded forms (e.g. in AuthCard) already get their card surface from the
29
+ // container; the self-carding DefaultForm would otherwise produce a
30
+ // card-in-card. BareFormProvider switches DefaultForm to a bare <form>
31
+ // (stacked fields, no frame of its own/max-width).
32
+ export const BareFormContext = createContext(false);
33
+ // Dashboard panels with an embedded settings form (`DashboardScreenPanel.chromeless`):
34
+ // the form renders in the page grid like a standalone screen form, without the
35
+ // card frame, but keeps its own (non-pinned) save bar.
36
+ export const EmbeddedFormContext = createContext(false);
37
+ export function EmbeddedFormProvider({ children }) {
38
+ return _jsx(EmbeddedFormContext.Provider, { value: true, children: children });
39
+ }
40
+ export function BareFormProvider({ children }) {
41
+ return _jsx(BareFormContext.Provider, { value: true, children: children });
42
+ }
43
+ // App-wide default for FormScreenShell's width when a screen doesn't set its
44
+ // own `layout.width`. Undefined (no `createKumikoApp({ screenWidth })`) lets each
45
+ // shell keep its own default: "4xl" for FormScreenShell, 640px for the
46
+ // screen-form column.
47
+ export const ScreenWidthContext = createContext(undefined);
48
+ export function ScreenWidthProvider({ width, children, }) {
49
+ return _jsx(ScreenWidthContext.Provider, { value: width, children: children });
50
+ }
@@ -0,0 +1,76 @@
1
+ import type { SelectOptionTone } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ import { type ReactNode } from "react";
3
+ import type { CellLink, CellLinkItem, ListColumnFileDisplay, ListRowViewModel, RuntimeCellLink, RuntimeCellLinkItems } from "../../headless/index.ts";
4
+ import type { DataTableSort } from "../../index.ts";
5
+ import { type DataTableRowGrouping } from "../../index.ts";
6
+ import "../react-css-custom-properties.ts";
7
+ export declare function isEmptyCellValue(value: unknown): boolean;
8
+ export declare function isUnlabeledFalse(type: string, value: unknown, renderer: unknown): boolean;
9
+ export declare function showsBareCheckMark(type: string, value: unknown, renderer: unknown): boolean;
10
+ export declare function isBadgeColumn(col: {
11
+ readonly type: string;
12
+ readonly renderer?: unknown;
13
+ }): boolean;
14
+ export declare function FirstCellLink({ enabled, empty, emptyLabel, onOpen, children, }: {
15
+ readonly enabled: boolean;
16
+ readonly empty: boolean;
17
+ readonly emptyLabel?: string | undefined;
18
+ readonly onOpen: () => void;
19
+ readonly children: ReactNode;
20
+ }): ReactNode;
21
+ export declare const FIT_CONTENT_CELL_CLASS = "w-px";
22
+ export declare function SortableHeader({ field, label, sortable, highlighted, numeric, fitContent, sort, onSortChange, }: {
23
+ readonly field: string;
24
+ readonly label: string;
25
+ readonly sortable: boolean;
26
+ readonly highlighted?: boolean;
27
+ readonly numeric?: boolean;
28
+ readonly fitContent?: boolean;
29
+ readonly sort?: DataTableSort;
30
+ readonly onSortChange?: (next: DataTableSort | null) => void;
31
+ }): ReactNode;
32
+ export declare function isComponentRendererRef(renderer: unknown): {
33
+ readonly name: string;
34
+ } | undefined;
35
+ export declare function defaultCellRender(value: unknown, type: string, optionLabels?: Readonly<Record<string, string>>, locale?: string, columnKey?: string, grouping?: boolean): string;
36
+ export declare function cellTitle(value: unknown): string | undefined;
37
+ type DataTableCellProps = {
38
+ readonly value: unknown;
39
+ readonly row: Readonly<Record<string, unknown>>;
40
+ readonly field: string;
41
+ readonly type: string;
42
+ readonly renderer?: unknown;
43
+ readonly optionLabels?: Readonly<Record<string, string>>;
44
+ readonly optionTones?: Readonly<Partial<Record<string, SelectOptionTone>>>;
45
+ readonly onChange?: (value: unknown) => void;
46
+ readonly translate?: (key: string, params?: Readonly<Record<string, unknown>>) => string;
47
+ readonly locale?: string;
48
+ readonly grouping?: boolean;
49
+ readonly fileDisplay?: ListColumnFileDisplay;
50
+ readonly compactFile?: boolean;
51
+ readonly link?: CellLink;
52
+ readonly linkItems?: readonly CellLinkItem[];
53
+ };
54
+ export declare function cellLinkProp(col: {
55
+ readonly field: string;
56
+ readonly linkFor?: RuntimeCellLink | undefined;
57
+ readonly linkItemsFor?: RuntimeCellLinkItems | undefined;
58
+ }, row: ListRowViewModel, allowed: boolean): {
59
+ readonly link?: CellLink;
60
+ readonly linkItems?: readonly CellLinkItem[];
61
+ };
62
+ export type TableItem = {
63
+ readonly kind: "group";
64
+ readonly key: string;
65
+ readonly label: string;
66
+ readonly collapsed: boolean;
67
+ } | {
68
+ readonly kind: "row";
69
+ readonly row: ListRowViewModel;
70
+ };
71
+ export declare function buildTableItems(rows: readonly ListRowViewModel[], grouping: DataTableRowGrouping | undefined, toggledGroups: ReadonlySet<string>): readonly TableItem[];
72
+ export declare function rowToneClass(tone: SelectOptionTone | undefined): string;
73
+ export declare const NUMERIC_COLUMN_TYPES: ReadonlySet<string>;
74
+ export declare const TABULAR_COLUMN_TYPES: ReadonlySet<string>;
75
+ export declare function DataTableCell({ value, row, field, type, renderer, optionLabels, optionTones, onChange, translate, locale, grouping, fileDisplay, compactFile, link, linkItems, }: DataTableCellProps): ReactNode;
76
+ export {};