@cosmicdrift/kumiko-renderer 0.383.1 → 0.385.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 +2 -2
- 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.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 +25 -6
- package/dist/headless/view-model/edit.d.ts +1 -1
- package/dist/headless/view-model/edit.js +16 -17
- 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/calendar-day-picker.d.ts +13 -0
- package/dist/web/primitives/calendar-day-picker.js +38 -0
- package/dist/web/primitives/calendar-popover.d.ts +1 -1
- package/dist/web/primitives/calendar-popover.js +5 -39
- 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/file-thumbnail.js +69 -7
- 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 -2005
- 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/dist/web/tokens.js +2 -1
- package/package.json +5 -5
- package/src/changes.json +24 -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,13 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export type CalendarDayPickerProps = {
|
|
3
|
+
readonly selected: Date | undefined;
|
|
4
|
+
readonly onSelect: (d: Date | undefined) => void;
|
|
5
|
+
readonly startMonth: Date;
|
|
6
|
+
readonly endMonth: Date;
|
|
7
|
+
readonly disabledMatchers: readonly ({
|
|
8
|
+
before: Date;
|
|
9
|
+
} | {
|
|
10
|
+
after: Date;
|
|
11
|
+
})[];
|
|
12
|
+
};
|
|
13
|
+
export default function CalendarDayPicker({ selected, onSelect, startMonth, endMonth, disabledMatchers, }: CalendarDayPickerProps): ReactNode;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { DayPicker } from "react-day-picker";
|
|
3
|
+
import { cn } from "../lib/cn.js";
|
|
4
|
+
const dropdownSelectClass = "rounded-sm border border-input bg-transparent px-1.5 py-0.5 text-sm font-medium " +
|
|
5
|
+
"cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring";
|
|
6
|
+
// rdp v9's default dropdown renders an aria-hidden <span> with the same label
|
|
7
|
+
// next to the <select>, hidden only by rdp's own style.css. Our token classes
|
|
8
|
+
// replace that CSS, so the label showed twice; a bare <select> avoids it
|
|
9
|
+
// regardless of CSS.
|
|
10
|
+
function SelectDropdown({ options, className, ...selectProps }) {
|
|
11
|
+
return (_jsx("select", { className: cn(dropdownSelectClass, className), ...selectProps, children: options?.map((opt) => (_jsx("option", { value: opt.value, disabled: opt.disabled, children: opt.label }, opt.value))) }));
|
|
12
|
+
}
|
|
13
|
+
// Month/year selects come from SelectDropdown (components prop), so only the dropdowns container is styled here.
|
|
14
|
+
const dayPickerClasses = {
|
|
15
|
+
root: "rdp-root",
|
|
16
|
+
months: "flex flex-col gap-2",
|
|
17
|
+
month: "flex flex-col gap-2",
|
|
18
|
+
month_caption: "flex justify-center items-center h-7 text-sm font-medium",
|
|
19
|
+
dropdowns: "flex items-center gap-1 text-sm font-medium",
|
|
20
|
+
nav: "flex items-center gap-1 absolute right-1 top-2",
|
|
21
|
+
button_previous: "inline-flex h-7 w-7 items-center justify-center rounded-sm hover:bg-accent",
|
|
22
|
+
button_next: "inline-flex h-7 w-7 items-center justify-center rounded-sm hover:bg-accent",
|
|
23
|
+
weekdays: "flex",
|
|
24
|
+
weekday: "w-8 text-xs font-normal text-muted-foreground",
|
|
25
|
+
week: "flex mt-1",
|
|
26
|
+
day: "w-8 h-8 p-0 text-sm",
|
|
27
|
+
day_button: "inline-flex h-8 w-8 items-center justify-center rounded-sm hover:bg-accent " +
|
|
28
|
+
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
29
|
+
selected: "[&_button]:bg-primary [&_button]:text-primary-foreground [&_button]:hover:bg-primary/90",
|
|
30
|
+
today: "[&_button]:underline",
|
|
31
|
+
outside: "text-muted-foreground/50",
|
|
32
|
+
disabled: "text-muted-foreground/30 pointer-events-none",
|
|
33
|
+
};
|
|
34
|
+
export default function CalendarDayPicker({ selected, onSelect, startMonth, endMonth, disabledMatchers, }) {
|
|
35
|
+
return (_jsx(DayPicker, { mode: "single", captionLayout: "dropdown", startMonth: startMonth, endMonth: endMonth, selected: selected,
|
|
36
|
+
// Without defaultMonth DayPicker opens on today, not on the chosen date.
|
|
37
|
+
defaultMonth: selected, ...(disabledMatchers.length > 0 && { disabled: [...disabledMatchers] }), onSelect: (d) => onSelect(d), classNames: dayPickerClasses, components: { Dropdown: SelectDropdown } }));
|
|
38
|
+
}
|
|
@@ -11,8 +11,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
11
11
|
// teilen beide dieselbe Kalender-Mechanik ohne ihre Eingabe-Form zu teilen.
|
|
12
12
|
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
|
13
13
|
import { Calendar as CalendarIcon } from "lucide-react";
|
|
14
|
-
import {
|
|
14
|
+
import { lazy, Suspense } from "react";
|
|
15
15
|
import { cn } from "../lib/cn.js";
|
|
16
|
+
const CalendarDayPicker = lazy(() => import("./calendar-day-picker.js"));
|
|
17
|
+
// Same footprint as the loaded calendar card, so opening the popover does not jump.
|
|
18
|
+
const CALENDAR_PLACEHOLDER_CLASS = "h-[17rem] w-56";
|
|
16
19
|
export const inFieldIconButtonClass = "absolute inset-y-0 right-0 inline-flex w-9 items-center justify-center rounded-md " +
|
|
17
20
|
"text-muted-foreground transition-colors hover:text-foreground " +
|
|
18
21
|
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring " +
|
|
@@ -35,42 +38,5 @@ export function CalendarPopover({ selected, onSelect, min, max, disabled, hasErr
|
|
|
35
38
|
...(min !== undefined ? [{ before: min }] : []),
|
|
36
39
|
...(max !== undefined ? [{ after: max }] : []),
|
|
37
40
|
];
|
|
38
|
-
return (_jsxs(PopoverPrimitive.Root, { children: [_jsx(PopoverPrimitive.Trigger, { asChild: true, children: _jsx("button", { type: "button", disabled: disabled, "aria-label": triggerLabel, "aria-invalid": hasError === true ? true : undefined, className: cn(inFieldIconButtonClass, hasError === true && "text-destructive"), children: _jsx(CalendarIcon, { className: "size-4", "aria-hidden": "true" }) }) }), _jsx(PopoverPrimitive.Portal, { children: _jsx(PopoverPrimitive.Content, { className: popoverClass, align: "end", sideOffset: 4, children: _jsx(
|
|
39
|
-
// Ohne defaultMonth zeigt DayPicker today statt selected —
|
|
40
|
-
// unintuitiv wenn der User schon ein Datum gewählt hat.
|
|
41
|
-
defaultMonth: selected, ...(disabledMatchers.length > 0 && { disabled: disabledMatchers }), onSelect: (d) => onSelect(d), classNames: dayPickerClasses, components: { Dropdown: SelectDropdown } }) }) })] }));
|
|
41
|
+
return (_jsxs(PopoverPrimitive.Root, { children: [_jsx(PopoverPrimitive.Trigger, { asChild: true, children: _jsx("button", { type: "button", disabled: disabled, "aria-label": triggerLabel, "aria-invalid": hasError === true ? true : undefined, className: cn(inFieldIconButtonClass, hasError === true && "text-destructive"), children: _jsx(CalendarIcon, { className: "size-4", "aria-hidden": "true" }) }) }), _jsx(PopoverPrimitive.Portal, { children: _jsx(PopoverPrimitive.Content, { className: popoverClass, align: "end", sideOffset: 4, children: _jsx(Suspense, { fallback: _jsx("div", { className: CALENDAR_PLACEHOLDER_CLASS, "aria-hidden": "true" }), children: _jsx(CalendarDayPicker, { selected: selected, onSelect: onSelect, startMonth: startMonth, endMonth: endMonth, disabledMatchers: disabledMatchers }) }) }) })] }));
|
|
42
42
|
}
|
|
43
|
-
const dropdownSelectClass = "rounded-sm border border-input bg-transparent px-1.5 py-0.5 text-sm font-medium " +
|
|
44
|
-
"cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring";
|
|
45
|
-
// rdp v9s Default-Dropdown rendert NEBEN dem <select> ein aria-hidden
|
|
46
|
-
// <span> mit demselben Label — sichtbar nur, wenn rdps eigene style.css
|
|
47
|
-
// das <select> transparent darüberlegt. Da wir die rdp-Klassen mit eigenen
|
|
48
|
-
// Tokens überschreiben, greift diese Positionierung nicht → Label doppelt
|
|
49
|
-
// (#369-Folgebug). Ein nacktes <select> ohne Begleit-Span vermeidet das
|
|
50
|
-
// CSS-unabhängig — eine Quelle für den sichtbaren Wert.
|
|
51
|
-
function SelectDropdown({ options, className, ...selectProps }) {
|
|
52
|
-
return (_jsx("select", { className: cn(dropdownSelectClass, className), ...selectProps, children: options?.map((opt) => (_jsx("option", { value: opt.value, disabled: opt.disabled, children: opt.label }, opt.value))) }));
|
|
53
|
-
}
|
|
54
|
-
// classNames-Map für react-day-picker v9 — überschreibt die default-
|
|
55
|
-
// Klassen mit Tailwind/shadcn-Tokens. Die Monat-/Jahr-Selects rendert
|
|
56
|
-
// SelectDropdown (components-Prop), daher hier nur der dropdowns-Container.
|
|
57
|
-
const dayPickerClasses = {
|
|
58
|
-
root: "rdp-root",
|
|
59
|
-
months: "flex flex-col gap-2",
|
|
60
|
-
month: "flex flex-col gap-2",
|
|
61
|
-
month_caption: "flex justify-center items-center h-7 text-sm font-medium",
|
|
62
|
-
dropdowns: "flex items-center gap-1 text-sm font-medium",
|
|
63
|
-
nav: "flex items-center gap-1 absolute right-1 top-2",
|
|
64
|
-
button_previous: "inline-flex h-7 w-7 items-center justify-center rounded-sm hover:bg-accent",
|
|
65
|
-
button_next: "inline-flex h-7 w-7 items-center justify-center rounded-sm hover:bg-accent",
|
|
66
|
-
weekdays: "flex",
|
|
67
|
-
weekday: "w-8 text-xs font-normal text-muted-foreground",
|
|
68
|
-
week: "flex mt-1",
|
|
69
|
-
day: "w-8 h-8 p-0 text-sm",
|
|
70
|
-
day_button: "inline-flex h-8 w-8 items-center justify-center rounded-sm hover:bg-accent " +
|
|
71
|
-
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
72
|
-
selected: "[&_button]:bg-primary [&_button]:text-primary-foreground [&_button]:hover:bg-primary/90",
|
|
73
|
-
today: "[&_button]:underline",
|
|
74
|
-
outside: "text-muted-foreground/50",
|
|
75
|
-
disabled: "text-muted-foreground/30 pointer-events-none",
|
|
76
|
-
};
|
|
@@ -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;
|