astryx-dracula 0.2.0 → 0.3.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 (83) hide show
  1. package/BRAND.md +30 -18
  2. package/README.md +22 -11
  3. package/USAGE.md +29 -10
  4. package/astryx-dracula.js +19 -13
  5. package/astryx-dracula.variants.d.ts +15 -0
  6. package/package.json +59 -17
  7. package/shared/auth-chrome-config.ts +18 -0
  8. package/shared/auth-chrome.tsx +25 -0
  9. package/shared/auth-copy.ts +22 -0
  10. package/shared/chaptered-doc-config.ts +92 -0
  11. package/shared/chaptered-doc.tsx +332 -0
  12. package/shared/chart-hues.ts +27 -0
  13. package/shared/chart-labels.tsx +44 -0
  14. package/shared/chart-legend.tsx +100 -0
  15. package/shared/chart-panel-style.ts +13 -0
  16. package/shared/data-bar.tsx +160 -0
  17. package/shared/gallery-image.ts +20 -0
  18. package/shared/login-demo.ts +7 -0
  19. package/shared/metric-delta.tsx +36 -0
  20. package/shared/order-desk.tsx +273 -0
  21. package/shared/revenue-chart.tsx +174 -0
  22. package/shared/scene-castle.tsx +322 -0
  23. package/shared/scene-frame.tsx +88 -0
  24. package/shared/scene-hues.ts +49 -0
  25. package/shared/scene-tile.tsx +128 -0
  26. package/shared/settings-data.ts +125 -0
  27. package/shared/settings-rows.tsx +179 -0
  28. package/shared/sparkline.tsx +94 -0
  29. package/shared/sso-icons.tsx +30 -0
  30. package/templates/ai-chat-landing.tsx +67 -32
  31. package/templates/ai-chat.tsx +41 -20
  32. package/templates/blank.tsx +5 -2
  33. package/templates/centered-hero.tsx +15 -124
  34. package/templates/classic-gallery.tsx +21 -65
  35. package/templates/contact-form.tsx +62 -40
  36. package/templates/dashboard-portfolio.tsx +146 -227
  37. package/templates/dashboard.tsx +123 -121
  38. package/templates/detail-page.tsx +101 -95
  39. package/templates/documentation-design.template.mjs +1 -1
  40. package/templates/documentation-design.tsx +85 -96
  41. package/templates/documentation-technical.template.mjs +1 -1
  42. package/templates/documentation-technical.tsx +33 -21
  43. package/templates/documentation.template.mjs +1 -1
  44. package/templates/documentation.tsx +54 -74
  45. package/templates/editor.tsx +119 -89
  46. package/templates/file-explorer.tsx +133 -84
  47. package/templates/form-two-column.tsx +62 -124
  48. package/templates/gallery-hero.tsx +17 -171
  49. package/templates/ide.tsx +27 -21
  50. package/templates/incident-console.tsx +105 -69
  51. package/templates/kanban-board.tsx +100 -41
  52. package/templates/library.template.mjs +2 -2
  53. package/templates/library.tsx +38 -41
  54. package/templates/login-card.tsx +59 -86
  55. package/templates/login-split.tsx +88 -128
  56. package/templates/login-sso.tsx +107 -68
  57. package/templates/login.tsx +108 -52
  58. package/templates/messaging-shell.tsx +172 -143
  59. package/templates/mixed-gallery.tsx +31 -130
  60. package/templates/payment-form.tsx +369 -289
  61. package/templates/product-detail.tsx +40 -57
  62. package/templates/product-gallery.tsx +31 -40
  63. package/templates/product-tour.template.mjs +6 -0
  64. package/templates/product-tour.tsx +916 -0
  65. package/templates/settings-dialog.tsx +145 -286
  66. package/templates/settings-sidebar.tsx +179 -267
  67. package/templates/settings.tsx +42 -13
  68. package/templates/shell-nav.tsx +32 -23
  69. package/templates/shell-side-nav.tsx +30 -19
  70. package/templates/shell-top-nav.tsx +81 -43
  71. package/templates/side-gallery.tsx +39 -93
  72. package/templates/table-grouped.tsx +63 -33
  73. package/templates/table-page-chart.tsx +24 -297
  74. package/templates/table-page-heatmap-status.tsx +157 -91
  75. package/templates/table-page-shoe-store-heatmap.tsx +19 -288
  76. package/templates/table-page.tsx +24 -6
  77. package/templates/table.tsx +24 -7
  78. package/templates/tech-report.template.mjs +6 -0
  79. package/templates/tech-report.tsx +1486 -0
  80. package/templates/theme-showcase.template.mjs +1 -1
  81. package/templates/theme-showcase.tsx +313 -249
  82. package/theme.css +76 -14
  83. package/tokens.css +425 -30
@@ -1,7 +1,25 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ // XLE (canonical structure, validated with `bunx astryx layout check`):
3
+ // Ctr > V[g=4 a=center] > (V[g=2 a=center] > Ic + Tx"Castle Dracula"[t=body]) + (C[p=4] > V[g=4] > (V[g=1 a=center] > Hd"Welcome back to the night"[level=1] + Tx"Sign in to your crypt"[t=body]) + (V[g=2] > TI"Email"[t=email] + (V[g=1] > TI"Password"[t=password] + Lk"Forgot password?")) + B.primary"Enter the night" + D"Or continue with" + (V[g=3] > B.secondary"Login with Apple" + B.secondary"Login with Google") + (V[a=center] > Tx"New to the castle?"[t=supporting] + Lk"Sign up")) + (V[a=center] > Tx"By clicking continue, you agree to our Terms of service and Privacy policy"[t=supporting])
2
4
 
3
- import {useState, type CSSProperties} from 'react';
4
- import {Moon} from 'lucide-react';
5
+ import {useState, useTransition} from 'react';
6
+ import {demoLogin} from 'astryx-dracula/shared/login-demo';
7
+ import {AppleIcon, GoogleIcon} from 'astryx-dracula/shared/sso-icons';
8
+ import {
9
+ AUTH_HEADING,
10
+ AUTH_SUBTITLE,
11
+ AUTH_PRIMARY_CTA,
12
+ AUTH_SIGNUP_PROMPT,
13
+ AUTH_SIGNUP_LINK,
14
+ AUTH_SSO_DIVIDER,
15
+ AUTH_FORGOT_PASSWORD,
16
+ AUTH_ERROR_MESSAGE,
17
+ AUTH_EMAIL_PLACEHOLDER,
18
+ AUTH_PASSWORD_PLACEHOLDER,
19
+ AUTH_TERMS_PREFIX,
20
+ AUTH_TERMS_SERVICE,
21
+ AUTH_TERMS_PRIVACY,
22
+ } from 'astryx-dracula/shared/auth-copy';
5
23
  import {VStack} from '@astryxdesign/core/Layout';
6
24
  import {Center} from '@astryxdesign/core/Center';
7
25
  import {Text, Heading} from '@astryxdesign/core/Text';
@@ -10,87 +28,43 @@ import {Button} from '@astryxdesign/core/Button';
10
28
  import {Card} from '@astryxdesign/core/Card';
11
29
  import {Link} from '@astryxdesign/core/Link';
12
30
  import {Divider} from '@astryxdesign/core/Divider';
13
- import {Icon} from '@astryxdesign/core/Icon';
14
31
 
15
- // Brand sign-in marks — bespoke inline glyphs, no icon-library equivalent.
16
- const AppleIcon = (props: React.SVGProps<SVGSVGElement>) => (
17
- <svg
18
- viewBox="0 0 24 24"
19
- fill="currentColor"
20
- width={16}
21
- height={16}
22
- aria-hidden="true"
23
- {...props}>
24
- <path d="M17.05 20.28c-.98.95-2.05.88-3.08.4-1.09-.5-2.08-.48-3.24 0-1.44.62-2.2.44-3.06-.4C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z" />
25
- </svg>
26
- );
27
-
28
- const GoogleIcon = (props: React.SVGProps<SVGSVGElement>) => (
29
- <svg
30
- viewBox="0 0 24 24"
31
- fill="currentColor"
32
- width={16}
33
- height={16}
34
- aria-hidden="true"
35
- {...props}>
36
- <path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 01-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" />
37
- <path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
38
- <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" />
39
- <path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
40
- </svg>
41
- );
42
-
43
- // Standalone auth page paints its own body background (no host shell).
44
- const pageStyle: CSSProperties = {
45
- minHeight: '100%',
46
- backgroundColor: 'var(--color-background-body)',
47
- padding: 'var(--spacing-6)',
48
- };
49
- // Cap the column at 400px but let it shrink to fit narrow screens (Stack
50
- // has no maxWidth prop, so it's set here).
51
- const contentStyle: CSSProperties = {
52
- width: '100%',
53
- maxWidth: 400,
54
- };
32
+ import {authPageStyle as pageStyle, authContentStyle as contentStyle, inputAutoComplete} from 'astryx-dracula/shared/auth-chrome-config';
33
+ import {LoginBrand} from 'astryx-dracula/shared/auth-chrome';
55
34
 
56
35
  export default function LoginCard() {
57
36
  const [email, setEmail] = useState('');
58
37
  const [password, setPassword] = useState('');
59
- const [loginFailed, setLoginFailed] = useState(false);
60
- const [isLoading, setIsLoading] = useState(false);
38
+ const [error, setError] = useState<string | null>(null);
39
+ const [isLoading, startTransition] = useTransition();
61
40
 
62
41
  const handleLogin = () => {
63
42
  if (!email || !password) {
64
- setLoginFailed(true);
43
+ setError(AUTH_ERROR_MESSAGE);
65
44
  return;
66
45
  }
67
- setIsLoading(true);
68
- setLoginFailed(false);
69
- setTimeout(() => {
70
- setIsLoading(false);
71
- setLoginFailed(true);
72
- }, 2000);
46
+ setError(null);
47
+ startTransition(async () => {
48
+ await demoLogin();
49
+ setError(AUTH_ERROR_MESSAGE);
50
+ });
73
51
  };
74
52
 
75
53
  return (
76
54
  <Center axis="both" style={pageStyle}>
77
55
  <VStack gap={4} hAlign="center" style={contentStyle}>
78
- {/* Logo */}
79
- <VStack gap={2} hAlign="center">
80
- <Icon icon={Moon} size="lg" color="accent" />
81
- <Text type="body" weight="bold" size="lg">
82
- Castle Dracula
83
- </Text>
84
- </VStack>
56
+ <LoginBrand />
85
57
 
86
58
  {/* Card */}
87
- <Card padding={8} width="100%">
59
+ <Card padding={4} width="100%">
88
60
  <VStack gap={4} hAlign="stretch">
89
61
  {/* Header */}
90
62
  <VStack gap={1} hAlign="center">
91
- <Heading level={2}>Welcome back to the night</Heading>
63
+ <Heading level={1} type="display-2">
64
+ {AUTH_HEADING}
65
+ </Heading>
92
66
  <Text type="body" color="secondary">
93
- Sign in to your crypt
67
+ {AUTH_SUBTITLE}
94
68
  </Text>
95
69
  </VStack>
96
70
 
@@ -100,40 +74,39 @@ export default function LoginCard() {
100
74
  label="Email"
101
75
  isLabelHidden
102
76
  type="email"
103
- placeholder="you@castle.dracula"
77
+ {...inputAutoComplete('email')}
78
+ placeholder={AUTH_EMAIL_PLACEHOLDER}
104
79
  value={email}
105
- onChange={setEmail}
80
+ onChange={(v: string) => {
81
+ setEmail(v);
82
+ setError(null);
83
+ }}
106
84
  size="lg"
85
+ onEnter={handleLogin}
86
+ status={error ? {type: 'error', message: error} : undefined}
107
87
  />
108
88
  <VStack gap={1}>
109
89
  <TextInput
110
90
  label="Password"
111
91
  isLabelHidden
112
- placeholder="Whisper your password"
92
+ placeholder={AUTH_PASSWORD_PLACEHOLDER}
113
93
  type="password"
94
+ {...inputAutoComplete('current-password')}
114
95
  value={password}
115
96
  onChange={(v: string) => {
116
97
  setPassword(v);
117
- setLoginFailed(false);
98
+ setError(null);
118
99
  }}
119
100
  size="lg"
120
- status={
121
- loginFailed
122
- ? {
123
- type: 'error',
124
- message: 'Wrong incantation. Try again.',
125
- }
126
- : undefined
127
- }
101
+ onEnter={handleLogin}
102
+ status={error ? {type: 'error', message: error} : undefined}
128
103
  />
129
- {loginFailed && (
104
+ {error && (
130
105
  <VStack hAlign="end">
131
106
  <Link
132
107
  href="#/templates/login-card"
133
- size="sm"
134
- color="secondary"
135
- type="supporting">
136
- Forgot password?
108
+ color="secondary">
109
+ {AUTH_FORGOT_PASSWORD}
137
110
  </Link>
138
111
  </VStack>
139
112
  )}
@@ -142,7 +115,7 @@ export default function LoginCard() {
142
115
 
143
116
  {/* Login button */}
144
117
  <Button
145
- label="Enter the night"
118
+ label={AUTH_PRIMARY_CTA}
146
119
  variant="primary"
147
120
  size="lg"
148
121
  isLoading={isLoading}
@@ -150,7 +123,7 @@ export default function LoginCard() {
150
123
  />
151
124
 
152
125
  {/* Divider */}
153
- <Divider label="Or continue with" />
126
+ <Divider label={AUTH_SSO_DIVIDER} />
154
127
 
155
128
  {/* Social buttons */}
156
129
  <VStack gap={3} hAlign="stretch">
@@ -171,9 +144,9 @@ export default function LoginCard() {
171
144
  {/* Sign up link */}
172
145
  <VStack hAlign="center">
173
146
  <Text type="supporting" color="secondary">
174
- New to the castle?{' '}
147
+ {AUTH_SIGNUP_PROMPT}{' '}
175
148
  <Link href="#/templates/login-card" type="supporting">
176
- Sign up
149
+ {AUTH_SIGNUP_LINK}
177
150
  </Link>
178
151
  </Text>
179
152
  </VStack>
@@ -183,13 +156,13 @@ export default function LoginCard() {
183
156
  {/* Terms */}
184
157
  <VStack hAlign="center" width="100%">
185
158
  <Text type="supporting" color="secondary" justify="center">
186
- By clicking continue, you agree to our{' '}
159
+ {AUTH_TERMS_PREFIX}{' '}
187
160
  <Link href="#/templates/login-card" type="supporting">
188
- Terms of Service
161
+ {AUTH_TERMS_SERVICE}
189
162
  </Link>{' '}
190
163
  and{' '}
191
164
  <Link href="#/templates/login-card" type="supporting">
192
- Privacy Policy
165
+ {AUTH_TERMS_PRIVACY}
193
166
  </Link>
194
167
  .
195
168
  </Text>
@@ -1,6 +1,28 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ // XLE (canonical structure, validated with `bunx astryx layout check`):
3
+ // Ctr > V[g=4] > (Ctr.horizontal > C[p=0] > G[c={min:240} g=8 a=stretch] > (S[p=0] > V[g=4] > (H[g=2 a=center] > Ic + Tx"Castle Dracula"[t=body]) + (V[g=4] > (V[g=1] > Hd"Welcome back to the night"[level=1] + Tx"Sign in to your crypt"[t=body]) + (V[g=2] > TI"Email"[t=email] + (V[g=1] > TI"Password"[t=password] + Lk"Forgot password?")) + B.primary"Enter the night" + D"Or continue with" + (G[c={min:200} g=3] > B.secondary"Login with Apple" + B.secondary"Login with Google")) + Tx"New to the castle?"[t=supporting]) + (C[p=0] > AR)) + (V[a=center] > Tx"By clicking continue, you agree to our Terms of service and Privacy policy"[t=supporting])
2
4
 
3
- import {useState, type CSSProperties} from 'react';
5
+ import {useState, useTransition, type CSSProperties} from 'react';
6
+ import {inputAutoComplete} from 'astryx-dracula/shared/auth-chrome-config';
7
+ import {demoLogin} from 'astryx-dracula/shared/login-demo';
8
+ import {AppleIcon, GoogleIcon} from 'astryx-dracula/shared/sso-icons';
9
+ import {
10
+ AUTH_HEADING,
11
+ AUTH_SUBTITLE,
12
+ AUTH_PRIMARY_CTA,
13
+ AUTH_SIGNUP_PROMPT,
14
+ AUTH_SIGNUP_LINK,
15
+ AUTH_SSO_DIVIDER,
16
+ AUTH_FORGOT_PASSWORD,
17
+ AUTH_ERROR_MESSAGE,
18
+ AUTH_EMAIL_PLACEHOLDER,
19
+ AUTH_PASSWORD_PLACEHOLDER,
20
+ AUTH_BRAND_NAME,
21
+ AUTH_TERMS_PREFIX,
22
+ AUTH_TERMS_SERVICE,
23
+ AUTH_TERMS_PRIVACY,
24
+ } from 'astryx-dracula/shared/auth-copy';
25
+ import {SceneCastle} from 'astryx-dracula/shared/scene-castle';
4
26
  import {VStack, HStack, StackItem} from '@astryxdesign/core/Layout';
5
27
  import {Grid} from '@astryxdesign/core/Grid';
6
28
  import {Center} from '@astryxdesign/core/Center';
@@ -19,77 +41,9 @@ import {Divider} from '@astryxdesign/core/Divider';
19
41
  // CheckCircleIcon. The SquaresPlusIcon brand mark has no place here.
20
42
  import {Moon, CircleCheck} from 'lucide-react';
21
43
 
22
- // Brand sign-in marks — bespoke inline glyphs, no icon-library equivalent.
23
- const AppleIcon = (props: React.SVGProps<SVGSVGElement>) => (
24
- <svg
25
- viewBox="0 0 24 24"
26
- fill="currentColor"
27
- width={16}
28
- height={16}
29
- aria-hidden="true"
30
- {...props}>
31
- <path d="M17.05 20.28c-.98.95-2.05.88-3.08.4-1.09-.5-2.08-.48-3.24 0-1.44.62-2.2.44-3.06-.4C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z" />
32
- </svg>
33
- );
34
-
35
- const GoogleIcon = (props: React.SVGProps<SVGSVGElement>) => (
36
- <svg
37
- viewBox="0 0 24 24"
38
- fill="currentColor"
39
- width={16}
40
- height={16}
41
- aria-hidden="true"
42
- {...props}>
43
- <path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 01-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" />
44
- <path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
45
- <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" />
46
- <path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
47
- </svg>
48
- );
49
-
50
- // Cover art is inline Dracula SVG — moonlit rooftops in brand tokens, no
51
- // external image. Fills the panel via slice, like the placeholder before it.
52
- const coverArt: CSSProperties = {
53
- width: '100%',
54
- height: '100%',
55
- display: 'block',
56
- };
57
-
58
- const nightCoverArt = (
59
- <svg
60
- viewBox="0 0 400 300"
61
- preserveAspectRatio="xMidYMid slice"
62
- style={coverArt}
63
- role="img"
64
- aria-label="Harvest moon over the castle rooftops">
65
- <rect width="400" height="300" fill="var(--dracula-bg)" />
66
- <circle cx="68" cy="52" r="2" fill="var(--dracula-fg)" />
67
- <circle cx="140" cy="36" r="1.5" fill="var(--dracula-comment)" />
68
- <circle cx="330" cy="70" r="2" fill="var(--dracula-fg)" />
69
- <circle cx="292" cy="34" r="1.5" fill="var(--dracula-comment)" />
70
- <circle cx="360" cy="140" r="1.5" fill="var(--dracula-comment)" />
71
- <circle cx="200" cy="118" r="52" fill="var(--dracula-yellow)" />
72
- <circle cx="182" cy="104" r="44" fill="var(--dracula-bg)" />
73
- <g
74
- fill="var(--dracula-current-line)"
75
- stroke="var(--dracula-comment)"
76
- strokeWidth="3"
77
- strokeLinejoin="round">
78
- <rect x="20" y="220" width="110" height="80" />
79
- <path d="M20 220 L75 178 L130 220 Z" />
80
- <rect x="150" y="200" width="100" height="100" />
81
- <path d="M150 200 L200 162 L250 200 Z" />
82
- <rect x="270" y="228" width="110" height="72" />
83
- <path d="M270 228 L325 190 L380 228 Z" />
84
- </g>
85
- <g fill="var(--dracula-purple)">
86
- <rect x="58" y="244" width="14" height="18" />
87
- <rect x="188" y="224" width="14" height="18" />
88
- <rect x="312" y="250" width="14" height="18" />
89
- </g>
90
- </svg>
91
- );
92
-
44
+ // Cover art lives in shared/scene-castle (`rooftops` variant:
45
+ // crescent-overlay moon over a rooftop skyline). The transparent Card clips
46
+ // it to rounded corners (overflow:clip + radius), so the art needs no radius.
93
47
  // Grid emits minmax(MIN, 1fr) where MIN is a hard floor, so MIN plus the
94
48
  // grid inset and page padding must fit the narrowest phone or the column is
95
49
  // clipped. 320 − 2×24 (page) − 2×16 (stacked inset) = 240.
@@ -122,7 +76,7 @@ const LOGIN_SPLIT_CSS = `
122
76
  .login-split-grid {
123
77
  container-type: inline-size;
124
78
  container-name: login-split;
125
- padding: var(--spacing-8);
79
+ padding: var(--spacing-4);
126
80
  }
127
81
  .login-split-image {
128
82
  width: 100%;
@@ -130,7 +84,7 @@ const LOGIN_SPLIT_CSS = `
130
84
  }
131
85
  @container login-split (max-width: 511px) {
132
86
  .login-split-grid {
133
- padding: var(--spacing-4);
87
+ padding: var(--spacing-3);
134
88
  }
135
89
  .login-split-image {
136
90
  order: -1;
@@ -141,28 +95,27 @@ const LOGIN_SPLIT_CSS = `
141
95
  export default function LoginSplit() {
142
96
  const [email, setEmail] = useState('');
143
97
  const [password, setPassword] = useState('');
144
- const [loginFailed, setLoginFailed] = useState(false);
145
- const [isLoading, setIsLoading] = useState(false);
98
+ const [error, setError] = useState<string | null>(null);
99
+ const [isLoading, startTransition] = useTransition();
146
100
  const [isSuccess, setIsSuccess] = useState(false);
147
101
 
148
102
  const handleLogin = () => {
149
103
  if (!email || !password) {
150
- setLoginFailed(true);
104
+ setError(AUTH_ERROR_MESSAGE);
151
105
  return;
152
106
  }
153
- setIsLoading(true);
154
- setLoginFailed(false);
155
- setTimeout(() => {
156
- setIsLoading(false);
107
+ setError(null);
108
+ startTransition(async () => {
109
+ await demoLogin();
157
110
  setIsSuccess(true);
158
- }, 2000);
111
+ });
159
112
  };
160
113
 
161
114
  return (
162
115
  <Center axis="both" style={pageStyle}>
163
116
  <style>{LOGIN_SPLIT_CSS}</style>
164
117
  <VStack gap={4} width="100%">
165
- <div style={cardWrap}>
118
+ <Center axis="horizontal" style={cardWrap}>
166
119
  <Card padding={0} width="100%">
167
120
  <Grid
168
121
  columns={{minWidth: COLUMN_MIN_WIDTH, repeat: 'fit'}}
@@ -173,9 +126,9 @@ export default function LoginSplit() {
173
126
  <Section variant="transparent" padding={0} height="100%">
174
127
  <VStack gap={4} height="100%">
175
128
  <HStack gap={2} vAlign="center">
176
- <Icon icon={Moon} color="accent" />
177
- <Text type="body" weight="bold">
178
- Castle Dracula
129
+ <Icon icon={Moon} color="secondary" size="lg" />
130
+ <Text type="body" weight="semibold">
131
+ {AUTH_BRAND_NAME}
179
132
  </Text>
180
133
  </HStack>
181
134
 
@@ -185,16 +138,22 @@ export default function LoginSplit() {
185
138
  <EmptyState
186
139
  title="You're in for the night"
187
140
  description="Drifting to your crypt…"
188
- icon={<Icon icon={CircleCheck} size="lg" />}
141
+ icon={
142
+ <Icon
143
+ icon={CircleCheck}
144
+ size="lg"
145
+ color="secondary"
146
+ />
147
+ }
189
148
  />
190
149
  ) : (
191
150
  <VStack gap={4} hAlign="stretch" width="100%">
192
151
  <VStack gap={1}>
193
- <Heading level={2}>
194
- Welcome back to the night
152
+ <Heading level={1} type="display-2">
153
+ {AUTH_HEADING}
195
154
  </Heading>
196
155
  <Text type="body" color="secondary">
197
- Sign in to your crypt
156
+ {AUTH_SUBTITLE}
198
157
  </Text>
199
158
  </VStack>
200
159
 
@@ -203,41 +162,37 @@ export default function LoginSplit() {
203
162
  label="Email"
204
163
  isLabelHidden
205
164
  type="email"
206
- placeholder="you@castle.dracula"
165
+ {...inputAutoComplete('email')}
166
+ placeholder={AUTH_EMAIL_PLACEHOLDER}
207
167
  value={email}
208
- onChange={setEmail}
168
+ onChange={(v: string) => {
169
+ setEmail(v);
170
+ setError(null);
171
+ }}
209
172
  size="lg"
173
+ onEnter={handleLogin}
174
+ status={error ? {type: 'error', message: error} : undefined}
210
175
  />
211
176
  <VStack gap={1}>
212
177
  <TextInput
213
178
  label="Password"
214
179
  isLabelHidden
215
- placeholder="Whisper your password"
180
+ placeholder={AUTH_PASSWORD_PLACEHOLDER}
216
181
  type="password"
182
+ {...inputAutoComplete('current-password')}
217
183
  value={password}
218
184
  onChange={(v: string) => {
219
185
  setPassword(v);
220
- setLoginFailed(false);
186
+ setError(null);
221
187
  }}
222
188
  size="lg"
223
- status={
224
- loginFailed
225
- ? {
226
- type: 'error',
227
- message:
228
- 'Wrong incantation. Try again.',
229
- }
230
- : undefined
231
- }
189
+ onEnter={handleLogin}
190
+ status={error ? {type: 'error', message: error} : undefined}
232
191
  />
233
- {loginFailed && (
192
+ {error && (
234
193
  <VStack hAlign="end">
235
- <Link
236
- href="#/templates/login-split"
237
- size="sm"
238
- color="secondary"
239
- type="supporting">
240
- Forgot your password?
194
+ <Link href="#/templates/login-split">
195
+ {AUTH_FORGOT_PASSWORD}
241
196
  </Link>
242
197
  </VStack>
243
198
  )}
@@ -245,16 +200,22 @@ export default function LoginSplit() {
245
200
  </VStack>
246
201
 
247
202
  <Button
248
- label="Enter the night"
203
+ label={AUTH_PRIMARY_CTA}
249
204
  variant="primary"
250
205
  size="lg"
251
206
  isLoading={isLoading}
252
207
  onClick={handleLogin}
253
208
  />
254
209
 
255
- <Divider label="Or continue with" />
210
+ <Divider label={AUTH_SSO_DIVIDER} />
256
211
 
257
- <Grid columns={2} gap={3} justify="stretch">
212
+ {/* minWidth (not a fixed 2-up) so the pair stacks inside
213
+ the narrow single-column container instead of
214
+ giving each button ~114px for an icon + label. */}
215
+ <Grid
216
+ columns={{minWidth: 200, repeat: 'fit'}}
217
+ gap={3}
218
+ justify="stretch">
258
219
  <Button
259
220
  label="Login with Apple"
260
221
  variant="secondary"
@@ -275,9 +236,9 @@ export default function LoginSplit() {
275
236
 
276
237
  {!isSuccess && (
277
238
  <Text type="supporting" color="secondary">
278
- New to the castle?{' '}
239
+ {AUTH_SIGNUP_PROMPT}{' '}
279
240
  <Link href="#/templates/login-split" type="supporting">
280
- Sign up
241
+ {AUTH_SIGNUP_LINK}
281
242
  </Link>
282
243
  </Text>
283
244
  )}
@@ -286,28 +247,27 @@ export default function LoginSplit() {
286
247
 
287
248
  {/* Cover art — the transparent Card clips it to rounded
288
249
  corners (overflow:clip + radius), so the art needs no radius. */}
289
- <div className="login-split-image">
290
- <Card
291
- variant="transparent"
292
- padding={0}
293
- width="100%"
294
- height="100%">
295
- {nightCoverArt}
296
- </Card>
297
- </div>
250
+ <Card
251
+ variant="transparent"
252
+ padding={0}
253
+ width="100%"
254
+ height="100%"
255
+ className="login-split-image">
256
+ <SceneCastle variant="rooftops" />
257
+ </Card>
298
258
  </Grid>
299
259
  </Card>
300
- </div>
260
+ </Center>
301
261
 
302
262
  <VStack hAlign="center">
303
263
  <Text type="supporting" color="secondary">
304
- By clicking continue, you agree to our{' '}
264
+ {AUTH_TERMS_PREFIX}{' '}
305
265
  <Link href="#/templates/login-split" type="supporting">
306
- Terms of Service
266
+ {AUTH_TERMS_SERVICE}
307
267
  </Link>{' '}
308
268
  and{' '}
309
269
  <Link href="#/templates/login-split" type="supporting">
310
- Privacy Policy
270
+ {AUTH_TERMS_PRIVACY}
311
271
  </Link>
312
272
  .
313
273
  </Text>