@cosmicdrift/kumiko-renderer 0.384.0 → 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 +3 -3
- package/dist/headless/view-model/edit.d.ts +1 -1
- package/dist/headless/view-model/edit.js +9 -16
- package/dist/headless/view-model/list.d.ts +1 -1
- package/dist/headless/view-model/list.js +3 -8
- package/dist/index.d.ts +2 -1
- package/dist/index.js +5 -5
- package/dist/live/error-mapping.js +3 -4
- package/dist/web/app/create-app.js +5 -8
- package/dist/web/app/dashboard-body.js +4 -599
- package/dist/web/app/dashboard-panels.d.ts +53 -0
- package/dist/web/app/dashboard-panels.js +373 -0
- package/dist/web/app/dashboard-shared.d.ts +90 -0
- package/dist/web/app/dashboard-shared.js +171 -0
- package/dist/web/app/dashboard-text.d.ts +6 -0
- package/dist/web/app/dashboard-text.js +65 -0
- package/dist/web/layout/nav-tree.js +3 -4
- package/dist/web/layout/target-resolver-stub.js +2 -2
- package/dist/web/primitives/banner.d.ts +4 -0
- package/dist/web/primitives/banner.js +28 -0
- package/dist/web/primitives/button.d.ts +7 -0
- package/dist/web/primitives/button.js +48 -0
- package/dist/web/primitives/card-styles.d.ts +8 -0
- package/dist/web/primitives/card-styles.js +18 -0
- package/dist/web/primitives/card.d.ts +4 -0
- package/dist/web/primitives/card.js +41 -0
- package/dist/web/primitives/contexts.d.ts +22 -0
- package/dist/web/primitives/contexts.js +50 -0
- package/dist/web/primitives/data-table-cell.d.ts +76 -0
- package/dist/web/primitives/data-table-cell.js +403 -0
- package/dist/web/primitives/data-table.d.ts +4 -0
- package/dist/web/primitives/data-table.js +279 -0
- package/dist/web/primitives/default-form.d.ts +4 -0
- package/dist/web/primitives/default-form.js +79 -0
- package/dist/web/primitives/facet-filter.d.ts +14 -0
- package/dist/web/primitives/facet-filter.js +39 -0
- package/dist/web/primitives/field.d.ts +63 -0
- package/dist/web/primitives/field.js +181 -0
- package/dist/web/primitives/form.d.ts +53 -0
- package/dist/web/primitives/form.js +252 -0
- package/dist/web/primitives/index.d.ts +7 -30
- package/dist/web/primitives/index.js +27 -2006
- package/dist/web/primitives/input.d.ts +4 -0
- package/dist/web/primitives/input.js +166 -0
- package/dist/web/primitives/layout-elements.d.ts +14 -0
- package/dist/web/primitives/layout-elements.js +194 -0
- package/dist/web/primitives/modal.js +1 -1
- package/dist/web/primitives/pager.d.ts +22 -0
- package/dist/web/primitives/pager.js +89 -0
- package/dist/web/primitives/row-actions.d.ts +13 -0
- package/dist/web/primitives/row-actions.js +178 -0
- package/dist/web/primitives/secret-reveal.d.ts +4 -0
- package/dist/web/primitives/secret-reveal.js +67 -0
- package/package.json +5 -5
- package/src/changes.json +6 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useContext } from "react";
|
|
3
|
+
import { cn } from "../lib/cn.js";
|
|
4
|
+
import { Checkbox } from "../ui/checkbox.js";
|
|
5
|
+
import { Input as UiInput } from "../ui/input.js";
|
|
6
|
+
import { Switch } from "../ui/switch.js";
|
|
7
|
+
import { Textarea } from "../ui/textarea.js";
|
|
8
|
+
import { ComboboxInput } from "./combobox.js";
|
|
9
|
+
import { DateInput } from "./date-input.js";
|
|
10
|
+
import { FileUploadInput } from "./file-upload.js";
|
|
11
|
+
import { FilesDropzoneInput } from "./files-dropzone.js";
|
|
12
|
+
import { LocatedTimestampInput } from "./located-timestamp-input.js";
|
|
13
|
+
import { MoneyInput } from "./money-input.js";
|
|
14
|
+
import { NumberInput } from "./number-input.js";
|
|
15
|
+
import { hasDescriptionOrGroup } from "./option-groups.js";
|
|
16
|
+
import { TimestampInput } from "./timestamp-input.js";
|
|
17
|
+
import { TzInput } from "./tz-input.js";
|
|
18
|
+
import "../react-css-custom-properties.js";
|
|
19
|
+
import { FieldDescriptionContext, FieldLayoutContext } from "./contexts.js";
|
|
20
|
+
import { defaultSelectPresentation, FIELD_ICON_CLASS, fieldIconFor, RadioListSelect, SegmentedSelect, textareaMinHeight, unitSuffixId, withFieldIcon, withInlineDisabledHint, withInputAddons, withoutSegmentedSelectPlaceholder, withTrailingAction, } from "./field.js";
|
|
21
|
+
export function DefaultInput(props) {
|
|
22
|
+
// Vendored ui/input + ui/checkbox style errors via `aria-invalid`
|
|
23
|
+
// themselves, no manual border-destructive needed anymore.
|
|
24
|
+
const booleanLayout = useContext(FieldLayoutContext);
|
|
25
|
+
const describedBy = useContext(FieldDescriptionContext);
|
|
26
|
+
const common = {
|
|
27
|
+
id: props.id,
|
|
28
|
+
name: props.name,
|
|
29
|
+
disabled: props.disabled,
|
|
30
|
+
"aria-describedby": describedBy,
|
|
31
|
+
"aria-required": props.required,
|
|
32
|
+
"aria-invalid": props.hasError === true ? true : undefined,
|
|
33
|
+
};
|
|
34
|
+
switch (props.kind) {
|
|
35
|
+
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) })));
|
|
37
|
+
case "email":
|
|
38
|
+
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
|
+
case "month":
|
|
40
|
+
return (_jsx(UiInput, { type: "month", ...common, "data-testid": props.testId, value: props.value, onChange: (e) => props.onChange(e.target.value) }));
|
|
41
|
+
case "password":
|
|
42
|
+
return withTrailingAction(props.trailingAction, _jsx(UiInput, { type: "password", ...common, "data-testid": props.testId, value: props.value, onChange: (e) => props.onChange(e.target.value), ...(props.placeholder !== undefined && { placeholder: props.placeholder }), autoComplete: props.autoComplete ?? "current-password", className: props.trailingAction !== undefined ? "pr-9" : undefined }), true);
|
|
43
|
+
case "number": {
|
|
44
|
+
const NumberIcon = fieldIconFor(props.icon);
|
|
45
|
+
// Icon, input and unit share this one box, and the grid cell sizes the
|
|
46
|
+
// box rather than the bare input: anchored to a wider wrapper, the unit
|
|
47
|
+
// would sit next to a narrower input instead of inside it.
|
|
48
|
+
const numberField = (_jsxs("div", { "data-slot": "number-field", className: "relative", children: [NumberIcon !== undefined && (_jsx(NumberIcon, { "aria-hidden": "true", className: FIELD_ICON_CLASS })), _jsx(NumberInput, { id: props.id, name: props.name, value: props.value, onChange: props.onChange, disabled: props.disabled, required: props.required, hasError: props.hasError, testId: props.testId, ...(props.unit !== undefined && { ariaDescribedBy: unitSuffixId(props.id) }), ...(props.locale !== undefined && { locale: props.locale }), ...(props.grouping !== undefined && { grouping: props.grouping }), ...(props.integer !== undefined && { integer: props.integer }), ...(props.placeholder !== undefined && { placeholder: props.placeholder }), className: cn(NumberIcon !== undefined ? "pl-8" : undefined, props.unit !== undefined ? "pr-8" : undefined) }), props.unit !== undefined && (_jsx("span", { id: unitSuffixId(props.id), className: "pointer-events-none absolute right-2.5 top-1/2 max-w-[40%] -translate-y-1/2 truncate text-sm text-muted-foreground", children: props.unit }))] }));
|
|
49
|
+
return withInputAddons(props, numberField);
|
|
50
|
+
}
|
|
51
|
+
case "range":
|
|
52
|
+
return (_jsx("input", { type: "range", ...common, min: props.min, max: props.max, step: props.step ?? 1, value: props.value, onChange: (e) => props.onChange(Number(e.target.value)), className: "w-full accent-primary" }));
|
|
53
|
+
case "boolean":
|
|
54
|
+
// layout="inline" (checkbox lists like MultiSelectCheckboxes, the
|
|
55
|
+
// BooleanField widget) keeps the checkbox optics; the standard form
|
|
56
|
+
// field (render-field.tsx, layout="stacked") gets the switch.
|
|
57
|
+
return booleanLayout === "inline" ? (_jsx(Checkbox, { id: props.id, name: props.name, disabled: props.disabled, "aria-required": props.required, "aria-invalid": props.hasError === true ? true : undefined, checked: props.value, onCheckedChange: (checked) => props.onChange(checked === true) })) : (_jsx(Switch, { id: props.id, name: props.name, disabled: props.disabled, "aria-required": props.required, "aria-invalid": props.hasError === true ? true : undefined, checked: props.value, onCheckedChange: (checked) => props.onChange(checked === true), className: "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40" }));
|
|
58
|
+
case "file":
|
|
59
|
+
case "image":
|
|
60
|
+
return (_jsx(FileUploadInput, { kind: props.kind, id: props.id, value: props.value, onChange: props.onChange, ...(props.accept !== undefined && { accept: props.accept }), ...(props.disabled !== undefined && { disabled: props.disabled }), ...(props.entityType !== undefined && { entityType: props.entityType }), ...(props.uploadActionForm !== undefined && {
|
|
61
|
+
uploadActionForm: props.uploadActionForm,
|
|
62
|
+
}), ...(props.entityId !== undefined && { entityId: props.entityId }), ...(props.fieldName !== undefined && { fieldName: props.fieldName }), ...(props.imageVariant !== undefined && { imageVariant: props.imageVariant }), ...(props.capture !== undefined && { capture: props.capture }), ...(props.directory !== undefined && { directory: props.directory }), ...(props.onFilesPicked !== undefined && { onFilesPicked: props.onFilesPicked }), ...(props.pickLabel !== undefined && { pickLabel: props.pickLabel }), ...(props.testId !== undefined && { testId: props.testId }) }));
|
|
63
|
+
case "files-dropzone":
|
|
64
|
+
return (_jsx(FilesDropzoneInput, { id: props.id, value: props.value, onChange: props.onChange, ...(props.accept !== undefined && { accept: props.accept }), ...(props.maxSize !== undefined && { maxSize: props.maxSize }), ...(props.maxCount !== undefined && { maxCount: props.maxCount }), ...(props.disabled !== undefined && { disabled: props.disabled }), ...(props.entityType !== undefined && { entityType: props.entityType }), ...(props.uploadActionForm !== undefined && {
|
|
65
|
+
uploadActionForm: props.uploadActionForm,
|
|
66
|
+
}), ...(props.entityId !== undefined && { entityId: props.entityId }), ...(props.fieldName !== undefined && { fieldName: props.fieldName }), ...(props.testId !== undefined && { testId: props.testId }) }));
|
|
67
|
+
case "date":
|
|
68
|
+
return (_jsx(DateInput, { id: props.id, name: props.name, value: props.value, onChange: props.onChange, ...(props.locale !== undefined && { locale: props.locale }), ...(props.min !== undefined && { min: props.min }), ...(props.max !== undefined && { max: props.max }), ...(props.disabled !== undefined && { disabled: props.disabled }), ...(props.required !== undefined && { required: props.required }), ...(props.hasError !== undefined && { hasError: props.hasError }) }));
|
|
69
|
+
case "select": {
|
|
70
|
+
// Visual consolidation: all selects go through ComboboxInput
|
|
71
|
+
// (cmdk + Radix popover). Before, there were two paths: Radix
|
|
72
|
+
// Select for `kind:"select"` and cmdk for `kind:"combobox"`. Three
|
|
73
|
+
// visually different variants (single select, combobox single, combobox
|
|
74
|
+
// multi) became unmanageable. With the merge the combobox is the only
|
|
75
|
+
// implementation: the search input is present even for 4-item status
|
|
76
|
+
// selects, a deliberately accepted UX consequence for style consistency.
|
|
77
|
+
const comboOptions = props.options.map((o) => typeof o === "string" ? { value: o, label: o } : o);
|
|
78
|
+
// The dropdown shows a disabled option's hint inline, so it stays readable in the trigger and search.
|
|
79
|
+
const dropdownOptions = comboOptions.map(withInlineDisabledHint);
|
|
80
|
+
// The radio group can't render the unselected placeholder as its own
|
|
81
|
+
// segment, so it counts and renders on the real options only — the
|
|
82
|
+
// dropdown below keeps comboOptions (placeholder included) since it
|
|
83
|
+
// has a genuine "nothing selected" row.
|
|
84
|
+
// An author who asks for `display: "radio"` explicitly declared the ""
|
|
85
|
+
// option as a real choice (e.g. "Any"), so it stays a segment.
|
|
86
|
+
const radioGroupOptions = props.display === "radio" ? comboOptions : withoutSegmentedSelectPlaceholder(comboOptions);
|
|
87
|
+
// An explicit `display` is an author decision and outranks the
|
|
88
|
+
// heuristic in both directions — a requested radio group renders as
|
|
89
|
+
// one even when the options outnumber the heuristic's threshold (#2711).
|
|
90
|
+
const requestedPresentation = props.radioVariant === "card"
|
|
91
|
+
? "radioList"
|
|
92
|
+
: props.display === "radio"
|
|
93
|
+
? "segmented"
|
|
94
|
+
: props.display === "dropdown"
|
|
95
|
+
? "dropdown"
|
|
96
|
+
: defaultSelectPresentation(radioGroupOptions);
|
|
97
|
+
// A segmented control has no room for a description line or group headings.
|
|
98
|
+
const presentation = requestedPresentation === "segmented" && hasDescriptionOrGroup(radioGroupOptions)
|
|
99
|
+
? "radioList"
|
|
100
|
+
: requestedPresentation;
|
|
101
|
+
if (presentation === "radioList" && radioGroupOptions.length > 0) {
|
|
102
|
+
return (_jsx(RadioListSelect, { id: props.id, name: props.name, value: props.value, onChange: props.onChange, options: radioGroupOptions, ...(props.radioVariant !== undefined && { variant: props.radioVariant }), ...(props.disabled !== undefined && { disabled: props.disabled }), ...(props.required !== undefined && { required: props.required }), ...(props.hasError !== undefined && { hasError: props.hasError }) }));
|
|
103
|
+
}
|
|
104
|
+
if (presentation === "segmented" && radioGroupOptions.length > 0) {
|
|
105
|
+
return (_jsx(SegmentedSelect, { id: props.id, name: props.name, value: props.value, onChange: props.onChange, options: radioGroupOptions, ...(props.disabled !== undefined && { disabled: props.disabled }), ...(props.required !== undefined && { required: props.required }), ...(props.hasError !== undefined && { hasError: props.hasError }) }));
|
|
106
|
+
}
|
|
107
|
+
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
|
+
}
|
|
109
|
+
case "combobox": {
|
|
110
|
+
// Tier 2.1c + tier 2.7e: discriminated union per `multiple`; we split
|
|
111
|
+
// TS-side into two branches so ComboboxInput's single/multi variants
|
|
112
|
+
// render with the right types.
|
|
113
|
+
const baseProps = {
|
|
114
|
+
id: props.id,
|
|
115
|
+
name: props.name,
|
|
116
|
+
options: props.options,
|
|
117
|
+
...(props.disabled !== undefined && { disabled: props.disabled }),
|
|
118
|
+
...(props.required !== undefined && { required: props.required }),
|
|
119
|
+
...(props.hasError !== undefined && { hasError: props.hasError }),
|
|
120
|
+
...(props.placeholder !== undefined && { placeholder: props.placeholder }),
|
|
121
|
+
...(props.searchPlaceholder !== undefined && {
|
|
122
|
+
searchPlaceholder: props.searchPlaceholder,
|
|
123
|
+
}),
|
|
124
|
+
...(props.emptyText !== undefined && { emptyText: props.emptyText }),
|
|
125
|
+
...(props.onSearchChange !== undefined && { onSearchChange: props.onSearchChange }),
|
|
126
|
+
...(props.loading !== undefined && { loading: props.loading }),
|
|
127
|
+
...(props.onCreate !== undefined && { onCreate: props.onCreate }),
|
|
128
|
+
...(props.createLabel !== undefined && { createLabel: props.createLabel }),
|
|
129
|
+
};
|
|
130
|
+
if (props.multiple === true) {
|
|
131
|
+
return (_jsx(ComboboxInput, { ...baseProps, multiple: true, value: props.value, onChange: props.onChange }));
|
|
132
|
+
}
|
|
133
|
+
return _jsx(ComboboxInput, { ...baseProps, value: props.value, onChange: props.onChange });
|
|
134
|
+
}
|
|
135
|
+
case "money":
|
|
136
|
+
return (_jsx(MoneyInput, { id: props.id, name: props.name, value: props.value, onChange: props.onChange, currency: props.currency ?? "EUR", ...(props.locale !== undefined && { locale: props.locale }), ...(props.disabled !== undefined && { disabled: props.disabled }), ...(props.required !== undefined && { required: props.required }), ...(props.hasError !== undefined && { hasError: props.hasError }) }));
|
|
137
|
+
case "timestamp":
|
|
138
|
+
return (_jsx(TimestampInput, { id: props.id, name: props.name, value: props.value, onChange: props.onChange, ...(props.wallClock !== undefined && { wallClock: props.wallClock }), ...(props.locale !== undefined && { locale: props.locale }), ...(props.min !== undefined && { min: props.min }), ...(props.max !== undefined && { max: props.max }), ...(props.disabled !== undefined && { disabled: props.disabled }), ...(props.required !== undefined && { required: props.required }), ...(props.hasError !== undefined && { hasError: props.hasError }) }));
|
|
139
|
+
case "locatedTimestamp":
|
|
140
|
+
return (_jsx(LocatedTimestampInput, { id: props.id, name: props.name, value: props.value, onChange: props.onChange, ...(props.locale !== undefined && { locale: props.locale }), ...(props.min !== undefined && { min: props.min }), ...(props.max !== undefined && { max: props.max }), ...(props.disabled !== undefined && { disabled: props.disabled }), ...(props.required !== undefined && { required: props.required }), ...(props.hasError !== undefined && { hasError: props.hasError }) }));
|
|
141
|
+
case "textarea": {
|
|
142
|
+
const rows = props.rows;
|
|
143
|
+
const onSubmitShortcut = props.onSubmitShortcut;
|
|
144
|
+
const onKeyDown = props.onKeyDown;
|
|
145
|
+
return (_jsx(Textarea, { ...common, ...props.dataAttributes, readOnly: props.readOnly, value: props.value, onChange: (e) => props.onChange(e.target.value), rows: rows ?? 4, className: "resize-y", ...(props.placeholder !== undefined && { placeholder: props.placeholder }), ...(rows !== undefined && { style: textareaMinHeight(rows) }), ...((onSubmitShortcut !== undefined || onKeyDown !== undefined) && {
|
|
146
|
+
...(onSubmitShortcut !== undefined && {
|
|
147
|
+
"aria-keyshortcuts": "Control+Enter Meta+Enter",
|
|
148
|
+
}),
|
|
149
|
+
onKeyDown: (e) => {
|
|
150
|
+
onKeyDown?.(e);
|
|
151
|
+
// Lets a caller that already handles Ctrl/Cmd+Enter (e.g. an
|
|
152
|
+
// "Enter sends" handler that ignores only Shift) opt out of the
|
|
153
|
+
// shortcut instead of submitting twice.
|
|
154
|
+
if (e.defaultPrevented)
|
|
155
|
+
return;
|
|
156
|
+
if (onSubmitShortcut === undefined || e.key !== "Enter" || !(e.metaKey || e.ctrlKey))
|
|
157
|
+
return;
|
|
158
|
+
e.preventDefault();
|
|
159
|
+
onSubmitShortcut();
|
|
160
|
+
},
|
|
161
|
+
}) }));
|
|
162
|
+
}
|
|
163
|
+
case "tz":
|
|
164
|
+
return (_jsx(TzInput, { id: props.id, name: props.name, value: props.value, onChange: props.onChange, ...(props.disabled !== undefined && { disabled: props.disabled }), ...(props.required !== undefined && { required: props.required }), ...(props.hasError !== undefined && { hasError: props.hasError }) }));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type MouseEvent, type ReactNode } from "react";
|
|
2
|
+
import type { FillContainerProps, GridCellProps, GridProps, HeadingProps, LinkProps, ProgressProps, SectionProps, StepBarProps, TextProps, WizardStepGroupProps } from "../../index.ts";
|
|
3
|
+
import "../react-css-custom-properties.ts";
|
|
4
|
+
export declare function DefaultSection({ id, title, subtitle, children, actions, variant, layout, testId, icon, }: SectionProps): ReactNode;
|
|
5
|
+
export declare function DefaultFillContainer({ children, testId, grow }: FillContainerProps): ReactNode;
|
|
6
|
+
export declare function DefaultGrid({ columns, children, testId, maxRows, flow, list, ariaLabelledBy, }: GridProps): ReactNode;
|
|
7
|
+
export declare function DefaultGridCell({ span, width, children }: GridCellProps): ReactNode;
|
|
8
|
+
export declare function DefaultText({ variant, decorative, children, testId, }: TextProps): ReactNode;
|
|
9
|
+
export declare function isPlainPrimaryClick(event: MouseEvent): boolean;
|
|
10
|
+
export declare function DefaultLink({ href, variant, target, rel, onPress, className, children, testId, dataAttributes, }: LinkProps): ReactNode;
|
|
11
|
+
export declare function DefaultProgress({ value, tone, ariaLabel, testId }: ProgressProps): ReactNode;
|
|
12
|
+
export declare function DefaultStepBar({ steps, currentIndex, compactLabel, onStepSelect, doneSteps, selectableSteps, narrowLayout, orientation, heading, description, subtitles, upNext, testId, compactTestId, }: StepBarProps): ReactNode;
|
|
13
|
+
export declare function DefaultWizardStepGroup({ hidden, inset, children, }: WizardStepGroupProps): ReactNode;
|
|
14
|
+
export declare function DefaultHeading({ variant, children, testId }: HeadingProps): ReactNode;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Children, useContext } from "react";
|
|
3
|
+
import { isSafeHref } from "../../headless/index.js";
|
|
4
|
+
import { Icon } from "../icons.js";
|
|
5
|
+
import { cn } from "../lib/cn.js";
|
|
6
|
+
import { buttonVariants } from "../ui/button.js";
|
|
7
|
+
import { ProgressBar } from "../widgets/progress-bar.js";
|
|
8
|
+
import { StepBar } from "../widgets/step-bar.js";
|
|
9
|
+
import "../react-css-custom-properties.js";
|
|
10
|
+
import { cardSurface } from "./card-styles.js";
|
|
11
|
+
import { DrawerBodyContext, InsideFormContext, ScreenFormContext } from "./contexts.js";
|
|
12
|
+
export function DefaultSection({ id, title, subtitle, children, actions, variant = "default", layout = "stacked", testId, icon, }) {
|
|
13
|
+
const insideForm = useContext(InsideFormContext);
|
|
14
|
+
const insideScreenForm = useContext(ScreenFormContext);
|
|
15
|
+
// h3 instead of CardTitle (= div): keeps the heading semantics for
|
|
16
|
+
// screen reader navigation. The subtitle flows below (no divider,
|
|
17
|
+
// shadcn CardTitle+CardDescription pattern).
|
|
18
|
+
// icon only renders alongside a title — a title-less section has nothing
|
|
19
|
+
// for a lone icon to sit next to, so it stays as-is (no heading grows).
|
|
20
|
+
// actions render top-right in this same title row, never a
|
|
21
|
+
// footer — a title-less section still draws the row when actions are
|
|
22
|
+
// present, so a hideTitle tabs-Section with `actions` isn't stranded.
|
|
23
|
+
const TitleTag = insideScreenForm ? "h2" : "h3";
|
|
24
|
+
const titleBlock = title !== undefined || subtitle !== undefined ? (_jsxs("div", { className: "flex flex-col gap-1", children: [title !== undefined && (_jsxs(TitleTag, { "data-testid": testId !== undefined ? `${testId}-title` : undefined, className: "flex items-center gap-2 text-base font-semibold leading-none tracking-tight", children: [icon !== undefined && _jsx(Icon, { name: icon, className: "size-4 text-muted-foreground" }), title] })), subtitle !== undefined && (_jsx("div", { "data-testid": testId !== undefined ? `${testId}-subtitle` : undefined, className: cn("text-sm text-muted-foreground", insideScreenForm && "text-[13px] text-foreground-secondary"), children: subtitle }))] })) : null;
|
|
25
|
+
const header = titleBlock !== null || actions !== undefined ? (_jsxs("div", { className: "flex items-start justify-between gap-4", children: [titleBlock, actions !== undefined && (_jsx("div", { "data-testid": testId !== undefined ? `${testId}-actions` : undefined, className: "ml-auto flex shrink-0 items-center gap-2", children: actions }))] })) : null;
|
|
26
|
+
// Inside a form: divider-less section WITHOUT a card surface of its own.
|
|
27
|
+
// The separator lines BETWEEN sections come from the divide-y wrapper in
|
|
28
|
+
// the form.
|
|
29
|
+
if (insideForm) {
|
|
30
|
+
return (_jsxs("section", { id: id, "data-testid": testId, className: cn(
|
|
31
|
+
// py-4 (not the standalone card's py-6): the border-t between
|
|
32
|
+
// sections already carries the section break, so the vertical
|
|
33
|
+
// gap only needs to read as roughly double the gap-4 field row
|
|
34
|
+
// spacing, not triple it.
|
|
35
|
+
insideScreenForm
|
|
36
|
+
? "flex scroll-mt-4 flex-col gap-4 [&:not(:first-of-type)]:border-t [&:not(:first-of-type)]:border-border [&:not(:first-of-type)]:pt-7"
|
|
37
|
+
: "flex flex-col gap-4 px-6 py-4", variant === "destructive" && "border-l-2 border-destructive/40", layout === "settings-list" && "@container max-w-4xl"), children: [header, children] }));
|
|
38
|
+
}
|
|
39
|
+
// Standalone: own card, header flows into the body (no divider).
|
|
40
|
+
// overflow-hidden clips the card's own corner radius correctly for
|
|
41
|
+
// portaled overlays (Combobox/Select/Tooltip escape to document.body,
|
|
42
|
+
// unaffected). A non-portaled overlay (e.g. a custom dropdown built
|
|
43
|
+
// directly into `children`) WOULD get silently clipped — verify this
|
|
44
|
+
// against any new standalone-section content that renders its own
|
|
45
|
+
// non-portaled overlay.
|
|
46
|
+
return (_jsx("div", { "data-slot": "card", "data-testid": testId, className: cn(cardSurface(), "overflow-hidden", variant === "destructive" && "border-destructive/40"), children: _jsxs("div", { className: "flex flex-col gap-4 px-6 py-6", children: [header, children] }) }));
|
|
47
|
+
}
|
|
48
|
+
export function DefaultFillContainer({ children, testId, grow }) {
|
|
49
|
+
// flex-1 only on request (`grow`, set under fillScreenHeight): without it the
|
|
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 (fw#2778).
|
|
52
|
+
return (_jsx("div", { "data-testid": testId, className: cn("flex min-h-0 flex-col", grow === true && "flex-1"), children: children }));
|
|
53
|
+
}
|
|
54
|
+
const DRAWER_FIELD_CELL_WIDTH_CLASS = {
|
|
55
|
+
money: "w-full sm:w-[200px] [&_label]:whitespace-nowrap",
|
|
56
|
+
select: "w-full",
|
|
57
|
+
};
|
|
58
|
+
const FIELD_CELL_WIDTH_CLASS = {
|
|
59
|
+
text: "w-full sm:w-60",
|
|
60
|
+
number: "w-fit min-w-24 shrink-0 [&_label]:whitespace-nowrap [&_[data-slot=number-field]]:w-32",
|
|
61
|
+
money: "w-40 shrink-0 [&_label]:whitespace-nowrap",
|
|
62
|
+
date: "w-full sm:w-[200px]",
|
|
63
|
+
timestamp: "w-full sm:w-[328px]",
|
|
64
|
+
select: "w-full sm:w-auto sm:min-w-[200px] sm:has-[[data-radio-list]]:w-full",
|
|
65
|
+
full: "w-full",
|
|
66
|
+
// self-start: the label shares the top line with neighbouring labels. The
|
|
67
|
+
// switch (1.15rem) gets (h-9 input − switch) / 2 margin to sit on the input line.
|
|
68
|
+
toggle: "w-auto min-w-40 shrink-0 self-start [&_[data-slot=switch]]:my-[0.55rem]",
|
|
69
|
+
};
|
|
70
|
+
export function DefaultGrid({ columns, children, testId, maxRows, flow, list, ariaLabelledBy, }) {
|
|
71
|
+
const insideDrawerBody = useContext(DrawerBodyContext);
|
|
72
|
+
const groupProps = ariaLabelledBy !== undefined
|
|
73
|
+
? { role: "group", "aria-labelledby": ariaLabelledBy }
|
|
74
|
+
: {};
|
|
75
|
+
if (list === true) {
|
|
76
|
+
return (_jsx("div", { ...groupProps, "data-testid": testId, className: "flex flex-col divide-y divide-border", children: children }));
|
|
77
|
+
}
|
|
78
|
+
if (flow === true) {
|
|
79
|
+
return (_jsx("div", { ...groupProps, "data-testid": testId, className: cn("flex gap-4", insideDrawerBody
|
|
80
|
+
? "flex-col items-start"
|
|
81
|
+
: // items-end aligns inputs when labels wrap to different heights; a
|
|
82
|
+
// below-control appendix grows only the one cell and would push its
|
|
83
|
+
// neighbours down, so such rows align to the top instead. An error
|
|
84
|
+
// line does the same: bottom-aligned, it would lift that field's
|
|
85
|
+
// label above its neighbours'.
|
|
86
|
+
"flex-wrap items-end has-[[data-appendix-below]]:items-start has-[[role=alert]]:items-start"), children: children }));
|
|
87
|
+
}
|
|
88
|
+
// "auto": content-sized items in a wrapping row (e.g. a metrics band of
|
|
89
|
+
// self-sized tiles) instead of N equal-width, container-stretched tracks.
|
|
90
|
+
// maxRows/scrolling don't apply — the row just wraps.
|
|
91
|
+
if (columns === "auto") {
|
|
92
|
+
return (_jsx("div", { ...groupProps, "data-testid": testId, className: "flex flex-wrap items-center gap-4", children: children }));
|
|
93
|
+
}
|
|
94
|
+
// Responsive: mobile (< sm = 640px) stays single-column, from sm on the
|
|
95
|
+
// author-declared column count applies. The inline style writes a CSS
|
|
96
|
+
// variable; the Tailwind class reads it with
|
|
97
|
+
// `grid-template-columns: var(--grid-cols)`. A clean solution because
|
|
98
|
+
// Tailwind JIT cannot resolve a dynamic `grid-cols-${N}`.
|
|
99
|
+
const rowCount = Math.ceil(Children.toArray(children).length / columns);
|
|
100
|
+
const clipped = maxRows !== undefined && rowCount > maxRows;
|
|
101
|
+
// gridAutoRows is a minmax MIN, not a fixed height, so wrapped labels grow
|
|
102
|
+
// their row instead of being clipped mid-row. maxHeight is therefore only
|
|
103
|
+
// an approximation of "maxRows rows tall" — a row taller than the minimum
|
|
104
|
+
// means fewer than maxRows rows end up visible before scrolling kicks in.
|
|
105
|
+
// Themes may override --kumiko-grid-row-h (default 2.5rem, set in
|
|
106
|
+
// styles.css) to change that minimum.
|
|
107
|
+
// Mobile note: the grid collapses to 1 column below `sm` (see className),
|
|
108
|
+
// but rowCount above is always computed against the desktop `columns` —
|
|
109
|
+
// inline styles can't express a media query, so the mobile row count is
|
|
110
|
+
// an under-count. Known, accepted.
|
|
111
|
+
const style = {
|
|
112
|
+
"--grid-cols": `repeat(${columns}, minmax(0, 1fr))`,
|
|
113
|
+
...(clipped && {
|
|
114
|
+
gridAutoRows: "minmax(var(--kumiko-grid-row-h, 2.5rem), auto)",
|
|
115
|
+
maxHeight: `calc(${maxRows} * var(--kumiko-grid-row-h, 2.5rem) + ${maxRows - 1} * var(--kumiko-grid-gap, 1rem))`,
|
|
116
|
+
overflowY: "auto",
|
|
117
|
+
}),
|
|
118
|
+
};
|
|
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)]", style: style, children: children }));
|
|
120
|
+
}
|
|
121
|
+
export function DefaultGridCell({ span, width, children }) {
|
|
122
|
+
const insideDrawerBody = useContext(DrawerBodyContext);
|
|
123
|
+
if (width !== undefined) {
|
|
124
|
+
return (_jsx("div", { className: cn("min-w-0 max-w-full", insideDrawerBody
|
|
125
|
+
? (DRAWER_FIELD_CELL_WIDTH_CLASS[width] ?? FIELD_CELL_WIDTH_CLASS[width])
|
|
126
|
+
: FIELD_CELL_WIDTH_CLASS[width]), children: children }));
|
|
127
|
+
}
|
|
128
|
+
const s = span !== undefined ? Math.min(span, 12) : 1;
|
|
129
|
+
// Below `sm` the grid has one explicit column (see DefaultGrid); an unconditional
|
|
130
|
+
// `span N` there would add implicit auto columns that squeeze the fr column.
|
|
131
|
+
const style = { "--grid-span": s };
|
|
132
|
+
return (_jsx("div", { className: "min-w-0 sm:[grid-column:span_var(--grid-span)]", style: style, children: children }));
|
|
133
|
+
}
|
|
134
|
+
export function DefaultText({ variant = "body", decorative, children, testId, }) {
|
|
135
|
+
switch (variant) {
|
|
136
|
+
case "code":
|
|
137
|
+
return (_jsx("code", { "data-testid": testId, className: "relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm", children: children }));
|
|
138
|
+
case "small":
|
|
139
|
+
return (_jsx("small", { "data-testid": testId, className: "text-xs text-muted-foreground", children: children }));
|
|
140
|
+
case "required-mark":
|
|
141
|
+
return (_jsx("span", { "data-testid": testId, "data-required": true, className: "text-destructive", children: children }));
|
|
142
|
+
case "muted":
|
|
143
|
+
return (_jsx("span", { "data-testid": testId, ...(decorative === true && { "aria-hidden": true }), className: "text-sm text-muted-foreground", children: children }));
|
|
144
|
+
default:
|
|
145
|
+
return _jsx("span", { "data-testid": testId, children: children });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// ---- Link (anchor with button/muted look) ----
|
|
149
|
+
// isSafeHref lives in @cosmicdrift/kumiko-renderer/headless — shared with
|
|
150
|
+
// page-render's server-side markdown renderer, both take untrusted
|
|
151
|
+
// tenant-authored hrefs.
|
|
152
|
+
// `button` uses the primary button surface on a semantic <a>, the
|
|
153
|
+
// standard for "continue to" navigations after success states (formerly
|
|
154
|
+
// authButtonClass), `muted` is the subtle secondary link (formerly
|
|
155
|
+
// authMutedLinkClass).
|
|
156
|
+
// Modified and middle clicks keep the browser default (new tab/window).
|
|
157
|
+
export function isPlainPrimaryClick(event) {
|
|
158
|
+
return event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey;
|
|
159
|
+
}
|
|
160
|
+
export function DefaultLink({ href, variant = "default", target, rel, onPress, className, children, testId, dataAttributes, }) {
|
|
161
|
+
const variantClass = variant === "button"
|
|
162
|
+
? buttonVariants({ variant: "default" })
|
|
163
|
+
: variant === "muted"
|
|
164
|
+
? "text-sm text-muted-foreground underline-offset-4 hover:text-foreground hover:underline"
|
|
165
|
+
: "text-primary underline-offset-4 hover:underline";
|
|
166
|
+
return (_jsx("a", { href: isSafeHref(href) ? href : "#", target: target, rel: rel ?? (target === "_blank" ? "noopener noreferrer" : undefined), ...dataAttributes, "data-testid": testId, className: cn(variantClass, className), onClick: onPress === undefined
|
|
167
|
+
? undefined
|
|
168
|
+
: (event) => {
|
|
169
|
+
if (!isPlainPrimaryClick(event))
|
|
170
|
+
return;
|
|
171
|
+
event.preventDefault();
|
|
172
|
+
onPress();
|
|
173
|
+
}, children: children }));
|
|
174
|
+
}
|
|
175
|
+
export function DefaultProgress({ value, tone, ariaLabel, testId }) {
|
|
176
|
+
return _jsx(ProgressBar, { value: value, tone: tone, ariaLabel: ariaLabel, testId: testId });
|
|
177
|
+
}
|
|
178
|
+
export function DefaultStepBar({ steps, currentIndex, compactLabel, onStepSelect, doneSteps, selectableSteps, narrowLayout, orientation, heading, description, subtitles, upNext, testId, compactTestId, }) {
|
|
179
|
+
return (_jsx(StepBar, { steps: steps, currentIndex: currentIndex, compactLabel: compactLabel, onStepSelect: onStepSelect, doneSteps: doneSteps, selectableSteps: selectableSteps, narrowLayout: narrowLayout, orientation: orientation, heading: heading, description: description, subtitles: subtitles, upNext: upNext, testId: testId, compactTestId: compactTestId }));
|
|
180
|
+
}
|
|
181
|
+
export function DefaultWizardStepGroup({ hidden, inset, children, }) {
|
|
182
|
+
const visibleClass = inset === true ? "flex min-w-0 flex-col gap-4 p-6" : "contents";
|
|
183
|
+
return (_jsx("fieldset", { disabled: hidden, hidden: hidden, className: hidden ? undefined : visibleClass, children: children }));
|
|
184
|
+
}
|
|
185
|
+
export function DefaultHeading({ variant = "page", children, testId }) {
|
|
186
|
+
// Page heading = h1, very rare in an app (max 1 per screen).
|
|
187
|
+
// Section heading = h2 with uppercase + muted-foreground, the same look as
|
|
188
|
+
// the section header in forms, but usable as a standalone component for
|
|
189
|
+
// demo pages and custom screens.
|
|
190
|
+
if (variant === "section") {
|
|
191
|
+
return (_jsx("h2", { "data-testid": testId, className: "text-xs font-semibold uppercase tracking-wider text-muted-foreground", children: children }));
|
|
192
|
+
}
|
|
193
|
+
return (_jsx("h1", { "data-testid": testId, className: "text-2xl font-semibold tracking-tight", children: children }));
|
|
194
|
+
}
|
|
@@ -5,7 +5,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
5
5
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
6
6
|
import { useTranslation } from "../../index.js";
|
|
7
7
|
import { cn } from "../lib/cn.js";
|
|
8
|
-
import { BareFormProvider } from "./
|
|
8
|
+
import { BareFormProvider } from "./contexts.js";
|
|
9
9
|
import { ModalShell } from "./modal-shell.js";
|
|
10
10
|
export function DefaultModal({ open, onOpenChange, title, children, testId, }) {
|
|
11
11
|
const t = useTranslation();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import "../react-css-custom-properties.ts";
|
|
3
|
+
export declare const TABLE_FOOTER_BAR_CLASS: string;
|
|
4
|
+
export declare const END_LABEL_MIN_ROWS = 50;
|
|
5
|
+
export declare function InfiniteSentinel({ onReachEnd, loadingMore, hasMore, rowsCount, testId, }: {
|
|
6
|
+
readonly onReachEnd: () => void;
|
|
7
|
+
readonly loadingMore: boolean;
|
|
8
|
+
readonly hasMore: boolean;
|
|
9
|
+
readonly rowsCount: number;
|
|
10
|
+
readonly testId?: string;
|
|
11
|
+
}): ReactNode;
|
|
12
|
+
export declare function Pager({ page, limit, total, onPageChange, pageSizeOptions, onPageSizeChange, itemNoun, testId, pinned, }: {
|
|
13
|
+
readonly page: number;
|
|
14
|
+
readonly limit: number;
|
|
15
|
+
readonly total: number;
|
|
16
|
+
readonly onPageChange: (next: number) => void;
|
|
17
|
+
readonly pageSizeOptions?: readonly number[];
|
|
18
|
+
readonly onPageSizeChange?: (next: number) => void;
|
|
19
|
+
readonly itemNoun?: (count: number) => string;
|
|
20
|
+
readonly testId?: string;
|
|
21
|
+
readonly pinned?: boolean;
|
|
22
|
+
}): ReactNode;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ChevronDown, ChevronLeft, ChevronRight, Loader2 } from "lucide-react";
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
import { useTranslation } from "../../index.js";
|
|
5
|
+
import { cn } from "../lib/cn.js";
|
|
6
|
+
import "../react-css-custom-properties.js";
|
|
7
|
+
export const TABLE_FOOTER_BAR_CLASS = cn("flex h-15 shrink-0 items-center gap-4 border-t border-border bg-card pl-4 pr-2", "text-[13px] tabular-nums text-foreground-secondary md:h-11 md:px-6");
|
|
8
|
+
// InfiniteSentinel — empty div at the end of the table that uses
|
|
9
|
+
// IntersectionObserver to detect when the user scrolls near the list end.
|
|
10
|
+
// onReachEnd fires exactly once per "becomes visible" transition; the
|
|
11
|
+
// caller debounces via loadingMore (further visibility events are ignored
|
|
12
|
+
// while a page is loading). No observer during server-side render, no
|
|
13
|
+
// observer when hasMore=false — then the sentinel only shows the
|
|
14
|
+
// end-of-list hint via t("kumiko.list.end-of-list"), which requires a
|
|
15
|
+
// LocaleProvider in the tree (same requirement as Field/useRowActionTrigger).
|
|
16
|
+
// Without one, useTranslation() throws; with one that's missing the framework
|
|
17
|
+
// defaults (fallbackBundles not including kumikoDefaultTranslations), the
|
|
18
|
+
// user sees the raw key instead of translated text.
|
|
19
|
+
// ponytail: threshold mirrors the framework default pageSize (kumiko-screen.tsx
|
|
20
|
+
// `screen.pageSize ?? 50`) rather than the real per-screen pageSize, which isn't
|
|
21
|
+
// threaded down to this component. A screen with a custom pageSize can still
|
|
22
|
+
// under- or over-show the marker; thread pageSize through KumikoScreen →
|
|
23
|
+
// RenderList → DataTable → InfiniteSentinel if that's needed (fw#1713).
|
|
24
|
+
export const END_LABEL_MIN_ROWS = 50;
|
|
25
|
+
export function InfiniteSentinel({ onReachEnd, loadingMore, hasMore, rowsCount, testId, }) {
|
|
26
|
+
const t = useTranslation();
|
|
27
|
+
const ref = useRef(null);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!hasMore)
|
|
30
|
+
return;
|
|
31
|
+
if (loadingMore)
|
|
32
|
+
return;
|
|
33
|
+
if (typeof IntersectionObserver === "undefined")
|
|
34
|
+
return;
|
|
35
|
+
const node = ref.current;
|
|
36
|
+
if (node === null)
|
|
37
|
+
return;
|
|
38
|
+
const observer = new IntersectionObserver((entries) => {
|
|
39
|
+
// Only fire the first visible event per mount: if the user keeps
|
|
40
|
+
// scrolling while loading is still in progress, loadingMore=true keeps the
|
|
41
|
+
// useEffect from remounting the observer at all.
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
if (entry.isIntersecting) {
|
|
44
|
+
onReachEnd();
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}, { rootMargin: "200px" });
|
|
49
|
+
observer.observe(node);
|
|
50
|
+
return () => observer.disconnect();
|
|
51
|
+
}, [onReachEnd, loadingMore, hasMore]);
|
|
52
|
+
const showEndLabel = !hasMore && rowsCount >= END_LABEL_MIN_ROWS;
|
|
53
|
+
return (_jsx("div", { ref: ref, "data-testid": testId, className: cn("flex items-center justify-center text-sm text-muted-foreground", showEndLabel || loadingMore ? "py-4" : ""), children: showEndLabel ? (_jsx("span", { "data-testid": testId !== undefined ? `${testId}-end` : undefined, children: t("kumiko.list.end-of-list") })) : loadingMore ? (_jsx(Loader2, { className: "size-4 animate-spin", "aria-hidden": "true" })) : hasMore ? (_jsx("span", { "aria-hidden": "true", children: "\u00A0" })) : null }));
|
|
54
|
+
}
|
|
55
|
+
// Pager — status text on the left ("X – Y of Z"), "Page X of Y" and the
|
|
56
|
+
// prev/next arrows on the right. No numbered page list: the board shows none.
|
|
57
|
+
// `pinned` renders the footer bar of a scrollBody table (fixed height, top
|
|
58
|
+
// border); unpinned it sits as a plain row below the table.
|
|
59
|
+
//
|
|
60
|
+
// Status text and aria-labels go through t(...), which requires a
|
|
61
|
+
// LocaleProvider in the tree — same requirement as InfiniteSentinel above,
|
|
62
|
+
// already established for DataTable consumers.
|
|
63
|
+
export function Pager({ page, limit, total, onPageChange, pageSizeOptions, onPageSizeChange, itemNoun, testId, pinned, }) {
|
|
64
|
+
const t = useTranslation();
|
|
65
|
+
const totalPages = Math.max(1, Math.ceil(total / limit));
|
|
66
|
+
const safePage = Math.max(1, Math.min(page, totalPages));
|
|
67
|
+
const from = (safePage - 1) * limit + 1;
|
|
68
|
+
const to = Math.min(safePage * limit, total);
|
|
69
|
+
return (_jsxs("div", { "data-testid": testId, className: pinned === true
|
|
70
|
+
? TABLE_FOOTER_BAR_CLASS
|
|
71
|
+
: "mt-3 flex items-center gap-4 text-[13px] tabular-nums text-foreground-secondary", children: [_jsx("div", { "data-testid": testId !== undefined ? `${testId}-status` : undefined, className: "flex-1", children: itemNoun !== undefined
|
|
72
|
+
? t("kumiko.pager.status.noun", {
|
|
73
|
+
from: from.toLocaleString(),
|
|
74
|
+
to: to.toLocaleString(),
|
|
75
|
+
total: total.toLocaleString(),
|
|
76
|
+
noun: itemNoun(total),
|
|
77
|
+
})
|
|
78
|
+
: t("kumiko.pager.status", {
|
|
79
|
+
from: from.toLocaleString(),
|
|
80
|
+
to: to.toLocaleString(),
|
|
81
|
+
total: total.toLocaleString(),
|
|
82
|
+
}) }), pageSizeOptions !== undefined && onPageSizeChange !== undefined && (_jsxs("div", { className: "relative hidden md:block", children: [_jsx("select", { "aria-label": t("kumiko.pager.pageSizeLabel"), "data-testid": testId !== undefined ? `${testId}-page-size` : undefined, value: limit, onChange: (e) => onPageSizeChange(Number(e.target.value)), className: "h-7 appearance-none rounded-md border border-input bg-card pl-2 pr-7 text-[13px] text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", children: pageSizeOptions.map((size) => (_jsx("option", { value: size, children: t("kumiko.pager.pageSize", { size: size.toLocaleString() }) }, size))) }), _jsx(ChevronDown, { className: "pointer-events-none absolute right-2 top-1/2 size-4 -translate-y-1/2 opacity-50", "aria-hidden": "true" })] })), _jsx("div", { className: "hidden md:block", children: t("kumiko.pager.pageOf", {
|
|
83
|
+
page: safePage.toLocaleString(),
|
|
84
|
+
pages: totalPages.toLocaleString(),
|
|
85
|
+
}) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(PagerButton, { ariaLabel: t("kumiko.pager.previousPage"), disabled: safePage <= 1, onClick: () => onPageChange(safePage - 1), testId: testId !== undefined ? `${testId}-prev` : undefined, children: _jsx(ChevronLeft, { className: "size-3.5", "aria-hidden": "true" }) }), _jsx(PagerButton, { ariaLabel: t("kumiko.pager.nextPage"), disabled: safePage >= totalPages, onClick: () => onPageChange(safePage + 1), testId: testId !== undefined ? `${testId}-next` : undefined, children: _jsx(ChevronRight, { className: "size-3.5", "aria-hidden": "true" }) })] })] }));
|
|
86
|
+
}
|
|
87
|
+
function PagerButton({ children, onClick, ariaLabel, disabled, testId, }) {
|
|
88
|
+
return (_jsx("button", { type: "button", onClick: onClick, disabled: disabled, "aria-label": ariaLabel, "data-testid": testId, className: cn("inline-flex size-11 items-center justify-center rounded-md border border-border md:size-7", "hover:bg-muted focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", "disabled:pointer-events-none disabled:text-foreground-disabled"), children: children }));
|
|
89
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { ListRowViewModel } from "../../headless/index.ts";
|
|
3
|
+
import type { DataTableRowAction, DataTableRowActionMode } from "../../index.ts";
|
|
4
|
+
import { type ActionOverflowMenuProps } from "../../index.ts";
|
|
5
|
+
import "../react-css-custom-properties.ts";
|
|
6
|
+
export declare function RowActionsCell({ row, actions, mode, rowIsLink, compact, }: {
|
|
7
|
+
readonly row: ListRowViewModel;
|
|
8
|
+
readonly actions: readonly DataTableRowAction[];
|
|
9
|
+
readonly mode?: DataTableRowActionMode;
|
|
10
|
+
readonly rowIsLink?: boolean;
|
|
11
|
+
readonly compact?: boolean;
|
|
12
|
+
}): ReactNode;
|
|
13
|
+
export declare function ActionOverflowMenu({ items, label, testId }: ActionOverflowMenuProps): ReactNode;
|