create-astram-app 0.1.2 → 0.2.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 (101) hide show
  1. package/dist/add-frontend.js +107 -0
  2. package/dist/add-frontend.js.map +1 -0
  3. package/dist/index.js +132 -33
  4. package/dist/index.js.map +1 -1
  5. package/package.json +3 -2
  6. package/template/CLAUDE.md +51 -0
  7. package/template/README.md +43 -44
  8. package/template/_gitignore +6 -0
  9. package/template/frontend/FRONTEND.md +92 -0
  10. package/template/frontend/README.md +16 -0
  11. package/template/frontend/_env.example +6 -0
  12. package/template/frontend/_gitignore +9 -0
  13. package/template/frontend/eslint.config.js +71 -0
  14. package/template/frontend/index.html +13 -0
  15. package/template/frontend/package.json +52 -0
  16. package/template/frontend/public/astram-logo-full.png +0 -0
  17. package/template/frontend/public/astram-logo-white.png +0 -0
  18. package/template/frontend/public/favicon.png +0 -0
  19. package/template/frontend/src/App.jsx +167 -0
  20. package/template/frontend/src/auth/AuthContext.jsx +338 -0
  21. package/template/frontend/src/components/ConfirmDialog.jsx +92 -0
  22. package/template/frontend/src/components/EntityFormModal.jsx +154 -0
  23. package/template/frontend/src/components/EntityList.jsx +480 -0
  24. package/template/frontend/src/components/PageHeader.jsx +54 -0
  25. package/template/frontend/src/components/access/CollapsibleGroup.jsx +34 -0
  26. package/template/frontend/src/components/ui/alert-dialog.tsx +175 -0
  27. package/template/frontend/src/components/ui/alert.tsx +83 -0
  28. package/template/frontend/src/components/ui/avatar.tsx +51 -0
  29. package/template/frontend/src/components/ui/badge.tsx +56 -0
  30. package/template/frontend/src/components/ui/button.tsx +67 -0
  31. package/template/frontend/src/components/ui/card.tsx +92 -0
  32. package/template/frontend/src/components/ui/checkbox.tsx +30 -0
  33. package/template/frontend/src/components/ui/date-field.tsx +47 -0
  34. package/template/frontend/src/components/ui/dialog.tsx +214 -0
  35. package/template/frontend/src/components/ui/dropdown-menu.tsx +255 -0
  36. package/template/frontend/src/components/ui/input-otp.tsx +81 -0
  37. package/template/frontend/src/components/ui/input.tsx +21 -0
  38. package/template/frontend/src/components/ui/label.tsx +46 -0
  39. package/template/frontend/src/components/ui/searchable-select.tsx +459 -0
  40. package/template/frontend/src/components/ui/select.tsx +199 -0
  41. package/template/frontend/src/components/ui/separator.tsx +26 -0
  42. package/template/frontend/src/components/ui/sheet.tsx +139 -0
  43. package/template/frontend/src/components/ui/skeleton.tsx +13 -0
  44. package/template/frontend/src/components/ui/sonner.tsx +23 -0
  45. package/template/frontend/src/components/ui/table.tsx +181 -0
  46. package/template/frontend/src/components/ui/tooltip.tsx +59 -0
  47. package/template/frontend/src/config/branding.js +80 -0
  48. package/template/frontend/src/hooks/useMobile.tsx +21 -0
  49. package/template/frontend/src/index.css +265 -0
  50. package/template/frontend/src/lib/utils.js +7 -0
  51. package/template/frontend/src/main.jsx +27 -0
  52. package/template/frontend/src/pages/Dashboard.jsx +127 -0
  53. package/template/frontend/src/pages/ForgotPassword.jsx +104 -0
  54. package/template/frontend/src/pages/Login.jsx +393 -0
  55. package/template/frontend/src/pages/ResetPassword.jsx +159 -0
  56. package/template/frontend/src/pages/access/AccessForm.jsx +416 -0
  57. package/template/frontend/src/pages/access/AccessPage.jsx +282 -0
  58. package/template/frontend/src/pages/access/RoleActionsMenu.jsx +42 -0
  59. package/template/frontend/src/pages/account/AccountLayout.jsx +229 -0
  60. package/template/frontend/src/pages/account/ChangePasswordPage.jsx +235 -0
  61. package/template/frontend/src/pages/account/ProfilePage.jsx +122 -0
  62. package/template/frontend/src/pages/account/ReenrollPage.jsx +98 -0
  63. package/template/frontend/src/pages/account/TwoFactorPage.jsx +833 -0
  64. package/template/frontend/src/pages/audit/AuditPage.jsx +268 -0
  65. package/template/frontend/src/pages/companies/CompaniesPage.jsx +232 -0
  66. package/template/frontend/src/pages/companies/CompanyForm.jsx +127 -0
  67. package/template/frontend/src/pages/roles/RoleForm.jsx +99 -0
  68. package/template/frontend/src/pages/roles/RolesPage.jsx +207 -0
  69. package/template/frontend/src/pages/users/AssignmentsEditor.jsx +193 -0
  70. package/template/frontend/src/pages/users/MembershipCard.jsx +303 -0
  71. package/template/frontend/src/pages/users/PostingRow.jsx +264 -0
  72. package/template/frontend/src/pages/users/UserForm.jsx +167 -0
  73. package/template/frontend/src/pages/users/UsersPage.jsx +232 -0
  74. package/template/frontend/src/platform/httpClient.js +125 -0
  75. package/template/frontend/src/platform/returnPathStore.js +43 -0
  76. package/template/frontend/src/platform/sessionStore.js +29 -0
  77. package/template/frontend/src/platform/shieldTransport.js +172 -0
  78. package/template/frontend/src/platform/shieldTransport.test.mjs +245 -0
  79. package/template/frontend/src/shell/AppShell.jsx +237 -0
  80. package/template/frontend/src/shell/Sidebar.jsx +257 -0
  81. package/template/frontend/src/shell/nav.js +90 -0
  82. package/template/frontend/src/theme/ThemeProvider.jsx +14 -0
  83. package/template/frontend/src/theme/ThemeToggle.jsx +44 -0
  84. package/template/frontend/tsconfig.json +22 -0
  85. package/template/frontend/vite.config.js +30 -0
  86. package/template/server/_env.example +45 -0
  87. package/template/server/eslint.config.js +95 -0
  88. package/template/server/package.json +49 -0
  89. package/template/server/scripts/dev.mjs +51 -0
  90. package/template/server/scripts/sync-astram-guide.mjs +63 -0
  91. package/template/_env.example +0 -33
  92. package/template/package.json +0 -37
  93. /package/template/{src → server/src}/app.module.ts +0 -0
  94. /package/template/{src → server/src}/features/_gitkeep +0 -0
  95. /package/template/{src → server/src}/health/demo.controller.ts +0 -0
  96. /package/template/{src → server/src}/health/health.controller.ts +0 -0
  97. /package/template/{src → server/src}/main.ts +0 -0
  98. /package/template/{test → server/test}/enforcement.e2e.spec.ts +0 -0
  99. /package/template/{tsconfig.build.json → server/tsconfig.build.json} +0 -0
  100. /package/template/{tsconfig.json → server/tsconfig.json} +0 -0
  101. /package/template/{vitest.config.ts → server/vitest.config.ts} +0 -0
@@ -0,0 +1,175 @@
1
+ import * as React from "react";
2
+ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
3
+
4
+ import { cn } from "@/lib/utils";
5
+ import { buttonVariants } from "@/components/ui/button";
6
+
7
+ /**
8
+ * Radix AlertDialog — the semantic confirm primitive (Phase B moves
9
+ * ConfirmDialog onto it). Styled to match this repo's `dialog.tsx`: the same
10
+ * `bg-black/50` overlay, the centered `bg-background` panel with `rounded-lg`
11
+ * border + `shadow-lg`, and the zoom/fade animations. The action/cancel buttons
12
+ * reuse `buttonVariants` so the confirm reads as a real (destructive-capable)
13
+ * button and cancel as an outline. Tokens only → light + dark both correct.
14
+ *
15
+ * React 18.3: every Radix-primitive-backed sub-component uses `React.forwardRef`
16
+ * (mirroring `dialog.tsx`) so Radix's ref-forwarding — e.g. into the animated
17
+ * Overlay/Content — lands on the underlying primitive and does NOT log
18
+ * "Function components cannot be given refs".
19
+ *
20
+ * NOTE: requires `@radix-ui/react-alert-dialog` to be installed.
21
+ */
22
+
23
+ function AlertDialog({
24
+ ...props
25
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
26
+ return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
27
+ }
28
+
29
+ const AlertDialogTrigger = React.forwardRef<
30
+ React.ElementRef<typeof AlertDialogPrimitive.Trigger>,
31
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Trigger>
32
+ >((props, ref) => (
33
+ <AlertDialogPrimitive.Trigger
34
+ ref={ref}
35
+ data-slot="alert-dialog-trigger"
36
+ {...props}
37
+ />
38
+ ));
39
+ AlertDialogTrigger.displayName = "AlertDialogTrigger";
40
+
41
+ function AlertDialogPortal({
42
+ ...props
43
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
44
+ return (
45
+ <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
46
+ );
47
+ }
48
+
49
+ const AlertDialogOverlay = React.forwardRef<
50
+ React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
51
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
52
+ >(({ className, ...props }, ref) => (
53
+ <AlertDialogPrimitive.Overlay
54
+ ref={ref}
55
+ data-slot="alert-dialog-overlay"
56
+ 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
59
+ )}
60
+ {...props}
61
+ />
62
+ ));
63
+ AlertDialogOverlay.displayName = "AlertDialogOverlay";
64
+
65
+ const AlertDialogContent = React.forwardRef<
66
+ React.ElementRef<typeof AlertDialogPrimitive.Content>,
67
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
68
+ >(({ className, ...props }, ref) => (
69
+ <AlertDialogPortal>
70
+ <AlertDialogOverlay />
71
+ <AlertDialogPrimitive.Content
72
+ ref={ref}
73
+ data-slot="alert-dialog-content"
74
+ 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
77
+ )}
78
+ {...props}
79
+ />
80
+ </AlertDialogPortal>
81
+ ));
82
+ AlertDialogContent.displayName = "AlertDialogContent";
83
+
84
+ function AlertDialogHeader({
85
+ className,
86
+ ...props
87
+ }: React.ComponentProps<"div">) {
88
+ return (
89
+ <div
90
+ data-slot="alert-dialog-header"
91
+ className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
92
+ {...props}
93
+ />
94
+ );
95
+ }
96
+
97
+ function AlertDialogFooter({
98
+ className,
99
+ ...props
100
+ }: React.ComponentProps<"div">) {
101
+ return (
102
+ <div
103
+ data-slot="alert-dialog-footer"
104
+ className={cn(
105
+ "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
106
+ className
107
+ )}
108
+ {...props}
109
+ />
110
+ );
111
+ }
112
+
113
+ const AlertDialogTitle = React.forwardRef<
114
+ React.ElementRef<typeof AlertDialogPrimitive.Title>,
115
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
116
+ >(({ className, ...props }, ref) => (
117
+ <AlertDialogPrimitive.Title
118
+ ref={ref}
119
+ data-slot="alert-dialog-title"
120
+ className={cn("text-lg leading-none font-semibold", className)}
121
+ {...props}
122
+ />
123
+ ));
124
+ AlertDialogTitle.displayName = "AlertDialogTitle";
125
+
126
+ const AlertDialogDescription = React.forwardRef<
127
+ React.ElementRef<typeof AlertDialogPrimitive.Description>,
128
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
129
+ >(({ className, ...props }, ref) => (
130
+ <AlertDialogPrimitive.Description
131
+ ref={ref}
132
+ data-slot="alert-dialog-description"
133
+ className={cn("text-muted-foreground text-sm", className)}
134
+ {...props}
135
+ />
136
+ ));
137
+ AlertDialogDescription.displayName = "AlertDialogDescription";
138
+
139
+ const AlertDialogAction = React.forwardRef<
140
+ React.ElementRef<typeof AlertDialogPrimitive.Action>,
141
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
142
+ >(({ className, ...props }, ref) => (
143
+ <AlertDialogPrimitive.Action
144
+ ref={ref}
145
+ className={cn(buttonVariants(), className)}
146
+ {...props}
147
+ />
148
+ ));
149
+ AlertDialogAction.displayName = "AlertDialogAction";
150
+
151
+ const AlertDialogCancel = React.forwardRef<
152
+ React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
153
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
154
+ >(({ className, ...props }, ref) => (
155
+ <AlertDialogPrimitive.Cancel
156
+ ref={ref}
157
+ className={cn(buttonVariants({ variant: "outline" }), className)}
158
+ {...props}
159
+ />
160
+ ));
161
+ AlertDialogCancel.displayName = "AlertDialogCancel";
162
+
163
+ export {
164
+ AlertDialog,
165
+ AlertDialogPortal,
166
+ AlertDialogOverlay,
167
+ AlertDialogTrigger,
168
+ AlertDialogContent,
169
+ AlertDialogHeader,
170
+ AlertDialogFooter,
171
+ AlertDialogTitle,
172
+ AlertDialogDescription,
173
+ AlertDialogAction,
174
+ AlertDialogCancel,
175
+ };
@@ -0,0 +1,83 @@
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ /**
7
+ * Inline alert / callout — the shared replacement for the ~8 ad-hoc
8
+ * `rounded-lg border border-destructive/30 bg-destructive/10` error boxes.
9
+ *
10
+ * Tonal variants map to the semantic tokens (`--destructive` / `--warn` / `--ok`
11
+ * / `--primary`) as a tinted surface + colored text + colored hairline border —
12
+ * the same "tint bg + colored label" treatment OCA uses for its status pills,
13
+ * scaled up to a block callout. An optional leading icon slot is supported via
14
+ * the grid layout (drop a lucide icon as the first child). Tokens only, so both
15
+ * light + dark (next-themes `.dark`) read correctly.
16
+ */
17
+
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",
20
+ {
21
+ variants: {
22
+ variant: {
23
+ default: "bg-card text-card-foreground border-border [&>svg]:text-foreground",
24
+ destructive:
25
+ "border-destructive/30 bg-destructive/10 text-destructive [&>svg]:text-destructive [&_[data-slot=alert-description]]:text-destructive/90",
26
+ warning:
27
+ "border-warn/30 bg-warn/10 text-warn [&>svg]:text-warn [&_[data-slot=alert-description]]:text-warn/90",
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",
31
+ },
32
+ },
33
+ defaultVariants: {
34
+ variant: "default",
35
+ },
36
+ }
37
+ );
38
+
39
+ function Alert({
40
+ className,
41
+ variant,
42
+ ...props
43
+ }: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
44
+ return (
45
+ <div
46
+ data-slot="alert"
47
+ role="alert"
48
+ className={cn(alertVariants({ variant }), className)}
49
+ {...props}
50
+ />
51
+ );
52
+ }
53
+
54
+ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
55
+ return (
56
+ <div
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
+ )}
62
+ {...props}
63
+ />
64
+ );
65
+ }
66
+
67
+ function AlertDescription({
68
+ className,
69
+ ...props
70
+ }: React.ComponentProps<"div">) {
71
+ return (
72
+ <div
73
+ data-slot="alert-description"
74
+ className={cn(
75
+ "text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
76
+ className
77
+ )}
78
+ {...props}
79
+ />
80
+ );
81
+ }
82
+
83
+ export { Alert, AlertTitle, AlertDescription, alertVariants };
@@ -0,0 +1,51 @@
1
+ import * as React from "react";
2
+ import * as AvatarPrimitive from "@radix-ui/react-avatar";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ function Avatar({
7
+ className,
8
+ ...props
9
+ }: React.ComponentProps<typeof AvatarPrimitive.Root>) {
10
+ return (
11
+ <AvatarPrimitive.Root
12
+ data-slot="avatar"
13
+ className={cn(
14
+ "relative flex size-8 shrink-0 overflow-hidden rounded-full",
15
+ className
16
+ )}
17
+ {...props}
18
+ />
19
+ );
20
+ }
21
+
22
+ function AvatarImage({
23
+ className,
24
+ ...props
25
+ }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
26
+ return (
27
+ <AvatarPrimitive.Image
28
+ data-slot="avatar-image"
29
+ className={cn("aspect-square size-full", className)}
30
+ {...props}
31
+ />
32
+ );
33
+ }
34
+
35
+ function AvatarFallback({
36
+ className,
37
+ ...props
38
+ }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
39
+ return (
40
+ <AvatarPrimitive.Fallback
41
+ data-slot="avatar-fallback"
42
+ className={cn(
43
+ "bg-muted flex size-full items-center justify-center rounded-full",
44
+ className
45
+ )}
46
+ {...props}
47
+ />
48
+ );
49
+ }
50
+
51
+ export { Avatar, AvatarImage, AvatarFallback };
@@ -0,0 +1,56 @@
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ // Pill shape (`rounded-full`) + OCA's `.dsa-badge` density (.72rem-ish text,
8
+ // weight 600, 3px/10px padding). Existing solid variants kept intact; the new
9
+ // `success` / `warning` / `info` are TONAL pills — a tinted token surface + the
10
+ // same-family colored text (OCA's tint-bg + colored-label treatment), driven by
11
+ // `--ok` / `--warn` / `--primary` so light + dark both read correctly.
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",
14
+ {
15
+ variants: {
16
+ variant: {
17
+ default:
18
+ "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
19
+ secondary:
20
+ "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
21
+ 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",
30
+ },
31
+ },
32
+ defaultVariants: {
33
+ variant: "default",
34
+ },
35
+ }
36
+ );
37
+
38
+ function Badge({
39
+ className,
40
+ variant,
41
+ asChild = false,
42
+ ...props
43
+ }: React.ComponentProps<"span"> &
44
+ VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
45
+ const Comp = asChild ? Slot : "span";
46
+
47
+ return (
48
+ <Comp
49
+ data-slot="badge"
50
+ className={cn(badgeVariants({ variant }), className)}
51
+ {...props}
52
+ />
53
+ );
54
+ }
55
+
56
+ export { Badge, badgeVariants };
@@ -0,0 +1,67 @@
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-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",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ // OCA `.oca-btn--primary` treatment: the blue fill gains a subtle
13
+ // depth (a crisp `shadow-sm` in light; the dark `.dark [data-slot=button].bg-primary`
14
+ // rule in index.css supplies the top-light inset + brand glow in dark),
15
+ // so it reads as a raised, saturated control rather than a flat swatch.
16
+ default: "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90",
17
+ destructive:
18
+ "bg-destructive text-white shadow-sm hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
19
+ outline:
20
+ "border bg-transparent shadow-xs hover:bg-accent dark:bg-transparent dark:border-input dark:hover:bg-input/50",
21
+ secondary:
22
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
23
+ ghost:
24
+ "hover:bg-accent dark:hover:bg-accent/50",
25
+ // Tonal button (OCA `.oca-btn--soft-primary`): a brand-tinted fill with
26
+ // brand-colored text — a lighter-weight primary for secondary actions.
27
+ soft: "bg-primary/10 text-primary hover:bg-primary/15",
28
+ link: "text-primary underline-offset-4 hover:underline",
29
+ },
30
+ size: {
31
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
32
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
33
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
34
+ icon: "size-9",
35
+ "icon-sm": "size-8",
36
+ "icon-lg": "size-10",
37
+ },
38
+ },
39
+ defaultVariants: {
40
+ variant: "default",
41
+ size: "default",
42
+ },
43
+ }
44
+ );
45
+
46
+ function Button({
47
+ className,
48
+ variant,
49
+ size,
50
+ asChild = false,
51
+ ...props
52
+ }: React.ComponentProps<"button"> &
53
+ VariantProps<typeof buttonVariants> & {
54
+ asChild?: boolean;
55
+ }) {
56
+ const Comp = asChild ? Slot : "button";
57
+
58
+ return (
59
+ <Comp
60
+ data-slot="button"
61
+ className={cn(buttonVariants({ variant, size, className }))}
62
+ {...props}
63
+ />
64
+ );
65
+ }
66
+
67
+ export { Button, buttonVariants };
@@ -0,0 +1,92 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ function Card({ className, ...props }: React.ComponentProps<"div">) {
6
+ return (
7
+ <div
8
+ data-slot="card"
9
+ className={cn(
10
+ "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ );
16
+ }
17
+
18
+ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
19
+ return (
20
+ <div
21
+ data-slot="card-header"
22
+ className={cn(
23
+ "@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",
24
+ className
25
+ )}
26
+ {...props}
27
+ />
28
+ );
29
+ }
30
+
31
+ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
32
+ return (
33
+ <div
34
+ data-slot="card-title"
35
+ className={cn("leading-none font-semibold", className)}
36
+ {...props}
37
+ />
38
+ );
39
+ }
40
+
41
+ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
42
+ return (
43
+ <div
44
+ data-slot="card-description"
45
+ className={cn("text-muted-foreground text-sm", className)}
46
+ {...props}
47
+ />
48
+ );
49
+ }
50
+
51
+ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
52
+ return (
53
+ <div
54
+ data-slot="card-action"
55
+ className={cn(
56
+ "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
57
+ className
58
+ )}
59
+ {...props}
60
+ />
61
+ );
62
+ }
63
+
64
+ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
65
+ return (
66
+ <div
67
+ data-slot="card-content"
68
+ className={cn("px-6", className)}
69
+ {...props}
70
+ />
71
+ );
72
+ }
73
+
74
+ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
75
+ return (
76
+ <div
77
+ data-slot="card-footer"
78
+ className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
79
+ {...props}
80
+ />
81
+ );
82
+ }
83
+
84
+ export {
85
+ Card,
86
+ CardHeader,
87
+ CardFooter,
88
+ CardTitle,
89
+ CardAction,
90
+ CardDescription,
91
+ CardContent,
92
+ };
@@ -0,0 +1,30 @@
1
+ import * as React from "react";
2
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
3
+ import { CheckIcon } from "lucide-react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ function Checkbox({
8
+ className,
9
+ ...props
10
+ }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
11
+ return (
12
+ <CheckboxPrimitive.Root
13
+ data-slot="checkbox"
14
+ className={cn(
15
+ "peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
16
+ className
17
+ )}
18
+ {...props}
19
+ >
20
+ <CheckboxPrimitive.Indicator
21
+ data-slot="checkbox-indicator"
22
+ className="flex items-center justify-center text-current transition-none"
23
+ >
24
+ <CheckIcon className="size-3.5" />
25
+ </CheckboxPrimitive.Indicator>
26
+ </CheckboxPrimitive.Root>
27
+ );
28
+ }
29
+
30
+ export { Checkbox };
@@ -0,0 +1,47 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ /**
6
+ * DateField — a styled `<input type="date">` wrapper matching the shadcn `Input`
7
+ * look, so native date inputs (e.g. PostingRow's) read consistently with the
8
+ * rest of the form controls.
9
+ *
10
+ * Why a styled native input and NOT a Radix Popover + calendar: this frontend
11
+ * has no calendar dependency and `@radix-ui/react-popover` is not installed, so
12
+ * a real date-picker would require adding deps (out of scope for Phase A / the
13
+ * no-install constraint). The native date input gives a consistent field
14
+ * surface today; a Popover-based picker can replace it later without changing
15
+ * the call sites (same props as `Input`). The mono `tabular-nums` + a nudged,
16
+ * token-tinted calendar-picker indicator keep it crisp in light + dark.
17
+ *
18
+ * React 18.3: `React.forwardRef` so a forwarded ref reaches the underlying
19
+ * `<input>` (form libraries / focus management) and never warns.
20
+ */
21
+ const DateField = React.forwardRef<
22
+ HTMLInputElement,
23
+ React.ComponentPropsWithoutRef<"input">
24
+ >(({ className, type = "date", ...props }, ref) => {
25
+ return (
26
+ <input
27
+ ref={ref}
28
+ type={type}
29
+ data-slot="date-field"
30
+ className={cn(
31
+ "border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none tabular-nums md:text-sm",
32
+ "placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground",
33
+ "disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
34
+ "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
35
+ "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
36
+ // The native calendar-picker glyph: tint to the muted ink, brighten on
37
+ // hover, and invert in dark mode so it stays visible on the dark field.
38
+ "[&::-webkit-calendar-picker-indicator]:cursor-pointer [&::-webkit-calendar-picker-indicator]:opacity-60 [&::-webkit-calendar-picker-indicator]:hover:opacity-100 dark:[&::-webkit-calendar-picker-indicator]:invert",
39
+ className
40
+ )}
41
+ {...props}
42
+ />
43
+ );
44
+ });
45
+ DateField.displayName = "DateField";
46
+
47
+ export { DateField };