astryx-dracula 0.5.0 → 0.6.2

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 (129) hide show
  1. package/BRAND.md +3 -3
  2. package/README.md +7 -4
  3. package/USAGE.md +49 -49
  4. package/astryx-dracula.d.ts +2 -2
  5. package/astryx-dracula.js +25 -9
  6. package/astryx-dracula.variants.d.ts +2 -2
  7. package/astryx.integration.mjs +1 -1
  8. package/icons.mjs +21 -17
  9. package/icons.tsx +69 -62
  10. package/package.json +125 -120
  11. package/shared/auth-chrome-config.ts +6 -6
  12. package/shared/auth-chrome.tsx +143 -144
  13. package/shared/auth-copy.ts +14 -14
  14. package/shared/chaptered-doc-config.ts +48 -51
  15. package/shared/chaptered-doc.tsx +267 -289
  16. package/shared/chart-hues.ts +25 -10
  17. package/shared/chart-labels.tsx +31 -30
  18. package/shared/chart-legend.tsx +55 -56
  19. package/shared/chart-panel-style.ts +3 -3
  20. package/shared/data-bar.tsx +97 -102
  21. package/shared/format-money.ts +1 -1
  22. package/shared/gallery-image.ts +6 -6
  23. package/shared/login-demo.ts +1 -1
  24. package/shared/metric-card.tsx +101 -105
  25. package/shared/metric-delta.tsx +28 -27
  26. package/shared/order-desk.tsx +205 -216
  27. package/shared/revenue-chart.tsx +145 -165
  28. package/shared/scene-castle.tsx +206 -291
  29. package/shared/scene-frame.tsx +33 -32
  30. package/shared/scene-hues.ts +17 -17
  31. package/shared/scene-tile.tsx +88 -92
  32. package/shared/settings-data.ts +119 -125
  33. package/shared/settings-rows.tsx +122 -159
  34. package/shared/sparkline.tsx +75 -88
  35. package/shared/sso-icons.tsx +10 -22
  36. package/shared/stars.tsx +16 -25
  37. package/templates/ai-chat-landing.template.mjs +5 -4
  38. package/templates/ai-chat-landing.tsx +473 -460
  39. package/templates/ai-chat.template.mjs +5 -4
  40. package/templates/ai-chat.tsx +543 -612
  41. package/templates/blank.template.mjs +5 -4
  42. package/templates/blank.tsx +18 -14
  43. package/templates/centered-hero.template.mjs +5 -4
  44. package/templates/centered-hero.tsx +55 -68
  45. package/templates/classic-gallery.template.mjs +5 -4
  46. package/templates/classic-gallery.tsx +97 -104
  47. package/templates/contact-form.template.mjs +5 -4
  48. package/templates/contact-form.tsx +247 -282
  49. package/templates/dashboard-portfolio.template.mjs +5 -4
  50. package/templates/dashboard-portfolio.tsx +717 -548
  51. package/templates/dashboard.template.mjs +5 -4
  52. package/templates/dashboard.tsx +596 -567
  53. package/templates/detail-page.template.mjs +5 -4
  54. package/templates/detail-page.tsx +546 -603
  55. package/templates/documentation-design.template.mjs +5 -4
  56. package/templates/documentation-design.tsx +231 -243
  57. package/templates/documentation-technical.template.mjs +5 -4
  58. package/templates/documentation-technical.tsx +236 -239
  59. package/templates/documentation.template.mjs +5 -4
  60. package/templates/documentation.tsx +267 -279
  61. package/templates/editor.template.mjs +5 -4
  62. package/templates/editor.tsx +844 -877
  63. package/templates/file-explorer.template.mjs +5 -4
  64. package/templates/file-explorer.tsx +566 -590
  65. package/templates/form-two-column.template.mjs +5 -4
  66. package/templates/form-two-column.tsx +195 -210
  67. package/templates/gallery-hero.template.mjs +5 -4
  68. package/templates/gallery-hero.tsx +93 -114
  69. package/templates/ide.template.mjs +5 -4
  70. package/templates/ide.tsx +407 -453
  71. package/templates/incident-console.template.mjs +5 -4
  72. package/templates/incident-console.tsx +593 -575
  73. package/templates/kanban-board.template.mjs +5 -4
  74. package/templates/kanban-board.tsx +665 -683
  75. package/templates/library.template.mjs +5 -4
  76. package/templates/library.tsx +409 -430
  77. package/templates/login-card.template.mjs +5 -4
  78. package/templates/login-card.tsx +63 -72
  79. package/templates/login-split.template.mjs +5 -4
  80. package/templates/login-split.tsx +192 -193
  81. package/templates/login-sso.template.mjs +5 -4
  82. package/templates/login-sso.tsx +257 -258
  83. package/templates/login.template.mjs +5 -4
  84. package/templates/login.tsx +53 -49
  85. package/templates/messaging-shell.template.mjs +5 -4
  86. package/templates/messaging-shell.tsx +556 -574
  87. package/templates/mixed-gallery.template.mjs +5 -4
  88. package/templates/mixed-gallery.tsx +60 -58
  89. package/templates/payment-form.template.mjs +5 -4
  90. package/templates/payment-form.tsx +882 -991
  91. package/templates/product-detail.template.mjs +5 -4
  92. package/templates/product-detail.tsx +261 -272
  93. package/templates/product-gallery.template.mjs +5 -4
  94. package/templates/product-gallery.tsx +126 -137
  95. package/templates/product-tour.template.mjs +5 -4
  96. package/templates/product-tour.tsx +741 -773
  97. package/templates/settings-dialog.template.mjs +5 -4
  98. package/templates/settings-dialog.tsx +688 -717
  99. package/templates/settings-sidebar.template.mjs +5 -4
  100. package/templates/settings-sidebar.tsx +762 -807
  101. package/templates/settings.template.mjs +5 -4
  102. package/templates/settings.tsx +289 -304
  103. package/templates/shell-nav.template.mjs +5 -4
  104. package/templates/shell-nav.tsx +212 -219
  105. package/templates/shell-side-nav.template.mjs +5 -4
  106. package/templates/shell-side-nav.tsx +135 -157
  107. package/templates/shell-top-nav.template.mjs +5 -4
  108. package/templates/shell-top-nav.tsx +182 -203
  109. package/templates/side-gallery.template.mjs +5 -4
  110. package/templates/side-gallery.tsx +85 -90
  111. package/templates/table-grouped.template.mjs +5 -4
  112. package/templates/table-grouped.tsx +1000 -1053
  113. package/templates/table-page-chart.template.mjs +5 -4
  114. package/templates/table-page-chart.tsx +400 -398
  115. package/templates/table-page-heatmap-status.template.mjs +5 -4
  116. package/templates/table-page-heatmap-status.tsx +483 -512
  117. package/templates/table-page-shoe-store-heatmap.template.mjs +5 -4
  118. package/templates/table-page-shoe-store-heatmap.tsx +748 -746
  119. package/templates/table-page.template.mjs +5 -4
  120. package/templates/table-page.tsx +396 -405
  121. package/templates/table.template.mjs +5 -4
  122. package/templates/table.tsx +92 -93
  123. package/templates/tech-report.template.mjs +5 -4
  124. package/templates/tech-report.tsx +1316 -1304
  125. package/templates/theme-showcase.template.mjs +5 -4
  126. package/templates/theme-showcase.tsx +871 -951
  127. package/theme.css +35 -77
  128. package/theme.css.d.ts +9 -0
  129. package/tokens.css +373 -380
@@ -1,57 +1,70 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
- // XLE (canonical structure, validated with `bunx astryx layout check`):
3
- // L > LC > Ctr[h=80vh] > B.primary"Open settings"[opens=#settings] ;; Dlg#settings > L > (LP[w=280 divider p=3] > V[g=4] > Tx"Account settings"[t=label] + (UL > LI"Personal information"*8)) + (LC[p=6] > V[g=6] > DH"Account" + SE"Settings section" + (V[g=0] > TabList + (V[g=0] > Hd"Login"[level=3] + (H[j=between a=start] > (V[g=0] > Tx"Password"[weight=semibold] + Tx"Not created"[t=supporting]) + B"Create") + (H[g=3 a=start] > Ic + (V[g=0] > (H[g=2 a=center wrap] > Tx"OS X 10.15.7 Chrome"[weight=semibold] + SD) + Tx"March 30, 2026"[t=supporting])))))
2
+ // XLE (canonical structure of the tree below; the CLI had no XLE validator
3
+ // after `astryx layout` was removed in 0.7.0, so this is a reading aid and a
4
+ // human drift check — update it with the file, never alone):
5
+ // L[h=auto] > LC[p=0] > Ctr[h=80vh] > B.primary"Open settings"[opens=#settings] ;; Dlg#settings > L[h=fill] > (LP[w=280 divider p=3] > V[g=4] > Tx"Account settings"[t=label] + (UL[density=spacious edge=flush] > LI"Personal information" + LI"Login & security"! + LI*6)) + (LC[p=6] > V[g=6] > DH"Login & security" + SE{Selector}"Settings section" + (V[g=0 mw=680] > (TL[divider] > Tab"Login"! + Tab"Shared access") + (V[g=8] > (V[g=2] > Hd"Login"[level=3] + (H[j=between a=start] > (V[g=1] > Tx"Password"[weight=semibold] + Tx"Not created"[t=body]) + B.secondary"Create")) + (V[g=2] > Hd"Social accounts"[level=3] + (H[j=between a=start] > (V[g=1] > Tx"Google"[weight=semibold] + Tx"Connected"[t=body]) + B.destructive"Disconnect")) + (V[g=2] > Hd"Device history"[level=3] + (H[g=3 a=start] > Ic + (SI[fill] > V[g=1] > (H[g=2 a=center wrap] > Tx"OS X 10.15.7 · Chrome"[weight=semibold] + SD.success) + Tx"March 30, 2026"[t=supporting]))*3) + (V[g=2] > Hd"Account"[level=3] + (H[j=between a=start] > (V[g=1] > Tx"Deactivate your account"[weight=semibold] + Tx"This action cannot be undone"[t=supporting]) + B.destructive"Deactivate")))))
4
6
 
5
7
  /**
6
8
  * Settings Dialog — account sections inside one modal. Frame: the trigger page, then a Dialog that sizes to (Frame/responsive/container: see XLE header above.)
7
9
  */
8
10
 
9
- import {useState, type CSSProperties} from 'react';
11
+ import { Button } from "@astryxdesign/core/Button";
12
+ import { Card } from "@astryxdesign/core/Card";
13
+ import { Center } from "@astryxdesign/core/Center";
14
+ import { Dialog, DialogHeader } from "@astryxdesign/core/Dialog";
15
+ import { useMediaQuery } from "@astryxdesign/core/hooks";
16
+ import { Icon } from "@astryxdesign/core/Icon";
10
17
  import {
11
- VStack,
12
- HStack,
13
- StackItem,
14
- Layout,
15
- LayoutPanel,
16
- LayoutContent,
17
- } from '@astryxdesign/core/Layout';
18
- import {Text, Heading} from '@astryxdesign/core/Text';
19
- import {Button} from '@astryxdesign/core/Button';
20
- import {Dialog, DialogHeader} from '@astryxdesign/core/Dialog';
21
- import {List, ListItem} from '@astryxdesign/core/List';
22
- import {Selector} from '@astryxdesign/core/Selector';
23
- import {TextInput} from '@astryxdesign/core/TextInput';
24
- import {Card} from '@astryxdesign/core/Card';
25
- import {Switch} from '@astryxdesign/core/Switch';
26
- import {Link} from '@astryxdesign/core/Link';
27
- import {TabList, Tab} from '@astryxdesign/core/TabList';
28
- import {StatusDot} from '@astryxdesign/core/StatusDot';
29
- import {Icon} from '@astryxdesign/core/Icon';
30
- import {Center} from '@astryxdesign/core/Center';
31
- import {useMediaQuery} from '@astryxdesign/core/hooks';
32
- import {Lock, ShieldCheck, Monitor} from 'lucide-react';
33
- import {NAV_ITEMS, LOGIN_ROWS, SOCIAL_ROWS, DEVICE_ROWS, INFO_TILES, LANGUAGES, CURRENCIES, TIMEZONES, iconBox, sideNavHeading} from 'astryx-dracula/shared/settings-data';
34
- import {InfoRowItem, ExpandableRow} from 'astryx-dracula/shared/settings-rows';
35
- import type {DeviceRow, InfoTileData} from 'astryx-dracula/shared/settings-data';
18
+ HStack,
19
+ Layout,
20
+ LayoutContent,
21
+ LayoutPanel,
22
+ StackItem,
23
+ VStack,
24
+ } from "@astryxdesign/core/Layout";
25
+ import { Link } from "@astryxdesign/core/Link";
26
+ import { List, ListItem } from "@astryxdesign/core/List";
27
+ import { Selector } from "@astryxdesign/core/Selector";
28
+ import { StatusDot } from "@astryxdesign/core/StatusDot";
29
+ import { Switch } from "@astryxdesign/core/Switch";
30
+ import { Tab, TabList } from "@astryxdesign/core/TabList";
31
+ import { Heading, Text } from "@astryxdesign/core/Text";
32
+ import { TextInput } from "@astryxdesign/core/TextInput";
33
+ import type { DeviceRow, InfoTileData } from "astryx-dracula/shared/settings-data";
34
+ import {
35
+ CURRENCIES,
36
+ DEVICE_ROWS,
37
+ INFO_TILES,
38
+ iconBox,
39
+ LANGUAGES,
40
+ LOGIN_ROWS,
41
+ NAV_ITEMS,
42
+ SOCIAL_ROWS,
43
+ sideNavHeading,
44
+ TIMEZONES,
45
+ } from "astryx-dracula/shared/settings-data";
46
+ import { ExpandableRow, InfoRowItem } from "astryx-dracula/shared/settings-rows";
47
+ import { Lock, Monitor, ShieldCheck } from "lucide-react";
48
+ import { type CSSProperties, useState } from "react";
36
49
 
37
50
  // Sticky dialog header bar — no Astryx prop for sticky/background/z-index.
38
51
  // Inline + block padding comes from the parent LayoutContent `padding`.
39
52
  const headerSticky: CSSProperties = {
40
- position: 'sticky',
41
- top: 0,
42
- backgroundColor: 'var(--color-background-surface)',
43
- zIndex: 1,
53
+ position: "sticky",
54
+ top: 0,
55
+ backgroundColor: "var(--color-background-surface)",
56
+ zIndex: 1,
44
57
  };
45
58
 
46
59
  // Width cap on the content column (VStack `maxWidth`; inline padding comes
47
60
  // from the parent LayoutContent `padding`).
48
61
  const dialogHeight: CSSProperties = {
49
- height: '85vh',
62
+ height: "85vh",
50
63
  };
51
64
 
52
65
  // Same-route hash: demo links stay focusable anchors without escaping the
53
66
  // template through the hash router (bare "#" would drop back to the home page).
54
- const SELF_HASH = '#/templates/settings-dialog';
67
+ const SELF_HASH = "#/templates/settings-dialog";
55
68
 
56
69
  // InfoTile renders the icon tile plus title/body stacked text. DeviceRowItem
57
70
  // renders the session icon, label + current-session badge, timestamp, and
@@ -59,696 +72,654 @@ const SELF_HASH = '#/templates/settings-dialog';
59
72
  // because that module already covered InfoRowItem/ExpandableRow at its size
60
73
  // limit, and these two are used by this dialog only.
61
74
 
62
-
63
- function InfoTile({icon, title, body}: InfoTileData) {
64
- return (
65
- <HStack gap={3} vAlign="start">
66
- <Center width={48} height={48} style={iconBox}>
67
- <Icon icon={icon} />
68
- </Center>
69
- <VStack gap={1}>
70
- <Text type="body" weight="semibold" display="block">
71
- {title}
72
- </Text>
73
- <Text type="body" color="secondary" display="block">
74
- {body}
75
- </Text>
76
- </VStack>
77
- </HStack>
78
- );
75
+ function InfoTile({ icon, title, body }: InfoTileData) {
76
+ return (
77
+ <HStack gap={3} vAlign="start">
78
+ <Center width={48} height={48} style={iconBox}>
79
+ <Icon icon={icon} />
80
+ </Center>
81
+ <VStack gap={1}>
82
+ <Text type="body" weight="semibold" display="block">
83
+ {title}
84
+ </Text>
85
+ <Text type="body" color="secondary" display="block">
86
+ {body}
87
+ </Text>
88
+ </VStack>
89
+ </HStack>
90
+ );
79
91
  }
80
92
 
81
-
82
- function DeviceRowItem({label, isCurrent, location, action}: DeviceRow) {
83
- return (
84
- <HStack gap={3} vAlign="start">
85
- <Icon icon={Monitor} />
86
- <StackItem size="fill">
87
- <VStack gap={1}>
88
- {/* wrap: the label plus the session status exceed the compact
93
+ function DeviceRowItem({ label, isCurrent, location, action }: DeviceRow) {
94
+ return (
95
+ <HStack gap={3} vAlign="start">
96
+ <Icon icon={Monitor} />
97
+ <StackItem size="fill">
98
+ <VStack gap={1}>
99
+ {/* wrap: the label plus the session status exceed the compact
89
100
  content width (~280px). */}
90
- <HStack gap={2} vAlign="center" wrap="wrap">
91
- <Text type="body" weight="semibold">
92
- {label}
93
- </Text>
94
- {isCurrent && (
95
- <HStack gap={1} vAlign="center">
96
- <StatusDot variant="success" label="Current session" />
97
- <Text type="supporting" color="secondary">
98
- Current session
99
- </Text>
100
- </HStack>
101
- )}
102
- </HStack>
103
- <Text
104
- type="supporting"
105
- color="secondary"
106
- display="block"
107
- hasTabularNumbers>
108
- {location}
109
- </Text>
110
- </VStack>
111
- </StackItem>
112
- {/* Ending a session changes state in place — a Button, not a Link. */}
113
- {action && <Button label={action} variant="secondary" size="sm" />}
114
- </HStack>
115
- );
101
+ <HStack gap={2} vAlign="center" wrap="wrap">
102
+ <Text type="body" weight="semibold">
103
+ {label}
104
+ </Text>
105
+ {isCurrent && (
106
+ <HStack gap={1} vAlign="center">
107
+ <StatusDot variant="success" label="Current session" />
108
+ <Text type="supporting" color="secondary">
109
+ Current session
110
+ </Text>
111
+ </HStack>
112
+ )}
113
+ </HStack>
114
+ <Text type="supporting" color="secondary" display="block" hasTabularNumbers>
115
+ {location}
116
+ </Text>
117
+ </VStack>
118
+ </StackItem>
119
+ {/* Ending a session changes state in place — a Button, not a Link. */}
120
+ {action && <Button label={action} variant="secondary" size="sm" />}
121
+ </HStack>
122
+ );
116
123
  }
117
124
 
118
125
  export default function SettingsDialog() {
119
- const [isOpen, setIsOpen] = useState(false);
120
- const [activeNav, setActiveNav] = useState('Login & security');
121
- const [expandedRow, setExpandedRow] = useState<string | null>(null);
122
-
123
- const [language, setLanguage] = useState('en-CA');
124
- const [currency, setCurrency] = useState('CAD');
125
- const [timezone, setTimezone] = useState('ET');
126
- const [activeTab, setActiveTab] = useState('login');
127
-
128
- const [legalName, setLegalName] = useState('Vlad Dracul');
129
- const [preferredName, setPreferredName] = useState('');
130
- const [email, setEmail] = useState('v***d@castle-dracula.ro');
131
- const [phone, setPhone] = useState('+1 ***-***-0123');
132
- const [address, setAddress] = useState('');
133
- const [mailingAddress, setMailingAddress] = useState('');
134
- const [emergencyContact, setEmergencyContact] = useState('Provided');
135
- const [readReceipts, setReadReceipts] = useState(true);
136
- const [searchEngines, setSearchEngines] = useState(true);
137
- const [showCity, setShowCity] = useState(true);
138
- const [showTripType, setShowTripType] = useState(true);
139
- const [showStayLength, setShowStayLength] = useState(true);
140
- const [showServices, setShowServices] = useState(true);
141
- const [aiFeatures, setAiFeatures] = useState(true);
142
- const [emailNotif, setEmailNotif] = useState(true);
143
- const [pushNotif, setPushNotif] = useState(false);
144
- const [workTravel, setWorkTravel] = useState(false);
145
-
146
- // Below ~640px the 280px section sidebar crushes the content column to
147
- // ~110px. The sidebar hides and a section picker renders above the content.
148
- const isCompact = useMediaQuery('(max-width: 640px)');
149
-
150
- const handleEdit = (row: string) => setExpandedRow(row);
151
- const handleCancel = () => setExpandedRow(null);
152
- const handleSave = () => setExpandedRow(null);
153
-
154
- return (
155
- <>
156
- <Layout
157
- height="auto"
158
- content={
159
- <LayoutContent padding={0}>
160
- <Center height="80vh">
161
- <Button
162
- label="Open settings"
163
- variant="primary"
164
- onClick={() => setIsOpen(true)}
165
- />
166
- </Center>
167
- </LayoutContent>
168
- }
169
- />
170
-
171
- <Dialog
172
- isOpen={isOpen}
173
- onOpenChange={open => setIsOpen(open)}
174
- // One width for every viewport: below 932px the dialog shrinks with the
175
- // window instead of clipping, so no compact branch is needed.
176
- width="min(900px, calc(100vw - 32px))"
177
- maxHeight="85vh"
178
- padding={0}
179
- purpose="form"
180
- style={dialogHeight}>
181
- <Layout
182
- height="fill"
183
- start={
184
- isCompact ? undefined : (
185
- <LayoutPanel width={280} hasDivider role="navigation" padding={3}>
186
- <VStack gap={4}>
187
- <Text type="label" style={sideNavHeading}>
188
- Account settings
189
- </Text>
190
- <List density="spacious">
191
- {NAV_ITEMS.map(item => (
192
- <ListItem
193
- key={item.label}
194
- label={item.label}
195
- startContent={<Icon icon={item.icon} />}
196
- isSelected={activeNav === item.label}
197
- onClick={() => {
198
- setActiveNav(item.label);
199
- setExpandedRow(null);
200
- }}
201
- />
202
- ))}
203
- </List>
204
- </VStack>
205
- </LayoutPanel>
206
- )
207
- }
208
- content={
209
- <LayoutContent isScrollable padding={6}>
210
- <VStack gap={6}>
211
- <VStack style={headerSticky}>
212
- <DialogHeader
213
- title={
214
- activeNav === 'Personal information'
215
- ? 'Personal info'
216
- : activeNav
217
- }
218
- onOpenChange={open => setIsOpen(open)}
219
- hasDivider={false}
220
- />
221
- </VStack>
222
- {isCompact && (
223
- <Selector
224
- label="Settings section"
225
- value={activeNav}
226
- onChange={value => {
227
- setActiveNav(value);
228
- setExpandedRow(null);
229
- }}
230
- options={NAV_ITEMS.map(item => ({
231
- label: item.label,
232
- value: item.label,
233
- }))}
234
- width="100%"
235
- />
236
- )}
237
- <VStack gap={0} maxWidth={680}>
238
- {activeNav === 'Personal information' && (
239
- <VStack gap={6}>
240
- <VStack gap={4}>
241
- <ExpandableRow
242
- label="Legal name"
243
- value={legalName}
244
- isExpanded={expandedRow === 'legalName'}
245
- onEdit={() => handleEdit('legalName')}
246
- onCancel={handleCancel}
247
- onSave={handleSave}
248
- hasDivider={false}>
249
- <TextInput
250
- label="Legal name"
251
- isLabelHidden
252
- value={legalName}
253
- onChange={setLegalName}
254
- />
255
- </ExpandableRow>
256
- <ExpandableRow
257
- label="Preferred first name"
258
- value={preferredName || 'Not provided'}
259
- isExpanded={expandedRow === 'preferredName'}
260
- onEdit={() => handleEdit('preferredName')}
261
- onCancel={handleCancel}
262
- onSave={handleSave}
263
- hasDivider={false}>
264
- <TextInput
265
- label="Preferred first name"
266
- isLabelHidden
267
- value={preferredName}
268
- onChange={setPreferredName}
269
- />
270
- </ExpandableRow>
271
- <ExpandableRow
272
- label="Email address"
273
- value={email}
274
- isExpanded={expandedRow === 'email'}
275
- onEdit={() => handleEdit('email')}
276
- onCancel={handleCancel}
277
- onSave={handleSave}
278
- hasDivider={false}>
279
- <TextInput
280
- label="Email address"
281
- isLabelHidden
282
- value={email}
283
- onChange={setEmail}
284
- />
285
- </ExpandableRow>
286
- <ExpandableRow
287
- label="Phone number"
288
- value={phone}
289
- isExpanded={expandedRow === 'phone'}
290
- onEdit={() => handleEdit('phone')}
291
- onCancel={handleCancel}
292
- onSave={handleSave}
293
- hasDivider={false}>
294
- <TextInput
295
- label="Phone number"
296
- isLabelHidden
297
- value={phone}
298
- onChange={setPhone}
299
- />
300
- </ExpandableRow>
301
- <InfoRowItem
302
- label="Identity verification"
303
- value="Verified"
304
- action=""
305
- hasDivider={false}
306
- />
307
- <ExpandableRow
308
- label="Residential address"
309
- value={address || 'Not provided'}
310
- isExpanded={expandedRow === 'address'}
311
- onEdit={() => handleEdit('address')}
312
- onCancel={handleCancel}
313
- onSave={handleSave}
314
- hasDivider={false}>
315
- <TextInput
316
- label="Residential address"
317
- isLabelHidden
318
- value={address}
319
- onChange={setAddress}
320
- />
321
- </ExpandableRow>
322
- <ExpandableRow
323
- label="Mailing address"
324
- value={mailingAddress || 'Not provided'}
325
- isExpanded={expandedRow === 'mailingAddress'}
326
- onEdit={() => handleEdit('mailingAddress')}
327
- onCancel={handleCancel}
328
- onSave={handleSave}
329
- hasDivider={false}>
330
- <TextInput
331
- label="Mailing address"
332
- isLabelHidden
333
- value={mailingAddress}
334
- onChange={setMailingAddress}
335
- />
336
- </ExpandableRow>
337
- <ExpandableRow
338
- label="Emergency contact"
339
- value={emergencyContact}
340
- isExpanded={expandedRow === 'emergencyContact'}
341
- onEdit={() => handleEdit('emergencyContact')}
342
- onCancel={handleCancel}
343
- onSave={handleSave}
344
- hasDivider={false}>
345
- <TextInput
346
- label="Emergency contact"
347
- isLabelHidden
348
- value={emergencyContact}
349
- onChange={setEmergencyContact}
350
- />
351
- </ExpandableRow>
352
- </VStack>
353
-
354
- {/* One divider at most per panel: the tiles are one
126
+ const [isOpen, setIsOpen] = useState(false);
127
+ const [activeNav, setActiveNav] = useState("Login & security");
128
+ const [expandedRow, setExpandedRow] = useState<string | null>(null);
129
+
130
+ const [language, setLanguage] = useState("en-CA");
131
+ const [currency, setCurrency] = useState("CAD");
132
+ const [timezone, setTimezone] = useState("ET");
133
+ const [activeTab, setActiveTab] = useState("login");
134
+
135
+ const [legalName, setLegalName] = useState("Vlad Dracul");
136
+ const [preferredName, setPreferredName] = useState("");
137
+ const [email, setEmail] = useState("v***d@castle-dracula.ro");
138
+ const [phone, setPhone] = useState("+1 ***-***-0123");
139
+ const [address, setAddress] = useState("");
140
+ const [mailingAddress, setMailingAddress] = useState("");
141
+ const [emergencyContact, setEmergencyContact] = useState("Provided");
142
+ const [readReceipts, setReadReceipts] = useState(true);
143
+ const [searchEngines, setSearchEngines] = useState(true);
144
+ const [showCity, setShowCity] = useState(true);
145
+ const [showTripType, setShowTripType] = useState(true);
146
+ const [showStayLength, setShowStayLength] = useState(true);
147
+ const [showServices, setShowServices] = useState(true);
148
+ const [aiFeatures, setAiFeatures] = useState(true);
149
+ const [emailNotif, setEmailNotif] = useState(true);
150
+ const [pushNotif, setPushNotif] = useState(false);
151
+ const [workTravel, setWorkTravel] = useState(false);
152
+
153
+ // Below ~640px the 280px section sidebar crushes the content column to
154
+ // ~110px. The sidebar hides and a section picker renders above the content.
155
+ const isCompact = useMediaQuery("(max-width: 640px)");
156
+
157
+ const handleEdit = (row: string) => setExpandedRow(row);
158
+ const handleCancel = () => setExpandedRow(null);
159
+ const handleSave = () => setExpandedRow(null);
160
+
161
+ return (
162
+ <>
163
+ <Layout
164
+ height="auto"
165
+ content={
166
+ <LayoutContent padding={0}>
167
+ <Center height="80vh">
168
+ <Button label="Open settings" variant="primary" onClick={() => setIsOpen(true)} />
169
+ </Center>
170
+ </LayoutContent>
171
+ }
172
+ />
173
+
174
+ <Dialog
175
+ isOpen={isOpen}
176
+ onOpenChange={(open) => setIsOpen(open)}
177
+ // One width for every viewport: below 932px the dialog shrinks with the
178
+ // window instead of clipping, so no compact branch is needed.
179
+ width="min(900px, calc(100vw - 32px))"
180
+ maxHeight="85vh"
181
+ padding={0}
182
+ purpose="form"
183
+ style={dialogHeight}
184
+ >
185
+ <Layout
186
+ height="fill"
187
+ start={
188
+ isCompact ? undefined : (
189
+ <LayoutPanel width={280} hasDivider role="navigation" padding={3}>
190
+ <VStack gap={4}>
191
+ <Text type="label" style={sideNavHeading}>
192
+ Account settings
193
+ </Text>
194
+ <List density="spacious" edge="flush">
195
+ {NAV_ITEMS.map((item) => (
196
+ <ListItem
197
+ key={item.label}
198
+ label={item.label}
199
+ startContent={<Icon icon={item.icon} />}
200
+ isSelected={activeNav === item.label}
201
+ onClick={() => {
202
+ setActiveNav(item.label);
203
+ setExpandedRow(null);
204
+ }}
205
+ />
206
+ ))}
207
+ </List>
208
+ </VStack>
209
+ </LayoutPanel>
210
+ )
211
+ }
212
+ content={
213
+ <LayoutContent isScrollable padding={6}>
214
+ <VStack gap={6}>
215
+ <VStack style={headerSticky}>
216
+ <DialogHeader
217
+ title={activeNav === "Personal information" ? "Personal info" : activeNav}
218
+ onOpenChange={(open) => setIsOpen(open)}
219
+ hasDivider={false}
220
+ />
221
+ </VStack>
222
+ {isCompact && (
223
+ <Selector
224
+ label="Settings section"
225
+ value={activeNav}
226
+ onChange={(value) => {
227
+ setActiveNav(value);
228
+ setExpandedRow(null);
229
+ }}
230
+ options={NAV_ITEMS.map((item) => ({
231
+ label: item.label,
232
+ value: item.label,
233
+ }))}
234
+ width="100%"
235
+ />
236
+ )}
237
+ <VStack gap={0} maxWidth={680}>
238
+ {activeNav === "Personal information" && (
239
+ <VStack gap={6}>
240
+ <VStack gap={4}>
241
+ <ExpandableRow
242
+ label="Legal name"
243
+ value={legalName}
244
+ isExpanded={expandedRow === "legalName"}
245
+ onEdit={() => handleEdit("legalName")}
246
+ onCancel={handleCancel}
247
+ onSave={handleSave}
248
+ hasDivider={false}
249
+ >
250
+ <TextInput
251
+ label="Legal name"
252
+ isLabelHidden
253
+ value={legalName}
254
+ onChange={setLegalName}
255
+ />
256
+ </ExpandableRow>
257
+ <ExpandableRow
258
+ label="Preferred first name"
259
+ value={preferredName || "Not provided"}
260
+ isExpanded={expandedRow === "preferredName"}
261
+ onEdit={() => handleEdit("preferredName")}
262
+ onCancel={handleCancel}
263
+ onSave={handleSave}
264
+ hasDivider={false}
265
+ >
266
+ <TextInput
267
+ label="Preferred first name"
268
+ isLabelHidden
269
+ value={preferredName}
270
+ onChange={setPreferredName}
271
+ />
272
+ </ExpandableRow>
273
+ <ExpandableRow
274
+ label="Email address"
275
+ value={email}
276
+ isExpanded={expandedRow === "email"}
277
+ onEdit={() => handleEdit("email")}
278
+ onCancel={handleCancel}
279
+ onSave={handleSave}
280
+ hasDivider={false}
281
+ >
282
+ <TextInput
283
+ label="Email address"
284
+ isLabelHidden
285
+ value={email}
286
+ onChange={setEmail}
287
+ />
288
+ </ExpandableRow>
289
+ <ExpandableRow
290
+ label="Phone number"
291
+ value={phone}
292
+ isExpanded={expandedRow === "phone"}
293
+ onEdit={() => handleEdit("phone")}
294
+ onCancel={handleCancel}
295
+ onSave={handleSave}
296
+ hasDivider={false}
297
+ >
298
+ <TextInput
299
+ label="Phone number"
300
+ isLabelHidden
301
+ value={phone}
302
+ onChange={setPhone}
303
+ />
304
+ </ExpandableRow>
305
+ <InfoRowItem
306
+ label="Identity verification"
307
+ value="Verified"
308
+ action=""
309
+ hasDivider={false}
310
+ />
311
+ <ExpandableRow
312
+ label="Residential address"
313
+ value={address || "Not provided"}
314
+ isExpanded={expandedRow === "address"}
315
+ onEdit={() => handleEdit("address")}
316
+ onCancel={handleCancel}
317
+ onSave={handleSave}
318
+ hasDivider={false}
319
+ >
320
+ <TextInput
321
+ label="Residential address"
322
+ isLabelHidden
323
+ value={address}
324
+ onChange={setAddress}
325
+ />
326
+ </ExpandableRow>
327
+ <ExpandableRow
328
+ label="Mailing address"
329
+ value={mailingAddress || "Not provided"}
330
+ isExpanded={expandedRow === "mailingAddress"}
331
+ onEdit={() => handleEdit("mailingAddress")}
332
+ onCancel={handleCancel}
333
+ onSave={handleSave}
334
+ hasDivider={false}
335
+ >
336
+ <TextInput
337
+ label="Mailing address"
338
+ isLabelHidden
339
+ value={mailingAddress}
340
+ onChange={setMailingAddress}
341
+ />
342
+ </ExpandableRow>
343
+ <ExpandableRow
344
+ label="Emergency contact"
345
+ value={emergencyContact}
346
+ isExpanded={expandedRow === "emergencyContact"}
347
+ onEdit={() => handleEdit("emergencyContact")}
348
+ onCancel={handleCancel}
349
+ onSave={handleSave}
350
+ hasDivider={false}
351
+ >
352
+ <TextInput
353
+ label="Emergency contact"
354
+ isLabelHidden
355
+ value={emergencyContact}
356
+ onChange={setEmergencyContact}
357
+ />
358
+ </ExpandableRow>
359
+ </VStack>
360
+
361
+ {/* One divider at most per panel: the tiles are one
355
362
  group, so `gap` carries the rhythm. */}
356
- <Card padding={4}>
357
- <VStack gap={4}>
358
- {INFO_TILES.map(tile => (
359
- <InfoTile key={tile.title} {...tile} />
360
- ))}
361
- </VStack>
362
- </Card>
363
- </VStack>
364
- )}
365
-
366
- {activeNav === 'Login & security' && (
367
- <VStack gap={6}>
368
- <TabList
369
- value={activeTab}
370
- onChange={setActiveTab}
371
- hasDivider>
372
- <Tab value="login" label="Login" />
373
- <Tab value="shared" label="Shared access" />
374
- </TabList>
375
-
376
- {/* Four genuinely different groups, each with its own
363
+ <Card padding={4}>
364
+ <VStack gap={4}>
365
+ {INFO_TILES.map((tile) => (
366
+ <InfoTile key={tile.title} {...tile} />
367
+ ))}
368
+ </VStack>
369
+ </Card>
370
+ </VStack>
371
+ )}
372
+
373
+ {activeNav === "Login & security" && (
374
+ <VStack gap={6}>
375
+ <TabList value={activeTab} onChange={setActiveTab} hasDivider>
376
+ <Tab value="login" label="Login" />
377
+ <Tab value="shared" label="Shared access" />
378
+ </TabList>
379
+
380
+ {/* Four genuinely different groups, each with its own
377
381
  heading: `gap` is the boundary, so no rule between
378
382
  them and none after the last row. */}
379
- {activeTab === 'login' && (
380
- <VStack gap={8}>
381
- <VStack gap={2}>
382
- <Heading level={3}>Login</Heading>
383
- {LOGIN_ROWS.map(row => (
384
- <InfoRowItem key={row.label} {...row} hasDivider={false} />
385
- ))}
386
- </VStack>
387
-
388
- <VStack gap={2}>
389
- <Heading level={3}>Social accounts</Heading>
390
- {SOCIAL_ROWS.map(row => (
391
- <InfoRowItem key={row.label} {...row} hasDivider={false} />
392
- ))}
393
- </VStack>
394
-
395
- <VStack gap={2}>
396
- <Heading level={3}>Device history</Heading>
397
- {DEVICE_ROWS.map(device => (
398
- <DeviceRowItem
399
- key={device.location}
400
- {...device}
401
- />
402
- ))}
403
- </VStack>
404
-
405
- <VStack gap={2}>
406
- <Heading level={3}>Account</Heading>
407
- <HStack hAlign="between" vAlign="start">
408
- <VStack gap={1}>
409
- <Text
410
- type="body"
411
- weight="semibold"
412
- display="block">
413
- Deactivate your account
414
- </Text>
415
- <Text
416
- type="supporting"
417
- color="secondary"
418
- display="block">
419
- This action cannot be undone
420
- </Text>
421
- </VStack>
422
- {/* Deactivating changes state in place. */}
423
- <Button
424
- label="Deactivate"
425
- variant="destructive"
426
- size="sm"
427
- />
428
- </HStack>
429
- </VStack>
430
- </VStack>
431
- )}
432
-
433
- {activeTab === 'shared' && (
434
- <VStack gap={8}>
435
- <VStack gap={2}>
436
- <Heading level={3}>Shared access</Heading>
437
- <Text type="body" color="secondary">
438
- Review each request carefully before approving
439
- access. We&apos;ll email your kin or coven-mate
440
- a 4-digit code that lets them enter your crypt
441
- from their trusted device.
442
- </Text>
443
- </VStack>
444
-
445
- <Card variant="muted">
446
- <HStack gap={4} vAlign="start">
447
- <Center width={48} height={48} style={iconBox}>
448
- <Icon icon={Lock} />
449
- </Center>
450
- <VStack gap={1}>
451
- <Text type="body">
452
- Adding devices for your trusted coven
453
- </Text>
454
- <Text type="body" color="secondary">
455
- When you approve a request, you grant them
456
- full passage through your crypt. They&apos;ll
457
- be able to change bookings and send ravens
458
- on your behalf.
459
- </Text>
460
- </VStack>
461
- </HStack>
462
- </Card>
463
- </VStack>
464
- )}
465
- </VStack>
466
- )}
467
-
468
- {activeNav === 'Languages & currency' && (
469
- <VStack gap={6}>
470
- <VStack gap={4}>
471
- <ExpandableRow
472
- label="Preferred language"
473
- value={
474
- LANGUAGES.find(l => l.value === language)?.label ??
475
- language
476
- }
477
- isExpanded={expandedRow === 'language'}
478
- onEdit={() => handleEdit('language')}
479
- onCancel={handleCancel}
480
- onSave={handleSave}
481
- hasDivider={false}>
482
- <Selector
483
- label="Language"
484
- isLabelHidden
485
- size="lg"
486
- value={language}
487
- onChange={setLanguage}
488
- options={LANGUAGES}
489
- />
490
- </ExpandableRow>
491
- <ExpandableRow
492
- label="Preferred currency"
493
- value={
494
- CURRENCIES.find(c => c.value === currency)?.label ??
495
- currency
496
- }
497
- isExpanded={expandedRow === 'currency'}
498
- onEdit={() => handleEdit('currency')}
499
- onCancel={handleCancel}
500
- onSave={handleSave}
501
- hasDivider={false}>
502
- <Selector
503
- label="Currency"
504
- isLabelHidden
505
- size="lg"
506
- value={currency}
507
- onChange={setCurrency}
508
- options={CURRENCIES}
509
- />
510
- </ExpandableRow>
511
- <ExpandableRow
512
- label="Time zone"
513
- value={
514
- TIMEZONES.find(t => t.value === timezone)?.label ??
515
- timezone
516
- }
517
- isExpanded={expandedRow === 'timezone'}
518
- onEdit={() => handleEdit('timezone')}
519
- onCancel={handleCancel}
520
- onSave={handleSave}
521
- hasDivider={false}>
522
- <Selector
523
- label="Time zone"
524
- isLabelHidden
525
- size="lg"
526
- value={timezone}
527
- onChange={setTimezone}
528
- options={TIMEZONES}
529
- />
530
- </ExpandableRow>
531
- </VStack>
532
- </VStack>
533
- )}
534
-
535
- {activeNav === 'Notifications' && (
536
- <VStack gap={6}>
537
- <VStack gap={4}>
538
- <Heading level={3}>Notifications</Heading>
539
- <Switch
540
- label="Email notifications"
541
- description="Booking updates, reminders, and coven decrees."
542
- value={emailNotif}
543
- onChange={setEmailNotif}
544
- labelPosition="start"
545
- labelSpacing="spread"
546
- />
547
- <Switch
548
- label="Push notifications"
549
- description="Time-sensitive alerts, delivered by raven."
550
- value={pushNotif}
551
- onChange={setPushNotif}
552
- labelPosition="start"
553
- labelSpacing="spread"
554
- />
555
- </VStack>
556
- </VStack>
557
- )}
558
-
559
- {activeNav === 'Payments' && (
560
- <VStack gap={6}>
561
- <VStack gap={4}>
562
- <Heading level={3}>Payments</Heading>
563
- <InfoRowItem
564
- label="Payout crypt"
565
- value="Visa ending in 4821"
566
- action=""
567
- hasDivider={false}
568
- />
569
- <InfoRowItem
570
- label="Past payouts"
571
- value="No tributes yet"
572
- action=""
573
- hasDivider={false}
574
- />
575
- </VStack>
576
- </VStack>
577
- )}
578
-
579
- {activeNav === 'Taxes' && (
580
- <VStack gap={6}>
581
- <VStack gap={4}>
582
- <Heading level={3}>Taxes</Heading>
583
- <InfoRowItem
584
- label="Tax scrolls"
585
- value="Not submitted"
586
- action=""
587
- hasDivider={false}
588
- />
589
- <InfoRowItem
590
- label="Past scrolls"
591
- value="Available after your first tribute"
592
- action=""
593
- hasDivider={false}
594
- />
595
- </VStack>
596
- </VStack>
597
- )}
598
-
599
- {activeNav === 'Travel for work' && (
600
- <VStack gap={6}>
601
- <VStack gap={4}>
602
- <Heading level={3}>Travel for work</Heading>
603
- <Switch
604
- label="Show night-errand options at checkout"
605
- description="Adds a night-errand toggle to your account."
606
- value={workTravel}
607
- onChange={setWorkTravel}
608
- labelPosition="start"
609
- labelSpacing="spread"
610
- />
611
- </VStack>
612
- </VStack>
613
- )}
614
-
615
- {activeNav === 'Privacy' && (
616
- <VStack gap={6}>
617
- <VStack gap={8}>
618
- <VStack gap={4}>
619
- <Heading level={3}>Messages</Heading>
620
- <Switch
621
- label="Show people when I've read their messages."
622
- value={readReceipts}
623
- onChange={setReadReceipts}
624
- labelPosition="start"
625
- labelSpacing="spread"
626
- />
627
- <HStack hAlign="between" vAlign="center">
628
- <Text type="body" weight="semibold">
629
- Blocked people
630
- </Text>
631
- <Link href={SELF_HASH}>View</Link>
632
- </HStack>
633
- </VStack>
634
-
635
- <VStack gap={4}>
636
- <Heading level={3}>Listings</Heading>
637
- <Switch
638
- label="List my wares in the scrying mirrors"
639
- description="Turning this on means scrying engines, like Google, will show your wares to seekers."
640
- value={searchEngines}
641
- onChange={setSearchEngines}
642
- labelPosition="start"
643
- labelSpacing="spread"
644
- />
645
- </VStack>
646
-
647
- <VStack gap={4}>
648
- <Heading level={3}>Reviews</Heading>
649
- <Text type="body" color="secondary">
650
- Choose what&apos;s shared when you write a review.{' '}
651
- <Link href={SELF_HASH} hasUnderline>
652
- Learn more
653
- </Link>
654
- </Text>
655
- <VStack gap={4}>
656
- <Switch
657
- label="Show my home haunt and country"
658
- description="Ex: City and country"
659
- value={showCity}
660
- onChange={setShowCity}
661
- labelPosition="start"
662
- labelSpacing="spread"
663
- />
664
- <Switch
665
- label="Show my journey type"
666
- description="Ex: Rested with kin or familiars"
667
- value={showTripType}
668
- onChange={setShowTripType}
669
- labelPosition="start"
670
- labelSpacing="spread"
671
- />
672
- <Switch
673
- label="Show my length of stay"
674
- description="Ex: A few nights, about a week, etc."
675
- value={showStayLength}
676
- onChange={setShowStayLength}
677
- labelPosition="start"
678
- labelSpacing="spread"
679
- />
680
- <Switch
681
- label="Show my booked revels"
682
- description="Ex: Midnight feasts and tasting rituals"
683
- value={showServices}
684
- onChange={setShowServices}
685
- labelPosition="start"
686
- labelSpacing="spread"
687
- />
688
- </VStack>
689
- </VStack>
690
-
691
- <VStack gap={4}>
692
- <Heading level={3}>Data privacy</Heading>
693
- <Card>
694
- <HStack hAlign="between" vAlign="center">
695
- <Text type="body">Request my personal data</Text>
696
- {/* A request is submitted in place, so it acts. */}
697
- <Button
698
- label="Request"
699
- variant="secondary"
700
- size="sm"
701
- />
702
- </HStack>
703
- </Card>
704
- <Switch
705
- label="Help improve AI-powered features"
706
- description="When this is on, we use your data to develop and improve AI models."
707
- value={aiFeatures}
708
- onChange={setAiFeatures}
709
- labelPosition="start"
710
- labelSpacing="spread"
711
- />
712
- <Card>
713
- <HStack hAlign="between" vAlign="center">
714
- <Text type="body">Delete my account</Text>
715
- <Button
716
- label="Delete"
717
- variant="destructive"
718
- size="sm"
719
- />
720
- </HStack>
721
- </Card>
722
- <Card variant="muted">
723
- <HStack gap={4} vAlign="start">
724
- <Center width={48} height={48} style={iconBox}>
725
- <Icon icon={ShieldCheck} />
726
- </Center>
727
- <VStack gap={1}>
728
- <Text type="body">
729
- Committed to privacy
730
- </Text>
731
- <Text type="body" color="secondary">
732
- We&apos;re committed to keeping your data
733
- protected. See details in our{' '}
734
- <Link href={SELF_HASH} hasUnderline>
735
- Privacy Policy
736
- </Link>
737
- .
738
- </Text>
739
- </VStack>
740
- </HStack>
741
- </Card>
742
- </VStack>
743
- </VStack>
744
- </VStack>
745
- )}
746
- </VStack>
747
- </VStack>
748
- </LayoutContent>
749
- }
750
- />
751
- </Dialog>
752
- </>
753
- );
383
+ {activeTab === "login" && (
384
+ <VStack gap={8}>
385
+ <VStack gap={2}>
386
+ <Heading level={3}>Login</Heading>
387
+ {LOGIN_ROWS.map((row) => (
388
+ <InfoRowItem key={row.label} {...row} hasDivider={false} />
389
+ ))}
390
+ </VStack>
391
+
392
+ <VStack gap={2}>
393
+ <Heading level={3}>Social accounts</Heading>
394
+ {SOCIAL_ROWS.map((row) => (
395
+ <InfoRowItem key={row.label} {...row} hasDivider={false} />
396
+ ))}
397
+ </VStack>
398
+
399
+ <VStack gap={2}>
400
+ <Heading level={3}>Device history</Heading>
401
+ {DEVICE_ROWS.map((device) => (
402
+ <DeviceRowItem key={device.location} {...device} />
403
+ ))}
404
+ </VStack>
405
+
406
+ <VStack gap={2}>
407
+ <Heading level={3}>Account</Heading>
408
+ <HStack hAlign="between" vAlign="start">
409
+ <VStack gap={1}>
410
+ <Text type="body" weight="semibold" display="block">
411
+ Deactivate your account
412
+ </Text>
413
+ <Text type="supporting" color="secondary" display="block">
414
+ This action cannot be undone
415
+ </Text>
416
+ </VStack>
417
+ {/* Deactivating changes state in place. */}
418
+ <Button label="Deactivate" variant="destructive" size="sm" />
419
+ </HStack>
420
+ </VStack>
421
+ </VStack>
422
+ )}
423
+
424
+ {activeTab === "shared" && (
425
+ <VStack gap={8}>
426
+ <VStack gap={2}>
427
+ <Heading level={3}>Shared access</Heading>
428
+ <Text type="body" color="secondary">
429
+ Review each request carefully before approving access. We&apos;ll
430
+ email your kin or coven-mate a 4-digit code that lets them enter your
431
+ crypt from their trusted device.
432
+ </Text>
433
+ </VStack>
434
+
435
+ <Card variant="muted">
436
+ <HStack gap={4} vAlign="start">
437
+ <Center width={48} height={48} style={iconBox}>
438
+ <Icon icon={Lock} />
439
+ </Center>
440
+ <VStack gap={1}>
441
+ <Text type="body">Adding devices for your trusted coven</Text>
442
+ <Text type="body" color="secondary">
443
+ When you approve a request, you grant them full passage through
444
+ your crypt. They&apos;ll be able to change bookings and send
445
+ ravens on your behalf.
446
+ </Text>
447
+ </VStack>
448
+ </HStack>
449
+ </Card>
450
+ </VStack>
451
+ )}
452
+ </VStack>
453
+ )}
454
+
455
+ {activeNav === "Languages & currency" && (
456
+ <VStack gap={6}>
457
+ <VStack gap={4}>
458
+ <ExpandableRow
459
+ label="Preferred language"
460
+ value={LANGUAGES.find((l) => l.value === language)?.label ?? language}
461
+ isExpanded={expandedRow === "language"}
462
+ onEdit={() => handleEdit("language")}
463
+ onCancel={handleCancel}
464
+ onSave={handleSave}
465
+ hasDivider={false}
466
+ >
467
+ <Selector
468
+ label="Language"
469
+ isLabelHidden
470
+ size="lg"
471
+ value={language}
472
+ onChange={setLanguage}
473
+ options={LANGUAGES}
474
+ />
475
+ </ExpandableRow>
476
+ <ExpandableRow
477
+ label="Preferred currency"
478
+ value={CURRENCIES.find((c) => c.value === currency)?.label ?? currency}
479
+ isExpanded={expandedRow === "currency"}
480
+ onEdit={() => handleEdit("currency")}
481
+ onCancel={handleCancel}
482
+ onSave={handleSave}
483
+ hasDivider={false}
484
+ >
485
+ <Selector
486
+ label="Currency"
487
+ isLabelHidden
488
+ size="lg"
489
+ value={currency}
490
+ onChange={setCurrency}
491
+ options={CURRENCIES}
492
+ />
493
+ </ExpandableRow>
494
+ <ExpandableRow
495
+ label="Time zone"
496
+ value={TIMEZONES.find((t) => t.value === timezone)?.label ?? timezone}
497
+ isExpanded={expandedRow === "timezone"}
498
+ onEdit={() => handleEdit("timezone")}
499
+ onCancel={handleCancel}
500
+ onSave={handleSave}
501
+ hasDivider={false}
502
+ >
503
+ <Selector
504
+ label="Time zone"
505
+ isLabelHidden
506
+ size="lg"
507
+ value={timezone}
508
+ onChange={setTimezone}
509
+ options={TIMEZONES}
510
+ />
511
+ </ExpandableRow>
512
+ </VStack>
513
+ </VStack>
514
+ )}
515
+
516
+ {activeNav === "Notifications" && (
517
+ <VStack gap={6}>
518
+ <VStack gap={4}>
519
+ <Heading level={3}>Notifications</Heading>
520
+ <Switch
521
+ label="Email notifications"
522
+ description="Booking updates, reminders, and coven decrees."
523
+ value={emailNotif}
524
+ onChange={setEmailNotif}
525
+ labelPosition="start"
526
+ labelSpacing="spread"
527
+ />
528
+ <Switch
529
+ label="Push notifications"
530
+ description="Time-sensitive alerts, delivered by raven."
531
+ value={pushNotif}
532
+ onChange={setPushNotif}
533
+ labelPosition="start"
534
+ labelSpacing="spread"
535
+ />
536
+ </VStack>
537
+ </VStack>
538
+ )}
539
+
540
+ {activeNav === "Payments" && (
541
+ <VStack gap={6}>
542
+ <VStack gap={4}>
543
+ <Heading level={3}>Payments</Heading>
544
+ <InfoRowItem
545
+ label="Payout crypt"
546
+ value="Visa ending in 4821"
547
+ action=""
548
+ hasDivider={false}
549
+ />
550
+ <InfoRowItem
551
+ label="Past payouts"
552
+ value="No tributes yet"
553
+ action=""
554
+ hasDivider={false}
555
+ />
556
+ </VStack>
557
+ </VStack>
558
+ )}
559
+
560
+ {activeNav === "Taxes" && (
561
+ <VStack gap={6}>
562
+ <VStack gap={4}>
563
+ <Heading level={3}>Taxes</Heading>
564
+ <InfoRowItem
565
+ label="Tax scrolls"
566
+ value="Not submitted"
567
+ action=""
568
+ hasDivider={false}
569
+ />
570
+ <InfoRowItem
571
+ label="Past scrolls"
572
+ value="Available after your first tribute"
573
+ action=""
574
+ hasDivider={false}
575
+ />
576
+ </VStack>
577
+ </VStack>
578
+ )}
579
+
580
+ {activeNav === "Travel for work" && (
581
+ <VStack gap={6}>
582
+ <VStack gap={4}>
583
+ <Heading level={3}>Travel for work</Heading>
584
+ <Switch
585
+ label="Show night-errand options at checkout"
586
+ description="Adds a night-errand toggle to your account."
587
+ value={workTravel}
588
+ onChange={setWorkTravel}
589
+ labelPosition="start"
590
+ labelSpacing="spread"
591
+ />
592
+ </VStack>
593
+ </VStack>
594
+ )}
595
+
596
+ {activeNav === "Privacy" && (
597
+ <VStack gap={6}>
598
+ <VStack gap={8}>
599
+ <VStack gap={4}>
600
+ <Heading level={3}>Messages</Heading>
601
+ <Switch
602
+ label="Show people when I've read their messages."
603
+ value={readReceipts}
604
+ onChange={setReadReceipts}
605
+ labelPosition="start"
606
+ labelSpacing="spread"
607
+ />
608
+ <HStack hAlign="between" vAlign="center">
609
+ <Text type="body" weight="semibold">
610
+ Blocked people
611
+ </Text>
612
+ <Link href={SELF_HASH}>View</Link>
613
+ </HStack>
614
+ </VStack>
615
+
616
+ <VStack gap={4}>
617
+ <Heading level={3}>Listings</Heading>
618
+ <Switch
619
+ label="List my wares in the scrying mirrors"
620
+ description="Turning this on means scrying engines, like Google, will show your wares to seekers."
621
+ value={searchEngines}
622
+ onChange={setSearchEngines}
623
+ labelPosition="start"
624
+ labelSpacing="spread"
625
+ />
626
+ </VStack>
627
+
628
+ <VStack gap={4}>
629
+ <Heading level={3}>Reviews</Heading>
630
+ <Text type="body" color="secondary">
631
+ Choose what&apos;s shared when you write a review.{" "}
632
+ <Link href={SELF_HASH} hasUnderline>
633
+ Learn more
634
+ </Link>
635
+ </Text>
636
+ <VStack gap={4}>
637
+ <Switch
638
+ label="Show my home haunt and country"
639
+ description="Ex: City and country"
640
+ value={showCity}
641
+ onChange={setShowCity}
642
+ labelPosition="start"
643
+ labelSpacing="spread"
644
+ />
645
+ <Switch
646
+ label="Show my journey type"
647
+ description="Ex: Rested with kin or familiars"
648
+ value={showTripType}
649
+ onChange={setShowTripType}
650
+ labelPosition="start"
651
+ labelSpacing="spread"
652
+ />
653
+ <Switch
654
+ label="Show my length of stay"
655
+ description="Ex: A few nights, about a week, etc."
656
+ value={showStayLength}
657
+ onChange={setShowStayLength}
658
+ labelPosition="start"
659
+ labelSpacing="spread"
660
+ />
661
+ <Switch
662
+ label="Show my booked revels"
663
+ description="Ex: Midnight feasts and tasting rituals"
664
+ value={showServices}
665
+ onChange={setShowServices}
666
+ labelPosition="start"
667
+ labelSpacing="spread"
668
+ />
669
+ </VStack>
670
+ </VStack>
671
+
672
+ <VStack gap={4}>
673
+ <Heading level={3}>Data privacy</Heading>
674
+ <Card>
675
+ <HStack hAlign="between" vAlign="center">
676
+ <Text type="body">Request my personal data</Text>
677
+ {/* A request is submitted in place, so it acts. */}
678
+ <Button label="Request" variant="secondary" size="sm" />
679
+ </HStack>
680
+ </Card>
681
+ <Switch
682
+ label="Help improve AI-powered features"
683
+ description="When this is on, we use your data to develop and improve AI models."
684
+ value={aiFeatures}
685
+ onChange={setAiFeatures}
686
+ labelPosition="start"
687
+ labelSpacing="spread"
688
+ />
689
+ <Card>
690
+ <HStack hAlign="between" vAlign="center">
691
+ <Text type="body">Delete my account</Text>
692
+ <Button label="Delete" variant="destructive" size="sm" />
693
+ </HStack>
694
+ </Card>
695
+ <Card variant="muted">
696
+ <HStack gap={4} vAlign="start">
697
+ <Center width={48} height={48} style={iconBox}>
698
+ <Icon icon={ShieldCheck} />
699
+ </Center>
700
+ <VStack gap={1}>
701
+ <Text type="body">Committed to privacy</Text>
702
+ <Text type="body" color="secondary">
703
+ We&apos;re committed to keeping your data protected. See details
704
+ in our{" "}
705
+ <Link href={SELF_HASH} hasUnderline>
706
+ Privacy Policy
707
+ </Link>
708
+ .
709
+ </Text>
710
+ </VStack>
711
+ </HStack>
712
+ </Card>
713
+ </VStack>
714
+ </VStack>
715
+ </VStack>
716
+ )}
717
+ </VStack>
718
+ </VStack>
719
+ </LayoutContent>
720
+ }
721
+ />
722
+ </Dialog>
723
+ </>
724
+ );
754
725
  }