create-astram-app 0.4.0 → 0.6.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 (87) hide show
  1. package/README.md +211 -38
  2. package/dist/add-frontend.js.map +1 -1
  3. package/dist/index.js +14 -9
  4. package/dist/index.js.map +1 -1
  5. package/package.json +2 -2
  6. package/template/README.md +51 -20
  7. package/template/frontend/README.md +21 -11
  8. package/template/frontend/index.html +16 -0
  9. package/template/frontend/package.json +1 -1
  10. package/template/frontend/src/App.jsx +95 -20
  11. package/template/frontend/src/auth/AuthContext.jsx +145 -21
  12. package/template/frontend/src/auth/routeGate.js +31 -0
  13. package/template/frontend/src/auth/routeGate.test.mjs +59 -0
  14. package/template/frontend/src/auth/sessionFlow.js +27 -0
  15. package/template/frontend/src/auth/sessionFlow.test.mjs +63 -0
  16. package/template/frontend/src/components/EntityList.jsx +1 -7
  17. package/template/frontend/src/components/MultiSelectChecklist.jsx +166 -0
  18. package/template/frontend/src/components/PageHeader.jsx +5 -4
  19. package/template/frontend/src/components/ShareDialog.jsx +3 -5
  20. package/template/frontend/src/components/access/CollapsibleGroup.jsx +4 -1
  21. package/template/frontend/src/components/ui/alert-dialog.tsx +27 -50
  22. package/template/frontend/src/components/ui/alert.tsx +17 -23
  23. package/template/frontend/src/components/ui/avatar.tsx +8 -20
  24. package/template/frontend/src/components/ui/badge.tsx +17 -26
  25. package/template/frontend/src/components/ui/button.tsx +22 -24
  26. package/template/frontend/src/components/ui/card.tsx +19 -36
  27. package/template/frontend/src/components/ui/checkbox.tsx +7 -10
  28. package/template/frontend/src/components/ui/date-field.tsx +26 -27
  29. package/template/frontend/src/components/ui/dialog.tsx +24 -40
  30. package/template/frontend/src/components/ui/dropdown-menu.tsx +25 -54
  31. package/template/frontend/src/components/ui/input-otp.tsx +1 -4
  32. package/template/frontend/src/components/ui/label.tsx +10 -12
  33. package/template/frontend/src/components/ui/searchable-select.tsx +1 -3
  34. package/template/frontend/src/components/ui/select.tsx +31 -43
  35. package/template/frontend/src/components/ui/separator.tsx +6 -6
  36. package/template/frontend/src/components/ui/sheet.tsx +29 -38
  37. package/template/frontend/src/components/ui/skeleton.tsx +3 -3
  38. package/template/frontend/src/components/ui/sonner.tsx +7 -7
  39. package/template/frontend/src/components/ui/table.tsx +56 -70
  40. package/template/frontend/src/components/ui/tooltip.tsx +7 -11
  41. package/template/frontend/src/config/branding.js +1 -4
  42. package/template/frontend/src/hooks/useMobile.tsx +4 -6
  43. package/template/frontend/src/index.css +9 -4
  44. package/template/frontend/src/pages/Dashboard.jsx +13 -7
  45. package/template/frontend/src/pages/ForgotPassword.jsx +9 -2
  46. package/template/frontend/src/pages/Login.jsx +85 -70
  47. package/template/frontend/src/pages/ResetPassword.jsx +9 -2
  48. package/template/frontend/src/pages/access/AccessForm.jsx +49 -15
  49. package/template/frontend/src/pages/access/AccessPage.jsx +10 -6
  50. package/template/frontend/src/pages/access/RecordPoliciesPage.jsx +9 -2
  51. package/template/frontend/src/pages/account/AccountLayout.jsx +11 -6
  52. package/template/frontend/src/pages/account/ChangePasswordPage.jsx +2 -2
  53. package/template/frontend/src/pages/account/ReenrollPage.jsx +3 -5
  54. package/template/frontend/src/pages/account/TwoFactorPage.jsx +3 -8
  55. package/template/frontend/src/pages/companies/CompaniesPage.jsx +7 -9
  56. package/template/frontend/src/pages/cross-tenant/GrantForm.jsx +41 -8
  57. package/template/frontend/src/pages/cross-tenant/GrantsPage.jsx +30 -7
  58. package/template/frontend/src/pages/cross-tenant/InboxPage.jsx +12 -3
  59. package/template/frontend/src/pages/roles/RolesPage.jsx +5 -9
  60. package/template/frontend/src/pages/service-accounts/KeyRevealDialog.jsx +104 -0
  61. package/template/frontend/src/pages/service-accounts/ServiceAccountAccessDialog.jsx +231 -0
  62. package/template/frontend/src/pages/service-accounts/ServiceAccountActionsMenu.jsx +56 -0
  63. package/template/frontend/src/pages/service-accounts/ServiceAccountsPage.jsx +368 -0
  64. package/template/frontend/src/pages/service-accounts/accessModel.js +158 -0
  65. package/template/frontend/src/pages/service-accounts/accessModel.test.mjs +189 -0
  66. package/template/frontend/src/pages/users/MembershipCard.jsx +3 -1
  67. package/template/frontend/src/pages/users/PostingRow.jsx +8 -14
  68. package/template/frontend/src/pages/users/UsersPage.jsx +7 -9
  69. package/template/frontend/src/platform/httpClient.js +5 -0
  70. package/template/frontend/src/platform/sessionStore.js +55 -13
  71. package/template/frontend/src/platform/sessionStore.test.mjs +89 -0
  72. package/template/frontend/src/platform/shieldTransport.js +6 -0
  73. package/template/frontend/src/platform/shieldTransport.test.mjs +84 -5
  74. package/template/frontend/src/shell/AppShell.jsx +10 -12
  75. package/template/frontend/src/shell/Sidebar.jsx +15 -3
  76. package/template/frontend/src/shell/nav.js +13 -3
  77. package/template/frontend/vite.config.js +19 -1
  78. package/template/server/_env.example +22 -1
  79. package/template/server/eslint.config.mjs +194 -0
  80. package/template/server/package.json +11 -11
  81. package/template/server/scripts/dev.mjs +17 -6
  82. package/template/server/scripts/sync-astram-guide.mjs +45 -19
  83. package/template/server/src/app.module.ts +11 -5
  84. package/template/server/src/health/demo.controller.ts +16 -12
  85. package/template/server/src/main.ts +23 -1
  86. package/template/server/test/enforcement.e2e.spec.ts +73 -6
  87. package/template/server/eslint.config.js +0 -95
@@ -0,0 +1,166 @@
1
+ /**
2
+ * MultiSelectChecklist — a searchable checkbox list for picking several items (roles, scope nodes).
3
+ * Shows a selected count and "Select all / None" (applied to the VISIBLE, unlocked options).
4
+ * Tree-shaped lists pass `depth` per option and are indented by it (a flat tree, no tree widget).
5
+ *
6
+ * Options: `{ id, name, depth?, locked? }`. `id` is only the value — never rendered; `name` is shown
7
+ * (ADR-0012). A `locked` option renders checked + disabled with `lockedHint` (e.g. "assigned by
8
+ * someone with more access"): it is not part of `value` and never toggled.
9
+ *
10
+ * Props:
11
+ * id — base id for the search input + list (a11y)
12
+ * label — field label
13
+ * options — the option list (above)
14
+ * value — array of selected (unlocked) ids
15
+ * onChange — (nextIds) => void
16
+ * searchPlaceholder, emptyLabel, lockedHint, hint, error, disabled, loading
17
+ */
18
+ import { useMemo, useState } from 'react';
19
+ import { Lock } from 'lucide-react';
20
+ import { cn } from '@/lib/utils';
21
+ import { Checkbox } from '@/components/ui/checkbox';
22
+ import { Input } from '@/components/ui/input';
23
+ import { Label } from '@/components/ui/label';
24
+ import { Badge } from '@/components/ui/badge';
25
+ import { Button } from '@/components/ui/button';
26
+
27
+ // Indent steps as literal classes (Tailwind scans literal strings; no inline spacing styles).
28
+ const INDENT = ['pl-0', 'pl-5', 'pl-10', 'pl-14', 'pl-20'];
29
+ const indentClass = (depth) => INDENT[Math.min(Math.max(depth || 0, 0), INDENT.length - 1)];
30
+
31
+ export default function MultiSelectChecklist({
32
+ id,
33
+ label,
34
+ options,
35
+ value,
36
+ onChange,
37
+ searchPlaceholder = 'Search…',
38
+ emptyLabel = 'Nothing to choose from.',
39
+ lockedHint = 'locked',
40
+ hint,
41
+ error,
42
+ disabled = false,
43
+ loading = false,
44
+ }) {
45
+ const [query, setQuery] = useState('');
46
+ const selected = useMemo(() => new Set(value), [value]);
47
+ const q = query.trim().toLowerCase();
48
+ // While searching, show matches flat (indent only makes sense for the whole tree).
49
+ const visible = q ? options.filter((o) => o.name.toLowerCase().includes(q)) : options;
50
+ const visibleUnlocked = visible.filter((o) => !o.locked);
51
+ const lockedTotal = options.filter((o) => o.locked).length;
52
+ const count = selected.size + lockedTotal;
53
+
54
+ const toggle = (optId) => {
55
+ const next = new Set(selected);
56
+ if (next.has(optId)) next.delete(optId);
57
+ else next.add(optId);
58
+ onChange(options.filter((o) => !o.locked && next.has(o.id)).map((o) => o.id));
59
+ };
60
+ const selectAll = () => {
61
+ const next = new Set(selected);
62
+ for (const o of visibleUnlocked) next.add(o.id);
63
+ onChange(options.filter((o) => !o.locked && next.has(o.id)).map((o) => o.id));
64
+ };
65
+ const selectNone = () => {
66
+ const drop = new Set(visibleUnlocked.map((o) => o.id));
67
+ onChange(value.filter((v) => !drop.has(v)));
68
+ };
69
+
70
+ const errorId = error ? `${id}-error` : undefined;
71
+
72
+ return (
73
+ <div className="space-y-1.5">
74
+ <div className="flex items-center justify-between gap-2">
75
+ <Label htmlFor={`${id}-search`} variant="field">
76
+ {label}
77
+ </Label>
78
+ <Badge variant="secondary" aria-live="polite">
79
+ {count} selected
80
+ </Badge>
81
+ </div>
82
+
83
+ <div className="flex items-center gap-2">
84
+ <Input
85
+ id={`${id}-search`}
86
+ value={query}
87
+ onChange={(e) => setQuery(e.target.value)}
88
+ placeholder={searchPlaceholder}
89
+ disabled={disabled || loading}
90
+ className="h-8"
91
+ />
92
+ <Button
93
+ type="button"
94
+ variant="ghost"
95
+ size="sm"
96
+ onClick={selectAll}
97
+ disabled={disabled || loading || visibleUnlocked.length === 0}
98
+ >
99
+ Select all
100
+ </Button>
101
+ <Button
102
+ type="button"
103
+ variant="ghost"
104
+ size="sm"
105
+ onClick={selectNone}
106
+ disabled={disabled || loading || visibleUnlocked.length === 0}
107
+ >
108
+ None
109
+ </Button>
110
+ </div>
111
+
112
+ <div
113
+ id={id}
114
+ role="group"
115
+ aria-label={label}
116
+ aria-describedby={errorId}
117
+ className={cn(
118
+ 'max-h-48 overflow-y-auto rounded-md border border-border p-2',
119
+ error && 'border-destructive',
120
+ )}
121
+ >
122
+ {loading ? (
123
+ <p className="px-1 py-2 text-xs text-muted-foreground">Loading…</p>
124
+ ) : visible.length === 0 ? (
125
+ <p className="px-1 py-2 text-xs text-muted-foreground">
126
+ {q ? 'No matches.' : emptyLabel}
127
+ </p>
128
+ ) : (
129
+ <ul className="space-y-1">
130
+ {visible.map((o) => (
131
+ <li key={o.id} className={cn(!q && indentClass(o.depth))}>
132
+ <label
133
+ className={cn(
134
+ 'flex items-center gap-2 rounded px-1 py-1 text-sm',
135
+ o.locked ? 'text-muted-foreground' : 'cursor-pointer hover:bg-accent',
136
+ )}
137
+ >
138
+ <Checkbox
139
+ checked={o.locked ? true : selected.has(o.id)}
140
+ disabled={disabled || o.locked}
141
+ onCheckedChange={() => !o.locked && toggle(o.id)}
142
+ />
143
+ <span className="truncate">{o.name}</span>
144
+ {o.locked && (
145
+ <span className="ml-auto inline-flex shrink-0 items-center gap-1 text-xs">
146
+ <Lock className="size-3" aria-hidden="true" />
147
+ {lockedHint}
148
+ </span>
149
+ )}
150
+ </label>
151
+ </li>
152
+ ))}
153
+ </ul>
154
+ )}
155
+ </div>
156
+
157
+ {error ? (
158
+ <p id={errorId} className="text-xs text-destructive">
159
+ {error}
160
+ </p>
161
+ ) : (
162
+ hint && <p className="text-xs text-muted-foreground">{hint}</p>
163
+ )}
164
+ </div>
165
+ );
166
+ }
@@ -26,7 +26,10 @@ export default function PageHeader({ title, section, icon: Icon = Shield, action
26
26
  {title}
27
27
  </h1>
28
28
  {/* Breadcrumb (relocated from the app header): Section › Page — no leading icon. */}
29
- <nav aria-label="Breadcrumb" className="mt-1 flex items-center gap-2 text-sm text-muted-foreground">
29
+ <nav
30
+ aria-label="Breadcrumb"
31
+ className="mt-1 flex items-center gap-2 text-sm text-muted-foreground"
32
+ >
30
33
  {section ? (
31
34
  <>
32
35
  <span>{section}</span>
@@ -46,9 +49,7 @@ export default function PageHeader({ title, section, icon: Icon = Shield, action
46
49
  return (
47
50
  <div className="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
48
51
  {titleAndCrumb}
49
- {actions && (
50
- <div className="flex shrink-0 items-center gap-2 max-sm:w-full">{actions}</div>
51
- )}
52
+ {actions && <div className="flex shrink-0 items-center gap-2 max-sm:w-full">{actions}</div>}
52
53
  </div>
53
54
  );
54
55
  }
@@ -96,8 +96,8 @@ export default function ShareDialog({
96
96
  value: row.id ?? row.uuid,
97
97
  label:
98
98
  granteeKind === 'role'
99
- ? row.fields?.name ?? '(role)'
100
- : row.fields?.username ?? row.fields?.email ?? '(user)',
99
+ ? (row.fields?.name ?? '(role)')
100
+ : (row.fields?.username ?? row.fields?.email ?? '(user)'),
101
101
  }));
102
102
  return { options, total: r.total ?? options.length, page: page ?? 1 };
103
103
  },
@@ -225,9 +225,7 @@ export default function ShareDialog({
225
225
  onChange={(value) =>
226
226
  setGrantee(value ? { value, label: grantee?.label ?? '' } : null)
227
227
  }
228
- onLabelChange={(label) =>
229
- setGrantee((g) => (g ? { ...g, label: label ?? '' } : g))
230
- }
228
+ onLabelChange={(label) => setGrantee((g) => (g ? { ...g, label: label ?? '' } : g))}
231
229
  fetchPage={fetchGrantees}
232
230
  placeholder={`Pick a ${granteeKind}`}
233
231
  />
@@ -18,7 +18,10 @@ export default function CollapsibleGroup({ title, count, total, open, onToggle,
18
18
  aria-expanded={open}
19
19
  >
20
20
  <ChevronRight
21
- className={cn('size-4 shrink-0 text-muted-foreground transition-transform', open && 'rotate-90')}
21
+ className={cn(
22
+ 'size-4 shrink-0 text-muted-foreground transition-transform',
23
+ open && 'rotate-90',
24
+ )}
22
25
  />
23
26
  <span className="flex-1">{title}</span>
24
27
  {typeof count === 'number' && typeof total === 'number' && (
@@ -1,8 +1,8 @@
1
- import * as React from "react";
2
- import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
1
+ import * as React from 'react';
2
+ import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
3
3
 
4
- import { cn } from "@/lib/utils";
5
- import { buttonVariants } from "@/components/ui/button";
4
+ import { cn } from '@/lib/utils';
5
+ import { buttonVariants } from '@/components/ui/button';
6
6
 
7
7
  /**
8
8
  * Radix AlertDialog — the semantic confirm primitive (Phase B moves
@@ -20,9 +20,7 @@ import { buttonVariants } from "@/components/ui/button";
20
20
  * NOTE: requires `@radix-ui/react-alert-dialog` to be installed.
21
21
  */
22
22
 
23
- function AlertDialog({
24
- ...props
25
- }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
23
+ function AlertDialog({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
26
24
  return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
27
25
  }
28
26
 
@@ -30,20 +28,12 @@ const AlertDialogTrigger = React.forwardRef<
30
28
  React.ElementRef<typeof AlertDialogPrimitive.Trigger>,
31
29
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Trigger>
32
30
  >((props, ref) => (
33
- <AlertDialogPrimitive.Trigger
34
- ref={ref}
35
- data-slot="alert-dialog-trigger"
36
- {...props}
37
- />
31
+ <AlertDialogPrimitive.Trigger ref={ref} data-slot="alert-dialog-trigger" {...props} />
38
32
  ));
39
- AlertDialogTrigger.displayName = "AlertDialogTrigger";
33
+ AlertDialogTrigger.displayName = 'AlertDialogTrigger';
40
34
 
41
- function AlertDialogPortal({
42
- ...props
43
- }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
44
- return (
45
- <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
46
- );
35
+ function AlertDialogPortal({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
36
+ return <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />;
47
37
  }
48
38
 
49
39
  const AlertDialogOverlay = React.forwardRef<
@@ -54,13 +44,13 @@ const AlertDialogOverlay = React.forwardRef<
54
44
  ref={ref}
55
45
  data-slot="alert-dialog-overlay"
56
46
  className={cn(
57
- "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
58
- className
47
+ 'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
48
+ className,
59
49
  )}
60
50
  {...props}
61
51
  />
62
52
  ));
63
- AlertDialogOverlay.displayName = "AlertDialogOverlay";
53
+ AlertDialogOverlay.displayName = 'AlertDialogOverlay';
64
54
 
65
55
  const AlertDialogContent = React.forwardRef<
66
56
  React.ElementRef<typeof AlertDialogPrimitive.Content>,
@@ -72,39 +62,30 @@ const AlertDialogContent = React.forwardRef<
72
62
  ref={ref}
73
63
  data-slot="alert-dialog-content"
74
64
  className={cn(
75
- "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100vw-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
76
- className
65
+ 'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100vw-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
66
+ className,
77
67
  )}
78
68
  {...props}
79
69
  />
80
70
  </AlertDialogPortal>
81
71
  ));
82
- AlertDialogContent.displayName = "AlertDialogContent";
72
+ AlertDialogContent.displayName = 'AlertDialogContent';
83
73
 
84
- function AlertDialogHeader({
85
- className,
86
- ...props
87
- }: React.ComponentProps<"div">) {
74
+ function AlertDialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
88
75
  return (
89
76
  <div
90
77
  data-slot="alert-dialog-header"
91
- className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
78
+ className={cn('flex flex-col gap-2 text-center sm:text-left', className)}
92
79
  {...props}
93
80
  />
94
81
  );
95
82
  }
96
83
 
97
- function AlertDialogFooter({
98
- className,
99
- ...props
100
- }: React.ComponentProps<"div">) {
84
+ function AlertDialogFooter({ className, ...props }: React.ComponentProps<'div'>) {
101
85
  return (
102
86
  <div
103
87
  data-slot="alert-dialog-footer"
104
- className={cn(
105
- "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
106
- className
107
- )}
88
+ className={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
108
89
  {...props}
109
90
  />
110
91
  );
@@ -117,11 +98,11 @@ const AlertDialogTitle = React.forwardRef<
117
98
  <AlertDialogPrimitive.Title
118
99
  ref={ref}
119
100
  data-slot="alert-dialog-title"
120
- className={cn("text-lg leading-none font-semibold", className)}
101
+ className={cn('text-lg leading-none font-semibold', className)}
121
102
  {...props}
122
103
  />
123
104
  ));
124
- AlertDialogTitle.displayName = "AlertDialogTitle";
105
+ AlertDialogTitle.displayName = 'AlertDialogTitle';
125
106
 
126
107
  const AlertDialogDescription = React.forwardRef<
127
108
  React.ElementRef<typeof AlertDialogPrimitive.Description>,
@@ -130,23 +111,19 @@ const AlertDialogDescription = React.forwardRef<
130
111
  <AlertDialogPrimitive.Description
131
112
  ref={ref}
132
113
  data-slot="alert-dialog-description"
133
- className={cn("text-muted-foreground text-sm", className)}
114
+ className={cn('text-muted-foreground text-sm', className)}
134
115
  {...props}
135
116
  />
136
117
  ));
137
- AlertDialogDescription.displayName = "AlertDialogDescription";
118
+ AlertDialogDescription.displayName = 'AlertDialogDescription';
138
119
 
139
120
  const AlertDialogAction = React.forwardRef<
140
121
  React.ElementRef<typeof AlertDialogPrimitive.Action>,
141
122
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
142
123
  >(({ className, ...props }, ref) => (
143
- <AlertDialogPrimitive.Action
144
- ref={ref}
145
- className={cn(buttonVariants(), className)}
146
- {...props}
147
- />
124
+ <AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
148
125
  ));
149
- AlertDialogAction.displayName = "AlertDialogAction";
126
+ AlertDialogAction.displayName = 'AlertDialogAction';
150
127
 
151
128
  const AlertDialogCancel = React.forwardRef<
152
129
  React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
@@ -154,11 +131,11 @@ const AlertDialogCancel = React.forwardRef<
154
131
  >(({ className, ...props }, ref) => (
155
132
  <AlertDialogPrimitive.Cancel
156
133
  ref={ref}
157
- className={cn(buttonVariants({ variant: "outline" }), className)}
134
+ className={cn(buttonVariants({ variant: 'outline' }), className)}
158
135
  {...props}
159
136
  />
160
137
  ));
161
- AlertDialogCancel.displayName = "AlertDialogCancel";
138
+ AlertDialogCancel.displayName = 'AlertDialogCancel';
162
139
 
163
140
  export {
164
141
  AlertDialog,
@@ -1,7 +1,7 @@
1
- import * as React from "react";
2
- import { cva, type VariantProps } from "class-variance-authority";
1
+ import * as React from 'react';
2
+ import { cva, type VariantProps } from 'class-variance-authority';
3
3
 
4
- import { cn } from "@/lib/utils";
4
+ import { cn } from '@/lib/utils';
5
5
 
6
6
  /**
7
7
  * Inline alert / callout — the shared replacement for the ~8 ad-hoc
@@ -16,31 +16,31 @@ import { cn } from "@/lib/utils";
16
16
  */
17
17
 
18
18
  const alertVariants = cva(
19
- "relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5",
19
+ 'relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5',
20
20
  {
21
21
  variants: {
22
22
  variant: {
23
- default: "bg-card text-card-foreground border-border [&>svg]:text-foreground",
23
+ default: 'bg-card text-card-foreground border-border [&>svg]:text-foreground',
24
24
  destructive:
25
- "border-destructive/30 bg-destructive/10 text-destructive [&>svg]:text-destructive [&_[data-slot=alert-description]]:text-destructive/90",
25
+ 'border-destructive/30 bg-destructive/10 text-destructive [&>svg]:text-destructive [&_[data-slot=alert-description]]:text-destructive/90',
26
26
  warning:
27
- "border-warn/30 bg-warn/10 text-warn [&>svg]:text-warn [&_[data-slot=alert-description]]:text-warn/90",
27
+ 'border-warn/30 bg-warn/10 text-warn [&>svg]:text-warn [&_[data-slot=alert-description]]:text-warn/90',
28
28
  success:
29
- "border-ok/30 bg-ok/10 text-ok [&>svg]:text-ok [&_[data-slot=alert-description]]:text-ok/90",
30
- info: "border-primary/30 bg-primary/10 text-primary [&>svg]:text-primary [&_[data-slot=alert-description]]:text-primary/90",
29
+ 'border-ok/30 bg-ok/10 text-ok [&>svg]:text-ok [&_[data-slot=alert-description]]:text-ok/90',
30
+ info: 'border-primary/30 bg-primary/10 text-primary [&>svg]:text-primary [&_[data-slot=alert-description]]:text-primary/90',
31
31
  },
32
32
  },
33
33
  defaultVariants: {
34
- variant: "default",
34
+ variant: 'default',
35
35
  },
36
- }
36
+ },
37
37
  );
38
38
 
39
39
  function Alert({
40
40
  className,
41
41
  variant,
42
42
  ...props
43
- }: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
43
+ }: React.ComponentProps<'div'> & VariantProps<typeof alertVariants>) {
44
44
  return (
45
45
  <div
46
46
  data-slot="alert"
@@ -51,29 +51,23 @@ function Alert({
51
51
  );
52
52
  }
53
53
 
54
- function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
54
+ function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) {
55
55
  return (
56
56
  <div
57
57
  data-slot="alert-title"
58
- className={cn(
59
- "col-start-2 line-clamp-1 min-h-4 font-semibold tracking-tight",
60
- className
61
- )}
58
+ className={cn('col-start-2 line-clamp-1 min-h-4 font-semibold tracking-tight', className)}
62
59
  {...props}
63
60
  />
64
61
  );
65
62
  }
66
63
 
67
- function AlertDescription({
68
- className,
69
- ...props
70
- }: React.ComponentProps<"div">) {
64
+ function AlertDescription({ className, ...props }: React.ComponentProps<'div'>) {
71
65
  return (
72
66
  <div
73
67
  data-slot="alert-description"
74
68
  className={cn(
75
- "text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
76
- className
69
+ 'text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed',
70
+ className,
77
71
  )}
78
72
  {...props}
79
73
  />
@@ -1,32 +1,23 @@
1
- import * as React from "react";
2
- import * as AvatarPrimitive from "@radix-ui/react-avatar";
1
+ import * as React from 'react';
2
+ import * as AvatarPrimitive from '@radix-ui/react-avatar';
3
3
 
4
- import { cn } from "@/lib/utils";
4
+ import { cn } from '@/lib/utils';
5
5
 
6
- function Avatar({
7
- className,
8
- ...props
9
- }: React.ComponentProps<typeof AvatarPrimitive.Root>) {
6
+ function Avatar({ className, ...props }: React.ComponentProps<typeof AvatarPrimitive.Root>) {
10
7
  return (
11
8
  <AvatarPrimitive.Root
12
9
  data-slot="avatar"
13
- className={cn(
14
- "relative flex size-8 shrink-0 overflow-hidden rounded-full",
15
- className
16
- )}
10
+ className={cn('relative flex size-8 shrink-0 overflow-hidden rounded-full', className)}
17
11
  {...props}
18
12
  />
19
13
  );
20
14
  }
21
15
 
22
- function AvatarImage({
23
- className,
24
- ...props
25
- }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
16
+ function AvatarImage({ className, ...props }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
26
17
  return (
27
18
  <AvatarPrimitive.Image
28
19
  data-slot="avatar-image"
29
- className={cn("aspect-square size-full", className)}
20
+ className={cn('aspect-square size-full', className)}
30
21
  {...props}
31
22
  />
32
23
  );
@@ -39,10 +30,7 @@ function AvatarFallback({
39
30
  return (
40
31
  <AvatarPrimitive.Fallback
41
32
  data-slot="avatar-fallback"
42
- className={cn(
43
- "bg-muted flex size-full items-center justify-center rounded-full",
44
- className
45
- )}
33
+ className={cn('bg-muted flex size-full items-center justify-center rounded-full', className)}
46
34
  {...props}
47
35
  />
48
36
  );
@@ -1,8 +1,8 @@
1
- import * as React from "react";
2
- import { Slot } from "@radix-ui/react-slot";
3
- import { cva, type VariantProps } from "class-variance-authority";
1
+ import * as React from 'react';
2
+ import { Slot } from '@radix-ui/react-slot';
3
+ import { cva, type VariantProps } from 'class-variance-authority';
4
4
 
5
- import { cn } from "@/lib/utils";
5
+ import { cn } from '@/lib/utils';
6
6
 
7
7
  // Pill shape (`rounded-full`) + OCA's `.dsa-badge` density (.72rem-ish text,
8
8
  // weight 600, 3px/10px padding). Existing solid variants kept intact; the new
@@ -10,29 +10,25 @@ import { cn } from "@/lib/utils";
10
10
  // same-family colored text (OCA's tint-bg + colored-label treatment), driven by
11
11
  // `--ok` / `--warn` / `--primary` so light + dark both read correctly.
12
12
  const badgeVariants = cva(
13
- "inline-flex items-center justify-center rounded-full border px-2.5 py-0.5 text-xs font-semibold w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
13
+ 'inline-flex items-center justify-center rounded-full border px-2.5 py-0.5 text-xs font-semibold w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
14
14
  {
15
15
  variants: {
16
16
  variant: {
17
- default:
18
- "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
17
+ default: 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
19
18
  secondary:
20
- "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
19
+ 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
21
20
  destructive:
22
- "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
23
- outline:
24
- "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
25
- success:
26
- "border-transparent bg-ok/15 text-ok [a&]:hover:bg-ok/20",
27
- warning:
28
- "border-transparent bg-warn/15 text-warn [a&]:hover:bg-warn/20",
29
- info: "border-transparent bg-primary/15 text-primary [a&]:hover:bg-primary/20",
21
+ 'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
22
+ outline: 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
23
+ success: 'border-transparent bg-ok/15 text-ok [a&]:hover:bg-ok/20',
24
+ warning: 'border-transparent bg-warn/15 text-warn [a&]:hover:bg-warn/20',
25
+ info: 'border-transparent bg-primary/15 text-primary [a&]:hover:bg-primary/20',
30
26
  },
31
27
  },
32
28
  defaultVariants: {
33
- variant: "default",
29
+ variant: 'default',
34
30
  },
35
- }
31
+ },
36
32
  );
37
33
 
38
34
  function Badge({
@@ -40,16 +36,11 @@ function Badge({
40
36
  variant,
41
37
  asChild = false,
42
38
  ...props
43
- }: React.ComponentProps<"span"> &
44
- VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
45
- const Comp = asChild ? Slot : "span";
39
+ }: React.ComponentProps<'span'> & VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
40
+ const Comp = asChild ? Slot : 'span';
46
41
 
47
42
  return (
48
- <Comp
49
- data-slot="badge"
50
- className={cn(badgeVariants({ variant }), className)}
51
- {...props}
52
- />
43
+ <Comp data-slot="badge" className={cn(badgeVariants({ variant }), className)} {...props} />
53
44
  );
54
45
  }
55
46