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,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
+ }