@flaredev/cli 0.4.0 → 0.5.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.
Files changed (59) hide show
  1. package/dist/index.js +851 -405
  2. package/package.json +2 -2
  3. package/templates/app/app/dashboard/actions.ts +38 -5
  4. package/templates/app/app/dashboard/costs/page.tsx +93 -0
  5. package/templates/app/app/dashboard/page.tsx +4 -7
  6. package/templates/app/app/globals.css +0 -6
  7. package/templates/app/app/layout.tsx +37 -28
  8. package/templates/app/app/page.tsx +10 -0
  9. package/templates/app/components/dashboard/cost-estimator.tsx +77 -0
  10. package/templates/app/components/dashboard/fields/field-widget.tsx +237 -186
  11. package/templates/app/components/dashboard/fields/file-field.tsx +5 -1
  12. package/templates/app/components/dashboard/fields/relation-field.tsx +25 -3
  13. package/templates/app/components/dashboard/import-dialog.tsx +2 -2
  14. package/templates/app/components/dashboard/record-actions.tsx +9 -9
  15. package/templates/app/components/dashboard/record-detail.tsx +6 -3
  16. package/templates/app/components/dashboard/resource-form-page.tsx +64 -61
  17. package/templates/app/components/dashboard/resource-form-sheet.tsx +86 -0
  18. package/templates/app/components/dashboard/resource-form.tsx +261 -165
  19. package/templates/app/components/dashboard/resource-icon.tsx +87 -85
  20. package/templates/app/components/dashboard/resource-stats.tsx +10 -9
  21. package/templates/app/components/dashboard/resource-table-toolbar.tsx +2 -2
  22. package/templates/app/components/dashboard/resource-table.tsx +12 -9
  23. package/templates/app/components/dashboard/row-actions.tsx +5 -5
  24. package/templates/app/lib/costs.ts +138 -0
  25. package/templates/app/lib/dashboard-nav.ts +5 -1
  26. package/templates/app/lib/dashboard.ts +66 -61
  27. package/templates/app/lib/number.ts +33 -0
  28. package/templates/app/lib/site.ts +56 -56
  29. package/templates/app/lib/suggest.ts +35 -0
  30. package/templates/app/lib/usage.ts +32 -0
  31. package/templates/locks/package-lock.json +9 -9
  32. package/templates/locks/pnpm-lock.yaml +12 -12
  33. package/templates/locks/yarn.lock +3 -3
  34. package/templates/next/.env.example +21 -0
  35. package/templates/next/README.md +16 -0
  36. package/templates/next/app/api/health/route.ts +7 -0
  37. package/templates/next/app/dashboard/account/password/page.tsx +19 -0
  38. package/templates/next/app/dashboard/account/security/page.tsx +31 -0
  39. package/templates/next/app/dashboard/actions.ts +277 -0
  40. package/templates/next/app/dashboard/layout.tsx +64 -0
  41. package/templates/next/app/dashboard/page.tsx +127 -0
  42. package/templates/next/components/dashboard/resource-chart.tsx +86 -0
  43. package/templates/next/lib/analytics.ts +71 -0
  44. package/templates/next/lib/audit.ts +80 -0
  45. package/templates/next/lib/auth-emails.ts +92 -0
  46. package/templates/next/lib/auth.ts +156 -0
  47. package/templates/next/lib/dashboard.ts +161 -0
  48. package/templates/next/lib/db.ts +24 -0
  49. package/templates/next/lib/mail.ts +35 -0
  50. package/templates/next/lib/realtime.ts +23 -0
  51. package/templates/next/lib/redis.ts +17 -0
  52. package/templates/next/lib/storage.ts +57 -0
  53. package/templates/next/lib/theme.ts +83 -0
  54. package/templates/next/lib/views.ts +23 -0
  55. package/templates/next/next.config.ts +13 -0
  56. package/templates/next/prisma/schema/base.prisma +162 -0
  57. package/templates/next/prisma.config.ts +15 -0
  58. package/templates/next/tsconfig.json +24 -0
  59. package/templates/app/components/dashboard/resource-form-dialog.tsx +0 -80
@@ -1,165 +1,261 @@
1
- "use client";
2
-
3
- import Link from "next/link";
4
- import { useRouter } from "next/navigation";
5
- import { useMemo, useState, useTransition, type FormEvent } from "react";
6
- import { createValidators, formValuesToInput, initialFormValues, issuesByField, storedFields, type Resource } from "@flaredev/core";
7
- import { toast } from "sonner";
8
- import { createRecordAction, updateRecordAction } from "@/app/dashboard/actions";
9
- import { Alert, AlertDescription } from "@/components/ui/alert";
10
- import { Button } from "@/components/ui/button";
11
- import { Card, CardContent, CardFooter } from "@/components/ui/card";
12
- import { Field, FieldDescription, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field";
13
- import { Spinner } from "@/components/ui/spinner";
14
- import { FieldWidget, type RelationMeta } from "./fields/field-widget";
15
-
16
- export interface ResourceFormProps {
17
- resource: Resource;
18
- mode: "create" | "edit";
19
- /** Record id (edit mode). */
20
- id?: string;
21
- record?: Record<string, unknown> | null;
22
- /** belongsTo field key → related resource metadata and the current value's title. */
23
- relations?: Record<string, RelationMeta & { initialTitle?: string }>;
24
- /** Where Cancel and a successful save go, when the form owns the page. */
25
- listHref: string;
26
- /** In a dialog: close it instead of navigating, and drop the card around the fields. */
27
- onDone?: () => void;
28
- }
29
-
30
- const WIDE = new Set(["text", "file"]);
31
-
32
- /**
33
- * Create/edit form rendered from field metadata. Values are validated in the browser
34
- * with the descriptor's zod schemas, then again on the server by the same store the
35
- * REST API uses; server errors (e.g. unique conflicts) land on their fields.
36
- */
37
- export function ResourceForm({ resource, mode, id, record, relations = {}, listHref, onDone }: ResourceFormProps) {
38
- const router = useRouter();
39
- const validators = useMemo(() => createValidators(resource), [resource]);
40
- const fields = useMemo(() => storedFields(resource), [resource]);
41
- const [values, setValues] = useState(() => initialFormValues(resource, record));
42
- const [errors, setErrors] = useState<Record<string, string>>({});
43
- const [pending, startTransition] = useTransition();
44
-
45
- const setValue = (key: string, value: string | boolean) => {
46
- setValues((current) => ({ ...current, [key]: value }));
47
- if (errors[key]) setErrors(({ [key]: _removed, ...rest }) => rest);
48
- };
49
-
50
- function focusFirstError(fieldErrors: Record<string, string>) {
51
- const first = fields.find(([key]) => fieldErrors[key]);
52
- if (first) document.getElementById(`field-${first[0]}`)?.focus();
53
- }
54
-
55
- function onSubmit(event: FormEvent) {
56
- event.preventDefault();
57
- const input = formValuesToInput(resource, values, mode);
58
- const parsed = (mode === "create" ? validators.create : validators.update).safeParse(input);
59
- if (!parsed.success) {
60
- const fieldErrors = issuesByField(parsed.error.issues);
61
- setErrors(fieldErrors);
62
- focusFirstError(fieldErrors);
63
- return;
64
- }
65
-
66
- startTransition(async () => {
67
- const result = mode === "create" ? await createRecordAction(resource.name, input) : await updateRecordAction(resource.name, id!, input);
68
- if (result.ok) {
69
- toast.success(`${resource.label} ${mode === "create" ? "created" : "saved"}.`);
70
- if (onDone) onDone();
71
- else router.push(listHref);
72
- router.refresh();
73
- return;
74
- }
75
- const fieldErrors = result.issues ? issuesByField(result.issues) : result.field ? { [result.field]: result.error } : { _form: result.error };
76
- setErrors(fieldErrors);
77
- focusFirstError(fieldErrors);
78
- });
79
- }
80
-
81
- const Frame = onDone ? DialogFrame : Card;
82
- const Body = onDone ? DialogBody : CardContent;
83
- const Footer = onDone ? DialogFooterRow : CardFooter;
84
-
85
- return (
86
- <form onSubmit={onSubmit} noValidate>
87
- <Frame>
88
- <Body>
89
- {errors._form && (
90
- <Alert variant="destructive" className="mb-6">
91
- <AlertDescription>{errors._form}</AlertDescription>
92
- </Alert>
93
- )}
94
- <FieldGroup className="grid gap-6 md:grid-cols-2">
95
- {fields.map(([key, def]) => {
96
- const error = errors[key];
97
- const widgetId = `field-${key}`;
98
- const horizontal = def.kind === "boolean";
99
- return (
100
- <Field
101
- key={key}
102
- data-invalid={error ? true : undefined}
103
- className={WIDE.has(def.kind) ? "md:col-span-2" : undefined}
104
- >
105
- <FieldLabel htmlFor={widgetId}>
106
- {def.label}
107
- {def.required && def.kind !== "boolean" && (
108
- <span className="ml-0.5 text-muted-foreground" aria-hidden>
109
- *
110
- </span>
111
- )}
112
- </FieldLabel>
113
- <div className={horizontal ? "flex h-9 items-center" : undefined}>
114
- <FieldWidget
115
- id={widgetId}
116
- name={key}
117
- resourceName={resource.name}
118
- fieldKey={key}
119
- field={def}
120
- value={values[key] ?? ""}
121
- onChange={(value) => setValue(key, value)}
122
- invalid={Boolean(error)}
123
- disabled={pending}
124
- relation={relations[key]}
125
- />
126
- </div>
127
- {def.helpText && <FieldDescription>{def.helpText}</FieldDescription>}
128
- {error && <FieldError>{error}</FieldError>}
129
- </Field>
130
- );
131
- })}
132
- </FieldGroup>
133
- </Body>
134
- <Footer className="flex justify-end gap-2 border-t pt-4">
135
- {onDone ? (
136
- <Button type="button" variant="outline" onClick={onDone} disabled={pending}>
137
- Cancel
138
- </Button>
139
- ) : (
140
- <Button variant="outline" asChild>
141
- <Link href={listHref}>Cancel</Link>
142
- </Button>
143
- )}
144
- <Button type="submit" disabled={pending}>
145
- {pending && <Spinner data-icon="inline-start" />}
146
- {mode === "create" ? `Create ${resource.label.toLowerCase()}` : "Save changes"}
147
- </Button>
148
- </Footer>
149
- </Frame>
150
- </form>
151
- );
152
- }
153
-
154
- /** Plain wrappers so the same form works in a page (a card) and in a dialog (no card). */
155
- function DialogFrame({ children }: { children: React.ReactNode }) {
156
- return <div className="flex flex-col gap-4">{children}</div>;
157
- }
158
-
159
- function DialogBody({ children }: { children: React.ReactNode }) {
160
- return <div className="max-h-[60vh] overflow-y-auto px-1">{children}</div>;
161
- }
162
-
163
- function DialogFooterRow({ children, className }: { children: React.ReactNode; className?: string }) {
164
- return <div className={className}>{children}</div>;
165
- }
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { useRouter } from "next/navigation";
5
+ import { useMemo, useState, useTransition, type FormEvent, type MouseEvent } from "react";
6
+ import { createValidators, formValuesToInput, initialFormValues, issuesByField, storedFields, type ClientResource, type StoredField } from "@flaredev/core";
7
+ import { toast } from "sonner";
8
+ import { createRecordAction, updateRecordAction } from "@/app/dashboard/actions";
9
+ import { Alert, AlertDescription } from "@/components/ui/alert";
10
+ import { Button } from "@/components/ui/button";
11
+ import { Card, CardContent, CardFooter } from "@/components/ui/card";
12
+ import { Field, FieldDescription, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field";
13
+ import { Spinner } from "@/components/ui/spinner";
14
+ import { canSuggest, suggestValue } from "@/lib/suggest";
15
+ import { cn } from "@/lib/utils";
16
+ import { FieldWidget, type RelationMeta } from "./fields/field-widget";
17
+
18
+ export interface ResourceFormProps {
19
+ resource: ClientResource;
20
+ mode: "create" | "edit";
21
+ /** Record id (edit mode). */
22
+ id?: string;
23
+ record?: Record<string, unknown> | null;
24
+ /** belongsTo field key → related resource metadata and the current value's title. */
25
+ relations?: Record<string, RelationMeta & { initialTitle?: string }>;
26
+ /** Where Cancel and a successful save go, when the form owns the page. */
27
+ listHref: string;
28
+ /** In a sheet: close it instead of navigating, and drop the card around the fields. */
29
+ onDone?: () => void;
30
+ }
31
+
32
+ const WIDE = new Set(["text", "file"]);
33
+
34
+ /**
35
+ * More fields than this and the form is split into steps.
36
+ *
37
+ * Six inputs in one column is where a form starts to read as a chore rather than a
38
+ * question, and a step at a time also means the first error appears before everything
39
+ * else has been filled in.
40
+ */
41
+ const FIELDS_PER_STEP = 5;
42
+
43
+ type Entry = [string, StoredField & { label: string }];
44
+
45
+ /** Fields in declaration order, cut into steps of at most {@link FIELDS_PER_STEP}. */
46
+ function toSteps(fields: Entry[]): Entry[][] {
47
+ if (fields.length <= FIELDS_PER_STEP) return [fields];
48
+ const steps: Entry[][] = [];
49
+ for (let index = 0; index < fields.length; index += FIELDS_PER_STEP) {
50
+ steps.push(fields.slice(index, index + FIELDS_PER_STEP));
51
+ }
52
+ return steps;
53
+ }
54
+
55
+ /**
56
+ * Create/edit form rendered from field metadata. Values are validated in the browser
57
+ * with the descriptor's zod schemas, then again on the server by the same store the
58
+ * REST API uses; server errors (e.g. unique conflicts) land on their fields.
59
+ */
60
+ export function ResourceForm({ resource, mode, id, record, relations = {}, listHref, onDone }: ResourceFormProps) {
61
+ const router = useRouter();
62
+ const validators = useMemo(() => createValidators(resource), [resource]);
63
+ const fields = useMemo(() => storedFields(resource), [resource]);
64
+ const steps = useMemo(() => toSteps(fields), [fields]);
65
+ const [step, setStep] = useState(0);
66
+ const [values, setValues] = useState(() => initialFormValues(resource, record));
67
+ const [errors, setErrors] = useState<Record<string, string>>({});
68
+ const [pending, startTransition] = useTransition();
69
+
70
+ const last = step === steps.length - 1;
71
+ const visible = steps[step] ?? [];
72
+
73
+ const setValue = (key: string, value: string | boolean) => {
74
+ setValues((current) => ({ ...current, [key]: value }));
75
+ if (errors[key]) setErrors(({ [key]: _removed, ...rest }) => rest);
76
+ };
77
+
78
+ function focusFirstError(fieldErrors: Record<string, string>, within: Entry[] = fields) {
79
+ const first = within.find(([key]) => fieldErrors[key]);
80
+ if (first) document.getElementById(`field-${first[0]}`)?.focus();
81
+ }
82
+
83
+ /** Everything the descriptor objects to, whichever step it is on. */
84
+ function validate(): Record<string, string> {
85
+ const input = formValuesToInput(resource, values, mode);
86
+ const parsed = (mode === "create" ? validators.create : validators.update).safeParse(input);
87
+ return parsed.success ? {} : issuesByField(parsed.error.issues);
88
+ }
89
+
90
+ /**
91
+ * Move on only when this step is right; problems further on can wait their turn.
92
+ *
93
+ * The click is cancelled first. This button and the submit button on the last step are
94
+ * the same node to the browser, and React has already turned this one into the submit
95
+ * button by the time the click's default action runs — without this, going to the last
96
+ * step also saves the record.
97
+ */
98
+ function next(event: MouseEvent<HTMLButtonElement>) {
99
+ event.preventDefault();
100
+ const found = validate();
101
+ const here = Object.fromEntries(Object.entries(found).filter(([key]) => visible.some(([field]) => field === key)));
102
+ if (Object.keys(here).length > 0) {
103
+ setErrors(here);
104
+ focusFirstError(here, visible);
105
+ return;
106
+ }
107
+ setErrors({});
108
+ setStep((current) => Math.min(current + 1, steps.length - 1));
109
+ }
110
+
111
+ function onSubmit(event: FormEvent) {
112
+ event.preventDefault();
113
+ const input = formValuesToInput(resource, values, mode);
114
+ const found = validate();
115
+ if (Object.keys(found).length > 0) {
116
+ setErrors(found);
117
+ // Send them to the step that holds the problem, not just to the field.
118
+ const broken = steps.findIndex((entries) => entries.some(([key]) => found[key]));
119
+ if (broken >= 0 && broken !== step) setStep(broken);
120
+ focusFirstError(found, broken >= 0 ? steps[broken]! : fields);
121
+ return;
122
+ }
123
+
124
+ startTransition(async () => {
125
+ const result = mode === "create" ? await createRecordAction(resource.name, input) : await updateRecordAction(resource.name, id!, input);
126
+ if (result.ok) {
127
+ toast.success(`${resource.label} ${mode === "create" ? "created" : "saved"}.`);
128
+ if (onDone) onDone();
129
+ else router.push(listHref);
130
+ router.refresh();
131
+ return;
132
+ }
133
+ const fieldErrors = result.issues ? issuesByField(result.issues) : result.field ? { [result.field]: result.error } : { _form: result.error };
134
+ setErrors(fieldErrors);
135
+ const broken = steps.findIndex((entries) => entries.some(([key]) => fieldErrors[key]));
136
+ if (broken >= 0 && broken !== step) setStep(broken);
137
+ focusFirstError(fieldErrors, broken >= 0 ? steps[broken]! : fields);
138
+ });
139
+ }
140
+
141
+ const Frame = onDone ? SheetFrame : Card;
142
+ const Body = onDone ? SheetBody : CardContent;
143
+ const Footer = onDone ? SheetFooterRow : CardFooter;
144
+
145
+ return (
146
+ <form onSubmit={onSubmit} noValidate className={onDone ? "flex min-h-0 flex-1 flex-col" : undefined}>
147
+ <Frame>
148
+ {steps.length > 1 && <Steps count={steps.length} current={step} onGo={setStep} />}
149
+ <Body>
150
+ {errors._form && (
151
+ <Alert variant="destructive" className="mb-6">
152
+ <AlertDescription>{errors._form}</AlertDescription>
153
+ </Alert>
154
+ )}
155
+ <FieldGroup className="grid gap-6 md:grid-cols-2">
156
+ {visible.map(([key, def]) => {
157
+ const error = errors[key];
158
+ const widgetId = `field-${key}`;
159
+ const horizontal = def.kind === "boolean";
160
+ return (
161
+ <Field key={key} data-invalid={error ? true : undefined} className={WIDE.has(def.kind) ? "md:col-span-2" : undefined}>
162
+ <FieldLabel htmlFor={widgetId}>
163
+ {def.label}
164
+ {def.required && def.kind !== "boolean" && (
165
+ <span className="ml-0.5 text-muted-foreground" aria-hidden>
166
+ *
167
+ </span>
168
+ )}
169
+ </FieldLabel>
170
+ <div className={horizontal ? "flex h-9 items-center" : undefined}>
171
+ <FieldWidget
172
+ id={widgetId}
173
+ name={key}
174
+ resourceName={resource.name}
175
+ fieldKey={key}
176
+ field={def}
177
+ value={values[key] ?? ""}
178
+ onChange={(value) => setValue(key, value)}
179
+ invalid={Boolean(error)}
180
+ disabled={pending}
181
+ relation={relations[key]}
182
+ suggest={canSuggest(def) ? () => suggestValue(key, def, String(values[resource.titleField] ?? "")) : undefined}
183
+ />
184
+ </div>
185
+ {def.helpText && <FieldDescription>{def.helpText}</FieldDescription>}
186
+ {error && <FieldError>{error}</FieldError>}
187
+ </Field>
188
+ );
189
+ })}
190
+ </FieldGroup>
191
+ </Body>
192
+ <Footer className="flex items-center justify-end gap-2 border-t pt-4">
193
+ {step > 0 ? (
194
+ <Button type="button" variant="outline" onClick={() => setStep((current) => current - 1)} disabled={pending}>
195
+ Back
196
+ </Button>
197
+ ) : onDone ? (
198
+ <Button type="button" variant="outline" onClick={onDone} disabled={pending}>
199
+ Cancel
200
+ </Button>
201
+ ) : (
202
+ <Button variant="outline" asChild>
203
+ <Link href={listHref}>Cancel</Link>
204
+ </Button>
205
+ )}
206
+ {last ? (
207
+ <Button key="submit" type="submit" disabled={pending}>
208
+ {pending && <Spinner data-icon="inline-start" />}
209
+ {mode === "create" ? `Create ${resource.label.toLowerCase()}` : "Save changes"}
210
+ </Button>
211
+ ) : (
212
+ <Button key="next" type="button" onClick={next} disabled={pending}>
213
+ Next
214
+ </Button>
215
+ )}
216
+ </Footer>
217
+ </Frame>
218
+ </form>
219
+ );
220
+ }
221
+
222
+ /** Where you are in a multi-step form, and a way back to a step already passed. */
223
+ function Steps({ count, current, onGo }: { count: number; current: number; onGo: (step: number) => void }) {
224
+ return (
225
+ <div className="flex items-center gap-3 pb-4">
226
+ <div className="flex flex-1 gap-1.5" role="presentation">
227
+ {Array.from({ length: count }, (_, index) => (
228
+ <button
229
+ key={index}
230
+ type="button"
231
+ aria-label={`Step ${index + 1} of ${count}`}
232
+ aria-current={index === current ? "step" : undefined}
233
+ disabled={index > current}
234
+ onClick={() => onGo(index)}
235
+ className={cn(
236
+ "h-1 flex-1 rounded-full transition-colors",
237
+ index <= current ? "bg-primary" : "bg-muted",
238
+ index < current && "cursor-pointer",
239
+ )}
240
+ />
241
+ ))}
242
+ </div>
243
+ <span className="text-xs text-muted-foreground tabular-nums">
244
+ Step {current + 1} of {count}
245
+ </span>
246
+ </div>
247
+ );
248
+ }
249
+
250
+ /** Plain wrappers so the same form works in a page (a card) and in a sheet (no card). */
251
+ function SheetFrame({ children }: { children: React.ReactNode }) {
252
+ return <div className="flex min-h-0 flex-1 flex-col gap-4">{children}</div>;
253
+ }
254
+
255
+ function SheetBody({ children }: { children: React.ReactNode }) {
256
+ return <div className="min-h-0 flex-1 overflow-y-auto px-1">{children}</div>;
257
+ }
258
+
259
+ function SheetFooterRow({ children, className }: { children: React.ReactNode; className?: string }) {
260
+ return <div className={className}>{children}</div>;
261
+ }
@@ -1,85 +1,87 @@
1
- import {
2
- BellIcon,
3
- BookIcon,
4
- BoxIcon,
5
- BriefcaseIcon,
6
- Building2Icon,
7
- CalendarIcon,
8
- ChartBarIcon,
9
- ClipboardListIcon,
10
- CreditCardIcon,
11
- DatabaseIcon,
12
- DollarSignIcon,
13
- FileTextIcon,
14
- FolderIcon,
15
- GlobeIcon,
16
- HeartIcon,
17
- ImageIcon,
18
- LayersIcon,
19
- MailIcon,
20
- MapPinIcon,
21
- MessageSquareIcon,
22
- PackageIcon,
23
- PhoneIcon,
24
- ReceiptIcon,
25
- SettingsIcon,
26
- ShieldAlertIcon,
27
- RadioIcon,
28
- ShieldIcon,
29
- ShoppingCartIcon,
30
- StarIcon,
31
- TagIcon,
32
- TicketIcon,
33
- TruckIcon,
34
- UserIcon,
35
- UsersIcon,
36
- WrenchIcon,
37
- type LucideIcon,
38
- } from "lucide-react";
39
-
40
- /**
41
- * Icons a resource descriptor can name (`icon: "users"`). Imported by name so unused
42
- * ones are tree-shaken; add the lucide icons your app needs.
43
- */
44
- export const RESOURCE_ICONS: Record<string, LucideIcon> = {
45
- bell: BellIcon,
46
- book: BookIcon,
47
- box: BoxIcon,
48
- briefcase: BriefcaseIcon,
49
- building: Building2Icon,
50
- calendar: CalendarIcon,
51
- chart: ChartBarIcon,
52
- clipboard: ClipboardListIcon,
53
- "credit-card": CreditCardIcon,
54
- database: DatabaseIcon,
55
- dollar: DollarSignIcon,
56
- file: FileTextIcon,
57
- folder: FolderIcon,
58
- globe: GlobeIcon,
59
- heart: HeartIcon,
60
- image: ImageIcon,
61
- layers: LayersIcon,
62
- mail: MailIcon,
63
- "map-pin": MapPinIcon,
64
- message: MessageSquareIcon,
65
- package: PackageIcon,
66
- phone: PhoneIcon,
67
- receipt: ReceiptIcon,
68
- settings: SettingsIcon,
69
- radio: RadioIcon,
70
- shield: ShieldIcon,
71
- "shield-alert": ShieldAlertIcon,
72
- cart: ShoppingCartIcon,
73
- star: StarIcon,
74
- tag: TagIcon,
75
- ticket: TicketIcon,
76
- truck: TruckIcon,
77
- user: UserIcon,
78
- users: UsersIcon,
79
- wrench: WrenchIcon,
80
- };
81
-
82
- /** The icon for a resource, falling back to a generic document icon. */
83
- export function resourceIcon(name: string | undefined): LucideIcon {
84
- return (name && RESOURCE_ICONS[name]) || FileTextIcon;
85
- }
1
+ import {
2
+ BellIcon,
3
+ BookIcon,
4
+ WalletIcon,
5
+ BoxIcon,
6
+ BriefcaseIcon,
7
+ Building2Icon,
8
+ CalendarIcon,
9
+ ChartBarIcon,
10
+ ClipboardListIcon,
11
+ CreditCardIcon,
12
+ DatabaseIcon,
13
+ DollarSignIcon,
14
+ FileTextIcon,
15
+ FolderIcon,
16
+ GlobeIcon,
17
+ HeartIcon,
18
+ ImageIcon,
19
+ LayersIcon,
20
+ MailIcon,
21
+ MapPinIcon,
22
+ MessageSquareIcon,
23
+ PackageIcon,
24
+ PhoneIcon,
25
+ ReceiptIcon,
26
+ SettingsIcon,
27
+ ShieldAlertIcon,
28
+ RadioIcon,
29
+ ShieldIcon,
30
+ ShoppingCartIcon,
31
+ StarIcon,
32
+ TagIcon,
33
+ TicketIcon,
34
+ TruckIcon,
35
+ UserIcon,
36
+ UsersIcon,
37
+ WrenchIcon,
38
+ type LucideIcon,
39
+ } from "lucide-react";
40
+
41
+ /**
42
+ * Icons a resource descriptor can name (`icon: "users"`). Imported by name so unused
43
+ * ones are tree-shaken; add the lucide icons your app needs.
44
+ */
45
+ export const RESOURCE_ICONS: Record<string, LucideIcon> = {
46
+ bell: BellIcon,
47
+ book: BookIcon,
48
+ wallet: WalletIcon,
49
+ box: BoxIcon,
50
+ briefcase: BriefcaseIcon,
51
+ building: Building2Icon,
52
+ calendar: CalendarIcon,
53
+ chart: ChartBarIcon,
54
+ clipboard: ClipboardListIcon,
55
+ "credit-card": CreditCardIcon,
56
+ database: DatabaseIcon,
57
+ dollar: DollarSignIcon,
58
+ file: FileTextIcon,
59
+ folder: FolderIcon,
60
+ globe: GlobeIcon,
61
+ heart: HeartIcon,
62
+ image: ImageIcon,
63
+ layers: LayersIcon,
64
+ mail: MailIcon,
65
+ "map-pin": MapPinIcon,
66
+ message: MessageSquareIcon,
67
+ package: PackageIcon,
68
+ phone: PhoneIcon,
69
+ receipt: ReceiptIcon,
70
+ settings: SettingsIcon,
71
+ radio: RadioIcon,
72
+ shield: ShieldIcon,
73
+ "shield-alert": ShieldAlertIcon,
74
+ cart: ShoppingCartIcon,
75
+ star: StarIcon,
76
+ tag: TagIcon,
77
+ ticket: TicketIcon,
78
+ truck: TruckIcon,
79
+ user: UserIcon,
80
+ users: UsersIcon,
81
+ wrench: WrenchIcon,
82
+ };
83
+
84
+ /** The icon for a resource, falling back to a generic document icon. */
85
+ export function resourceIcon(name: string | undefined): LucideIcon {
86
+ return (name && RESOURCE_ICONS[name]) || FileTextIcon;
87
+ }