@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.
- package/dist/app/action-form-body.d.ts +50 -0
- package/dist/app/action-form-body.js +275 -0
- package/dist/app/client-actions.js +2 -2
- package/dist/app/config-edit-body.d.ts +9 -0
- package/dist/app/config-edit-body.js +340 -0
- package/dist/app/entity-edit-prefill.d.ts +27 -0
- package/dist/app/entity-edit-prefill.js +377 -0
- package/dist/app/entity-edit-screen.d.ts +11 -0
- package/dist/app/entity-edit-screen.js +395 -0
- package/dist/app/entity-list-screen.d.ts +10 -0
- package/dist/app/entity-list-screen.js +547 -0
- package/dist/app/kumiko-screen.d.ts +1 -36
- package/dist/app/kumiko-screen.js +16 -2855
- package/dist/app/projection-detail-body.d.ts +12 -0
- package/dist/app/projection-detail-body.js +464 -0
- package/dist/app/projection-detail-shim.d.ts +1 -1
- package/dist/app/projection-detail-shim.js +5 -1
- package/dist/app/projection-list-body.d.ts +10 -0
- package/dist/app/projection-list-body.js +245 -0
- package/dist/app/projection-list-shim.d.ts +2 -1
- package/dist/app/projection-list-shim.js +4 -0
- package/dist/app/reference-facet-bridge.js +2 -2
- package/dist/app/row-actions.js +3 -4
- package/dist/app/screen-shared.d.ts +72 -0
- package/dist/app/screen-shared.js +325 -0
- package/dist/app/secret-mint-body.js +3 -3
- package/dist/components/edit-mounts.d.ts +33 -0
- package/dist/components/edit-mounts.js +53 -0
- package/dist/components/error-boundary.js +2 -2
- package/dist/components/reference-create-dialog.js +1 -1
- package/dist/components/related-list-section.js +29 -6
- package/dist/components/render-edit-helpers.d.ts +29 -0
- package/dist/components/render-edit-helpers.js +121 -0
- package/dist/components/render-edit.d.ts +1 -4
- package/dist/components/render-edit.js +41 -443
- package/dist/components/render-field.js +3 -4
- package/dist/components/render-list.d.ts +1 -1
- package/dist/components/render-list.js +2 -2
- package/dist/components/use-wizard-form-draft.d.ts +51 -0
- package/dist/components/use-wizard-form-draft.js +291 -0
- package/dist/dev-console.d.ts +3 -0
- package/dist/dev-console.js +15 -0
- package/dist/headless/contracts/primitives.js +14 -17
- package/dist/headless/format/index.js +3 -3
- package/dist/headless/view-model/edit.d.ts +1 -1
- package/dist/headless/view-model/edit.js +9 -16
- package/dist/headless/view-model/list.d.ts +1 -1
- package/dist/headless/view-model/list.js +3 -8
- package/dist/index.d.ts +2 -1
- package/dist/index.js +5 -5
- package/dist/live/error-mapping.js +3 -4
- package/dist/web/app/create-app.js +5 -8
- package/dist/web/app/dashboard-body.js +4 -599
- package/dist/web/app/dashboard-panels.d.ts +53 -0
- package/dist/web/app/dashboard-panels.js +373 -0
- package/dist/web/app/dashboard-shared.d.ts +90 -0
- package/dist/web/app/dashboard-shared.js +171 -0
- package/dist/web/app/dashboard-text.d.ts +6 -0
- package/dist/web/app/dashboard-text.js +65 -0
- package/dist/web/layout/nav-tree.js +3 -4
- package/dist/web/layout/target-resolver-stub.js +2 -2
- package/dist/web/primitives/banner.d.ts +4 -0
- package/dist/web/primitives/banner.js +28 -0
- package/dist/web/primitives/button.d.ts +7 -0
- package/dist/web/primitives/button.js +48 -0
- package/dist/web/primitives/card-styles.d.ts +8 -0
- package/dist/web/primitives/card-styles.js +18 -0
- package/dist/web/primitives/card.d.ts +4 -0
- package/dist/web/primitives/card.js +41 -0
- package/dist/web/primitives/contexts.d.ts +22 -0
- package/dist/web/primitives/contexts.js +50 -0
- package/dist/web/primitives/data-table-cell.d.ts +76 -0
- package/dist/web/primitives/data-table-cell.js +403 -0
- package/dist/web/primitives/data-table.d.ts +4 -0
- package/dist/web/primitives/data-table.js +279 -0
- package/dist/web/primitives/default-form.d.ts +4 -0
- package/dist/web/primitives/default-form.js +79 -0
- package/dist/web/primitives/facet-filter.d.ts +14 -0
- package/dist/web/primitives/facet-filter.js +39 -0
- package/dist/web/primitives/field.d.ts +63 -0
- package/dist/web/primitives/field.js +181 -0
- package/dist/web/primitives/form.d.ts +53 -0
- package/dist/web/primitives/form.js +252 -0
- package/dist/web/primitives/index.d.ts +7 -30
- package/dist/web/primitives/index.js +27 -2006
- package/dist/web/primitives/input.d.ts +4 -0
- package/dist/web/primitives/input.js +166 -0
- package/dist/web/primitives/layout-elements.d.ts +14 -0
- package/dist/web/primitives/layout-elements.js +194 -0
- package/dist/web/primitives/modal.js +1 -1
- package/dist/web/primitives/pager.d.ts +22 -0
- package/dist/web/primitives/pager.js +89 -0
- package/dist/web/primitives/row-actions.d.ts +13 -0
- package/dist/web/primitives/row-actions.js +178 -0
- package/dist/web/primitives/secret-reveal.d.ts +4 -0
- package/dist/web/primitives/secret-reveal.js +67 -0
- package/package.json +5 -5
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,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 {};
|