@cosmicdrift/kumiko-renderer-web 0.133.0 → 0.135.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.
@@ -0,0 +1,38 @@
1
+ import type { ReactNode } from "react";
2
+ import { ProgressBar } from "./progress-bar";
3
+
4
+ export type ProgressListRow = {
5
+ readonly id: string;
6
+ readonly label: string;
7
+ readonly value: string;
8
+ readonly fraction: number;
9
+ };
10
+
11
+ /** Liste aus Label/Wert-Kopfzeile + Fortschrittsbalken pro Eintrag (z.B.
12
+ * Tilgungsfortschritt pro Kredit). */
13
+ export function ProgressList({
14
+ rows,
15
+ emptyContent,
16
+ testId,
17
+ }: {
18
+ readonly rows: readonly ProgressListRow[];
19
+ readonly emptyContent?: ReactNode;
20
+ readonly testId?: string;
21
+ }): ReactNode {
22
+ if (rows.length === 0) {
23
+ return <div data-testid={testId}>{emptyContent}</div>;
24
+ }
25
+ return (
26
+ <ul data-testid={testId} className="flex max-h-64 flex-col gap-3 overflow-y-auto pr-1">
27
+ {rows.map((row) => (
28
+ <li key={row.id} className="flex flex-col gap-1">
29
+ <div className="flex items-baseline justify-between gap-2 text-sm">
30
+ <span className="font-medium">{row.label}</span>
31
+ <span className="tabular-nums text-muted-foreground">{row.value}</span>
32
+ </div>
33
+ <ProgressBar value={row.fraction} />
34
+ </li>
35
+ ))}
36
+ </ul>
37
+ );
38
+ }
@@ -0,0 +1,101 @@
1
+ import { usePrimitives } from "@cosmicdrift/kumiko-renderer";
2
+ import type { ReactNode } from "react";
3
+ import { cn } from "../lib/cn";
4
+ import { DetailList } from "./detail-list";
5
+ import { SectionCard } from "./section-card";
6
+
7
+ /** Ergebnis-Sektion eines Rechners: SectionCard mit Empty-Zustand (Banner)
8
+ * oder Kennzahl-Liste (DetailList) + optionalen Extras (Tabelle, Hinweise).
9
+ * Ersetzt die handgebauten `<dl>`/Banner-Blöcke in Custom-Screens. */
10
+ export function ResultPanel({
11
+ title,
12
+ subtitle,
13
+ empty,
14
+ emptyText,
15
+ rows,
16
+ children,
17
+ testId,
18
+ }: {
19
+ readonly title: string;
20
+ readonly subtitle?: string;
21
+ readonly empty?: boolean;
22
+ readonly emptyText?: ReactNode;
23
+ readonly rows?: readonly {
24
+ readonly label: string;
25
+ readonly value: ReactNode;
26
+ readonly emphasize?: boolean;
27
+ }[];
28
+ readonly children?: ReactNode;
29
+ readonly testId?: string;
30
+ }): ReactNode {
31
+ const { Banner } = usePrimitives();
32
+ return (
33
+ <SectionCard title={title} subtitle={subtitle} testId={testId}>
34
+ {empty === true ? (
35
+ <Banner variant="info" padded>
36
+ {emptyText}
37
+ </Banner>
38
+ ) : (
39
+ <div className="flex flex-col gap-4">
40
+ {rows !== undefined && rows.length > 0 && <DetailList rows={rows} />}
41
+ {children}
42
+ </div>
43
+ )}
44
+ </SectionCard>
45
+ );
46
+ }
47
+
48
+ export interface ResultColumn<Row> {
49
+ readonly header: string;
50
+ readonly align?: "left" | "right";
51
+ readonly cell: (row: Row) => ReactNode;
52
+ }
53
+
54
+ /** Statische, prop-getriebene Ergebnistabelle für berechnete Zeilen (Tranchen,
55
+ * Szenarien). Nimmt dem Screen die `<table>`+tabular-nums-Ketten ab.
56
+ * ponytail: bewusst die minimale Read-only-Tabelle — für Sort/Pager/Facets
57
+ * stattdessen usePrimitives().DataTable bzw. QueryTable nutzen. */
58
+ export function ResultTable<Row>({
59
+ columns,
60
+ rows,
61
+ rowKey,
62
+ testId,
63
+ }: {
64
+ readonly columns: readonly ResultColumn<Row>[];
65
+ readonly rows: readonly Row[];
66
+ readonly rowKey: (row: Row, index: number) => string;
67
+ readonly testId?: string;
68
+ }): ReactNode {
69
+ return (
70
+ <div className="overflow-x-auto">
71
+ <table data-testid={testId} className="w-full text-sm">
72
+ <thead>
73
+ <tr className="border-b text-left text-muted-foreground">
74
+ {columns.map((col) => (
75
+ <th
76
+ key={col.header}
77
+ className={cn("py-1.5 font-medium", col.align === "right" && "text-right")}
78
+ >
79
+ {col.header}
80
+ </th>
81
+ ))}
82
+ </tr>
83
+ </thead>
84
+ <tbody>
85
+ {rows.map((row, i) => (
86
+ <tr key={rowKey(row, i)} className="border-b last:border-0">
87
+ {columns.map((col) => (
88
+ <td
89
+ key={col.header}
90
+ className={cn("py-1.5", col.align === "right" && "text-right tabular-nums")}
91
+ >
92
+ {col.cell(row)}
93
+ </td>
94
+ ))}
95
+ </tr>
96
+ ))}
97
+ </tbody>
98
+ </table>
99
+ </div>
100
+ );
101
+ }
@@ -0,0 +1,35 @@
1
+ import { useCallback, useState } from "react";
2
+
3
+ /** Formular-State für Rechner-Screens: ein Draft-Objekt + `patch` für
4
+ * Teil-Updates + `field(name)` das die `{ id, name, value, onChange }`-Props
5
+ * fertig für die Feld-Widgets (NumberField/MoneyField/PercentField) liefert.
6
+ * Ersetzt das pro Screen wiederholte Draft-Interface + patch-Helper. */
7
+ export function useDraft<T extends object>(
8
+ defaults: T,
9
+ ): {
10
+ readonly draft: T;
11
+ readonly patch: (changes: Partial<T>) => void;
12
+ readonly reset: () => void;
13
+ readonly field: <K extends keyof T>(
14
+ name: K,
15
+ ) => {
16
+ readonly id: string;
17
+ readonly name: string;
18
+ readonly value: T[K];
19
+ readonly onChange: (v: T[K]) => void;
20
+ };
21
+ } {
22
+ const [draft, setDraft] = useState<T>(defaults);
23
+ const patch = useCallback((changes: Partial<T>) => setDraft((d) => ({ ...d, ...changes })), []);
24
+ const reset = useCallback(() => setDraft(defaults), [defaults]);
25
+ const field = useCallback(
26
+ <K extends keyof T>(name: K) => ({
27
+ id: String(name),
28
+ name: String(name),
29
+ value: draft[name],
30
+ onChange: (v: T[K]) => setDraft((d) => ({ ...d, [name]: v })),
31
+ }),
32
+ [draft],
33
+ );
34
+ return { draft, patch, reset, field };
35
+ }
package/src/ui/card.tsx DELETED
@@ -1,93 +0,0 @@
1
- // @ts-nocheck — vendored shadcn, regenerate via scripts/sync-shadcn.ts
2
- import * as React from "react"
3
-
4
- import { cn } from "../lib/cn"
5
-
6
- function Card({ className, ...props }: React.ComponentProps<"div">) {
7
- return (
8
- <div
9
- data-slot="card"
10
- className={cn(
11
- "flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm",
12
- className
13
- )}
14
- {...props}
15
- />
16
- )
17
- }
18
-
19
- function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
20
- return (
21
- <div
22
- data-slot="card-header"
23
- className={cn(
24
- "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
25
- className
26
- )}
27
- {...props}
28
- />
29
- )
30
- }
31
-
32
- function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
33
- return (
34
- <div
35
- data-slot="card-title"
36
- className={cn("leading-none font-semibold", className)}
37
- {...props}
38
- />
39
- )
40
- }
41
-
42
- function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
43
- return (
44
- <div
45
- data-slot="card-description"
46
- className={cn("text-sm text-muted-foreground", className)}
47
- {...props}
48
- />
49
- )
50
- }
51
-
52
- function CardAction({ className, ...props }: React.ComponentProps<"div">) {
53
- return (
54
- <div
55
- data-slot="card-action"
56
- className={cn(
57
- "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
58
- className
59
- )}
60
- {...props}
61
- />
62
- )
63
- }
64
-
65
- function CardContent({ className, ...props }: React.ComponentProps<"div">) {
66
- return (
67
- <div
68
- data-slot="card-content"
69
- className={cn("px-6", className)}
70
- {...props}
71
- />
72
- )
73
- }
74
-
75
- function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
76
- return (
77
- <div
78
- data-slot="card-footer"
79
- className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
80
- {...props}
81
- />
82
- )
83
- }
84
-
85
- export {
86
- Card,
87
- CardHeader,
88
- CardFooter,
89
- CardTitle,
90
- CardAction,
91
- CardDescription,
92
- CardContent,
93
- }
@@ -1,34 +0,0 @@
1
- // @ts-nocheck — vendored shadcn, regenerate via scripts/sync-shadcn.ts
2
- "use client"
3
-
4
- import { Collapsible as CollapsiblePrimitive } from "radix-ui"
5
-
6
- function Collapsible({
7
- ...props
8
- }: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
9
- return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
10
- }
11
-
12
- function CollapsibleTrigger({
13
- ...props
14
- }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
15
- return (
16
- <CollapsiblePrimitive.CollapsibleTrigger
17
- data-slot="collapsible-trigger"
18
- {...props}
19
- />
20
- )
21
- }
22
-
23
- function CollapsibleContent({
24
- ...props
25
- }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
26
- return (
27
- <CollapsiblePrimitive.CollapsibleContent
28
- data-slot="collapsible-content"
29
- {...props}
30
- />
31
- )
32
- }
33
-
34
- export { Collapsible, CollapsibleTrigger, CollapsibleContent }
@@ -1,258 +0,0 @@
1
- // @ts-nocheck — vendored shadcn, regenerate via scripts/sync-shadcn.ts
2
- "use client"
3
-
4
- import * as React from "react"
5
- import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
6
- import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
7
-
8
- import { cn } from "../lib/cn"
9
-
10
- function DropdownMenu({
11
- ...props
12
- }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
13
- return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
14
- }
15
-
16
- function DropdownMenuPortal({
17
- ...props
18
- }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
19
- return (
20
- <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
21
- )
22
- }
23
-
24
- function DropdownMenuTrigger({
25
- ...props
26
- }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
27
- return (
28
- <DropdownMenuPrimitive.Trigger
29
- data-slot="dropdown-menu-trigger"
30
- {...props}
31
- />
32
- )
33
- }
34
-
35
- function DropdownMenuContent({
36
- className,
37
- sideOffset = 4,
38
- ...props
39
- }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
40
- return (
41
- <DropdownMenuPrimitive.Portal>
42
- <DropdownMenuPrimitive.Content
43
- data-slot="dropdown-menu-content"
44
- sideOffset={sideOffset}
45
- className={cn(
46
- "z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
47
- className
48
- )}
49
- {...props}
50
- />
51
- </DropdownMenuPrimitive.Portal>
52
- )
53
- }
54
-
55
- function DropdownMenuGroup({
56
- ...props
57
- }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
58
- return (
59
- <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
60
- )
61
- }
62
-
63
- function DropdownMenuItem({
64
- className,
65
- inset,
66
- variant = "default",
67
- ...props
68
- }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
69
- inset?: boolean
70
- variant?: "default" | "destructive"
71
- }) {
72
- return (
73
- <DropdownMenuPrimitive.Item
74
- data-slot="dropdown-menu-item"
75
- data-inset={inset}
76
- data-variant={variant}
77
- className={cn(
78
- "relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!",
79
- className
80
- )}
81
- {...props}
82
- />
83
- )
84
- }
85
-
86
- function DropdownMenuCheckboxItem({
87
- className,
88
- children,
89
- checked,
90
- ...props
91
- }: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
92
- return (
93
- <DropdownMenuPrimitive.CheckboxItem
94
- data-slot="dropdown-menu-checkbox-item"
95
- className={cn(
96
- "relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
97
- className
98
- )}
99
- checked={checked}
100
- {...props}
101
- >
102
- <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
103
- <DropdownMenuPrimitive.ItemIndicator>
104
- <CheckIcon className="size-4" />
105
- </DropdownMenuPrimitive.ItemIndicator>
106
- </span>
107
- {children}
108
- </DropdownMenuPrimitive.CheckboxItem>
109
- )
110
- }
111
-
112
- function DropdownMenuRadioGroup({
113
- ...props
114
- }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
115
- return (
116
- <DropdownMenuPrimitive.RadioGroup
117
- data-slot="dropdown-menu-radio-group"
118
- {...props}
119
- />
120
- )
121
- }
122
-
123
- function DropdownMenuRadioItem({
124
- className,
125
- children,
126
- ...props
127
- }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
128
- return (
129
- <DropdownMenuPrimitive.RadioItem
130
- data-slot="dropdown-menu-radio-item"
131
- className={cn(
132
- "relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
133
- className
134
- )}
135
- {...props}
136
- >
137
- <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
138
- <DropdownMenuPrimitive.ItemIndicator>
139
- <CircleIcon className="size-2 fill-current" />
140
- </DropdownMenuPrimitive.ItemIndicator>
141
- </span>
142
- {children}
143
- </DropdownMenuPrimitive.RadioItem>
144
- )
145
- }
146
-
147
- function DropdownMenuLabel({
148
- className,
149
- inset,
150
- ...props
151
- }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
152
- inset?: boolean
153
- }) {
154
- return (
155
- <DropdownMenuPrimitive.Label
156
- data-slot="dropdown-menu-label"
157
- data-inset={inset}
158
- className={cn(
159
- "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
160
- className
161
- )}
162
- {...props}
163
- />
164
- )
165
- }
166
-
167
- function DropdownMenuSeparator({
168
- className,
169
- ...props
170
- }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
171
- return (
172
- <DropdownMenuPrimitive.Separator
173
- data-slot="dropdown-menu-separator"
174
- className={cn("-mx-1 my-1 h-px bg-border", className)}
175
- {...props}
176
- />
177
- )
178
- }
179
-
180
- function DropdownMenuShortcut({
181
- className,
182
- ...props
183
- }: React.ComponentProps<"span">) {
184
- return (
185
- <span
186
- data-slot="dropdown-menu-shortcut"
187
- className={cn(
188
- "ml-auto text-xs tracking-widest text-muted-foreground",
189
- className
190
- )}
191
- {...props}
192
- />
193
- )
194
- }
195
-
196
- function DropdownMenuSub({
197
- ...props
198
- }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
199
- return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
200
- }
201
-
202
- function DropdownMenuSubTrigger({
203
- className,
204
- inset,
205
- children,
206
- ...props
207
- }: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
208
- inset?: boolean
209
- }) {
210
- return (
211
- <DropdownMenuPrimitive.SubTrigger
212
- data-slot="dropdown-menu-sub-trigger"
213
- data-inset={inset}
214
- className={cn(
215
- "flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[inset]:pl-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
216
- className
217
- )}
218
- {...props}
219
- >
220
- {children}
221
- <ChevronRightIcon className="ml-auto size-4" />
222
- </DropdownMenuPrimitive.SubTrigger>
223
- )
224
- }
225
-
226
- function DropdownMenuSubContent({
227
- className,
228
- ...props
229
- }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
230
- return (
231
- <DropdownMenuPrimitive.SubContent
232
- data-slot="dropdown-menu-sub-content"
233
- className={cn(
234
- "z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
235
- className
236
- )}
237
- {...props}
238
- />
239
- )
240
- }
241
-
242
- export {
243
- DropdownMenu,
244
- DropdownMenuPortal,
245
- DropdownMenuTrigger,
246
- DropdownMenuContent,
247
- DropdownMenuGroup,
248
- DropdownMenuLabel,
249
- DropdownMenuItem,
250
- DropdownMenuCheckboxItem,
251
- DropdownMenuRadioGroup,
252
- DropdownMenuRadioItem,
253
- DropdownMenuSeparator,
254
- DropdownMenuShortcut,
255
- DropdownMenuSub,
256
- DropdownMenuSubTrigger,
257
- DropdownMenuSubContent,
258
- }