astryx-dracula 0.2.1 → 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 +17 -12
  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 +66 -33
  31. package/templates/ai-chat.tsx +36 -19
  32. package/templates/blank.tsx +4 -3
  33. package/templates/centered-hero.tsx +14 -125
  34. package/templates/classic-gallery.tsx +20 -66
  35. package/templates/contact-form.tsx +59 -39
  36. package/templates/dashboard-portfolio.tsx +143 -226
  37. package/templates/dashboard.tsx +122 -122
  38. package/templates/detail-page.tsx +95 -91
  39. package/templates/documentation-design.template.mjs +1 -1
  40. package/templates/documentation-design.tsx +83 -96
  41. package/templates/documentation-technical.template.mjs +1 -1
  42. package/templates/documentation-technical.tsx +29 -19
  43. package/templates/documentation.template.mjs +1 -1
  44. package/templates/documentation.tsx +52 -74
  45. package/templates/editor.tsx +112 -84
  46. package/templates/file-explorer.tsx +131 -84
  47. package/templates/form-two-column.tsx +61 -125
  48. package/templates/gallery-hero.tsx +15 -171
  49. package/templates/ide.tsx +22 -20
  50. package/templates/incident-console.tsx +104 -70
  51. package/templates/kanban-board.tsx +99 -42
  52. package/templates/library.template.mjs +2 -2
  53. package/templates/library.tsx +37 -42
  54. package/templates/login-card.tsx +58 -87
  55. package/templates/login-split.tsx +88 -126
  56. package/templates/login-sso.tsx +105 -64
  57. package/templates/login.tsx +107 -53
  58. package/templates/messaging-shell.tsx +171 -144
  59. package/templates/mixed-gallery.tsx +29 -130
  60. package/templates/payment-form.tsx +368 -290
  61. package/templates/product-detail.tsx +39 -58
  62. package/templates/product-gallery.tsx +30 -41
  63. package/templates/product-tour.template.mjs +6 -0
  64. package/templates/product-tour.tsx +916 -0
  65. package/templates/settings-dialog.tsx +135 -282
  66. package/templates/settings-sidebar.tsx +178 -268
  67. package/templates/settings.tsx +41 -14
  68. package/templates/shell-nav.tsx +31 -24
  69. package/templates/shell-side-nav.tsx +29 -20
  70. package/templates/shell-top-nav.tsx +80 -44
  71. package/templates/side-gallery.tsx +38 -94
  72. package/templates/table-grouped.tsx +62 -34
  73. package/templates/table-page-chart.tsx +23 -302
  74. package/templates/table-page-heatmap-status.tsx +156 -92
  75. package/templates/table-page-shoe-store-heatmap.tsx +18 -293
  76. package/templates/table-page.tsx +23 -7
  77. package/templates/table.tsx +23 -8
  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 +299 -237
  82. package/theme.css +76 -14
  83. package/tokens.css +425 -30
@@ -1,9 +1,25 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
  // XLE (canonical structure, validated with `bunx astryx layout check`):
3
- // C[p4] > V[g3] > (Hd"Sign in"[level=3] + F > (TI"Email"[t=email req] + TI"Password"[t=password req]) + B.primary"Enter the night")
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])
4
4
 
5
- import {useState, type CSSProperties} from 'react';
6
- 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';
7
23
  import {VStack} from '@astryxdesign/core/Layout';
8
24
  import {Center} from '@astryxdesign/core/Center';
9
25
  import {Text, Heading} from '@astryxdesign/core/Text';
@@ -12,87 +28,43 @@ import {Button} from '@astryxdesign/core/Button';
12
28
  import {Card} from '@astryxdesign/core/Card';
13
29
  import {Link} from '@astryxdesign/core/Link';
14
30
  import {Divider} from '@astryxdesign/core/Divider';
15
- import {Icon} from '@astryxdesign/core/Icon';
16
31
 
17
- // Brand sign-in marks — bespoke inline glyphs, no icon-library equivalent.
18
- const AppleIcon = (props: React.SVGProps<SVGSVGElement>) => (
19
- <svg
20
- viewBox="0 0 24 24"
21
- fill="currentColor"
22
- width={16}
23
- height={16}
24
- aria-hidden="true"
25
- {...props}>
26
- <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" />
27
- </svg>
28
- );
29
-
30
- const GoogleIcon = (props: React.SVGProps<SVGSVGElement>) => (
31
- <svg
32
- viewBox="0 0 24 24"
33
- fill="currentColor"
34
- width={16}
35
- height={16}
36
- aria-hidden="true"
37
- {...props}>
38
- <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" />
39
- <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" />
40
- <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" />
41
- <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" />
42
- </svg>
43
- );
44
-
45
- // Standalone auth page paints its own body background (no host shell).
46
- const pageStyle: CSSProperties = {
47
- minHeight: '100%',
48
- backgroundColor: 'var(--color-background-body)',
49
- padding: 'var(--spacing-6)',
50
- };
51
- // Cap the column at 400px but let it shrink to fit narrow screens (Stack
52
- // has no maxWidth prop, so it's set here).
53
- const contentStyle: CSSProperties = {
54
- width: '100%',
55
- maxWidth: 400,
56
- };
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';
57
34
 
58
35
  export default function LoginCard() {
59
36
  const [email, setEmail] = useState('');
60
37
  const [password, setPassword] = useState('');
61
- const [loginFailed, setLoginFailed] = useState(false);
62
- const [isLoading, setIsLoading] = useState(false);
38
+ const [error, setError] = useState<string | null>(null);
39
+ const [isLoading, startTransition] = useTransition();
63
40
 
64
41
  const handleLogin = () => {
65
42
  if (!email || !password) {
66
- setLoginFailed(true);
43
+ setError(AUTH_ERROR_MESSAGE);
67
44
  return;
68
45
  }
69
- setIsLoading(true);
70
- setLoginFailed(false);
71
- setTimeout(() => {
72
- setIsLoading(false);
73
- setLoginFailed(true);
74
- }, 2000);
46
+ setError(null);
47
+ startTransition(async () => {
48
+ await demoLogin();
49
+ setError(AUTH_ERROR_MESSAGE);
50
+ });
75
51
  };
76
52
 
77
53
  return (
78
54
  <Center axis="both" style={pageStyle}>
79
55
  <VStack gap={4} hAlign="center" style={contentStyle}>
80
- {/* Logo */}
81
- <VStack gap={2} hAlign="center">
82
- <Icon icon={Moon} size="lg" color="accent" />
83
- <Text type="body" weight="bold" size="lg">
84
- Castle Dracula
85
- </Text>
86
- </VStack>
56
+ <LoginBrand />
87
57
 
88
58
  {/* Card */}
89
- <Card padding={8} width="100%">
59
+ <Card padding={4} width="100%">
90
60
  <VStack gap={4} hAlign="stretch">
91
61
  {/* Header */}
92
62
  <VStack gap={1} hAlign="center">
93
- <Heading level={2}>Welcome back to the night</Heading>
63
+ <Heading level={1} type="display-2">
64
+ {AUTH_HEADING}
65
+ </Heading>
94
66
  <Text type="body" color="secondary">
95
- Sign in to your crypt
67
+ {AUTH_SUBTITLE}
96
68
  </Text>
97
69
  </VStack>
98
70
 
@@ -102,40 +74,39 @@ export default function LoginCard() {
102
74
  label="Email"
103
75
  isLabelHidden
104
76
  type="email"
105
- placeholder="you@castle.dracula"
77
+ {...inputAutoComplete('email')}
78
+ placeholder={AUTH_EMAIL_PLACEHOLDER}
106
79
  value={email}
107
- onChange={setEmail}
80
+ onChange={(v: string) => {
81
+ setEmail(v);
82
+ setError(null);
83
+ }}
108
84
  size="lg"
85
+ onEnter={handleLogin}
86
+ status={error ? {type: 'error', message: error} : undefined}
109
87
  />
110
88
  <VStack gap={1}>
111
89
  <TextInput
112
90
  label="Password"
113
91
  isLabelHidden
114
- placeholder="Whisper your password"
92
+ placeholder={AUTH_PASSWORD_PLACEHOLDER}
115
93
  type="password"
94
+ {...inputAutoComplete('current-password')}
116
95
  value={password}
117
96
  onChange={(v: string) => {
118
97
  setPassword(v);
119
- setLoginFailed(false);
98
+ setError(null);
120
99
  }}
121
100
  size="lg"
122
- status={
123
- loginFailed
124
- ? {
125
- type: 'error',
126
- message: 'Wrong incantation. Try again.',
127
- }
128
- : undefined
129
- }
101
+ onEnter={handleLogin}
102
+ status={error ? {type: 'error', message: error} : undefined}
130
103
  />
131
- {loginFailed && (
104
+ {error && (
132
105
  <VStack hAlign="end">
133
106
  <Link
134
107
  href="#/templates/login-card"
135
- size="sm"
136
- color="secondary"
137
- type="supporting">
138
- Forgot password?
108
+ color="secondary">
109
+ {AUTH_FORGOT_PASSWORD}
139
110
  </Link>
140
111
  </VStack>
141
112
  )}
@@ -144,7 +115,7 @@ export default function LoginCard() {
144
115
 
145
116
  {/* Login button */}
146
117
  <Button
147
- label="Enter the night"
118
+ label={AUTH_PRIMARY_CTA}
148
119
  variant="primary"
149
120
  size="lg"
150
121
  isLoading={isLoading}
@@ -152,7 +123,7 @@ export default function LoginCard() {
152
123
  />
153
124
 
154
125
  {/* Divider */}
155
- <Divider label="Or continue with" />
126
+ <Divider label={AUTH_SSO_DIVIDER} />
156
127
 
157
128
  {/* Social buttons */}
158
129
  <VStack gap={3} hAlign="stretch">
@@ -173,9 +144,9 @@ export default function LoginCard() {
173
144
  {/* Sign up link */}
174
145
  <VStack hAlign="center">
175
146
  <Text type="supporting" color="secondary">
176
- New to the castle?{' '}
147
+ {AUTH_SIGNUP_PROMPT}{' '}
177
148
  <Link href="#/templates/login-card" type="supporting">
178
- Sign up
149
+ {AUTH_SIGNUP_LINK}
179
150
  </Link>
180
151
  </Text>
181
152
  </VStack>
@@ -185,13 +156,13 @@ export default function LoginCard() {
185
156
  {/* Terms */}
186
157
  <VStack hAlign="center" width="100%">
187
158
  <Text type="supporting" color="secondary" justify="center">
188
- By clicking continue, you agree to our{' '}
159
+ {AUTH_TERMS_PREFIX}{' '}
189
160
  <Link href="#/templates/login-card" type="supporting">
190
- Terms of Service
161
+ {AUTH_TERMS_SERVICE}
191
162
  </Link>{' '}
192
163
  and{' '}
193
164
  <Link href="#/templates/login-card" type="supporting">
194
- Privacy Policy
165
+ {AUTH_TERMS_PRIVACY}
195
166
  </Link>
196
167
  .
197
168
  </Text>
@@ -1,8 +1,28 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
  // XLE (canonical structure, validated with `bunx astryx layout check`):
3
- // Ctr > V[g4] > (C[p0] > G[c{min:240} g8 a=stretch] > (S[p0] > V[g4] > (H[g2] > Ic + Tx"Castle Dracula"[t=body]) + (V[g4] > (V[g1] > Hd"Welcome back to the night"[level=2] + Tx"Sign in to your crypt"[t=body]) + (V[g2] > TI"Email"[t=email] + TI"Password"[t=password]) + B.primary"Enter the night" + D"Or continue with" + (G[c2 g3] > B.secondary"Apple" + B.secondary"Google")) + (Tx"New to the castle?"[t=supporting] > Lk"Sign up")) + (C[p0] > AR)) + (V[a=center] > Tx"Terms"[t=supporting])
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])
4
4
 
5
- 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';
6
26
  import {VStack, HStack, StackItem} from '@astryxdesign/core/Layout';
7
27
  import {Grid} from '@astryxdesign/core/Grid';
8
28
  import {Center} from '@astryxdesign/core/Center';
@@ -21,77 +41,9 @@ import {Divider} from '@astryxdesign/core/Divider';
21
41
  // CheckCircleIcon. The SquaresPlusIcon brand mark has no place here.
22
42
  import {Moon, CircleCheck} from 'lucide-react';
23
43
 
24
- // Brand sign-in marks — bespoke inline glyphs, no icon-library equivalent.
25
- const AppleIcon = (props: React.SVGProps<SVGSVGElement>) => (
26
- <svg
27
- viewBox="0 0 24 24"
28
- fill="currentColor"
29
- width={16}
30
- height={16}
31
- aria-hidden="true"
32
- {...props}>
33
- <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" />
34
- </svg>
35
- );
36
-
37
- const GoogleIcon = (props: React.SVGProps<SVGSVGElement>) => (
38
- <svg
39
- viewBox="0 0 24 24"
40
- fill="currentColor"
41
- width={16}
42
- height={16}
43
- aria-hidden="true"
44
- {...props}>
45
- <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" />
46
- <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" />
47
- <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" />
48
- <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" />
49
- </svg>
50
- );
51
-
52
- // Cover art is inline Dracula SVG — moonlit rooftops in brand tokens, no
53
- // external image. Fills the panel via slice, like the placeholder before it.
54
- const coverArt: CSSProperties = {
55
- width: '100%',
56
- height: '100%',
57
- display: 'block',
58
- };
59
-
60
- const nightCoverArt = (
61
- <svg
62
- viewBox="0 0 400 300"
63
- preserveAspectRatio="xMidYMid slice"
64
- style={coverArt}
65
- role="img"
66
- aria-label="Harvest moon over the castle rooftops">
67
- <rect width="400" height="300" fill="var(--dracula-bg)" />
68
- <circle cx="68" cy="52" r="2" fill="var(--dracula-fg)" />
69
- <circle cx="140" cy="36" r="1.5" fill="var(--dracula-comment)" />
70
- <circle cx="330" cy="70" r="2" fill="var(--dracula-fg)" />
71
- <circle cx="292" cy="34" r="1.5" fill="var(--dracula-comment)" />
72
- <circle cx="360" cy="140" r="1.5" fill="var(--dracula-comment)" />
73
- <circle cx="200" cy="118" r="52" fill="var(--dracula-yellow)" />
74
- <circle cx="182" cy="104" r="44" fill="var(--dracula-bg)" />
75
- <g
76
- fill="var(--dracula-current-line)"
77
- stroke="var(--dracula-comment)"
78
- strokeWidth="3"
79
- strokeLinejoin="round">
80
- <rect x="20" y="220" width="110" height="80" />
81
- <path d="M20 220 L75 178 L130 220 Z" />
82
- <rect x="150" y="200" width="100" height="100" />
83
- <path d="M150 200 L200 162 L250 200 Z" />
84
- <rect x="270" y="228" width="110" height="72" />
85
- <path d="M270 228 L325 190 L380 228 Z" />
86
- </g>
87
- <g fill="var(--dracula-purple)">
88
- <rect x="58" y="244" width="14" height="18" />
89
- <rect x="188" y="224" width="14" height="18" />
90
- <rect x="312" y="250" width="14" height="18" />
91
- </g>
92
- </svg>
93
- );
94
-
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.
95
47
  // Grid emits minmax(MIN, 1fr) where MIN is a hard floor, so MIN plus the
96
48
  // grid inset and page padding must fit the narrowest phone or the column is
97
49
  // clipped. 320 − 2×24 (page) − 2×16 (stacked inset) = 240.
@@ -124,7 +76,7 @@ const LOGIN_SPLIT_CSS = `
124
76
  .login-split-grid {
125
77
  container-type: inline-size;
126
78
  container-name: login-split;
127
- padding: var(--spacing-8);
79
+ padding: var(--spacing-4);
128
80
  }
129
81
  .login-split-image {
130
82
  width: 100%;
@@ -132,7 +84,7 @@ const LOGIN_SPLIT_CSS = `
132
84
  }
133
85
  @container login-split (max-width: 511px) {
134
86
  .login-split-grid {
135
- padding: var(--spacing-4);
87
+ padding: var(--spacing-3);
136
88
  }
137
89
  .login-split-image {
138
90
  order: -1;
@@ -143,28 +95,27 @@ const LOGIN_SPLIT_CSS = `
143
95
  export default function LoginSplit() {
144
96
  const [email, setEmail] = useState('');
145
97
  const [password, setPassword] = useState('');
146
- const [loginFailed, setLoginFailed] = useState(false);
147
- const [isLoading, setIsLoading] = useState(false);
98
+ const [error, setError] = useState<string | null>(null);
99
+ const [isLoading, startTransition] = useTransition();
148
100
  const [isSuccess, setIsSuccess] = useState(false);
149
101
 
150
102
  const handleLogin = () => {
151
103
  if (!email || !password) {
152
- setLoginFailed(true);
104
+ setError(AUTH_ERROR_MESSAGE);
153
105
  return;
154
106
  }
155
- setIsLoading(true);
156
- setLoginFailed(false);
157
- setTimeout(() => {
158
- setIsLoading(false);
107
+ setError(null);
108
+ startTransition(async () => {
109
+ await demoLogin();
159
110
  setIsSuccess(true);
160
- }, 2000);
111
+ });
161
112
  };
162
113
 
163
114
  return (
164
115
  <Center axis="both" style={pageStyle}>
165
116
  <style>{LOGIN_SPLIT_CSS}</style>
166
117
  <VStack gap={4} width="100%">
167
- <div style={cardWrap}>
118
+ <Center axis="horizontal" style={cardWrap}>
168
119
  <Card padding={0} width="100%">
169
120
  <Grid
170
121
  columns={{minWidth: COLUMN_MIN_WIDTH, repeat: 'fit'}}
@@ -175,9 +126,9 @@ export default function LoginSplit() {
175
126
  <Section variant="transparent" padding={0} height="100%">
176
127
  <VStack gap={4} height="100%">
177
128
  <HStack gap={2} vAlign="center">
178
- <Icon icon={Moon} color="accent" />
129
+ <Icon icon={Moon} color="secondary" size="lg" />
179
130
  <Text type="body" weight="semibold">
180
- Castle Dracula
131
+ {AUTH_BRAND_NAME}
181
132
  </Text>
182
133
  </HStack>
183
134
 
@@ -187,16 +138,22 @@ export default function LoginSplit() {
187
138
  <EmptyState
188
139
  title="You're in for the night"
189
140
  description="Drifting to your crypt…"
190
- icon={<Icon icon={CircleCheck} size="lg" />}
141
+ icon={
142
+ <Icon
143
+ icon={CircleCheck}
144
+ size="lg"
145
+ color="secondary"
146
+ />
147
+ }
191
148
  />
192
149
  ) : (
193
150
  <VStack gap={4} hAlign="stretch" width="100%">
194
151
  <VStack gap={1}>
195
- <Heading level={2}>
196
- Welcome back to the night
152
+ <Heading level={1} type="display-2">
153
+ {AUTH_HEADING}
197
154
  </Heading>
198
155
  <Text type="body" color="secondary">
199
- Sign in to your crypt
156
+ {AUTH_SUBTITLE}
200
157
  </Text>
201
158
  </VStack>
202
159
 
@@ -205,37 +162,37 @@ export default function LoginSplit() {
205
162
  label="Email"
206
163
  isLabelHidden
207
164
  type="email"
208
- placeholder="you@castle.dracula"
165
+ {...inputAutoComplete('email')}
166
+ placeholder={AUTH_EMAIL_PLACEHOLDER}
209
167
  value={email}
210
- onChange={setEmail}
168
+ onChange={(v: string) => {
169
+ setEmail(v);
170
+ setError(null);
171
+ }}
211
172
  size="lg"
173
+ onEnter={handleLogin}
174
+ status={error ? {type: 'error', message: error} : undefined}
212
175
  />
213
176
  <VStack gap={1}>
214
177
  <TextInput
215
178
  label="Password"
216
179
  isLabelHidden
217
- placeholder="Whisper your password"
180
+ placeholder={AUTH_PASSWORD_PLACEHOLDER}
218
181
  type="password"
182
+ {...inputAutoComplete('current-password')}
219
183
  value={password}
220
184
  onChange={(v: string) => {
221
185
  setPassword(v);
222
- setLoginFailed(false);
186
+ setError(null);
223
187
  }}
224
188
  size="lg"
225
- status={
226
- loginFailed
227
- ? {
228
- type: 'error',
229
- message:
230
- 'Wrong incantation. Try again.',
231
- }
232
- : undefined
233
- }
189
+ onEnter={handleLogin}
190
+ status={error ? {type: 'error', message: error} : undefined}
234
191
  />
235
- {loginFailed && (
192
+ {error && (
236
193
  <VStack hAlign="end">
237
194
  <Link href="#/templates/login-split">
238
- Forgot your password?
195
+ {AUTH_FORGOT_PASSWORD}
239
196
  </Link>
240
197
  </VStack>
241
198
  )}
@@ -243,16 +200,22 @@ export default function LoginSplit() {
243
200
  </VStack>
244
201
 
245
202
  <Button
246
- label="Enter the night"
203
+ label={AUTH_PRIMARY_CTA}
247
204
  variant="primary"
248
205
  size="lg"
249
206
  isLoading={isLoading}
250
207
  onClick={handleLogin}
251
208
  />
252
209
 
253
- <Divider label="Or continue with" />
210
+ <Divider label={AUTH_SSO_DIVIDER} />
254
211
 
255
- <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">
256
219
  <Button
257
220
  label="Login with Apple"
258
221
  variant="secondary"
@@ -273,9 +236,9 @@ export default function LoginSplit() {
273
236
 
274
237
  {!isSuccess && (
275
238
  <Text type="supporting" color="secondary">
276
- New to the castle?{' '}
277
- <Link href="#/templates/login-split">
278
- Sign up
239
+ {AUTH_SIGNUP_PROMPT}{' '}
240
+ <Link href="#/templates/login-split" type="supporting">
241
+ {AUTH_SIGNUP_LINK}
279
242
  </Link>
280
243
  </Text>
281
244
  )}
@@ -284,28 +247,27 @@ export default function LoginSplit() {
284
247
 
285
248
  {/* Cover art — the transparent Card clips it to rounded
286
249
  corners (overflow:clip + radius), so the art needs no radius. */}
287
- <div className="login-split-image">
288
- <Card
289
- variant="transparent"
290
- padding={0}
291
- width="100%"
292
- height="100%">
293
- {nightCoverArt}
294
- </Card>
295
- </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>
296
258
  </Grid>
297
259
  </Card>
298
- </div>
260
+ </Center>
299
261
 
300
262
  <VStack hAlign="center">
301
263
  <Text type="supporting" color="secondary">
302
- By clicking continue, you agree to our{' '}
303
- <Link href="#/templates/login-split">
304
- Terms of Service
264
+ {AUTH_TERMS_PREFIX}{' '}
265
+ <Link href="#/templates/login-split" type="supporting">
266
+ {AUTH_TERMS_SERVICE}
305
267
  </Link>{' '}
306
268
  and{' '}
307
- <Link href="#/templates/login-split">
308
- Privacy Policy
269
+ <Link href="#/templates/login-split" type="supporting">
270
+ {AUTH_TERMS_PRIVACY}
309
271
  </Link>
310
272
  .
311
273
  </Text>