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.
- package/dist/add-frontend.js +107 -0
- package/dist/add-frontend.js.map +1 -0
- package/dist/index.js +132 -33
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/template/CLAUDE.md +51 -0
- package/template/README.md +43 -44
- package/template/_gitignore +6 -0
- package/template/frontend/FRONTEND.md +92 -0
- package/template/frontend/README.md +16 -0
- package/template/frontend/_env.example +6 -0
- package/template/frontend/_gitignore +9 -0
- package/template/frontend/eslint.config.js +71 -0
- package/template/frontend/index.html +13 -0
- package/template/frontend/package.json +52 -0
- package/template/frontend/public/astram-logo-full.png +0 -0
- package/template/frontend/public/astram-logo-white.png +0 -0
- package/template/frontend/public/favicon.png +0 -0
- package/template/frontend/src/App.jsx +167 -0
- package/template/frontend/src/auth/AuthContext.jsx +338 -0
- package/template/frontend/src/components/ConfirmDialog.jsx +92 -0
- package/template/frontend/src/components/EntityFormModal.jsx +154 -0
- package/template/frontend/src/components/EntityList.jsx +480 -0
- package/template/frontend/src/components/PageHeader.jsx +54 -0
- package/template/frontend/src/components/access/CollapsibleGroup.jsx +34 -0
- package/template/frontend/src/components/ui/alert-dialog.tsx +175 -0
- package/template/frontend/src/components/ui/alert.tsx +83 -0
- package/template/frontend/src/components/ui/avatar.tsx +51 -0
- package/template/frontend/src/components/ui/badge.tsx +56 -0
- package/template/frontend/src/components/ui/button.tsx +67 -0
- package/template/frontend/src/components/ui/card.tsx +92 -0
- package/template/frontend/src/components/ui/checkbox.tsx +30 -0
- package/template/frontend/src/components/ui/date-field.tsx +47 -0
- package/template/frontend/src/components/ui/dialog.tsx +214 -0
- package/template/frontend/src/components/ui/dropdown-menu.tsx +255 -0
- package/template/frontend/src/components/ui/input-otp.tsx +81 -0
- package/template/frontend/src/components/ui/input.tsx +21 -0
- package/template/frontend/src/components/ui/label.tsx +46 -0
- package/template/frontend/src/components/ui/searchable-select.tsx +459 -0
- package/template/frontend/src/components/ui/select.tsx +199 -0
- package/template/frontend/src/components/ui/separator.tsx +26 -0
- package/template/frontend/src/components/ui/sheet.tsx +139 -0
- package/template/frontend/src/components/ui/skeleton.tsx +13 -0
- package/template/frontend/src/components/ui/sonner.tsx +23 -0
- package/template/frontend/src/components/ui/table.tsx +181 -0
- package/template/frontend/src/components/ui/tooltip.tsx +59 -0
- package/template/frontend/src/config/branding.js +80 -0
- package/template/frontend/src/hooks/useMobile.tsx +21 -0
- package/template/frontend/src/index.css +265 -0
- package/template/frontend/src/lib/utils.js +7 -0
- package/template/frontend/src/main.jsx +27 -0
- package/template/frontend/src/pages/Dashboard.jsx +127 -0
- package/template/frontend/src/pages/ForgotPassword.jsx +104 -0
- package/template/frontend/src/pages/Login.jsx +393 -0
- package/template/frontend/src/pages/ResetPassword.jsx +159 -0
- package/template/frontend/src/pages/access/AccessForm.jsx +416 -0
- package/template/frontend/src/pages/access/AccessPage.jsx +282 -0
- package/template/frontend/src/pages/access/RoleActionsMenu.jsx +42 -0
- package/template/frontend/src/pages/account/AccountLayout.jsx +229 -0
- package/template/frontend/src/pages/account/ChangePasswordPage.jsx +235 -0
- package/template/frontend/src/pages/account/ProfilePage.jsx +122 -0
- package/template/frontend/src/pages/account/ReenrollPage.jsx +98 -0
- package/template/frontend/src/pages/account/TwoFactorPage.jsx +833 -0
- package/template/frontend/src/pages/audit/AuditPage.jsx +268 -0
- package/template/frontend/src/pages/companies/CompaniesPage.jsx +232 -0
- package/template/frontend/src/pages/companies/CompanyForm.jsx +127 -0
- package/template/frontend/src/pages/roles/RoleForm.jsx +99 -0
- package/template/frontend/src/pages/roles/RolesPage.jsx +207 -0
- package/template/frontend/src/pages/users/AssignmentsEditor.jsx +193 -0
- package/template/frontend/src/pages/users/MembershipCard.jsx +303 -0
- package/template/frontend/src/pages/users/PostingRow.jsx +264 -0
- package/template/frontend/src/pages/users/UserForm.jsx +167 -0
- package/template/frontend/src/pages/users/UsersPage.jsx +232 -0
- package/template/frontend/src/platform/httpClient.js +125 -0
- package/template/frontend/src/platform/returnPathStore.js +43 -0
- package/template/frontend/src/platform/sessionStore.js +29 -0
- package/template/frontend/src/platform/shieldTransport.js +172 -0
- package/template/frontend/src/platform/shieldTransport.test.mjs +245 -0
- package/template/frontend/src/shell/AppShell.jsx +237 -0
- package/template/frontend/src/shell/Sidebar.jsx +257 -0
- package/template/frontend/src/shell/nav.js +90 -0
- package/template/frontend/src/theme/ThemeProvider.jsx +14 -0
- package/template/frontend/src/theme/ThemeToggle.jsx +44 -0
- package/template/frontend/tsconfig.json +22 -0
- package/template/frontend/vite.config.js +30 -0
- package/template/server/_env.example +45 -0
- package/template/server/eslint.config.js +95 -0
- package/template/server/package.json +49 -0
- package/template/server/scripts/dev.mjs +51 -0
- package/template/server/scripts/sync-astram-guide.mjs +63 -0
- package/template/_env.example +0 -33
- package/template/package.json +0 -37
- /package/template/{src → server/src}/app.module.ts +0 -0
- /package/template/{src → server/src}/features/_gitkeep +0 -0
- /package/template/{src → server/src}/health/demo.controller.ts +0 -0
- /package/template/{src → server/src}/health/health.controller.ts +0 -0
- /package/template/{src → server/src}/main.ts +0 -0
- /package/template/{test → server/test}/enforcement.e2e.spec.ts +0 -0
- /package/template/{tsconfig.build.json → server/tsconfig.build.json} +0 -0
- /package/template/{tsconfig.json → server/tsconfig.json} +0 -0
- /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 };
|