@cosmicdrift/kumiko-renderer 0.386.0 → 0.388.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/LICENSE +21 -57
- package/dist/app/action-form-body.d.ts +2 -2
- package/dist/app/action-form-body.js +12 -4
- package/dist/app/action-form-shim.d.ts +8 -8
- package/dist/app/action-form-shim.js +20 -22
- package/dist/app/action-form-support.d.ts +1 -0
- package/dist/app/action-form-support.js +1 -1
- package/dist/app/app-features-context.js +4 -4
- package/dist/app/column-renderers.d.ts +7 -9
- package/dist/app/column-renderers.js +15 -18
- package/dist/app/config-edit-body.js +7 -3
- package/dist/app/config-edit-shim.d.ts +7 -8
- package/dist/app/config-edit-shim.js +18 -20
- package/dist/app/custom-screens.d.ts +3 -3
- package/dist/app/custom-screens.js +8 -8
- package/dist/app/dashboard-body.d.ts +2 -2
- package/dist/app/dashboard-body.js +2 -2
- package/dist/app/entity-edit-prefill.d.ts +11 -0
- package/dist/app/entity-edit-prefill.js +18 -13
- package/dist/app/entity-edit-screen.js +4 -4
- package/dist/app/entity-list-screen.js +3 -3
- package/dist/app/extension-form-submit.d.ts +1 -1
- package/dist/app/extension-form-submit.js +14 -15
- package/dist/app/extension-sections.d.ts +29 -28
- package/dist/app/extension-sections.js +10 -10
- package/dist/app/feature-schema.js +11 -11
- package/dist/app/form-schema.js +2 -2
- package/dist/app/header-actions-bar.js +2 -2
- package/dist/app/kumiko-screen.js +8 -10
- package/dist/app/layout-fields.js +1 -1
- package/dist/app/list-facets.js +3 -3
- package/dist/app/nav.d.ts +10 -10
- package/dist/app/nav.js +13 -13
- package/dist/app/projection-detail-body.js +4 -4
- package/dist/app/projection-detail-shim.d.ts +6 -5
- package/dist/app/projection-detail-shim.js +23 -22
- package/dist/app/projection-list-body.js +10 -10
- package/dist/app/projection-list-shim.d.ts +5 -5
- package/dist/app/projection-list-shim.js +17 -18
- package/dist/app/qn.js +10 -12
- package/dist/app/qualify-screen-id.js +1 -1
- package/dist/app/screen-access.js +2 -2
- package/dist/app/screen-shared.js +1 -1
- package/dist/app/secret-mint-body.js +4 -4
- package/dist/app/write-failed-error.js +8 -7
- package/dist/components/edit-mounts.d.ts +2 -2
- package/dist/components/embedded-list-field.js +5 -5
- package/dist/components/grid-cell-for-field.d.ts +1 -1
- package/dist/components/reference-create-dialog.js +3 -3
- package/dist/components/related-list-section.d.ts +1 -1
- package/dist/components/related-list-section.js +15 -2
- package/dist/components/render-edit-action-button.d.ts +1 -3
- package/dist/components/render-edit-action-button.js +2 -2
- package/dist/components/render-edit-logic.js +1 -1
- package/dist/components/render-edit-types.d.ts +6 -6
- package/dist/components/render-edit.js +74 -73
- package/dist/components/render-field.d.ts +7 -7
- package/dist/components/render-field.js +62 -61
- package/dist/components/render-list.d.ts +42 -43
- package/dist/components/render-list.js +93 -84
- package/dist/components/use-wizard-form-draft.js +10 -10
- package/dist/context/dispatcher-context.js +5 -5
- package/dist/context/user-roles-context.js +2 -2
- package/dist/flow-diagram/render.js +9 -1
- package/dist/headless/apex/index.d.ts +1 -2
- package/dist/headless/apex/index.js +5 -6
- package/dist/headless/form/form-controller.js +14 -6
- package/dist/headless/form/types.d.ts +1 -0
- package/dist/headless/form/zod-bridge.js +2 -2
- package/dist/headless/format/index.js +89 -64
- package/dist/headless/format/money.js +1 -1
- package/dist/headless/index.js +2 -2
- package/dist/headless/view-model/edit.js +4 -2
- package/dist/headless/view-model/list.js +184 -150
- package/dist/headless/view-model/types.d.ts +6 -3
- package/dist/hooks/live-refetch-scheduler.js +4 -0
- package/dist/hooks/reference-limits.js +11 -12
- package/dist/hooks/use-list-url-state.d.ts +13 -13
- package/dist/hooks/use-list-url-state.js +21 -22
- package/dist/hooks/use-query.js +3 -3
- package/dist/hooks/use-reference-lookup.d.ts +9 -1
- package/dist/hooks/use-reference-lookup.js +32 -19
- package/dist/hooks/use-store.js +18 -22
- package/dist/hooks/use-stream-handler.js +4 -2
- package/dist/i18n-defaults.js +6 -8
- package/dist/i18n.d.ts +10 -10
- package/dist/i18n.js +25 -25
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -4
- package/dist/live/dispatcher-live.js +2 -2
- package/dist/primitives.d.ts +244 -221
- package/dist/primitives.js +1 -1
- package/dist/sse/live-events.d.ts +5 -5
- package/dist/sse/live-events.js +17 -17
- package/dist/tokens.d.ts +8 -8
- package/dist/tokens.js +16 -17
- package/dist/web/app/browser-locale.d.ts +9 -9
- package/dist/web/app/browser-locale.js +19 -19
- package/dist/web/app/client-plugin.d.ts +44 -52
- package/dist/web/app/client-plugin.js +4 -5
- package/dist/web/app/create-app.d.ts +28 -31
- package/dist/web/app/create-app.js +29 -25
- package/dist/web/app/dashboard-body.js +17 -1
- package/dist/web/app/dashboard-panels.js +12 -2
- package/dist/web/app/document-lang-sync.js +1 -1
- package/dist/web/app/draft-storage.js +1 -1
- package/dist/web/app/nav-providers-context.d.ts +3 -3
- package/dist/web/app/nav-providers-context.js +3 -3
- package/dist/web/app/nav.d.ts +18 -17
- package/dist/web/app/nav.js +66 -64
- package/dist/web/app/rich-content-editor.js +1 -1
- package/dist/web/components/config-cascade.js +17 -18
- package/dist/web/components/config-source-badge.js +4 -4
- package/dist/web/index.d.ts +1 -1
- package/dist/web/index.js +1 -1
- package/dist/web/layout/app-layout.d.ts +9 -9
- package/dist/web/layout/app-layout.js +2 -2
- package/dist/web/layout/avatar.d.ts +4 -3
- package/dist/web/layout/avatar.js +15 -17
- package/dist/web/layout/default-app-shell.d.ts +22 -23
- package/dist/web/layout/editor-panel.js +3 -3
- package/dist/web/layout/nav-tree.js +86 -84
- package/dist/web/layout/profile-menu.d.ts +1 -1
- package/dist/web/layout/shell-header.js +2 -1
- package/dist/web/layout/sidebar-brand.d.ts +7 -7
- package/dist/web/layout/sidebar-user.d.ts +1 -1
- package/dist/web/layout/sidebar-user.js +4 -4
- package/dist/web/layout/sidebar.d.ts +8 -8
- package/dist/web/layout/sidebar.js +7 -7
- package/dist/web/layout/target-resolver-stub.d.ts +5 -5
- package/dist/web/layout/target-resolver-stub.js +20 -20
- package/dist/web/layout/target-url.d.ts +7 -7
- package/dist/web/layout/target-url.js +22 -22
- package/dist/web/layout/theme-toggle.js +4 -5
- package/dist/web/layout/uniform-bar-controls.d.ts +1 -0
- package/dist/web/layout/uniform-bar-controls.js +4 -0
- package/dist/web/layout/workspace-shell.d.ts +21 -21
- package/dist/web/layout/workspace-shell.js +29 -30
- package/dist/web/lib/cn.d.ts +4 -4
- package/dist/web/lib/cn.js +4 -4
- package/dist/web/lib/file-ref-download.js +4 -1
- package/dist/web/primitives/action-menu.d.ts +8 -8
- package/dist/web/primitives/action-menu.js +14 -13
- package/dist/web/primitives/calendar-popover.d.ts +1 -1
- package/dist/web/primitives/calendar-popover.js +10 -10
- package/dist/web/primitives/card-styles.js +1 -1
- package/dist/web/primitives/card.js +2 -2
- package/dist/web/primitives/code-input.d.ts +6 -0
- package/dist/web/primitives/code-input.js +31 -0
- package/dist/web/primitives/combobox.d.ts +12 -12
- package/dist/web/primitives/combobox.js +34 -35
- package/dist/web/primitives/data-table-cell.js +2 -2
- package/dist/web/primitives/data-table.js +6 -6
- package/dist/web/primitives/date-field.js +2 -2
- package/dist/web/primitives/date-input.js +4 -4
- package/dist/web/primitives/default-form.js +2 -2
- package/dist/web/primitives/dropdown-menu.js +14 -15
- package/dist/web/primitives/embedded-list-input.js +3 -3
- package/dist/web/primitives/facet-filter.js +2 -2
- package/dist/web/primitives/field.js +9 -8
- package/dist/web/primitives/file-thumbnail.d.ts +3 -0
- package/dist/web/primitives/file-thumbnail.js +5 -0
- package/dist/web/primitives/form.d.ts +1 -1
- package/dist/web/primitives/form.js +6 -6
- package/dist/web/primitives/input.js +8 -3
- package/dist/web/primitives/layout-elements.js +7 -5
- package/dist/web/primitives/layout.js +2 -2
- package/dist/web/primitives/located-timestamp-input.js +4 -4
- package/dist/web/primitives/money-input.js +13 -17
- package/dist/web/primitives/page-header.js +3 -1
- package/dist/web/primitives/pager.js +1 -1
- package/dist/web/primitives/row-actions.js +6 -7
- package/dist/web/primitives/secret-reveal.js +1 -1
- package/dist/web/primitives/timestamp-input.d.ts +9 -9
- package/dist/web/primitives/timestamp-input.js +21 -21
- package/dist/web/primitives/toast.js +12 -13
- package/dist/web/primitives/use-focus-on-mount.d.ts +2 -0
- package/dist/web/primitives/use-focus-on-mount.js +9 -0
- package/dist/web/sse/live-events.d.ts +6 -6
- package/dist/web/sse/live-events.js +3 -3
- package/dist/web/tokens.d.ts +11 -12
- package/dist/web/tokens.js +35 -37
- package/dist/web/version/update-checker.js +14 -13
- package/dist/web/widgets/ai-text-field.js +1 -1
- package/dist/web/widgets/bounded-load-cache.d.ts +1 -1
- package/dist/web/widgets/bounded-load-cache.js +10 -4
- package/dist/web/widgets/charts.d.ts +10 -10
- package/dist/web/widgets/charts.js +8 -8
- package/dist/web/widgets/choice-cards.d.ts +17 -0
- package/dist/web/widgets/choice-cards.js +12 -0
- package/dist/web/widgets/collapsible-section.d.ts +3 -3
- package/dist/web/widgets/collapsible-section.js +7 -7
- package/dist/web/widgets/detail-list.d.ts +5 -3
- package/dist/web/widgets/detail-list.js +5 -3
- package/dist/web/widgets/feed-list.d.ts +2 -2
- package/dist/web/widgets/feed-list.js +2 -2
- package/dist/web/widgets/form-fields.d.ts +14 -14
- package/dist/web/widgets/form-fields.js +14 -13
- package/dist/web/widgets/index.d.ts +1 -0
- package/dist/web/widgets/index.js +5 -4
- package/dist/web/widgets/infinity-list.js +3 -3
- package/dist/web/widgets/mode-switch.d.ts +2 -2
- package/dist/web/widgets/mode-switch.js +2 -2
- package/dist/web/widgets/pdf-preview.js +1 -1
- package/dist/web/widgets/plan-card.d.ts +2 -0
- package/dist/web/widgets/plan-card.js +1 -1
- package/dist/web/widgets/progress-list.d.ts +1 -2
- package/dist/web/widgets/progress-list.js +1 -2
- package/dist/web/widgets/query-table.d.ts +6 -6
- package/dist/web/widgets/result-panel.d.ts +16 -16
- package/dist/web/widgets/result-panel.js +10 -10
- package/dist/web/widgets/section-card.d.ts +3 -3
- package/dist/web/widgets/section-card.js +3 -3
- package/dist/web/widgets/stat.d.ts +4 -4
- package/dist/web/widgets/stat.js +6 -6
- package/dist/web/widgets/states.d.ts +4 -4
- package/dist/web/widgets/states.js +4 -4
- package/dist/web/widgets/upload-zone.js +13 -5
- package/dist/web/widgets/use-draft.d.ts +7 -7
- package/dist/web/widgets/use-draft.js +7 -7
- package/package.json +6 -6
- package/src/changes.json +49 -0
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
//
|
|
2
|
+
// Searchable Select / Combobox.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// cmdk
|
|
4
|
+
// The pattern follows the shadcn standard: a popover trigger rendered as a
|
|
5
|
+
// button showing the current value or placeholder, with a popover containing
|
|
6
|
+
// a cmdk command with a search input and filterable item list.
|
|
7
7
|
//
|
|
8
|
-
// cmdk
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// MVP — kommt später als zweiter Mode-Schalter.
|
|
8
|
+
// cmdk provides client-side fuzzy matching through `Command.Filter` by
|
|
9
|
+
// default. Server-side remote search with a debounced query is not included
|
|
10
|
+
// in the MVP and can be enabled later as a separate mode.
|
|
12
11
|
//
|
|
13
|
-
// Multi-
|
|
14
|
-
//
|
|
15
|
-
//
|
|
12
|
+
// Multi-mode: `multiple: true` switches to tag display and multi-selection.
|
|
13
|
+
// Selected values render as small chips, while the search input remains open
|
|
14
|
+
// for additional selections.
|
|
16
15
|
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
|
17
16
|
import { Command } from "cmdk";
|
|
18
17
|
import { Check, ChevronDown, Loader2, Plus } from "lucide-react";
|
|
@@ -31,23 +30,24 @@ const popoverContentClass = "z-50 w-[var(--radix-popover-trigger-width)] rounded
|
|
|
31
30
|
"data-[state=open]:fade-in-0";
|
|
32
31
|
const tagClass = "inline-flex items-center gap-1 rounded-sm bg-muted px-2 py-0.5 text-xs " +
|
|
33
32
|
"font-medium text-muted-foreground";
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
// Substring
|
|
37
|
-
// `command-score` (fuzzy),
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
33
|
+
// The debounce default is centralized in hooks/reference-limits.ts so the
|
|
34
|
+
// same typing interval applies throughout the app.
|
|
35
|
+
// Substring filter for cmdk's local mode. cmdk's default is
|
|
36
|
+
// `command-score` (fuzzy), which matches "Ad" against "Backend" because 'a'
|
|
37
|
+
// and 'd' occur somewhere in the text. That is misleading for reference
|
|
38
|
+
// lookups, where users expect prefix or substring matching rather than
|
|
39
|
+
// subsequence matching.
|
|
40
|
+
// Returns 1 when search occurs in value or keywords, and 0 otherwise;
|
|
41
|
+
// cmdk hides items with a score of 0.
|
|
42
42
|
function substringFilter(value, search, keywords) {
|
|
43
43
|
const needle = search.toLowerCase();
|
|
44
44
|
return [value, ...(keywords ?? [])].some((text) => text.toLowerCase().includes(needle)) ? 1 : 0;
|
|
45
45
|
}
|
|
46
46
|
export function ComboboxInput(props) {
|
|
47
47
|
const { id, name, options, disabled, required, hasError, ariaLabel, placeholder, searchPlaceholder, emptyText, onSearchChange, loading, defaultOpen, onCreate, createLabel, } = props;
|
|
48
|
-
// i18n
|
|
49
|
-
//
|
|
50
|
-
//
|
|
48
|
+
// i18n defaults come from the framework bundle (kumikoDefaultTranslations).
|
|
49
|
+
// A caller override takes precedence, followed by the bundle override;
|
|
50
|
+
// the raw key is used only when neither is available.
|
|
51
51
|
const t = useTranslation();
|
|
52
52
|
const effectivePlaceholder = placeholder ?? t("kumiko.combobox.placeholder");
|
|
53
53
|
const effectiveSearchPlaceholder = searchPlaceholder ?? t("kumiko.combobox.search-placeholder");
|
|
@@ -56,10 +56,10 @@ export function ComboboxInput(props) {
|
|
|
56
56
|
const effectiveCreateLabel = createLabel ?? t("kumiko.actions.create");
|
|
57
57
|
const multiple = props.multiple === true;
|
|
58
58
|
const [open, setOpen] = useState(defaultOpen === true);
|
|
59
|
-
// Local
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
59
|
+
// Local search buffer for remote mode. Keystrokes are passed to
|
|
60
|
+
// onSearchChange after a 300ms debounce, avoiding a server round-trip for
|
|
61
|
+
// every keystroke. In local mode, this state is unused because cmdk
|
|
62
|
+
// controls its search input internally.
|
|
63
63
|
const [searchTerm, setSearchTerm] = useState("");
|
|
64
64
|
const isRemote = onSearchChange !== undefined;
|
|
65
65
|
useEffect(() => {
|
|
@@ -68,15 +68,14 @@ export function ComboboxInput(props) {
|
|
|
68
68
|
const timer = setTimeout(() => onSearchChange(searchTerm), REFERENCE_SEARCH_DEBOUNCE_MS);
|
|
69
69
|
return () => clearTimeout(timer);
|
|
70
70
|
}, [isRemote, searchTerm, onSearchChange]);
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
// den Server-State stale halten zwischen Aufrufen.
|
|
71
|
+
// Reset the search term when the popover closes so the next opening does not
|
|
72
|
+
// retain the previous term and leave the server state stale between uses.
|
|
74
73
|
useEffect(() => {
|
|
75
74
|
if (!open && searchTerm !== "")
|
|
76
75
|
setSearchTerm("");
|
|
77
76
|
}, [open, searchTerm]);
|
|
78
|
-
//
|
|
79
|
-
//
|
|
77
|
+
// The discriminated union lets TypeScript narrow props.value and onChange
|
|
78
|
+
// automatically, so no runtime cast is needed.
|
|
80
79
|
const selectedValues = multiple ? new Set(props.value) : new Set();
|
|
81
80
|
const singleValue = !multiple ? props.value : "";
|
|
82
81
|
const singleLabel = options.find((o) => o.value === singleValue)?.label ?? "";
|
|
@@ -93,10 +92,10 @@ export function ComboboxInput(props) {
|
|
|
93
92
|
};
|
|
94
93
|
const triggerLabel = multiple ? null : singleLabel === "" ? placeholder : singleLabel;
|
|
95
94
|
return (_jsxs(PopoverPrimitive.Root, { open: open, onOpenChange: setOpen, children: [_jsx("input", { type: "hidden", name: name, value: multiple ? props.value.join(",") : props.value }), _jsxs(PopoverPrimitive.Trigger, { id: id, "data-testid": `combobox-${id}`, type: "button", disabled: disabled, "aria-required": required, "aria-invalid": hasError === true ? true : undefined, "aria-label": ariaLabel, "aria-haspopup": "listbox", "aria-expanded": open, title: !multiple && singleLabel !== "" ? singleLabel : undefined, className: cn(triggerClass, hasError === true && "border-destructive focus-visible:ring-destructive"), children: [multiple ? (_jsx("span", { className: "flex flex-wrap items-center gap-1", children: selectedValues.size === 0 ? (_jsx("span", { className: "text-muted-foreground", children: effectivePlaceholder })) : (
|
|
96
|
-
// Tags
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
95
|
+
// Tags are read-only because the trigger is a <button> and cannot contain a
|
|
96
|
+
// nested <button>. Removing a value therefore requires selecting
|
|
97
|
+
// it again in the combobox popup, where the item toggle deselects
|
|
98
|
+
// the corresponding option.
|
|
100
99
|
[...selectedValues].map((v) => {
|
|
101
100
|
const opt = options.find((o) => o.value === v);
|
|
102
101
|
return (_jsx("span", { className: tagClass, children: opt?.label ?? v }, v));
|
|
@@ -152,7 +152,7 @@ function isMoneyValue(value) {
|
|
|
152
152
|
//
|
|
153
153
|
// - boolean -> check mark / empty
|
|
154
154
|
// - timestamp/date -> locale-formatted (no raw ISO string)
|
|
155
|
-
// - number/decimal/bigInt → locale-formatted via Intl.NumberFormat
|
|
155
|
+
// - number/decimal/bigInt → locale-formatted via Intl.NumberFormat
|
|
156
156
|
// - select/multiSelect → human-readable (kebab-case → Title Case), multiSelect values joined with ", "
|
|
157
157
|
// - money → { amount, currency } formatted via Intl (not "[object Object]")
|
|
158
158
|
// - text/else -> toString
|
|
@@ -390,7 +390,7 @@ export function DataTableCell({ value, row, field, type, renderer, optionLabels,
|
|
|
390
390
|
// A select value renders as a pill instead of plain text. When the raw
|
|
391
391
|
// value is a known status word, it carries the same tone as the
|
|
392
392
|
// projectionDetail header badge — a status column stayed grey while the
|
|
393
|
-
// detail view of the same value was coloured
|
|
393
|
+
// detail view of the same value was coloured. Unknown values
|
|
394
394
|
// keep the neutral outline pill.
|
|
395
395
|
if (type === "select" && value !== null && value !== undefined && value !== "") {
|
|
396
396
|
return selectPill(value, optionLabels, optionTones, locale, RegisteredStatusBadge);
|
|
@@ -3,6 +3,7 @@ import { cardColumnRoles } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
|
3
3
|
import { ArrowDown, ArrowUp, ChevronDown, ChevronRight, Search, SlidersHorizontal, X, } from "lucide-react";
|
|
4
4
|
import { Fragment, useId, useState, } from "react";
|
|
5
5
|
import { useOptionalLocale, useOptionalTranslation } from "../../index.js";
|
|
6
|
+
import { UNIFORM_BAR_CONTROLS_CLASSES } from "../layout/uniform-bar-controls.js";
|
|
6
7
|
import { cn } from "../lib/cn.js";
|
|
7
8
|
import { Button as UiButton } from "../ui/button.js";
|
|
8
9
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table.js";
|
|
@@ -58,13 +59,12 @@ function showFullTextOnHover(event) {
|
|
|
58
59
|
element.title = element.scrollWidth > element.clientWidth ? (element.textContent ?? "") : "";
|
|
59
60
|
}
|
|
60
61
|
export function DefaultDataTable({ columns, rows, onRowClick, sort, onSortChange, emptyState, toolbarStart, toolbarDescription, toolbarEnd, pager, onReachEnd, loadingMore, hasMore, itemNoun, rowActions, rowActionMode, rowGrouping, rowTone, filterFacets, filterValues, onFilterChange, onFilterReset, dateRangeFacets, onDateRangeChange, testId, onCellChange, getRowTestId, getCellTestId, chromeless, scrollBody, screenPadding, expandedRowIds, onToggleRowExpanded, renderExpandedRow, }) {
|
|
61
|
-
// One locale/translate subscription per table — not per cell
|
|
62
|
+
// One locale/translate subscription per table — not per cell.
|
|
62
63
|
// Optional hooks: a bare DataTable outside LocaleProvider must not crash.
|
|
63
64
|
const tableTranslate = useOptionalTranslation();
|
|
64
65
|
const tableLocale = useOptionalLocale();
|
|
65
66
|
// Below 768px a table scrolls its columns out of reach with no visible
|
|
66
|
-
// affordance
|
|
67
|
-
// one). Cards replace the table entirely below the breakpoint — same
|
|
67
|
+
// affordance. Cards replace the table entirely below the breakpoint — same
|
|
68
68
|
// single-mount pattern as EmbeddedListInput/embedded-list-input.tsx.
|
|
69
69
|
const isNarrow = useIsNarrowViewport();
|
|
70
70
|
const [facetsOpenNarrow, setFacetsOpenNarrow] = useState(false);
|
|
@@ -269,11 +269,11 @@ export function DefaultDataTable({ columns, rows, onRowClick, sort, onSortChange
|
|
|
269
269
|
? screenPaddingClassName
|
|
270
270
|
: screenPadding === false
|
|
271
271
|
? undefined
|
|
272
|
-
: "p-6")), children: [hasToolbar && (_jsxs("div", { "data-testid": testId !== undefined ? `${testId}-toolbar` : "render-list-toolbar", className: cn("flex flex-wrap items-center", fillsHeight
|
|
272
|
+
: "p-6")), children: [hasToolbar && (_jsxs("div", { "data-testid": testId !== undefined ? `${testId}-toolbar` : "render-list-toolbar", className: cn("flex flex-wrap items-center", UNIFORM_BAR_CONTROLS_CLASSES, fillsHeight
|
|
273
273
|
? isNarrow
|
|
274
274
|
? "shrink-0 gap-2 p-3"
|
|
275
275
|
: "min-h-13 shrink-0 gap-2 px-6 py-2 md:py-0"
|
|
276
276
|
: "gap-3"), children: [toolbarStart !== undefined && (_jsxs("div", { className: cn(fillsHeight
|
|
277
|
-
? "relative w-full md:w-[300px] max-md:flex-1 [&_input]:border-border-strong [&_input]:pl-8 [&_input]:max-md:h-11
|
|
278
|
-
: "flex-1 min-w-48 max-w-sm"), children: [fillsHeight && (_jsx(Search, { className: "pointer-events-none absolute left-2.5 top-1/2 z-10 size-4 -translate-y-1/2 text-muted-foreground", "aria-hidden": "true" })), toolbarStart] })), collapsesFacetsBehindButton ? (_jsxs(_Fragment, { children: [_jsx(UiButton, { type: "button", variant: "outline", size: "icon", className: "size-11", "aria-label": tableTranslate?.("kumiko.list.filter.toggle") ?? "Filter", "aria-expanded": facetsOpenNarrow, "data-testid": testId !== undefined ? `${testId}-filter-toggle` : "filter-toggle", onClick: () => setFacetsOpenNarrow((open) => !open), children: _jsx(SlidersHorizontal, { className: "size-4", "aria-hidden": "true" }) }), facetsOpenNarrow && _jsx("div", { className: "w-full", children: facetCluster })] })) : (facetCluster), toolbarEnd !== undefined && (_jsx("div", { className: "flex flex-wrap items-center gap-2 ml-auto", children: toolbarEnd }))] })), toolbarDescription !== undefined && (_jsx("p", { "data-testid": testId !== undefined ? `${testId}-description` : "render-list-description", className: cn("m-0 pb-2 text-sm text-muted-foreground", fillsHeight && (isNarrow ? "px-3" : "px-6")), children: toolbarDescription })), content] }));
|
|
277
|
+
? "relative w-full md:w-[300px] max-md:flex-1 [&_input]:border-border-strong [&_input]:pl-8 [&_input]:max-md:h-11"
|
|
278
|
+
: "flex-1 min-w-48 max-w-sm"), children: [fillsHeight && (_jsx(Search, { className: "pointer-events-none absolute left-2.5 top-1/2 z-10 size-4 -translate-y-1/2 text-muted-foreground", "aria-hidden": "true" })), toolbarStart] })), collapsesFacetsBehindButton ? (_jsxs(_Fragment, { children: [_jsx(UiButton, { type: "button", variant: "outline", size: "icon", className: "size-11", "aria-label": tableTranslate?.("kumiko.list.filter.toggle") ?? "Filter", "aria-expanded": facetsOpenNarrow, "data-testid": testId !== undefined ? `${testId}-filter-toggle` : "filter-toggle", onClick: () => setFacetsOpenNarrow((open) => !open), children: _jsx(SlidersHorizontal, { className: "size-4", "aria-hidden": "true" }) }), facetsOpenNarrow && _jsx("div", { className: "w-full", children: facetCluster })] })) : (facetCluster), toolbarEnd !== undefined && (_jsx("div", { className: "flex flex-wrap items-center gap-2 ml-auto", children: toolbarEnd }))] })), toolbarDescription !== undefined && (_jsx("p", { "data-testid": testId !== undefined ? `${testId}-description` : "render-list-description", className: cn("m-0 pb-2 text-sm text-muted-foreground", fillsHeight && (isNarrow ? "px-3" : "px-6"), fillsHeight && !hasToolbar && "pt-4"), children: toolbarDescription })), content] }));
|
|
279
279
|
}
|
|
@@ -6,7 +6,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
6
6
|
// One source for both date primitives: DateInput (kind:"date") is a thin
|
|
7
7
|
// re-export of this, TimestampInput (kind:"timestamp") uses it as the
|
|
8
8
|
// date part next to the time input. So `date` and `timestamp` share the
|
|
9
|
-
// same typing/navigation UX instead of two diverging primitives
|
|
9
|
+
// same typing/navigation UX instead of two diverging primitives.
|
|
10
10
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
11
11
|
import { useState } from "react";
|
|
12
12
|
import { useTranslation } from "../../index.js";
|
|
@@ -57,7 +57,7 @@ export function DateField({ id, name, value, onChange, disabled, required, hasEr
|
|
|
57
57
|
// the app's UI locale via t() — a caller rendering a date in a
|
|
58
58
|
// locale that differs from the UI locale gets a mixed-locale
|
|
59
59
|
// placeholder (e.g. German UI + locale="en-US" → "MM/TT/JJJJ").
|
|
60
|
-
// Accepted for now
|
|
60
|
+
// Accepted for now: a static per-locale letter lookup
|
|
61
61
|
// would need its own locale-root resolution and tests for a
|
|
62
62
|
// placeholder-only, Low-severity mismatch.
|
|
63
63
|
placeholder: formatDatePlaceholder(resolvedLocale, {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// DateInput (kind:"date") —
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// date-field.tsx,
|
|
1
|
+
// DateInput (kind:"date") — the typeable date input lives in DateField,
|
|
2
|
+
// so `date` and `timestamp` share the same typing/calendar UX.
|
|
3
|
+
// This file only provides the public mapping; the logic is in
|
|
4
|
+
// date-field.tsx, and the date parsing utilities are in date-parse.ts.
|
|
5
5
|
export { DateField as DateInput } from "./date-field.js";
|
|
@@ -49,7 +49,7 @@ export function DefaultForm({ onSubmit, focusFirstErrorKey, children, title, sub
|
|
|
49
49
|
// it above the viewport edge; the shell shrinks (`flex-1`) to make room.
|
|
50
50
|
const pinnedFooter = isPinnedFooter(stickyActions, fillHeight);
|
|
51
51
|
const pinnedShellClassName = pinnedFooter ? "h-auto flex-1 pb-6" : undefined;
|
|
52
|
-
// Tab content
|
|
52
|
+
// Tab content: the head card is the ONLY card on the
|
|
53
53
|
// screen, so a tabbed projectionDetail's tab content must render as a
|
|
54
54
|
// sibling of that card, not nest another one around itself — chromeless
|
|
55
55
|
// skips the card wrapper and its border/padding so sections and footer
|
|
@@ -71,7 +71,7 @@ export function DefaultForm({ onSubmit, focusFirstErrorKey, children, title, sub
|
|
|
71
71
|
// FormScreenShell ancestor is itself height-constrained and
|
|
72
72
|
// shrinks it back down via min-h-0 — flex-1 would instead force
|
|
73
73
|
// it to always fill the remaining height, stretching a short
|
|
74
|
-
// relatedList tab to the bottom of the panel
|
|
74
|
+
// relatedList tab to the bottom of the panel.
|
|
75
75
|
// The min-height floor keeps the card from collapsing to ~0 on short
|
|
76
76
|
// viewports; the shell then overflows and <main> scrolls instead of
|
|
77
77
|
// the overflow-hidden card clipping the table.
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
// shadcn-style DropdownMenu
|
|
3
|
-
// @radix-ui/react-dropdown-menu
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// Separator
|
|
2
|
+
// shadcn-style DropdownMenu built on Radix. Wrappers around
|
|
3
|
+
// @radix-ui/react-dropdown-menu use the same Tailwind token classes as
|
|
4
|
+
// the other primitives. The caller provides the trigger-button styling
|
|
5
|
+
// through the asChild pattern; this module supplies only Content, Item,
|
|
6
|
+
// Label, Separator, and CheckboxItem with consistent styling.
|
|
7
7
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
8
|
+
// Advantages over the custom useDropdownMenu hook:
|
|
9
|
+
// - Click-outside, Escape, focus trapping, roving tabindex, ARIA roles,
|
|
10
|
+
// and pointer-versus-keyboard subtleties are handled by Radix.
|
|
11
|
+
// - Portaled content renders across stacking-context boundaries, which is
|
|
12
|
+
// useful in dialogs and popovers.
|
|
13
|
+
// - Keyboard navigation (↑↓ + Home/End) works by default.
|
|
14
14
|
import * as Primitive from "@radix-ui/react-dropdown-menu";
|
|
15
15
|
import { Check } from "lucide-react";
|
|
16
16
|
import { cn } from "../lib/cn.js";
|
|
@@ -33,10 +33,9 @@ const itemClass = "relative flex cursor-default select-none items-center gap-2 r
|
|
|
33
33
|
export function DropdownMenuItem({ className, ...props }) {
|
|
34
34
|
return _jsx(Primitive.Item, { className: cn(itemClass, className), ...props });
|
|
35
35
|
}
|
|
36
|
-
// CheckboxItem
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
// nötig.
|
|
36
|
+
// CheckboxItem with a check icon on the left for "selected item in a list"
|
|
37
|
+
// patterns (TenantSwitcher, LanguageSwitcher). Radix renders
|
|
38
|
+
// <ItemIndicator> only when checked=true, so no separate branch is needed.
|
|
40
39
|
export function DropdownMenuCheckboxItem({ className, children, checked, ...props }) {
|
|
41
40
|
return (_jsxs(Primitive.CheckboxItem, { checked: checked, className: cn(itemClass, "pl-8 pr-2", className), ...props, children: [_jsx("span", { className: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center", children: _jsx(Primitive.ItemIndicator, { children: _jsx(Check, { className: "h-3.5 w-3.5" }) }) }), children] }));
|
|
42
41
|
}
|
|
@@ -8,7 +8,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
8
8
|
// Only one of the two layouts (table for md+, cards below md) is mounted
|
|
9
9
|
// at a time, picked via useIsNarrowViewport — mounting both and toggling
|
|
10
10
|
// with `hidden`/`md:hidden` left two live inputs per cell sharing one DOM
|
|
11
|
-
// id
|
|
11
|
+
// id. The `hidden md:block` / `md:hidden` classes stay on the
|
|
12
12
|
// wrapper divs as a defensive fallback (e.g. SSR/no-JS, where
|
|
13
13
|
// getServerSnapshot forces the desktop layout) even though
|
|
14
14
|
// useIsNarrowViewport — unlike the vendored useIsMobile it replaced —
|
|
@@ -211,7 +211,7 @@ function RowActions({ rowIndex, rowsLength, minItems, maxItems, onDuplicateRow,
|
|
|
211
211
|
}
|
|
212
212
|
export function EmbeddedListInput({ id, columns, rows, totals, currency, disabled, fixedRows, minItems, maxItems, listIssues, rowIssues, cellIssues, onCellChange, onAddRow, onRemoveRow, onDuplicateRow, onMoveRow, onPasteCells, addLabel, actionsLabel, removeLabel, duplicateLabel, moveUpLabel, moveDownLabel, emptyLabel, emptyCtaLabel, testId, }) {
|
|
213
213
|
// Callers that don't wire up the field-currency plumbing (or direct
|
|
214
|
-
// callers/tests
|
|
214
|
+
// callers/tests) keep getting the same "EUR" this
|
|
215
215
|
// component always hardcoded.
|
|
216
216
|
const effectiveCurrency = currency ?? "EUR";
|
|
217
217
|
const t = useTranslation();
|
|
@@ -231,7 +231,7 @@ export function EmbeddedListInput({ id, columns, rows, totals, currency, disable
|
|
|
231
231
|
// notice the desktop table remounting with a fresh DOM node.
|
|
232
232
|
const [scrollContainerEl, setScrollContainerEl] = useState(null);
|
|
233
233
|
// Resetting scrollLeft here is what guarantees the table always opens
|
|
234
|
-
// scrolled fully left
|
|
234
|
+
// scrolled fully left: browsers still auto-scroll a focused
|
|
235
235
|
// descendant into view even inside an overflow-x-auto container, but
|
|
236
236
|
// that's now a real, user-drivable scroll instead of an invisible one.
|
|
237
237
|
useEffect(() => {
|
|
@@ -26,9 +26,9 @@ export function FacetFilter({ facet, selected, onChange, dense = false, }) {
|
|
|
26
26
|
if (facet.chips !== undefined) {
|
|
27
27
|
return _jsx(FacetChips, { facet: facet, selected: selected, onChange: onChange });
|
|
28
28
|
}
|
|
29
|
-
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(UiButton, { variant: "outline", size: "sm", className: cn("h-9", dense && "
|
|
29
|
+
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(UiButton, { variant: "outline", size: "sm", className: cn("h-9", dense && "border-border-strong px-2.5"), "data-testid": `facet-${facet.field}`, children: [facet.label, selected.length > 0 && (_jsx(Badge, { variant: "secondary", className: "ml-1 rounded-sm px-1 font-normal tabular-nums", children: selected.length })), _jsx(ChevronDown, { className: "text-muted-foreground" })] }) }), _jsx(DropdownMenuContent, { align: "start", className: "max-h-[var(--radix-dropdown-menu-content-available-height)] w-44 overflow-y-auto", children: facet.options.map((opt) => (_jsx(DropdownMenuCheckboxItem, { checked: selected.includes(opt.value), onCheckedChange: (checked) => toggle(opt.value, checked), onSelect: (e) => e.preventDefault(), "data-testid": `facet-${facet.field}-${opt.value}`, children: opt.label }, opt.value))) })] }));
|
|
30
30
|
}
|
|
31
|
-
// Time-range filter
|
|
31
|
+
// Time-range filter: two native <input type="date"> instead of a
|
|
32
32
|
// dropdown — the browser supplies the calendar, the locale and the keyboard
|
|
33
33
|
// handling, so no date dependency is needed. Each bound constrains the other
|
|
34
34
|
// through min/max so the picker can't offer an inverted span; the caller
|
|
@@ -81,10 +81,11 @@ const FIELD_ICONS = {
|
|
|
81
81
|
key: KeyRound,
|
|
82
82
|
"map-pin": MapPin,
|
|
83
83
|
};
|
|
84
|
+
function isFieldIconKey(key) {
|
|
85
|
+
return Object.hasOwn(FIELD_ICONS, key);
|
|
86
|
+
}
|
|
84
87
|
export function fieldIconFor(icon) {
|
|
85
|
-
return icon !== undefined &&
|
|
86
|
-
? FIELD_ICONS[icon]
|
|
87
|
-
: undefined;
|
|
88
|
+
return icon !== undefined && isFieldIconKey(icon) ? FIELD_ICONS[icon] : undefined;
|
|
88
89
|
}
|
|
89
90
|
// Wraps a text/number input with a left-positioned prefix icon when
|
|
90
91
|
// `icon` carries a known FIELD_ICONS key. `pl-8` overrides (via
|
|
@@ -127,7 +128,7 @@ export function withTrailingAction(action, input, keepWrapper = false) {
|
|
|
127
128
|
// Default presentation for `kind: "select"` without a `display` of its own
|
|
128
129
|
// (board rule): up to 3 short options read as a segmented control, up to 3
|
|
129
130
|
// longer ones as a vertical radio list, everything else as a dropdown. Labels
|
|
130
|
-
// arrive translated, so the widget can differ between UI languages
|
|
131
|
+
// arrive translated, so the widget can differ between UI languages.
|
|
131
132
|
const SEGMENTED_SELECT_MAX_OPTIONS = 3;
|
|
132
133
|
const SEGMENTED_SELECT_MAX_LABEL_LENGTH = 16;
|
|
133
134
|
export function defaultSelectPresentation(options) {
|
|
@@ -138,9 +139,9 @@ export function defaultSelectPresentation(options) {
|
|
|
138
139
|
: "radioList";
|
|
139
140
|
}
|
|
140
141
|
// A "" value is the unselected placeholder, not a real choice — a radio
|
|
141
|
-
// group can't render an unchecked-everything state as its own segment
|
|
142
|
-
//
|
|
143
|
-
//
|
|
142
|
+
// group can't render an unchecked-everything state as its own segment.
|
|
143
|
+
// Filtered once so the eligibility count and the rendered segments never drift
|
|
144
|
+
// apart.
|
|
144
145
|
export function withoutSegmentedSelectPlaceholder(options) {
|
|
145
146
|
return options.filter((option) => option.value !== "");
|
|
146
147
|
}
|
|
@@ -162,7 +163,7 @@ export function RadioListSelect({ id, name, value, onChange, options, variant =
|
|
|
162
163
|
: "flex items-start gap-2 text-sm font-normal text-foreground has-[:disabled]:opacity-50", children: [_jsx("input", { type: "radio", name: name, value: opt.value, checked: opt.value === value, disabled: disabled === true || opt.disabled === true, "aria-describedby": opt.description !== undefined ? descriptionIdOf(opt) : undefined, "data-testid": `radio-list-${id}-${opt.value}`, onChange: () => onChange(opt.value), className: cn("size-4 accent-primary", card ? "mt-0.5 shrink-0" : "mt-0.5") }), _jsxs("span", { className: "flex flex-col gap-0.5", children: [_jsx("span", { className: cn(card && "font-semibold"), children: opt.label }), opt.description !== undefined && (_jsx("span", { id: descriptionIdOf(opt), className: "text-[13px] text-foreground-secondary", children: opt.description }))] })] }, opt.value)))] }, section.heading ?? ""))) }));
|
|
163
164
|
}
|
|
164
165
|
// The vendored shadcn Textarea carries `field-sizing: content`, which derives
|
|
165
|
-
// the box height from the content and makes the `rows` attribute inert
|
|
166
|
+
// the box height from the content and makes the `rows` attribute inert.
|
|
166
167
|
// A declared row count therefore only survives as a min-height floor: the field
|
|
167
168
|
// starts at `rows` lines tall and still grows with its content. The addend is
|
|
168
169
|
// the textarea frame — py-2 top+bottom plus the 1px borders.
|
|
@@ -12,6 +12,9 @@ type FileMeta = {
|
|
|
12
12
|
readonly fileName: string;
|
|
13
13
|
readonly mimeType: string | undefined;
|
|
14
14
|
};
|
|
15
|
+
/** File names are per-user data and the server re-checks access per request, so
|
|
16
|
+
* the cache must not outlive the session that filled it. */
|
|
17
|
+
export declare function clearFileMetaCache(): void;
|
|
15
18
|
export declare function useFileMeta(fileId: string): FileMeta | null;
|
|
16
19
|
export declare function FileNameLink({ fileId, fallbackLabel, className, }: {
|
|
17
20
|
readonly fileId: string;
|
|
@@ -46,6 +46,11 @@ const META_BATCH_SIZE = 100;
|
|
|
46
46
|
const META_CACHE_LIMIT = 500;
|
|
47
47
|
const metaCache = new Map();
|
|
48
48
|
let pendingMetaIds = new Map();
|
|
49
|
+
/** File names are per-user data and the server re-checks access per request, so
|
|
50
|
+
* the cache must not outlive the session that filled it. */
|
|
51
|
+
export function clearFileMetaCache() {
|
|
52
|
+
metaCache.clear();
|
|
53
|
+
}
|
|
49
54
|
function parseBatchMeta(json) {
|
|
50
55
|
const metaById = new Map();
|
|
51
56
|
if (typeof json !== "object" || json === null || !("files" in json))
|
|
@@ -47,7 +47,7 @@ export declare function FormScreenShell({ children, className, testId, maxWidth,
|
|
|
47
47
|
readonly maxWidth?: FormScreenShellWidth;
|
|
48
48
|
/** Stacks children in a `h-full` flex column instead of normal document
|
|
49
49
|
* flow, so a `flex-1 min-h-0` child can claim the remaining height below
|
|
50
|
-
* the others (
|
|
50
|
+
* the others (DefaultForm's tabs+relatedList case). Default
|
|
51
51
|
* false: unchanged, content-sized height. */
|
|
52
52
|
readonly fillHeight?: boolean;
|
|
53
53
|
}): ReactNode;
|
|
@@ -11,11 +11,11 @@ import "../react-css-custom-properties.js";
|
|
|
11
11
|
import { cardFooterBorder, cardHeaderBorder } from "./card-styles.js";
|
|
12
12
|
import { InsideFormContext, ScreenFormContext, ScreenWidthContext } from "./contexts.js";
|
|
13
13
|
// Extension sections that render their own <form> via BareFormProvider
|
|
14
|
-
// (legacy custom-form pattern, e.g. ChangeEmailSection
|
|
14
|
+
// (legacy custom-form pattern, e.g. ChangeEmailSection) land
|
|
15
15
|
// inside RenderEdit's host <form> (ExtensionSectionMount, render-edit.tsx) —
|
|
16
16
|
// nested <form> elements are invalid DOM, and real browsers can silently
|
|
17
17
|
// fall back to a native GET submit of the OUTER form, leaking field values
|
|
18
|
-
// into the URL
|
|
18
|
+
// into the URL. Degrading to a <div> when already inside
|
|
19
19
|
// a form avoids the nesting; the captured click still routes to THIS form's
|
|
20
20
|
// onSubmit instead of activating the real ancestor <form>.
|
|
21
21
|
const SUBMIT_CONTROL_SELECTOR = [
|
|
@@ -95,7 +95,7 @@ export function FormSections({ children, chromeless, stickyActions, fillHeight,
|
|
|
95
95
|
// Margins, not padding: padding would land inside a child that has its own border/height.
|
|
96
96
|
!chromeless && "[&>:not(section)]:mx-6 [&>:not(section)]:my-3", !chromeless && "[&>:not(section):first-child]:mt-6 [&>:not(section):last-child]:mb-6",
|
|
97
97
|
// ponytail: fixed footer now pins only the primary action (single
|
|
98
|
-
// button row + its own p-4
|
|
98
|
+
// button row + its own p-4) instead of the whole footer —
|
|
99
99
|
// shrunk from pb-32 accordingly. Widen again if a wizard's primary
|
|
100
100
|
// action ever wraps to two rows.
|
|
101
101
|
stickyActions === true && fillHeight !== true && STICKY_FOOTER_SPACER_CLASS,
|
|
@@ -117,7 +117,7 @@ export function FormTitleBlock({ title, subtitle, titleAction, testId, bordered,
|
|
|
117
117
|
}
|
|
118
118
|
// `formActions` (render-edit.tsx) mixes a non-submit wizard Back button in
|
|
119
119
|
// with the submit-type Next/Finish button inside one Fragment — only the
|
|
120
|
-
// submit one is the "primary" action that
|
|
120
|
+
// submit one is the "primary" action that needs pinned above a
|
|
121
121
|
// virtual keyboard. Back reads as `type="button"`, same as `secondaryActions`
|
|
122
122
|
// (Cancel/Delete/…), so it can safely join that group on mobile. A footer-slot
|
|
123
123
|
// mount can't expose `type="submit"` (its button is opaque app code), so it
|
|
@@ -162,7 +162,7 @@ export function FormFooter({ actions, secondaryActions, testId, chromeless, stic
|
|
|
162
162
|
const hasPrimaryAction = primaryActionNodes !== undefined && primaryActionNodes.length > 0;
|
|
163
163
|
// No primary node found among `actions` (e.g. a wizard step with only
|
|
164
164
|
// secondary buttons): fall back to pinning the whole, unpartitioned
|
|
165
|
-
// `actions` node instead of splitting it — matches the
|
|
165
|
+
// `actions` node instead of splitting it — matches the previous
|
|
166
166
|
// behaviour where sticky footers always kept `actions` fixed.
|
|
167
167
|
const nonPrimaryActionNodes = hasPrimaryAction
|
|
168
168
|
? actionNodes?.filter((node) => !isPrimaryStickyAction(node))
|
|
@@ -196,7 +196,7 @@ export function FormFooter({ actions, secondaryActions, testId, chromeless, stic
|
|
|
196
196
|
: "flex-wrap max-sm:w-full max-sm:[&>button]:flex-1 max-sm:[&>button]:min-h-11 sm:ml-auto",
|
|
197
197
|
// Below sm (640px): pin only the primary action to the viewport
|
|
198
198
|
// bottom instead of normal flow, so a virtual keyboard shrinking
|
|
199
|
-
// the viewport can't push it out of reach
|
|
199
|
+
// the viewport can't push it out of reach. `fixed`
|
|
200
200
|
// escapes the card's `overflow-hidden` (only transform/filter/
|
|
201
201
|
// contain ancestors trap it, confirmed against
|
|
202
202
|
// AppLayout/SidebarInset — neither sets those).
|
|
@@ -5,6 +5,7 @@ import { Checkbox } from "../ui/checkbox.js";
|
|
|
5
5
|
import { Input as UiInput } from "../ui/input.js";
|
|
6
6
|
import { Switch } from "../ui/switch.js";
|
|
7
7
|
import { Textarea } from "../ui/textarea.js";
|
|
8
|
+
import { CodeInput } from "./code-input.js";
|
|
8
9
|
import { ComboboxInput } from "./combobox.js";
|
|
9
10
|
import { DateInput } from "./date-input.js";
|
|
10
11
|
import { FileUploadInput } from "./file-upload.js";
|
|
@@ -15,6 +16,7 @@ import { NumberInput } from "./number-input.js";
|
|
|
15
16
|
import { hasDescriptionOrGroup } from "./option-groups.js";
|
|
16
17
|
import { TimestampInput } from "./timestamp-input.js";
|
|
17
18
|
import { TzInput } from "./tz-input.js";
|
|
19
|
+
import { useFocusOnMount } from "./use-focus-on-mount.js";
|
|
18
20
|
import "../react-css-custom-properties.js";
|
|
19
21
|
import { FieldDescriptionContext, FieldLayoutContext } from "./contexts.js";
|
|
20
22
|
import { defaultSelectPresentation, FIELD_ICON_CLASS, fieldIconFor, RadioListSelect, SegmentedSelect, textareaMinHeight, unitSuffixId, withFieldIcon, withInlineDisabledHint, withInputAddons, withoutSegmentedSelectPlaceholder, withTrailingAction, } from "./field.js";
|
|
@@ -23,6 +25,7 @@ export function DefaultInput(props) {
|
|
|
23
25
|
// themselves, no manual border-destructive needed anymore.
|
|
24
26
|
const booleanLayout = useContext(FieldLayoutContext);
|
|
25
27
|
const describedBy = useContext(FieldDescriptionContext);
|
|
28
|
+
const textInputRef = useFocusOnMount(props.kind === "text" ? props.autoFocus : undefined);
|
|
26
29
|
const common = {
|
|
27
30
|
id: props.id,
|
|
28
31
|
name: props.name,
|
|
@@ -33,7 +36,9 @@ export function DefaultInput(props) {
|
|
|
33
36
|
};
|
|
34
37
|
switch (props.kind) {
|
|
35
38
|
case "text":
|
|
36
|
-
return withInputAddons(props, withFieldIcon(props.icon, _jsx(UiInput, { type: "text", ...common, ...props.dataAttributes, "data-testid": props.testId, readOnly: props.readOnly, value: props.value, onChange: (e) => props.onChange(e.target.value), onKeyDown: props.onKeyDown, ...(props.placeholder !== undefined && { placeholder: props.placeholder }), ...(props.autoComplete !== undefined && { autoComplete: props.autoComplete }), className: cn(fieldIconFor(props.icon) !== undefined ? "pl-8" : undefined) })));
|
|
39
|
+
return withInputAddons(props, withFieldIcon(props.icon, _jsx(UiInput, { type: "text", ref: textInputRef, ...common, ...props.dataAttributes, "data-testid": props.testId, readOnly: props.readOnly, value: props.value, onChange: (e) => props.onChange(e.target.value), onKeyDown: props.onKeyDown, ...(props.placeholder !== undefined && { placeholder: props.placeholder }), ...(props.autoComplete !== undefined && { autoComplete: props.autoComplete }), className: cn(fieldIconFor(props.icon) !== undefined ? "pl-8" : undefined) })));
|
|
40
|
+
case "code":
|
|
41
|
+
return _jsx(CodeInput, { ...props });
|
|
37
42
|
case "email":
|
|
38
43
|
return (_jsx(UiInput, { type: "email", ...common, "data-testid": props.testId, value: props.value, onChange: (e) => props.onChange(e.target.value), ...(props.placeholder !== undefined && { placeholder: props.placeholder }), autoComplete: props.autoComplete ?? "email" }));
|
|
39
44
|
case "month":
|
|
@@ -86,7 +91,7 @@ export function DefaultInput(props) {
|
|
|
86
91
|
const radioGroupOptions = props.display === "radio" ? comboOptions : withoutSegmentedSelectPlaceholder(comboOptions);
|
|
87
92
|
// An explicit `display` is an author decision and outranks the
|
|
88
93
|
// heuristic in both directions — a requested radio group renders as
|
|
89
|
-
// one even when the options outnumber the heuristic's threshold
|
|
94
|
+
// one even when the options outnumber the heuristic's threshold.
|
|
90
95
|
const requestedPresentation = props.radioVariant === "card"
|
|
91
96
|
? "radioList"
|
|
92
97
|
: props.display === "radio"
|
|
@@ -107,7 +112,7 @@ export function DefaultInput(props) {
|
|
|
107
112
|
return (_jsx(ComboboxInput, { id: props.id, name: props.name, value: props.value, onChange: props.onChange, options: dropdownOptions, ...(props.disabled !== undefined && { disabled: props.disabled }), ...(props.required !== undefined && { required: props.required }), ...(props.hasError !== undefined && { hasError: props.hasError }) }));
|
|
108
113
|
}
|
|
109
114
|
case "combobox": {
|
|
110
|
-
//
|
|
115
|
+
// Discriminated union per `multiple`; we split
|
|
111
116
|
// TS-side into two branches so ComboboxInput's single/multi variants
|
|
112
117
|
// render with the right types.
|
|
113
118
|
const baseProps = {
|
|
@@ -48,7 +48,7 @@ export function DefaultSection({ id, title, subtitle, children, actions, variant
|
|
|
48
48
|
export function DefaultFillContainer({ children, testId, grow }) {
|
|
49
49
|
// flex-1 only on request (`grow`, set under fillScreenHeight): without it the
|
|
50
50
|
// container sizes to its content and only shrinks (min-h-0), so a two-row
|
|
51
|
-
// table in a non-fixed-height screen does not stretch
|
|
51
|
+
// table in a non-fixed-height screen does not stretch.
|
|
52
52
|
return (_jsx("div", { "data-testid": testId, className: cn("flex min-h-0 flex-col", grow === true && "flex-1"), children: children }));
|
|
53
53
|
}
|
|
54
54
|
const DRAWER_FIELD_CELL_WIDTH_CLASS = {
|
|
@@ -61,7 +61,7 @@ const FIELD_CELL_WIDTH_CLASS = {
|
|
|
61
61
|
money: "w-40 shrink-0 [&_label]:whitespace-nowrap",
|
|
62
62
|
date: "w-full sm:w-[200px]",
|
|
63
63
|
timestamp: "w-full sm:w-[328px]",
|
|
64
|
-
select: "w-full sm:w-
|
|
64
|
+
select: "w-full sm:w-60 sm:has-[[data-radio-list]]:w-full",
|
|
65
65
|
full: "w-full",
|
|
66
66
|
// self-start: the label shares the top line with neighbouring labels. The
|
|
67
67
|
// switch (1.15rem) gets (h-9 input − switch) / 2 margin to sit on the input line.
|
|
@@ -75,7 +75,8 @@ export function DefaultGrid({ columns, children, testId, maxRows, flow, list, ar
|
|
|
75
75
|
if (list === true) {
|
|
76
76
|
return (_jsx("div", { ...groupProps, "data-testid": testId, className: "flex flex-col divide-y divide-border", children: children }));
|
|
77
77
|
}
|
|
78
|
-
|
|
78
|
+
const declaresColumns = typeof columns === "number" && columns >= 2;
|
|
79
|
+
if (flow === true && (insideDrawerBody || !declaresColumns)) {
|
|
79
80
|
return (_jsx("div", { ...groupProps, "data-testid": testId, className: cn("flex gap-4", insideDrawerBody
|
|
80
81
|
? "flex-col items-start"
|
|
81
82
|
: // items-end aligns inputs when labels wrap to different heights; a
|
|
@@ -116,12 +117,13 @@ export function DefaultGrid({ columns, children, testId, maxRows, flow, list, ar
|
|
|
116
117
|
overflowY: "auto",
|
|
117
118
|
}),
|
|
118
119
|
};
|
|
119
|
-
return (_jsx("div", { ...groupProps, "data-testid": testId, className: "grid gap-[var(--kumiko-grid-gap,1rem)] grid-cols-1 sm:[grid-template-columns:var(--grid-cols)]",
|
|
120
|
+
return (_jsx("div", { ...groupProps, "data-testid": testId, className: cn("grid gap-[var(--kumiko-grid-gap,1rem)] grid-cols-1 sm:[grid-template-columns:var(--grid-cols)]", flow === true &&
|
|
121
|
+
"items-end has-[[data-appendix-below]]:items-start has-[[role=alert]]:items-start"), style: style, children: children }));
|
|
120
122
|
}
|
|
121
123
|
export function DefaultGridCell({ span, width, children }) {
|
|
122
124
|
const insideDrawerBody = useContext(DrawerBodyContext);
|
|
123
125
|
if (width !== undefined) {
|
|
124
|
-
return (_jsx("div", { className: cn("min-w-0 max-w-full", insideDrawerBody
|
|
126
|
+
return (_jsx("div", { className: cn("min-w-0 max-w-full", width === "full" && "sm:[grid-column:1/-1]", insideDrawerBody
|
|
125
127
|
? (DRAWER_FIELD_CELL_WIDTH_CLASS[width] ?? FIELD_CELL_WIDTH_CLASS[width])
|
|
126
128
|
: FIELD_CELL_WIDTH_CLASS[width]), children: children }));
|
|
127
129
|
}
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { cn } from "../lib/cn.js";
|
|
3
3
|
// Shared 3xl/4xl/full → max-w-*/mx-auto map. FormScreenShell
|
|
4
4
|
// (primitives/index.tsx) reuses this instead of keeping its own copy —
|
|
5
|
-
// same width tokens must render the same class everywhere
|
|
5
|
+
// same width tokens must render the same class everywhere.
|
|
6
6
|
export const screenWidthClassName = {
|
|
7
7
|
"3xl": "max-w-3xl mx-auto",
|
|
8
8
|
"4xl": "max-w-4xl mx-auto",
|
|
@@ -19,7 +19,7 @@ export const screenFormColumnWidthClassName = {
|
|
|
19
19
|
// FormScreenShell, list screens and the pinned-footer screen form). The inset
|
|
20
20
|
// is the one the settings screens use, so a list, a dashboard and a form start
|
|
21
21
|
// at the same edge. The bottom inset keeps the last field or table row off the
|
|
22
|
-
// viewport edge
|
|
22
|
+
// viewport edge.
|
|
23
23
|
export const screenPaddingClassName = "px-4 pb-6 pt-5 md:px-10 md:pb-10 md:pt-7";
|
|
24
24
|
const STACK_GAP = { sm: "gap-2", md: "gap-4", lg: "gap-6" };
|
|
25
25
|
export function Stack({ gap = "md", className, children, testId }) {
|
|
@@ -7,10 +7,10 @@ export function LocatedTimestampInput({ id, name, value, onChange, locale, min,
|
|
|
7
7
|
const t = useTranslation();
|
|
8
8
|
const at = value === "" ? "" : value.at;
|
|
9
9
|
const tz = value === "" ? "" : value.tz;
|
|
10
|
-
// `at`
|
|
11
|
-
// tz
|
|
12
|
-
//
|
|
13
|
-
//
|
|
10
|
+
// `at` is a minute-precision wall-clock value (z.iso.datetime({local:true}) accepts it);
|
|
11
|
+
// tz is an IANA name. The field is empty while both are empty; once either
|
|
12
|
+
// is set, the pair is emitted — Required/Zod validation on submit enforces
|
|
13
|
+
// completeness (so a partial input remains visible).
|
|
14
14
|
function emit(nextAt, nextTz) {
|
|
15
15
|
if (nextAt === "" && nextTz === "") {
|
|
16
16
|
onChange(undefined);
|