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,139 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
|
5
|
+
import { XIcon } from "lucide-react";
|
|
6
|
+
|
|
7
|
+
import { cn } from "@/lib/utils";
|
|
8
|
+
|
|
9
|
+
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
|
10
|
+
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function SheetTrigger({
|
|
14
|
+
...props
|
|
15
|
+
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
|
16
|
+
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function SheetClose({
|
|
20
|
+
...props
|
|
21
|
+
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
|
22
|
+
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function SheetPortal({
|
|
26
|
+
...props
|
|
27
|
+
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
|
28
|
+
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function SheetOverlay({
|
|
32
|
+
className,
|
|
33
|
+
...props
|
|
34
|
+
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
|
35
|
+
return (
|
|
36
|
+
<SheetPrimitive.Overlay
|
|
37
|
+
data-slot="sheet-overlay"
|
|
38
|
+
className={cn(
|
|
39
|
+
"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",
|
|
40
|
+
className
|
|
41
|
+
)}
|
|
42
|
+
{...props}
|
|
43
|
+
/>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function SheetContent({
|
|
48
|
+
className,
|
|
49
|
+
children,
|
|
50
|
+
side = "right",
|
|
51
|
+
...props
|
|
52
|
+
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
|
53
|
+
side?: "top" | "right" | "bottom" | "left";
|
|
54
|
+
}) {
|
|
55
|
+
return (
|
|
56
|
+
<SheetPortal>
|
|
57
|
+
<SheetOverlay />
|
|
58
|
+
<SheetPrimitive.Content
|
|
59
|
+
data-slot="sheet-content"
|
|
60
|
+
className={cn(
|
|
61
|
+
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
|
62
|
+
side === "right" &&
|
|
63
|
+
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
|
|
64
|
+
side === "left" &&
|
|
65
|
+
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
|
|
66
|
+
side === "top" &&
|
|
67
|
+
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
|
|
68
|
+
side === "bottom" &&
|
|
69
|
+
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
|
|
70
|
+
className
|
|
71
|
+
)}
|
|
72
|
+
{...props}
|
|
73
|
+
>
|
|
74
|
+
{children}
|
|
75
|
+
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
|
76
|
+
<XIcon className="size-4" />
|
|
77
|
+
<span className="sr-only">Close</span>
|
|
78
|
+
</SheetPrimitive.Close>
|
|
79
|
+
</SheetPrimitive.Content>
|
|
80
|
+
</SheetPortal>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
85
|
+
return (
|
|
86
|
+
<div
|
|
87
|
+
data-slot="sheet-header"
|
|
88
|
+
className={cn("flex flex-col gap-1.5 p-4", className)}
|
|
89
|
+
{...props}
|
|
90
|
+
/>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
95
|
+
return (
|
|
96
|
+
<div
|
|
97
|
+
data-slot="sheet-footer"
|
|
98
|
+
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
|
99
|
+
{...props}
|
|
100
|
+
/>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function SheetTitle({
|
|
105
|
+
className,
|
|
106
|
+
...props
|
|
107
|
+
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
|
108
|
+
return (
|
|
109
|
+
<SheetPrimitive.Title
|
|
110
|
+
data-slot="sheet-title"
|
|
111
|
+
className={cn("text-foreground font-semibold", className)}
|
|
112
|
+
{...props}
|
|
113
|
+
/>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function SheetDescription({
|
|
118
|
+
className,
|
|
119
|
+
...props
|
|
120
|
+
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
|
121
|
+
return (
|
|
122
|
+
<SheetPrimitive.Description
|
|
123
|
+
data-slot="sheet-description"
|
|
124
|
+
className={cn("text-muted-foreground text-sm", className)}
|
|
125
|
+
{...props}
|
|
126
|
+
/>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export {
|
|
131
|
+
Sheet,
|
|
132
|
+
SheetTrigger,
|
|
133
|
+
SheetClose,
|
|
134
|
+
SheetContent,
|
|
135
|
+
SheetHeader,
|
|
136
|
+
SheetFooter,
|
|
137
|
+
SheetTitle,
|
|
138
|
+
SheetDescription,
|
|
139
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { cn } from "@/lib/utils";
|
|
2
|
+
|
|
3
|
+
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
|
4
|
+
return (
|
|
5
|
+
<div
|
|
6
|
+
data-slot="skeleton"
|
|
7
|
+
className={cn("bg-accent animate-pulse rounded-md", className)}
|
|
8
|
+
{...props}
|
|
9
|
+
/>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { Skeleton };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useTheme } from "next-themes";
|
|
2
|
+
import { Toaster as Sonner, type ToasterProps } from "sonner";
|
|
3
|
+
|
|
4
|
+
const Toaster = ({ ...props }: ToasterProps) => {
|
|
5
|
+
const { theme = "system" } = useTheme();
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<Sonner
|
|
9
|
+
theme={theme as ToasterProps["theme"]}
|
|
10
|
+
className="toaster group"
|
|
11
|
+
style={
|
|
12
|
+
{
|
|
13
|
+
"--normal-bg": "var(--popover)",
|
|
14
|
+
"--normal-text": "var(--popover-foreground)",
|
|
15
|
+
"--normal-border": "var(--border)",
|
|
16
|
+
} as React.CSSProperties
|
|
17
|
+
}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export { Toaster };
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* shadcn Table primitive set, tuned to OCA's matured DataTable look
|
|
7
|
+
* (translated from oca/src/index.css `.oca-dt*` — plain CSS → Tailwind tokens):
|
|
8
|
+
* - `TableHead` renders mono-uppercase, tiny, wide-tracked, muted labels on a
|
|
9
|
+
* subtle surface (OCA's `thead th`: IBM Plex Mono, .66rem, .08em tracking).
|
|
10
|
+
* - Hairline row borders + comfortable-but-dense cell padding.
|
|
11
|
+
* - Row hover lifts to `bg-muted/40`.
|
|
12
|
+
* - `<Table>` is wrapped in an `overflow-x-auto` scroll container so wide
|
|
13
|
+
* tables scroll horizontally instead of breaking the layout.
|
|
14
|
+
* - Sticky-right ACTIONS column: pass `sticky` to `TableHead` / `TableCell`
|
|
15
|
+
* (OCA's `.oca-dt-pinned` — `position:sticky; right:0`). The frozen edge is
|
|
16
|
+
* marked with BOTH a token `border-l` (reads in LIGHT mode, where a soft
|
|
17
|
+
* shadow all but vanishes) AND a left drop-shadow (adds depth in dark, where
|
|
18
|
+
* the border is faint) — so scrolling content reads as sliding UNDER the
|
|
19
|
+
* pinned column in both themes. Header/body backgrounds stay opaque and track
|
|
20
|
+
* the row (incl. hover) via the `group`/`group-hover` on `TableRow`.
|
|
21
|
+
*
|
|
22
|
+
* All colors are tokens (`--muted`, `--muted-foreground`, `--border`, `--card`,
|
|
23
|
+
* `--background`) so light + dark (next-themes `.dark`) are both correct.
|
|
24
|
+
*
|
|
25
|
+
* React 18.3: sub-components use `React.forwardRef` (mirroring `dialog.tsx`) so a
|
|
26
|
+
* forwarded ref lands on the underlying DOM element and never logs "Function
|
|
27
|
+
* components cannot be given refs".
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const Table = React.forwardRef<
|
|
31
|
+
HTMLTableElement,
|
|
32
|
+
React.ComponentPropsWithoutRef<"table">
|
|
33
|
+
>(({ className, ...props }, ref) => (
|
|
34
|
+
<div data-slot="table-container" className="relative w-full overflow-x-auto">
|
|
35
|
+
<table
|
|
36
|
+
ref={ref}
|
|
37
|
+
data-slot="table"
|
|
38
|
+
className={cn("w-full caption-bottom border-collapse text-sm", className)}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
</div>
|
|
42
|
+
));
|
|
43
|
+
Table.displayName = "Table";
|
|
44
|
+
|
|
45
|
+
const TableHeader = React.forwardRef<
|
|
46
|
+
HTMLTableSectionElement,
|
|
47
|
+
React.ComponentPropsWithoutRef<"thead">
|
|
48
|
+
>(({ className, ...props }, ref) => (
|
|
49
|
+
<thead
|
|
50
|
+
ref={ref}
|
|
51
|
+
data-slot="table-header"
|
|
52
|
+
className={cn("[&_tr]:border-b", className)}
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
));
|
|
56
|
+
TableHeader.displayName = "TableHeader";
|
|
57
|
+
|
|
58
|
+
const TableBody = React.forwardRef<
|
|
59
|
+
HTMLTableSectionElement,
|
|
60
|
+
React.ComponentPropsWithoutRef<"tbody">
|
|
61
|
+
>(({ className, ...props }, ref) => (
|
|
62
|
+
<tbody
|
|
63
|
+
ref={ref}
|
|
64
|
+
data-slot="table-body"
|
|
65
|
+
className={cn("[&_tr:last-child]:border-0", className)}
|
|
66
|
+
{...props}
|
|
67
|
+
/>
|
|
68
|
+
));
|
|
69
|
+
TableBody.displayName = "TableBody";
|
|
70
|
+
|
|
71
|
+
const TableFooter = React.forwardRef<
|
|
72
|
+
HTMLTableSectionElement,
|
|
73
|
+
React.ComponentPropsWithoutRef<"tfoot">
|
|
74
|
+
>(({ className, ...props }, ref) => (
|
|
75
|
+
<tfoot
|
|
76
|
+
ref={ref}
|
|
77
|
+
data-slot="table-footer"
|
|
78
|
+
className={cn(
|
|
79
|
+
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
|
80
|
+
className
|
|
81
|
+
)}
|
|
82
|
+
{...props}
|
|
83
|
+
/>
|
|
84
|
+
));
|
|
85
|
+
TableFooter.displayName = "TableFooter";
|
|
86
|
+
|
|
87
|
+
const TableRow = React.forwardRef<
|
|
88
|
+
HTMLTableRowElement,
|
|
89
|
+
React.ComponentPropsWithoutRef<"tr">
|
|
90
|
+
>(({ className, ...props }, ref) => (
|
|
91
|
+
<tr
|
|
92
|
+
ref={ref}
|
|
93
|
+
data-slot="table-row"
|
|
94
|
+
// `group` so sticky cells can react to row hover (OCA keeps the pinned
|
|
95
|
+
// Actions cell in step with the row's hover surface).
|
|
96
|
+
className={cn(
|
|
97
|
+
"group border-b border-border transition-colors hover:bg-muted/40 data-[state=selected]:bg-muted",
|
|
98
|
+
className
|
|
99
|
+
)}
|
|
100
|
+
{...props}
|
|
101
|
+
/>
|
|
102
|
+
));
|
|
103
|
+
TableRow.displayName = "TableRow";
|
|
104
|
+
|
|
105
|
+
const TableHead = React.forwardRef<
|
|
106
|
+
HTMLTableCellElement,
|
|
107
|
+
React.ComponentPropsWithoutRef<"th"> & {
|
|
108
|
+
/** Freeze this header to the right edge (the Actions column). */
|
|
109
|
+
sticky?: boolean;
|
|
110
|
+
}
|
|
111
|
+
>(({ className, sticky = false, ...props }, ref) => (
|
|
112
|
+
<th
|
|
113
|
+
ref={ref}
|
|
114
|
+
data-slot="table-head"
|
|
115
|
+
data-sticky={sticky ? "" : undefined}
|
|
116
|
+
className={cn(
|
|
117
|
+
// OCA thead th: mono-uppercase, tiny, wide-tracked, muted, on a subtle
|
|
118
|
+
// surface with a hairline bottom border.
|
|
119
|
+
"bg-muted/60 text-muted-foreground h-9 px-3.5 text-left align-middle font-mono text-[0.68rem] font-semibold tracking-[0.08em] whitespace-nowrap uppercase [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
|
120
|
+
sticky &&
|
|
121
|
+
// Pinned to the right edge: opaque header surface, sits above the
|
|
122
|
+
// other sticky-top cells, centered label, token left-border (light) +
|
|
123
|
+
// left shadow divider (dark).
|
|
124
|
+
"bg-muted/60 border-border sticky right-0 z-20 w-[1%] border-l text-center shadow-[-8px_0_10px_-8px_rgba(0,0,0,0.28)]",
|
|
125
|
+
className
|
|
126
|
+
)}
|
|
127
|
+
{...props}
|
|
128
|
+
/>
|
|
129
|
+
));
|
|
130
|
+
TableHead.displayName = "TableHead";
|
|
131
|
+
|
|
132
|
+
const TableCell = React.forwardRef<
|
|
133
|
+
HTMLTableCellElement,
|
|
134
|
+
React.ComponentPropsWithoutRef<"td"> & {
|
|
135
|
+
/** Freeze this cell to the right edge (the Actions column). */
|
|
136
|
+
sticky?: boolean;
|
|
137
|
+
}
|
|
138
|
+
>(({ className, sticky = false, ...props }, ref) => (
|
|
139
|
+
<td
|
|
140
|
+
ref={ref}
|
|
141
|
+
data-slot="table-cell"
|
|
142
|
+
data-sticky={sticky ? "" : undefined}
|
|
143
|
+
className={cn(
|
|
144
|
+
// OCA tbody td: 11px/14px padding (px-3.5 py-2.5), hairline border,
|
|
145
|
+
// secondary ink, middle-aligned.
|
|
146
|
+
"text-foreground/90 px-3.5 py-2.5 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
|
147
|
+
sticky &&
|
|
148
|
+
// Pinned cell: opaque background that tracks the row hover surface
|
|
149
|
+
// (via the row's `group` + group-hover), centered actions, token
|
|
150
|
+
// left-border (light) + left shadow (dark).
|
|
151
|
+
"bg-background group-hover:bg-muted/40 border-border sticky right-0 z-10 w-[1%] border-l text-center shadow-[-8px_0_10px_-8px_rgba(0,0,0,0.28)]",
|
|
152
|
+
className
|
|
153
|
+
)}
|
|
154
|
+
{...props}
|
|
155
|
+
/>
|
|
156
|
+
));
|
|
157
|
+
TableCell.displayName = "TableCell";
|
|
158
|
+
|
|
159
|
+
const TableCaption = React.forwardRef<
|
|
160
|
+
HTMLTableCaptionElement,
|
|
161
|
+
React.ComponentPropsWithoutRef<"caption">
|
|
162
|
+
>(({ className, ...props }, ref) => (
|
|
163
|
+
<caption
|
|
164
|
+
ref={ref}
|
|
165
|
+
data-slot="table-caption"
|
|
166
|
+
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
|
167
|
+
{...props}
|
|
168
|
+
/>
|
|
169
|
+
));
|
|
170
|
+
TableCaption.displayName = "TableCaption";
|
|
171
|
+
|
|
172
|
+
export {
|
|
173
|
+
Table,
|
|
174
|
+
TableHeader,
|
|
175
|
+
TableBody,
|
|
176
|
+
TableFooter,
|
|
177
|
+
TableHead,
|
|
178
|
+
TableRow,
|
|
179
|
+
TableCell,
|
|
180
|
+
TableCaption,
|
|
181
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
|
|
6
|
+
function TooltipProvider({
|
|
7
|
+
delayDuration = 0,
|
|
8
|
+
...props
|
|
9
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
|
10
|
+
return (
|
|
11
|
+
<TooltipPrimitive.Provider
|
|
12
|
+
data-slot="tooltip-provider"
|
|
13
|
+
delayDuration={delayDuration}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function Tooltip({
|
|
20
|
+
...props
|
|
21
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
|
22
|
+
return (
|
|
23
|
+
<TooltipProvider>
|
|
24
|
+
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
|
25
|
+
</TooltipProvider>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function TooltipTrigger({
|
|
30
|
+
...props
|
|
31
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
|
32
|
+
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function TooltipContent({
|
|
36
|
+
className,
|
|
37
|
+
sideOffset = 0,
|
|
38
|
+
children,
|
|
39
|
+
...props
|
|
40
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
|
41
|
+
return (
|
|
42
|
+
<TooltipPrimitive.Portal>
|
|
43
|
+
<TooltipPrimitive.Content
|
|
44
|
+
data-slot="tooltip-content"
|
|
45
|
+
sideOffset={sideOffset}
|
|
46
|
+
className={cn(
|
|
47
|
+
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
|
48
|
+
className
|
|
49
|
+
)}
|
|
50
|
+
{...props}
|
|
51
|
+
>
|
|
52
|
+
{children}
|
|
53
|
+
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
|
54
|
+
</TooltipPrimitive.Content>
|
|
55
|
+
</TooltipPrimitive.Portal>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Branding config — the ONE place the app's visual identity is defined, so a re-skin
|
|
3
|
+
* means editing this file (not hunting through components). Everything brandable reads
|
|
4
|
+
* from here: the app name, logo (light/dark), tagline, brand colors, and background.
|
|
5
|
+
*
|
|
6
|
+
* DYNAMIC-READY: today this is a static object. To make branding per-tenant later,
|
|
7
|
+
* fetch it from the server at load and merge over these defaults (a `GET /branding`
|
|
8
|
+
* endpoint), or swap the whole object — the consuming components don't change.
|
|
9
|
+
* `applyBrandColors()` writes the brand colors into CSS custom properties at runtime,
|
|
10
|
+
* so even colors can come from data.
|
|
11
|
+
*
|
|
12
|
+
* BUILD-TIME OVERRIDES: `appName`, the logo, and the login copy (tagline / sub-tagline)
|
|
13
|
+
* can all be set per-deploy via Vite env vars (`VITE_APP_NAME`, `VITE_APP_LOGO`,
|
|
14
|
+
* `VITE_APP_LOGO_DARK`, `VITE_APP_TAGLINE`, `VITE_APP_SUB_TAGLINE`) — see `.env.example`.
|
|
15
|
+
* `import.meta.env` is inlined at build time, so each deployment bakes in its own copy
|
|
16
|
+
* without a code change. When a var is unset (or empty), we fall back to the static
|
|
17
|
+
* astram default below, so the app always renders sensible copy out of the box.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** Read a Vite env string, falling back to `fallback` when unset or blank. */
|
|
21
|
+
function envString(value, fallback) {
|
|
22
|
+
const trimmed = typeof value === 'string' ? value.trim() : '';
|
|
23
|
+
return trimmed || fallback;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const branding = {
|
|
27
|
+
/** App / product name shown in the shell + titles. Overridable via `VITE_APP_NAME`. */
|
|
28
|
+
appName: envString(import.meta.env.VITE_APP_NAME, 'Astram'),
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Login/header logo. Served from /public. Light + dark variants, each overridable
|
|
32
|
+
* per-deploy via `VITE_APP_LOGO` / `VITE_APP_LOGO_DARK`.
|
|
33
|
+
*/
|
|
34
|
+
logo: {
|
|
35
|
+
light: envString(import.meta.env.VITE_APP_LOGO, '/astram-logo-full.png'),
|
|
36
|
+
dark: envString(import.meta.env.VITE_APP_LOGO_DARK, '/astram-logo-white.png'),
|
|
37
|
+
/** A compact mark for the collapsed sidebar rail (optional; falls back to the icon tile). */
|
|
38
|
+
icon: null,
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Login screen copy. Overridable per-deploy via Vite env (`VITE_APP_TAGLINE` /
|
|
43
|
+
* `VITE_APP_SUB_TAGLINE`); falls back to these static strings when the env is unset.
|
|
44
|
+
*/
|
|
45
|
+
tagline: envString(import.meta.env.VITE_APP_TAGLINE, 'Welcome back'),
|
|
46
|
+
subTagline: envString(
|
|
47
|
+
import.meta.env.VITE_APP_SUB_TAGLINE,
|
|
48
|
+
'Sign in to your account',
|
|
49
|
+
),
|
|
50
|
+
poweredBy: 'Astram',
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Brand accent (reference value). The accent + sidebar are BOTH theme-driven by the
|
|
54
|
+
* CSS tokens in index.css (`--primary`, `--sidebar*`), which define DISTINCT light and
|
|
55
|
+
* dark values — e.g. a brighter primary in dark mode so text/fills stay legible.
|
|
56
|
+
*
|
|
57
|
+
* These are intentionally NOT pushed to inline CSS variables at runtime: an inline
|
|
58
|
+
* `root.style` override beats the theme tokens' cascade, pinning one value across BOTH
|
|
59
|
+
* themes. That is exactly what made dark-mode primary text ("200 OK …", active states)
|
|
60
|
+
* too dim — the light primary was clobbering the intended brighter dark primary. To
|
|
61
|
+
* re-skin per tenant later, override the tokens PER THEME (`:root` + `.dark`), not via a
|
|
62
|
+
* single inline value.
|
|
63
|
+
*/
|
|
64
|
+
colors: {
|
|
65
|
+
primary: 'oklch(0.55 0.216 265)', // astram blue (#3366ff family) — light-mode value
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
/** Auth/content background style: 'grid' (dot grid) | 'plain'. */
|
|
69
|
+
background: 'grid',
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Runtime brand-color hook. Currently a NO-OP: the accent + sidebar are theme-driven by
|
|
74
|
+
* the CSS tokens (distinct light/dark values), and a blanket inline override would break
|
|
75
|
+
* the dark variants (see the `colors` note above). Kept as the seam so per-tenant theming
|
|
76
|
+
* can be wired later — the right way is to set the tokens PER THEME, not one inline value.
|
|
77
|
+
*/
|
|
78
|
+
export function applyBrandColors() {
|
|
79
|
+
/* no-op — theme tokens own the accent + sidebar colors (see branding.colors note). */
|
|
80
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
const MOBILE_BREAKPOINT = 768;
|
|
4
|
+
|
|
5
|
+
export function useIsMobile() {
|
|
6
|
+
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
|
7
|
+
undefined
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
React.useEffect(() => {
|
|
11
|
+
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
|
12
|
+
const onChange = () => {
|
|
13
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
|
14
|
+
};
|
|
15
|
+
mql.addEventListener("change", onChange);
|
|
16
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
|
17
|
+
return () => mql.removeEventListener("change", onChange);
|
|
18
|
+
}, []);
|
|
19
|
+
|
|
20
|
+
return !!isMobile;
|
|
21
|
+
}
|