@cosmicdrift/kumiko-renderer 0.326.1 → 0.328.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/package.json +5 -5
- package/src/app/__tests__/entity-edit-fill-height.test.tsx +118 -0
- package/src/app/__tests__/entity-list-screen-padding.test.tsx +64 -47
- package/src/app/action-form-shim.ts +2 -0
- package/src/app/kumiko-screen.tsx +274 -87
- package/src/app/projection-detail-shim.ts +1 -0
- package/src/app/projection-list-shim.ts +3 -0
- package/src/app/row-actions.ts +6 -0
- package/src/changes.json +52 -0
- package/src/components/__tests__/related-list-section.test.tsx +71 -2
- package/src/components/__tests__/render-list-toolbar-icon-only.test.tsx +2 -2
- package/src/components/__tests__/write-form-section.test.tsx +1 -1
- package/src/components/grid-cell-for-field.tsx +30 -2
- package/src/components/related-list-section.tsx +53 -29
- package/src/components/render-edit-action-button.tsx +4 -1
- package/src/components/render-edit-types.ts +10 -0
- package/src/components/render-edit.tsx +564 -393
- package/src/components/render-field.tsx +11 -2
- package/src/components/render-list.tsx +45 -13
- package/src/components/write-form-section.tsx +0 -1
- package/src/hooks/__tests__/use-query.test.tsx +149 -0
- package/src/hooks/use-list-url-state.ts +37 -1
- package/src/hooks/use-query.ts +57 -23
- package/src/i18n-defaults.ts +27 -1
- package/src/index.ts +7 -0
- package/src/inside-drawer.ts +12 -0
- package/src/page-header-slot.ts +12 -0
- package/src/primitives.tsx +137 -16
- package/src/screen-fills-height.ts +3 -0
package/src/primitives.tsx
CHANGED
|
@@ -40,6 +40,7 @@ import type {
|
|
|
40
40
|
FormWidth,
|
|
41
41
|
IconKey,
|
|
42
42
|
NavIconKey,
|
|
43
|
+
SelectOptionTone,
|
|
43
44
|
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
44
45
|
import type {
|
|
45
46
|
FieldIssue,
|
|
@@ -91,8 +92,9 @@ export type ButtonProps = {
|
|
|
91
92
|
* becomes visually; the renderers use "primary" for Save, "danger" for
|
|
92
93
|
* Delete, "secondary" for a Confirm state, "link" for inline actions in
|
|
93
94
|
* running text (no background, underline), "danger-ghost" for a
|
|
94
|
-
* destructive action as red text instead of a red fill
|
|
95
|
-
|
|
95
|
+
* destructive action as red text instead of a red fill, "ghost" for a
|
|
96
|
+
* low-emphasis primary-colored text action beside a filled primary. */
|
|
97
|
+
readonly variant?: "primary" | "secondary" | "danger" | "link" | "danger-ghost" | "ghost";
|
|
96
98
|
/** Größe — default="md". "sm" für kompakte Inline-Aktionen (Toolbar,
|
|
97
99
|
* Listen-Zeilen), "icon" für quadratische Icon-only-Buttons. */
|
|
98
100
|
readonly size?: "sm" | "md" | "icon";
|
|
@@ -130,8 +132,10 @@ export type ButtonProps = {
|
|
|
130
132
|
export type LinkProps = {
|
|
131
133
|
readonly href: string;
|
|
132
134
|
readonly variant?: "default" | "button" | "muted";
|
|
133
|
-
/** `_blank`
|
|
134
|
-
|
|
135
|
+
/** `_blank` makes the web impl set `rel="noopener noreferrer"` unless
|
|
136
|
+
* `rel` is set explicitly. */
|
|
137
|
+
readonly target?: "_blank" | "_self" | "_parent" | "_top";
|
|
138
|
+
readonly rel?: string;
|
|
135
139
|
/** Layout-Zusätze (self-center, text-xs) — Web merged via cn(),
|
|
136
140
|
* Native-Impls ignorieren es (Präzedenz: CardProps.className). */
|
|
137
141
|
readonly className?: string;
|
|
@@ -184,6 +188,10 @@ export type FieldProps = {
|
|
|
184
188
|
* `htmlFor` (no separate `aria-label` needed). For tables/grids whose
|
|
185
189
|
* column header already carries the label for each row. */
|
|
186
190
|
readonly hideLabel?: boolean;
|
|
191
|
+
/** Marks the field as edited relative to its saved value — the label row
|
|
192
|
+
* shows a "changed" marker. Set only in edit mode for fields present in
|
|
193
|
+
* the form's change set. */
|
|
194
|
+
readonly changed?: boolean;
|
|
187
195
|
readonly testId?: string;
|
|
188
196
|
};
|
|
189
197
|
|
|
@@ -350,10 +358,19 @@ export type InputProps =
|
|
|
350
358
|
* {value,label}-Form für DB-getragene Refs (Tier 2.7e-3). */
|
|
351
359
|
readonly options:
|
|
352
360
|
| readonly string[]
|
|
353
|
-
| readonly {
|
|
361
|
+
| readonly {
|
|
362
|
+
readonly value: string;
|
|
363
|
+
readonly label: string;
|
|
364
|
+
/** Second line under the label; shown by `radioVariant="card"`. */
|
|
365
|
+
readonly description?: string;
|
|
366
|
+
}[];
|
|
354
367
|
readonly disabled?: boolean;
|
|
355
368
|
readonly required?: boolean;
|
|
356
369
|
readonly hasError?: boolean;
|
|
370
|
+
/** `"card"` renders the radio group as bordered cards with label plus
|
|
371
|
+
* optional description per option and always uses the radio group,
|
|
372
|
+
* regardless of option count or `display`. Default `"list"`. */
|
|
373
|
+
readonly radioVariant?: "list" | "card";
|
|
357
374
|
/** Requested presentation. `"radio"` renders the options as a visible
|
|
358
375
|
* radio group (WAI-ARIA radiogroup, one click per choice),
|
|
359
376
|
* `"dropdown"` renders the combobox. Omitted = the implementation
|
|
@@ -552,6 +569,9 @@ export type DataTableRowAction = {
|
|
|
552
569
|
readonly onTrigger: (row: ListRowViewModel) => Promise<void> | void;
|
|
553
570
|
/** Conditional Visibility pro Row (z.B. "Start" nur wenn status==="scheduled"). */
|
|
554
571
|
readonly isVisible?: (row: ListRowViewModel) => boolean;
|
|
572
|
+
/** This action is what a click on the row does. The card layout drops it
|
|
573
|
+
* from the row menu, since the whole card is already the tap target. */
|
|
574
|
+
readonly rowClick?: boolean;
|
|
555
575
|
/** Resolved icon (author `RowAction.icon` or the id-derived default) —
|
|
556
576
|
* drives both the icon-left-of-text render and the icon-only collapse
|
|
557
577
|
* rule (see `shouldRenderActionsIconOnly`). */
|
|
@@ -612,13 +632,13 @@ export type DataTableProps = {
|
|
|
612
632
|
* Form (Labels + onTrigger schon verdrahtet); DataTable kümmert
|
|
613
633
|
* sich nur um Render + Confirm-Dialog. */
|
|
614
634
|
readonly rowActions?: readonly DataTableRowAction[];
|
|
615
|
-
/**
|
|
616
|
-
* - `"adaptive"` (
|
|
617
|
-
*
|
|
618
|
-
*
|
|
619
|
-
*
|
|
620
|
-
*
|
|
621
|
-
*
|
|
635
|
+
/** How the row-action column renders:
|
|
636
|
+
* - `"adaptive"` (default): with `onRowClick` all actions go in the kebab;
|
|
637
|
+
* without it the primary action is a link button and the rest go in the
|
|
638
|
+
* kebab (a single action gets no kebab).
|
|
639
|
+
* - `"inline"`: ALWAYS inline buttons, left-aligned, even with >2 (no
|
|
640
|
+
* kebab). Actions then sit at the same x-position across all rows
|
|
641
|
+
* (no drifting with label width) and all lists in an app look alike. */
|
|
622
642
|
readonly rowActionMode?: DataTableRowActionMode;
|
|
623
643
|
/** Custom Empty-State-Inhalt (z. B. Icon + Heading + CTA-Button).
|
|
624
644
|
* Default-Renderer rahmt ihn in einer dashed-border Box. */
|
|
@@ -657,7 +677,16 @@ export type DataTableProps = {
|
|
|
657
677
|
readonly limit: number;
|
|
658
678
|
readonly total: number;
|
|
659
679
|
readonly onPageChange: (next: number) => void;
|
|
680
|
+
/** Offered page sizes; with `onPageSizeChange` the footer shows a
|
|
681
|
+
* "N pro Seite" select. */
|
|
682
|
+
readonly pageSizeOptions?: readonly number[];
|
|
683
|
+
readonly onPageSizeChange?: (next: number) => void;
|
|
660
684
|
};
|
|
685
|
+
/** Hint at the left of the toolbar (relatedList tab description). */
|
|
686
|
+
readonly toolbarDescription?: string;
|
|
687
|
+
/** Noun for the footer count ("1–19 of 19 leases", "1 position"),
|
|
688
|
+
* resolved for the given count. Omitted = generic "entries" wording. */
|
|
689
|
+
readonly itemNoun?: (count: number) => string;
|
|
661
690
|
/** Infinite-Scroll Callback. Wenn gesetzt, rendert der Renderer einen
|
|
662
691
|
* Bottom-Sentinel und ruft `onReachEnd` wenn der ins Viewport rückt
|
|
663
692
|
* (Web: IntersectionObserver). Caller verwaltet accumulation +
|
|
@@ -693,8 +722,12 @@ export type DataTableProps = {
|
|
|
693
722
|
* leave dead space below it (a tab panel, fw#2722). Requires the same
|
|
694
723
|
* flex-fill chain FormProps.fillHeight sets up above it; without that
|
|
695
724
|
* ancestor chain this collapses to zero height (Web: `flex-1 min-h-0`
|
|
696
|
-
* has no effect outside a sized flex-col ancestor).
|
|
697
|
-
*
|
|
725
|
+
* has no effect outside a sized flex-col ancestor). When true the table
|
|
726
|
+
* also takes the full board layout: the wrapper fills its container
|
|
727
|
+
* (`h-full`), toolbar and footer (pager or entry count) stay pinned
|
|
728
|
+
* outside the scrolling body, no card frame and no outer padding.
|
|
729
|
+
* Default false: unchanged document-flow table that grows with its
|
|
730
|
+
* content, pager below the last row. */
|
|
698
731
|
readonly scrollBody?: boolean;
|
|
699
732
|
/** Uses the shared screen padding (wider bottom inset) instead of the
|
|
700
733
|
* table's symmetric embedded inset — for a table that IS the screen body
|
|
@@ -800,6 +833,13 @@ export type EmbeddedListInputProps = {
|
|
|
800
833
|
|
|
801
834
|
export type { FormWidth };
|
|
802
835
|
|
|
836
|
+
/** One entry of a form's "on this page" navigation: `id` matches the
|
|
837
|
+
* `SectionProps.id` of the section it scrolls to. */
|
|
838
|
+
export type FormSectionNavItem = {
|
|
839
|
+
readonly id: string;
|
|
840
|
+
readonly title: string;
|
|
841
|
+
};
|
|
842
|
+
|
|
803
843
|
/** Submit wrapper. Web: `<form onSubmit>`, native: a View that triggers an
|
|
804
844
|
* onSubmit callback via button press. `onSubmit` gets an abstract
|
|
805
845
|
* signature (no FormEvent) so native impls can fill it meaningfully.
|
|
@@ -843,8 +883,10 @@ export type FormProps = {
|
|
|
843
883
|
/** Sizes the form to fill its container's height (instead of the page's
|
|
844
884
|
* natural content height) so a single scrolling child — a lone
|
|
845
885
|
* relatedList tab's table — can scroll internally instead of stretching
|
|
846
|
-
* the whole page (fw#2722).
|
|
847
|
-
*
|
|
886
|
+
* the whole page (fw#2722). Set by RenderEdit for a lone relatedList tab
|
|
887
|
+
* section and for screens that fill the shell height (together with
|
|
888
|
+
* `stickyActions`: sections scroll, footer stays pinned without
|
|
889
|
+
* `position: fixed`); every other caller leaves it unset and keeps normal
|
|
848
890
|
* document-flow height. Native impls may ignore this prop (already a
|
|
849
891
|
* bounded viewport there). */
|
|
850
892
|
readonly fillHeight?: boolean;
|
|
@@ -857,12 +899,31 @@ export type FormProps = {
|
|
|
857
899
|
* card chrome. Native impls may ignore this prop (no card chrome there
|
|
858
900
|
* to begin with). */
|
|
859
901
|
readonly chromeless?: boolean;
|
|
902
|
+
/** Set by RenderEdit for entityEdit/actionForm screens that fill the shell
|
|
903
|
+
* height: the form renders without a surrounding card, in a padded scroll
|
|
904
|
+
* surface with a fixed-width form column. Requires `fillHeight`; projection
|
|
905
|
+
* detail screens (which also fill the height) do not set it. Native impls
|
|
906
|
+
* may ignore this prop. */
|
|
907
|
+
readonly screenForm?: boolean;
|
|
908
|
+
/** Number of unsaved changes, shown in the pinned footer. Omitted in create
|
|
909
|
+
* mode and when nothing changed — the footer then shows no status. */
|
|
910
|
+
readonly unsavedCount?: number;
|
|
911
|
+
/** Drawer-hosted forms: context box above the fields. */
|
|
912
|
+
readonly summary?: { readonly title: string; readonly subtitle?: string };
|
|
913
|
+
/** Titled sections for the "on this page" navigation next to a
|
|
914
|
+
* `screenForm`. The web impl shows it only with three or more entries. */
|
|
915
|
+
readonly sectionNav?: readonly FormSectionNavItem[];
|
|
916
|
+
/** Leading column next to the scroll surface of a `screenForm` (wizard step
|
|
917
|
+
* navigation). Native impls may ignore this prop. */
|
|
918
|
+
readonly sideRail?: ReactNode;
|
|
860
919
|
};
|
|
861
920
|
|
|
862
921
|
/** Titled Gruppe von Feldern. Web: `<fieldset>` + `<legend>`, Native:
|
|
863
922
|
* View mit Header-Text. Native-Impls können den Title als Accordion
|
|
864
923
|
* oder Collapsible rendern. */
|
|
865
924
|
export type SectionProps = {
|
|
925
|
+
/** DOM id — the scroll target of a form's "on this page" navigation. */
|
|
926
|
+
readonly id?: string;
|
|
866
927
|
/** Optional — ohne Titel rendert die Section nur die Gruppierung
|
|
867
928
|
* (kein Header). RenderEdit lässt ihn weg, wenn er den Screen-Titel
|
|
868
929
|
* der Action-Bar 1:1 wiederholen würde. */
|
|
@@ -900,6 +961,11 @@ export type SectionProps = {
|
|
|
900
961
|
export type FillContainerProps = {
|
|
901
962
|
readonly children: ReactNode;
|
|
902
963
|
readonly testId?: string;
|
|
964
|
+
/** Claims the remaining height of the flex chain (`flex-1`) instead of
|
|
965
|
+
* sizing to its content, so a footer inside the child sits at the bottom
|
|
966
|
+
* edge even with few rows. Set only when the screen fills the shell
|
|
967
|
+
* height. */
|
|
968
|
+
readonly grow?: boolean;
|
|
903
969
|
};
|
|
904
970
|
|
|
905
971
|
/** Columns-basiertes Layout. Web: CSS grid, Native: Flex-Wrap mit
|
|
@@ -919,12 +985,20 @@ export type GridProps = {
|
|
|
919
985
|
* the grid grows with its content and never scrolls. Ignored when
|
|
920
986
|
* `columns` is "auto". */
|
|
921
987
|
readonly maxRows?: number;
|
|
988
|
+
/** Fields flow left to right in wrapping rows sized by `GridCell.width`
|
|
989
|
+
* instead of equal-width tracks (screen forms and drawers). */
|
|
990
|
+
readonly flow?: boolean;
|
|
922
991
|
};
|
|
923
992
|
|
|
993
|
+
/** Width class of a field cell inside a `flow` grid: sized by what the field holds. */
|
|
994
|
+
export type FieldCellWidth = "text" | "number" | "money" | "date" | "select" | "full" | "auto";
|
|
995
|
+
|
|
924
996
|
/** Span-Wrapper für ein Kind innerhalb eines Grid. Web: `style={{gridColumn: span N}}`,
|
|
925
997
|
* Native: eigenes Width-Rechnen. */
|
|
926
998
|
export type GridCellProps = {
|
|
927
999
|
readonly span?: number;
|
|
1000
|
+
/** Only read inside a `flow` grid. */
|
|
1001
|
+
readonly width?: FieldCellWidth;
|
|
928
1002
|
readonly children: ReactNode;
|
|
929
1003
|
};
|
|
930
1004
|
|
|
@@ -970,6 +1044,10 @@ export type DialogProps = {
|
|
|
970
1044
|
readonly cancelLabel?: string;
|
|
971
1045
|
/** `default` = Confirm primary, `danger` = Confirm danger. */
|
|
972
1046
|
readonly variant?: "default" | "danger";
|
|
1047
|
+
/** Which button takes focus on open. Default: Confirm for a bare dialog,
|
|
1048
|
+
* the browser/Radix default when `children` are present. `"cancel"` for
|
|
1049
|
+
* dialogs where the safe choice must be the one Enter triggers. */
|
|
1050
|
+
readonly initialFocus?: "confirm" | "cancel";
|
|
973
1051
|
/** Wird gefeuert wenn der User Confirm drückt. Async-Funktion ist
|
|
974
1052
|
* ok — Dialog setzt automatisch loading-State, ruft danach
|
|
975
1053
|
* onOpenChange(false). */
|
|
@@ -1101,6 +1179,7 @@ export type ProgressTone = "default" | "success" | "warn" | "danger";
|
|
|
1101
1179
|
export type ProgressProps = {
|
|
1102
1180
|
readonly value: number;
|
|
1103
1181
|
readonly tone?: ProgressTone;
|
|
1182
|
+
readonly ariaLabel?: string;
|
|
1104
1183
|
readonly testId?: string;
|
|
1105
1184
|
};
|
|
1106
1185
|
|
|
@@ -1122,6 +1201,12 @@ export type StepBarProps = {
|
|
|
1122
1201
|
* viewports; "steps" keeps the row there too — for short wizards whose
|
|
1123
1202
|
* labels fit a phone and whose done steps must stay tappable. */
|
|
1124
1203
|
readonly narrowLayout?: "label" | "steps";
|
|
1204
|
+
/** "vertical" renders a step rail (from `lg`, with `compactLabel` as the
|
|
1205
|
+
* single line below it) instead of the chip row. Default "horizontal". */
|
|
1206
|
+
readonly orientation?: "horizontal" | "vertical";
|
|
1207
|
+
/** Vertical only: rail heading ("Step 4 of 6") and optional description. */
|
|
1208
|
+
readonly heading?: string;
|
|
1209
|
+
readonly description?: string;
|
|
1125
1210
|
readonly testId?: string;
|
|
1126
1211
|
readonly compactTestId?: string;
|
|
1127
1212
|
};
|
|
@@ -1136,6 +1221,8 @@ export type StepBarProps = {
|
|
|
1136
1221
|
* on `hidden` alone. */
|
|
1137
1222
|
export type WizardStepGroupProps = {
|
|
1138
1223
|
readonly hidden: boolean;
|
|
1224
|
+
/** Pads the step content (tab panels sit full-bleed under the tab strip). */
|
|
1225
|
+
readonly inset?: boolean;
|
|
1139
1226
|
readonly children: ReactNode;
|
|
1140
1227
|
};
|
|
1141
1228
|
|
|
@@ -1194,6 +1281,17 @@ const STATUS_TONE_BY_VALUE: Readonly<Record<string, StatusTone>> = {
|
|
|
1194
1281
|
critical: "bad",
|
|
1195
1282
|
};
|
|
1196
1283
|
|
|
1284
|
+
const STATUS_TONE_BY_OPTION_TONE: Readonly<Record<SelectOptionTone, StatusTone>> = {
|
|
1285
|
+
ok: "ok",
|
|
1286
|
+
warn: "warn",
|
|
1287
|
+
bad: "bad",
|
|
1288
|
+
neutral: "muted",
|
|
1289
|
+
};
|
|
1290
|
+
|
|
1291
|
+
export function statusToneForOptionTone(tone: SelectOptionTone): StatusTone {
|
|
1292
|
+
return STATUS_TONE_BY_OPTION_TONE[tone];
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1197
1295
|
export function statusToneForValue(value: string): StatusTone | undefined {
|
|
1198
1296
|
const slug = value
|
|
1199
1297
|
.trim()
|
|
@@ -1227,6 +1325,24 @@ export type JsonViewProps = {
|
|
|
1227
1325
|
readonly testId?: string;
|
|
1228
1326
|
};
|
|
1229
1327
|
|
|
1328
|
+
/** Band above the tabs of a record detail: optional subtitle line plus the
|
|
1329
|
+
* metrics as a definition list. `children` are `Metric` tiles. */
|
|
1330
|
+
export type MetricBandProps = {
|
|
1331
|
+
readonly subtitle?: ReactNode;
|
|
1332
|
+
readonly children: ReactNode;
|
|
1333
|
+
readonly testId?: string;
|
|
1334
|
+
};
|
|
1335
|
+
|
|
1336
|
+
/** Page title area rendered by the app shell's header instead of inside the
|
|
1337
|
+
* screen: `title` replaces the last breadcrumb, `status` sits right after it,
|
|
1338
|
+
* `actions` at the right edge. Renders nothing itself; without a shell slot
|
|
1339
|
+
* (see `usePageHeaderSlotAvailable`) callers keep their previous placement. */
|
|
1340
|
+
export type PageHeaderProps = {
|
|
1341
|
+
readonly title?: string;
|
|
1342
|
+
readonly status?: ReactNode;
|
|
1343
|
+
readonly actions?: ReactNode;
|
|
1344
|
+
};
|
|
1345
|
+
|
|
1230
1346
|
/** One item in an `ActionOverflowMenu` (A7: header/row actions beyond the
|
|
1231
1347
|
* primary one collapse into this menu instead of a wall of buttons). */
|
|
1232
1348
|
export type ActionMenuItemSpec = {
|
|
@@ -1382,6 +1498,11 @@ export type CorePrimitives = {
|
|
|
1382
1498
|
* CorePrimitives mocks in tests keep compiling — additive rollout of
|
|
1383
1499
|
* a new primitive shouldn't force every test double to grow a stub. */
|
|
1384
1500
|
readonly FillContainer?: ComponentType<FillContainerProps>;
|
|
1501
|
+
/** Optional — additive rollout, see StickyActionBar. Without it, header
|
|
1502
|
+
* content stays where the screen always rendered it. */
|
|
1503
|
+
readonly PageHeader?: ComponentType<PageHeaderProps>;
|
|
1504
|
+
/** Optional — additive rollout, see StickyActionBar. */
|
|
1505
|
+
readonly MetricBand?: ComponentType<MetricBandProps>;
|
|
1385
1506
|
/** Optional: without an implementation, callers with >2 header/row
|
|
1386
1507
|
* actions fall back to today's all-buttons-inline rendering (A7). */
|
|
1387
1508
|
readonly ActionOverflowMenu?: ComponentType<ActionOverflowMenuProps>;
|