create-astram-app 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +211 -38
  2. package/dist/add-frontend.js.map +1 -1
  3. package/dist/index.js +14 -9
  4. package/dist/index.js.map +1 -1
  5. package/package.json +2 -2
  6. package/template/README.md +51 -20
  7. package/template/frontend/README.md +21 -11
  8. package/template/frontend/index.html +16 -0
  9. package/template/frontend/package.json +1 -1
  10. package/template/frontend/src/App.jsx +95 -20
  11. package/template/frontend/src/auth/AuthContext.jsx +145 -21
  12. package/template/frontend/src/auth/routeGate.js +31 -0
  13. package/template/frontend/src/auth/routeGate.test.mjs +59 -0
  14. package/template/frontend/src/auth/sessionFlow.js +27 -0
  15. package/template/frontend/src/auth/sessionFlow.test.mjs +63 -0
  16. package/template/frontend/src/components/EntityList.jsx +1 -7
  17. package/template/frontend/src/components/MultiSelectChecklist.jsx +166 -0
  18. package/template/frontend/src/components/PageHeader.jsx +5 -4
  19. package/template/frontend/src/components/ShareDialog.jsx +3 -5
  20. package/template/frontend/src/components/access/CollapsibleGroup.jsx +4 -1
  21. package/template/frontend/src/components/ui/alert-dialog.tsx +27 -50
  22. package/template/frontend/src/components/ui/alert.tsx +17 -23
  23. package/template/frontend/src/components/ui/avatar.tsx +8 -20
  24. package/template/frontend/src/components/ui/badge.tsx +17 -26
  25. package/template/frontend/src/components/ui/button.tsx +22 -24
  26. package/template/frontend/src/components/ui/card.tsx +19 -36
  27. package/template/frontend/src/components/ui/checkbox.tsx +7 -10
  28. package/template/frontend/src/components/ui/date-field.tsx +26 -27
  29. package/template/frontend/src/components/ui/dialog.tsx +24 -40
  30. package/template/frontend/src/components/ui/dropdown-menu.tsx +25 -54
  31. package/template/frontend/src/components/ui/input-otp.tsx +1 -4
  32. package/template/frontend/src/components/ui/label.tsx +10 -12
  33. package/template/frontend/src/components/ui/searchable-select.tsx +1 -3
  34. package/template/frontend/src/components/ui/select.tsx +31 -43
  35. package/template/frontend/src/components/ui/separator.tsx +6 -6
  36. package/template/frontend/src/components/ui/sheet.tsx +29 -38
  37. package/template/frontend/src/components/ui/skeleton.tsx +3 -3
  38. package/template/frontend/src/components/ui/sonner.tsx +7 -7
  39. package/template/frontend/src/components/ui/table.tsx +56 -70
  40. package/template/frontend/src/components/ui/tooltip.tsx +7 -11
  41. package/template/frontend/src/config/branding.js +1 -4
  42. package/template/frontend/src/hooks/useMobile.tsx +4 -6
  43. package/template/frontend/src/index.css +9 -4
  44. package/template/frontend/src/pages/Dashboard.jsx +13 -7
  45. package/template/frontend/src/pages/ForgotPassword.jsx +9 -2
  46. package/template/frontend/src/pages/Login.jsx +85 -70
  47. package/template/frontend/src/pages/ResetPassword.jsx +9 -2
  48. package/template/frontend/src/pages/access/AccessForm.jsx +49 -15
  49. package/template/frontend/src/pages/access/AccessPage.jsx +10 -6
  50. package/template/frontend/src/pages/access/RecordPoliciesPage.jsx +9 -2
  51. package/template/frontend/src/pages/account/AccountLayout.jsx +11 -6
  52. package/template/frontend/src/pages/account/ChangePasswordPage.jsx +2 -2
  53. package/template/frontend/src/pages/account/ReenrollPage.jsx +3 -5
  54. package/template/frontend/src/pages/account/TwoFactorPage.jsx +3 -8
  55. package/template/frontend/src/pages/companies/CompaniesPage.jsx +7 -9
  56. package/template/frontend/src/pages/cross-tenant/GrantForm.jsx +41 -8
  57. package/template/frontend/src/pages/cross-tenant/GrantsPage.jsx +30 -7
  58. package/template/frontend/src/pages/cross-tenant/InboxPage.jsx +12 -3
  59. package/template/frontend/src/pages/roles/RolesPage.jsx +5 -9
  60. package/template/frontend/src/pages/service-accounts/KeyRevealDialog.jsx +104 -0
  61. package/template/frontend/src/pages/service-accounts/ServiceAccountAccessDialog.jsx +231 -0
  62. package/template/frontend/src/pages/service-accounts/ServiceAccountActionsMenu.jsx +56 -0
  63. package/template/frontend/src/pages/service-accounts/ServiceAccountsPage.jsx +368 -0
  64. package/template/frontend/src/pages/service-accounts/accessModel.js +158 -0
  65. package/template/frontend/src/pages/service-accounts/accessModel.test.mjs +189 -0
  66. package/template/frontend/src/pages/users/MembershipCard.jsx +3 -1
  67. package/template/frontend/src/pages/users/PostingRow.jsx +8 -14
  68. package/template/frontend/src/pages/users/UsersPage.jsx +7 -9
  69. package/template/frontend/src/platform/httpClient.js +5 -0
  70. package/template/frontend/src/platform/sessionStore.js +55 -13
  71. package/template/frontend/src/platform/sessionStore.test.mjs +89 -0
  72. package/template/frontend/src/platform/shieldTransport.js +6 -0
  73. package/template/frontend/src/platform/shieldTransport.test.mjs +84 -5
  74. package/template/frontend/src/shell/AppShell.jsx +10 -12
  75. package/template/frontend/src/shell/Sidebar.jsx +15 -3
  76. package/template/frontend/src/shell/nav.js +13 -3
  77. package/template/frontend/vite.config.js +19 -1
  78. package/template/server/_env.example +22 -1
  79. package/template/server/eslint.config.mjs +194 -0
  80. package/template/server/package.json +11 -11
  81. package/template/server/scripts/dev.mjs +17 -6
  82. package/template/server/scripts/sync-astram-guide.mjs +45 -19
  83. package/template/server/src/app.module.ts +11 -5
  84. package/template/server/src/health/demo.controller.ts +16 -12
  85. package/template/server/src/main.ts +23 -1
  86. package/template/server/test/enforcement.e2e.spec.ts +73 -6
  87. package/template/server/eslint.config.js +0 -95
@@ -1,6 +1,6 @@
1
- import * as React from "react";
1
+ import * as React from 'react';
2
2
 
3
- import { cn } from "@/lib/utils";
3
+ import { cn } from '@/lib/utils';
4
4
 
5
5
  /**
6
6
  * shadcn Table primitive set, tuned to OCA's matured DataTable look
@@ -27,84 +27,79 @@ import { cn } from "@/lib/utils";
27
27
  * components cannot be given refs".
28
28
  */
29
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";
30
+ const Table = React.forwardRef<HTMLTableElement, React.ComponentPropsWithoutRef<'table'>>(
31
+ ({ className, ...props }, ref) => (
32
+ <div data-slot="table-container" className="relative w-full overflow-x-auto">
33
+ <table
34
+ ref={ref}
35
+ data-slot="table"
36
+ className={cn('w-full caption-bottom border-collapse text-sm', className)}
37
+ {...props}
38
+ />
39
+ </div>
40
+ ),
41
+ );
42
+ Table.displayName = 'Table';
44
43
 
45
44
  const TableHeader = React.forwardRef<
46
45
  HTMLTableSectionElement,
47
- React.ComponentPropsWithoutRef<"thead">
46
+ React.ComponentPropsWithoutRef<'thead'>
48
47
  >(({ className, ...props }, ref) => (
49
48
  <thead
50
49
  ref={ref}
51
50
  data-slot="table-header"
52
- className={cn("[&_tr]:border-b", className)}
51
+ className={cn('[&_tr]:border-b', className)}
53
52
  {...props}
54
53
  />
55
54
  ));
56
- TableHeader.displayName = "TableHeader";
55
+ TableHeader.displayName = 'TableHeader';
57
56
 
58
57
  const TableBody = React.forwardRef<
59
58
  HTMLTableSectionElement,
60
- React.ComponentPropsWithoutRef<"tbody">
59
+ React.ComponentPropsWithoutRef<'tbody'>
61
60
  >(({ className, ...props }, ref) => (
62
61
  <tbody
63
62
  ref={ref}
64
63
  data-slot="table-body"
65
- className={cn("[&_tr:last-child]:border-0", className)}
64
+ className={cn('[&_tr:last-child]:border-0', className)}
66
65
  {...props}
67
66
  />
68
67
  ));
69
- TableBody.displayName = "TableBody";
68
+ TableBody.displayName = 'TableBody';
70
69
 
71
70
  const TableFooter = React.forwardRef<
72
71
  HTMLTableSectionElement,
73
- React.ComponentPropsWithoutRef<"tfoot">
72
+ React.ComponentPropsWithoutRef<'tfoot'>
74
73
  >(({ className, ...props }, ref) => (
75
74
  <tfoot
76
75
  ref={ref}
77
76
  data-slot="table-footer"
78
- className={cn(
79
- "bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
80
- className
81
- )}
77
+ className={cn('bg-muted/50 border-t font-medium [&>tr]:last:border-b-0', className)}
82
78
  {...props}
83
79
  />
84
80
  ));
85
- TableFooter.displayName = "TableFooter";
81
+ TableFooter.displayName = 'TableFooter';
86
82
 
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";
83
+ const TableRow = React.forwardRef<HTMLTableRowElement, React.ComponentPropsWithoutRef<'tr'>>(
84
+ ({ className, ...props }, ref) => (
85
+ <tr
86
+ ref={ref}
87
+ data-slot="table-row"
88
+ // `group` so sticky cells can react to row hover (OCA keeps the pinned
89
+ // Actions cell in step with the row's hover surface).
90
+ className={cn(
91
+ 'group border-b border-border transition-colors hover:bg-muted/40 data-[state=selected]:bg-muted',
92
+ className,
93
+ )}
94
+ {...props}
95
+ />
96
+ ),
97
+ );
98
+ TableRow.displayName = 'TableRow';
104
99
 
105
100
  const TableHead = React.forwardRef<
106
101
  HTMLTableCellElement,
107
- React.ComponentPropsWithoutRef<"th"> & {
102
+ React.ComponentPropsWithoutRef<'th'> & {
108
103
  /** Freeze this header to the right edge (the Actions column). */
109
104
  sticky?: boolean;
110
105
  }
@@ -112,26 +107,26 @@ const TableHead = React.forwardRef<
112
107
  <th
113
108
  ref={ref}
114
109
  data-slot="table-head"
115
- data-sticky={sticky ? "" : undefined}
110
+ data-sticky={sticky ? '' : undefined}
116
111
  className={cn(
117
112
  // OCA thead th: mono-uppercase, tiny, wide-tracked, muted, on a subtle
118
113
  // 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]",
114
+ '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
115
  sticky &&
121
116
  // Pinned to the right edge: opaque header surface, sits above the
122
117
  // other sticky-top cells, centered label, token left-border (light) +
123
118
  // 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
119
+ '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)]',
120
+ className,
126
121
  )}
127
122
  {...props}
128
123
  />
129
124
  ));
130
- TableHead.displayName = "TableHead";
125
+ TableHead.displayName = 'TableHead';
131
126
 
132
127
  const TableCell = React.forwardRef<
133
128
  HTMLTableCellElement,
134
- React.ComponentPropsWithoutRef<"td"> & {
129
+ React.ComponentPropsWithoutRef<'td'> & {
135
130
  /** Freeze this cell to the right edge (the Actions column). */
136
131
  sticky?: boolean;
137
132
  }
@@ -139,43 +134,34 @@ const TableCell = React.forwardRef<
139
134
  <td
140
135
  ref={ref}
141
136
  data-slot="table-cell"
142
- data-sticky={sticky ? "" : undefined}
137
+ data-sticky={sticky ? '' : undefined}
143
138
  className={cn(
144
139
  // OCA tbody td: 11px/14px padding (px-3.5 py-2.5), hairline border,
145
140
  // 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]",
141
+ 'text-foreground/90 px-3.5 py-2.5 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
147
142
  sticky &&
148
143
  // Pinned cell: opaque background that tracks the row hover surface
149
144
  // (via the row's `group` + group-hover), centered actions, token
150
145
  // 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
146
+ '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)]',
147
+ className,
153
148
  )}
154
149
  {...props}
155
150
  />
156
151
  ));
157
- TableCell.displayName = "TableCell";
152
+ TableCell.displayName = 'TableCell';
158
153
 
159
154
  const TableCaption = React.forwardRef<
160
155
  HTMLTableCaptionElement,
161
- React.ComponentPropsWithoutRef<"caption">
156
+ React.ComponentPropsWithoutRef<'caption'>
162
157
  >(({ className, ...props }, ref) => (
163
158
  <caption
164
159
  ref={ref}
165
160
  data-slot="table-caption"
166
- className={cn("text-muted-foreground mt-4 text-sm", className)}
161
+ className={cn('text-muted-foreground mt-4 text-sm', className)}
167
162
  {...props}
168
163
  />
169
164
  ));
170
- TableCaption.displayName = "TableCaption";
165
+ TableCaption.displayName = 'TableCaption';
171
166
 
172
- export {
173
- Table,
174
- TableHeader,
175
- TableBody,
176
- TableFooter,
177
- TableHead,
178
- TableRow,
179
- TableCell,
180
- TableCaption,
181
- };
167
+ export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };
@@ -1,7 +1,7 @@
1
- import * as React from "react";
2
- import * as TooltipPrimitive from "@radix-ui/react-tooltip";
1
+ import * as React from 'react';
2
+ import * as TooltipPrimitive from '@radix-ui/react-tooltip';
3
3
 
4
- import { cn } from "@/lib/utils";
4
+ import { cn } from '@/lib/utils';
5
5
 
6
6
  function TooltipProvider({
7
7
  delayDuration = 0,
@@ -16,9 +16,7 @@ function TooltipProvider({
16
16
  );
17
17
  }
18
18
 
19
- function Tooltip({
20
- ...props
21
- }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
19
+ function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
22
20
  return (
23
21
  <TooltipProvider>
24
22
  <TooltipPrimitive.Root data-slot="tooltip" {...props} />
@@ -26,9 +24,7 @@ function Tooltip({
26
24
  );
27
25
  }
28
26
 
29
- function TooltipTrigger({
30
- ...props
31
- }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
27
+ function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
32
28
  return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
33
29
  }
34
30
 
@@ -44,8 +40,8 @@ function TooltipContent({
44
40
  data-slot="tooltip-content"
45
41
  sideOffset={sideOffset}
46
42
  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
43
+ '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',
44
+ className,
49
45
  )}
50
46
  {...props}
51
47
  >
@@ -43,10 +43,7 @@ export const branding = {
43
43
  * `VITE_APP_SUB_TAGLINE`); falls back to these static strings when the env is unset.
44
44
  */
45
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
- ),
46
+ subTagline: envString(import.meta.env.VITE_APP_SUB_TAGLINE, 'Sign in to your account'),
50
47
  poweredBy: 'Astram',
51
48
 
52
49
  /**
@@ -1,20 +1,18 @@
1
- import * as React from "react";
1
+ import * as React from 'react';
2
2
 
3
3
  const MOBILE_BREAKPOINT = 768;
4
4
 
5
5
  export function useIsMobile() {
6
- const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
7
- undefined
8
- );
6
+ const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined);
9
7
 
10
8
  React.useEffect(() => {
11
9
  const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
12
10
  const onChange = () => {
13
11
  setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
14
12
  };
15
- mql.addEventListener("change", onChange);
13
+ mql.addEventListener('change', onChange);
16
14
  setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
17
- return () => mql.removeEventListener("change", onChange);
15
+ return () => mql.removeEventListener('change', onChange);
18
16
  }, []);
19
17
 
20
18
  return !!isMobile;
@@ -1,7 +1,9 @@
1
+ /* Inter (UI) + JetBrains Mono (code/data) — the professional SaaS standard.
2
+ Must stay FIRST: `@import 'tailwindcss'` inlines rules, and a url @import after any
3
+ rule is invalid CSS (the build warns and browsers ignore it). */
4
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
1
5
  @import 'tailwindcss';
2
6
  @import 'tw-animate-css';
3
- /* Inter (UI) + JetBrains Mono (code/data) — the professional SaaS standard. */
4
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
5
7
 
6
8
  /*
7
9
  astram3 design tokens — shadcn/ui + Tailwind v4 (CSS-var driven).
@@ -180,8 +182,11 @@
180
182
  @layer components {
181
183
  /* Subtle dot-grid for auth/content backgrounds (astram-tinted). */
182
184
  .grid-bg {
183
- background-image:
184
- radial-gradient(circle at 1px 1px, color-mix(in oklch, var(--primary) 12%, transparent) 1px, transparent 0);
185
+ background-image: radial-gradient(
186
+ circle at 1px 1px,
187
+ color-mix(in oklch, var(--primary) 12%, transparent) 1px,
188
+ transparent 0
189
+ );
185
190
  background-size: 22px 22px;
186
191
  }
187
192
 
@@ -53,7 +53,8 @@ export default function Dashboard() {
53
53
  <div className="mb-4">
54
54
  <h1 className="text-xl font-bold tracking-tight">Dashboard</h1>
55
55
  <p className="mt-0.5 text-sm text-muted-foreground">
56
- You are signed in. Below, the app calls a protected API route as you — the framework enforces it.
56
+ You are signed in. Below, the app calls a protected API route as you — the framework
57
+ enforces it.
57
58
  </p>
58
59
  </div>
59
60
 
@@ -67,10 +68,11 @@ export default function Dashboard() {
67
68
  </CardHeader>
68
69
  <CardContent className="space-y-3">
69
70
  <p className="text-sm text-muted-foreground">
70
- <code className="rounded bg-muted px-1 py-0.5 text-xs">GET /identity/companies</code> is a{' '}
71
- <code className="rounded bg-muted px-1 py-0.5 text-xs">@Classified</code> route — the framework
72
- resolves your bearer token to a principal and runs <code className="rounded bg-muted px-1 py-0.5 text-xs">decide()</code>{' '}
73
- before the handler.
71
+ <code className="rounded bg-muted px-1 py-0.5 text-xs">GET /identity/companies</code>{' '}
72
+ is a <code className="rounded bg-muted px-1 py-0.5 text-xs">@Classified</code> route —
73
+ the framework resolves your bearer token to a principal and runs{' '}
74
+ <code className="rounded bg-muted px-1 py-0.5 text-xs">decide()</code> before the
75
+ handler.
74
76
  </p>
75
77
  <div className="flex flex-wrap gap-2">
76
78
  <Button variant="outline" size="sm" onClick={() => load(true)}>
@@ -104,7 +106,9 @@ export default function Dashboard() {
104
106
  </CardTitle>
105
107
  </CardHeader>
106
108
  <CardContent>
107
- {status === 'loading' && <p className="py-6 text-center text-sm text-muted-foreground">Loading…</p>}
109
+ {status === 'loading' && (
110
+ <p className="py-6 text-center text-sm text-muted-foreground">Loading…</p>
111
+ )}
108
112
  {status !== 'loading' && rows.length === 0 && (
109
113
  <p className="py-6 text-center text-sm text-muted-foreground">
110
114
  No companies visible (or the call was denied).
@@ -115,7 +119,9 @@ export default function Dashboard() {
115
119
  {i > 0 && <Separator />}
116
120
  <div className="flex items-center justify-between py-2.5 text-sm">
117
121
  <span className="font-medium">{r.fields?.name ?? '(unnamed)'}</span>
118
- <span className="font-mono text-xs text-muted-foreground">{r.fields?.status ?? ''}</span>
122
+ <span className="font-mono text-xs text-muted-foreground">
123
+ {r.fields?.status ?? ''}
124
+ </span>
119
125
  </div>
120
126
  </div>
121
127
  ))}
@@ -47,7 +47,11 @@ export default function ForgotPassword() {
47
47
  <CardContent className="p-8">
48
48
  <div className="mb-7 flex flex-col items-center gap-4">
49
49
  {/* Brand logo — color in light, white in dark (from branding config). */}
50
- <img src={branding.logo.light} alt={branding.appName} className="h-9 w-auto dark:hidden" />
50
+ <img
51
+ src={branding.logo.light}
52
+ alt={branding.appName}
53
+ className="h-9 w-auto dark:hidden"
54
+ />
51
55
  <img
52
56
  src={branding.logo.dark}
53
57
  alt={branding.appName}
@@ -92,7 +96,10 @@ export default function ForgotPassword() {
92
96
  )}
93
97
 
94
98
  <div className="mt-6 text-center">
95
- <Link to="/login" className="inline-flex items-center gap-1.5 text-sm font-medium text-primary hover:underline">
99
+ <Link
100
+ to="/login"
101
+ className="inline-flex items-center gap-1.5 text-sm font-medium text-primary hover:underline"
102
+ >
96
103
  <ArrowLeft className="size-4" />
97
104
  Back to sign in
98
105
  </Link>
@@ -171,7 +171,11 @@ export default function Login() {
171
171
  <CardContent className="p-8">
172
172
  {/* Brand logo — color in light, white in dark (from branding config). */}
173
173
  <div className="mb-6 flex justify-center">
174
- <img src={branding.logo.light} alt={branding.appName} className="h-9 w-auto dark:hidden" />
174
+ <img
175
+ src={branding.logo.light}
176
+ alt={branding.appName}
177
+ className="h-9 w-auto dark:hidden"
178
+ />
175
179
  <img
176
180
  src={branding.logo.dark}
177
181
  alt={branding.appName}
@@ -306,85 +310,96 @@ export default function Login() {
306
310
  </button>
307
311
  </form>
308
312
  ) : (
309
- <form onSubmit={onSubmit} className="space-y-4" noValidate>
310
- <div className="space-y-1.5">
311
- <label
312
- htmlFor="identityHint"
313
- className="block text-[11px] font-semibold uppercase tracking-wider text-muted-foreground"
314
- >
315
- Email or username
316
- </label>
317
- <Input
318
- id="identityHint"
319
- autoComplete="username"
320
- autoFocus
321
- value={identityHint}
322
- onChange={(e) => setIdentityHint(e.target.value)}
323
- onKeyDown={onFieldKeyDown}
324
- placeholder="you@example.com"
325
- className="h-11 bg-secondary/60"
326
- />
327
- </div>
328
-
329
- <div className="space-y-1.5">
330
- <label
331
- htmlFor="password"
332
- className="block text-[11px] font-semibold uppercase tracking-wider text-muted-foreground"
333
- >
334
- Password
335
- </label>
336
- <div className="relative">
313
+ <form onSubmit={onSubmit} className="space-y-4" noValidate>
314
+ <div className="space-y-1.5">
315
+ <label
316
+ htmlFor="identityHint"
317
+ className="block text-[11px] font-semibold uppercase tracking-wider text-muted-foreground"
318
+ >
319
+ Email or username
320
+ </label>
337
321
  <Input
338
- id="password"
339
- type={showPw ? 'text' : 'password'}
340
- autoComplete="current-password"
341
- value={password}
342
- onChange={(e) => setPassword(e.target.value)}
322
+ id="identityHint"
323
+ autoComplete="username"
324
+ autoFocus
325
+ value={identityHint}
326
+ onChange={(e) => setIdentityHint(e.target.value)}
343
327
  onKeyDown={onFieldKeyDown}
344
- placeholder="••••••••"
345
- className="h-11 bg-secondary/60 pr-10"
328
+ placeholder="you@example.com"
329
+ className="h-11 bg-secondary/60"
346
330
  />
347
- <button
348
- type="button"
349
- onClick={() => setShowPw((s) => !s)}
350
- className="absolute right-2 top-1/2 -translate-y-1/2 rounded p-1 text-muted-foreground hover:text-foreground"
351
- aria-label={showPw ? 'Hide password' : 'Show password'}
331
+ </div>
332
+
333
+ <div className="space-y-1.5">
334
+ <label
335
+ htmlFor="password"
336
+ className="block text-[11px] font-semibold uppercase tracking-wider text-muted-foreground"
352
337
  >
353
- {showPw ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
354
- </button>
338
+ Password
339
+ </label>
340
+ <div className="relative">
341
+ <Input
342
+ id="password"
343
+ type={showPw ? 'text' : 'password'}
344
+ autoComplete="current-password"
345
+ value={password}
346
+ onChange={(e) => setPassword(e.target.value)}
347
+ onKeyDown={onFieldKeyDown}
348
+ placeholder="••••••••"
349
+ className="h-11 bg-secondary/60 pr-10"
350
+ />
351
+ <button
352
+ type="button"
353
+ onClick={() => setShowPw((s) => !s)}
354
+ className="absolute right-2 top-1/2 -translate-y-1/2 rounded p-1 text-muted-foreground hover:text-foreground"
355
+ aria-label={showPw ? 'Hide password' : 'Show password'}
356
+ >
357
+ {showPw ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
358
+ </button>
359
+ </div>
355
360
  </div>
356
- </div>
357
361
 
358
- {/* Remember me + Forgot password, inline below the password. */}
359
- <div className="flex items-center justify-between pt-0.5">
360
- <label htmlFor="remember" className="flex items-center gap-2 text-sm text-muted-foreground">
361
- <Checkbox id="remember" checked={remember} onCheckedChange={(v) => setRemember(Boolean(v))} />
362
- Remember me
363
- </label>
364
- <Link to="/forgot-password" className="text-sm font-medium text-primary hover:underline">
365
- Forgot password?
366
- </Link>
367
- </div>
362
+ {/* Remember me + Forgot password, inline below the password. */}
363
+ <div className="flex items-center justify-between pt-0.5">
364
+ <label
365
+ htmlFor="remember"
366
+ className="flex items-center gap-2 text-sm text-muted-foreground"
367
+ >
368
+ <Checkbox
369
+ id="remember"
370
+ checked={remember}
371
+ onCheckedChange={(v) => setRemember(Boolean(v))}
372
+ />
373
+ Remember me
374
+ </label>
375
+ <Link
376
+ to="/forgot-password"
377
+ className="text-sm font-medium text-primary hover:underline"
378
+ >
379
+ Forgot password?
380
+ </Link>
381
+ </div>
368
382
 
369
- <Button
370
- type="submit"
371
- disabled={busy}
372
- className="h-11 w-full bg-primary font-mono text-xs font-semibold uppercase tracking-widest text-primary-foreground hover:bg-primary/90"
373
- >
374
- {busy ? (
375
- <Loader2 className="size-4 animate-spin" />
376
- ) : (
377
- <>
378
- <LogIn className="size-4" />
379
- Log in
380
- </>
381
- )}
382
- </Button>
383
- </form>
383
+ <Button
384
+ type="submit"
385
+ disabled={busy}
386
+ className="h-11 w-full bg-primary font-mono text-xs font-semibold uppercase tracking-widest text-primary-foreground hover:bg-primary/90"
387
+ >
388
+ {busy ? (
389
+ <Loader2 className="size-4 animate-spin" />
390
+ ) : (
391
+ <>
392
+ <LogIn className="size-4" />
393
+ Log in
394
+ </>
395
+ )}
396
+ </Button>
397
+ </form>
384
398
  )}
385
399
 
386
400
  <p className="mt-6 text-center text-[11px] tracking-wide text-muted-foreground">
387
- Powered by <span className="font-semibold text-foreground/70">{branding.poweredBy}</span>
401
+ Powered by{' '}
402
+ <span className="font-semibold text-foreground/70">{branding.poweredBy}</span>
388
403
  </p>
389
404
  </CardContent>
390
405
  </Card>
@@ -78,7 +78,11 @@ export default function ResetPassword() {
78
78
  <CardContent className="p-8">
79
79
  <div className="mb-7 flex flex-col items-center gap-4">
80
80
  {/* Brand logo — color in light, white in dark (from branding config). */}
81
- <img src={branding.logo.light} alt={branding.appName} className="h-9 w-auto dark:hidden" />
81
+ <img
82
+ src={branding.logo.light}
83
+ alt={branding.appName}
84
+ className="h-9 w-auto dark:hidden"
85
+ />
82
86
  <img
83
87
  src={branding.logo.dark}
84
88
  alt={branding.appName}
@@ -147,7 +151,10 @@ export default function ResetPassword() {
147
151
  )}
148
152
 
149
153
  <div className="mt-6 text-center">
150
- <Link to="/login" className="inline-flex items-center gap-1.5 text-sm font-medium text-primary hover:underline">
154
+ <Link
155
+ to="/login"
156
+ className="inline-flex items-center gap-1.5 text-sm font-medium text-primary hover:underline"
157
+ >
151
158
  <ArrowLeft className="size-4" />
152
159
  Back to sign in
153
160
  </Link>