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,843 +1,798 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
- // XLE (canonical structure, validated with `bunx astryx layout check`):
3
- // L > (LP[p=0] > V[g=4] > Tx"Account settings"[t=label] + (UL > LI*8) + D + LI"Professional hosting tools") + (LC[p=6] > V[g=0] > (Tbar > B.ghost + Hd"Personal info"[level=1]) + Hd"Personal info"[level=1] + (V[g=0] > (H[j=between a=start] > (V[g=1] > Tx"Legal name"[weight=semibold] + Tx"Vlad Dracul"[t=supporting]) + B.secondary"Edit")*7) + (C.muted > V[g=4] > (H[g=3 a=start] > Ic + (V[g=1] > Tx"Why is info hidden?"[weight=semibold] + Tx[t=supporting]))*3))
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=fill] > (LP[divider p=0] > V[g=4 paddingBlock=4 paddingInline=3] > Tx"Account settings"[t=label] + (UL[density=spacious edge=flush] > LI*8) + D + (UL[density=spacious edge=flush] > LI"Professional hosting tools")) + (LC[p=6] > V[g=0] > (Tbar[g=2] > B.ghost"Back to Account settings" + Hd"Personal information"[level=1]) + Hd"Personal information"[level=1] + (V[g=0] > (H[j=between a=start] > (V[g=1] > Tx"Legal name"[weight=semibold] + Tx"Vlad Dracul"[t=body]) + B.secondary"Edit")*7 + (H[j=between a=start] > (V[g=1] > Tx"Identity verification"[weight=semibold] + Tx"Verified"[t=body]))) + (C[p=4] > V[g=4] > (H[g=3 a=start] > Ctr[w=48 h=48] > Ic + (V[g=1] > Tx"Why isn't my info shown here?"[weight=semibold] + Tx[t=body]))*3)) ;; L[h=fill] > LC[p=0] > V[g=4 paddingBlock=4 paddingInline=3] > Tx"Account settings"[t=label] + (UL[density=spacious edge=flush] > LI*8) + D + (UL[density=spacious edge=flush] > LI"Professional hosting tools")
4
6
 
5
7
  /**
6
8
  * Settings Panels — account sections with a nav panel and spaced rows. Frame: Layout nav panel (fill) | content column of section views. One (Frame/responsive/container: see XLE header above.)
7
9
  */
8
10
 
9
- import {useState, type CSSProperties} from 'react';
10
- import {useMediaQuery} from '@astryxdesign/core/hooks';
11
+ import { Button } from "@astryxdesign/core/Button";
12
+ import { Card } from "@astryxdesign/core/Card";
13
+ import { Center } from "@astryxdesign/core/Center";
14
+ import { Divider } from "@astryxdesign/core/Divider";
15
+ import { useMediaQuery } from "@astryxdesign/core/hooks";
16
+ import { Icon } from "@astryxdesign/core/Icon";
11
17
  import {
12
- VStack,
13
- HStack,
14
- StackItem,
15
- Layout,
16
- LayoutContent,
17
- LayoutPanel,
18
- } from '@astryxdesign/core/Layout';
19
- import {List, ListItem} from '@astryxdesign/core/List';
20
- import {Toolbar} from '@astryxdesign/core/Toolbar';
21
- import {Text, Heading} from '@astryxdesign/core/Text';
22
- import {Link} from '@astryxdesign/core/Link';
23
- import {Button} from '@astryxdesign/core/Button';
24
- import {Selector} from '@astryxdesign/core/Selector';
25
- import {TextInput} from '@astryxdesign/core/TextInput';
26
- import {Card} from '@astryxdesign/core/Card';
27
- import {Switch} from '@astryxdesign/core/Switch';
28
- import {Divider} from '@astryxdesign/core/Divider';
29
- import {TabList, Tab} from '@astryxdesign/core/TabList';
30
- import {StatusDot} from '@astryxdesign/core/StatusDot';
31
- import {Icon} from '@astryxdesign/core/Icon';
32
- import {Center} from '@astryxdesign/core/Center';
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 { Toolbar } from "@astryxdesign/core/Toolbar";
34
+ import type { InfoRow } from "astryx-dracula/shared/settings-data";
33
35
  import {
34
- Lock,
35
- ShieldCheck,
36
- Wrench,
37
- Monitor,
38
- ArrowLeft,
39
- ChevronRight,
40
- } from 'lucide-react';
41
- import {NAV_ITEMS, LOGIN_ROWS, SOCIAL_ROWS, DEVICE_ROWS, INFO_TILES, LANGUAGES, CURRENCIES, TIMEZONES, iconBox, actionNoWrap, sideNavHeading} from 'astryx-dracula/shared/settings-data';
42
- import {InfoRowItem, ExpandableRow} from 'astryx-dracula/shared/settings-rows';
43
- import type {InfoRow} from 'astryx-dracula/shared/settings-data';
36
+ actionNoWrap,
37
+ CURRENCIES,
38
+ DEVICE_ROWS,
39
+ INFO_TILES,
40
+ iconBox,
41
+ LANGUAGES,
42
+ LOGIN_ROWS,
43
+ NAV_ITEMS,
44
+ SOCIAL_ROWS,
45
+ sideNavHeading,
46
+ TIMEZONES,
47
+ } from "astryx-dracula/shared/settings-data";
48
+ import { ExpandableRow, InfoRowItem } from "astryx-dracula/shared/settings-rows";
49
+ import { ArrowLeft, ChevronRight, Lock, Monitor, ShieldCheck, Wrench } from "lucide-react";
50
+ import { type CSSProperties, useState } from "react";
44
51
 
45
52
  // Anchor the page to the viewport height so the sidebar + content fill the
46
53
  // screen. Layout height="fill" is min-height:100% which collapses when the
47
54
  // host container is content-sized; Layout has no viewport-height prop.
48
55
  const fillViewport: CSSProperties = {
49
- minHeight: '100dvh',
56
+ minHeight: "100dvh",
50
57
  };
51
58
  const rowPadding: CSSProperties = {
52
- paddingBlock: 'var(--spacing-4)',
59
+ paddingBlock: "var(--spacing-4)",
53
60
  };
54
61
 
55
62
  // Same-route hash: demo links stay focusable anchors without escaping the
56
63
  // template through the hash router (bare "#" would drop back to the home page).
57
- const SELF_HASH = '#/templates/settings-sidebar';
64
+ const SELF_HASH = "#/templates/settings-sidebar";
58
65
 
59
66
  // Title beside the mobile back button: the section's own heading, except the
60
67
  // long "Professional hosting tools" which shortens to "Hosting tools".
61
- const HOSTING_TOOLS_TITLE = 'Professional hosting tools';
68
+ const HOSTING_TOOLS_TITLE = "Professional hosting tools";
62
69
 
63
70
  // Both record sections pair a setup row ("Add") with a history row that
64
71
  // navigates to past records — one factory, not two copied arrays.
65
72
  function recordRows(
66
- currentLabel: string,
67
- currentValue: string,
68
- pastLabel: string,
69
- pastValue: string,
73
+ currentLabel: string,
74
+ currentValue: string,
75
+ pastLabel: string,
76
+ pastValue: string,
70
77
  ): InfoRow[] {
71
- return [
72
- {label: currentLabel, value: currentValue, action: 'Add'},
73
- {
74
- label: pastLabel,
75
- value: pastValue,
76
- action: 'View',
77
- actionKind: 'destination',
78
- href: SELF_HASH,
79
- },
80
- ];
78
+ return [
79
+ { label: currentLabel, value: currentValue, action: "Add" },
80
+ {
81
+ label: pastLabel,
82
+ value: pastValue,
83
+ action: "View",
84
+ actionKind: "destination",
85
+ href: SELF_HASH,
86
+ },
87
+ ];
81
88
  }
82
89
 
83
- const TAX_ROWS = recordRows(
84
- 'Tax scrolls',
85
- 'Not provided',
86
- 'Past scrolls',
87
- 'No scrolls yet',
88
- );
90
+ const TAX_ROWS = recordRows("Tax scrolls", "Not provided", "Past scrolls", "No scrolls yet");
89
91
 
90
- const PAYOUT_ROWS = recordRows(
91
- 'Payout crypt',
92
- 'Not set up',
93
- 'Past payouts',
94
- 'No tributes yet',
95
- );
92
+ const PAYOUT_ROWS = recordRows("Payout crypt", "Not set up", "Past payouts", "No tributes yet");
96
93
 
97
94
  export default function SettingsSidebar() {
98
- const isNarrow = useMediaQuery('(max-width: 768px)');
99
- // Mobile is a master→detail drill-down: 'nav' shows the menu, 'detail' shows
100
- // the selected section with a back button. Desktop shows both side-by-side.
101
- const [mobileView, setMobileView] = useState<'nav' | 'detail'>('nav');
102
- const [activeNav, setActiveNav] = useState('Personal information');
103
- const [activeTab, setActiveTab] = useState('login');
104
- const [readReceipts, setReadReceipts] = useState(true);
105
- const [searchEngines, setSearchEngines] = useState(true);
106
- const [showCity, setShowCity] = useState(true);
107
- const [showTripType, setShowTripType] = useState(true);
108
- const [showStayLength, setShowStayLength] = useState(true);
109
- const [showServices, setShowServices] = useState(true);
110
- const [aiFeatures, setAiFeatures] = useState(true);
111
- const [emailNotif, setEmailNotif] = useState(true);
112
- const [pushNotif, setPushNotif] = useState(true);
113
- const [workTrips, setWorkTrips] = useState(false);
114
-
115
- const [expandedRow, setExpandedRow] = useState<string | null>(null);
116
- const [language, setLanguage] = useState('en-CA');
117
- const [currency, setCurrency] = useState('CAD');
118
- const [timezone, setTimezone] = useState('ET');
119
-
120
- const [legalName, setLegalName] = useState('Vlad Dracul');
121
- const [preferredName, setPreferredName] = useState('');
122
- const [email, setEmail] = useState('v***d@castle-dracula.ro');
123
- const [phone, setPhone] = useState('+1 ***-***-0123');
124
- const [address, setAddress] = useState('');
125
- const [mailingAddress, setMailingAddress] = useState('');
126
- const [emergencyContact, setEmergencyContact] = useState('Provided');
127
-
128
- // Selecting a nav item also drills into the detail view on mobile.
129
- const selectNav = (label: string) => {
130
- setActiveNav(label);
131
- setMobileView('detail');
132
- };
133
-
134
- const navList = (
135
- <VStack gap={4} paddingBlock={4} paddingInline={3}>
136
- <Text type="label" style={sideNavHeading}>
137
- Account settings
138
- </Text>
139
- <List density="spacious">
140
- {NAV_ITEMS.map(item => (
141
- <ListItem
142
- key={item.label}
143
- label={item.label}
144
- startContent={<Icon icon={item.icon} />}
145
- endContent={
146
- isNarrow ? (
147
- <Icon icon={ChevronRight} size="sm" color="secondary" />
148
- ) : undefined
149
- }
150
- isSelected={!isNarrow && activeNav === item.label}
151
- onClick={() => selectNav(item.label)}
152
- />
153
- ))}
154
- </List>
155
- <Divider />
156
- <List density="spacious">
157
- <ListItem
158
- label="Professional hosting tools"
159
- startContent={<Icon icon={Wrench} />}
160
- />
161
- </List>
162
- </VStack>
163
- );
164
-
165
- // Mobile, nav view: show only the menu (full width, no sidebar slot).
166
- if (isNarrow && mobileView === 'nav') {
167
- return (
168
- <Layout
169
- height="fill"
170
- style={fillViewport}
171
- // padding={0}, not padding={2}. This wraps the SAME navList element
172
- // the desktop LayoutPanel renders with padding={0}, and the navList's
173
- // own paddingBlock/paddingInline props supply the 16px/12px inset.
174
- // The extra 8px here is what made the same nav list measure 20px on
175
- // mobile against 12px on desktop -- and against 24px further down,
176
- // disagreed with each other too. The container owns no gutter; the
177
- // list owns its own.
178
- content={<LayoutContent padding={0}>{navList}</LayoutContent>}
179
- />
180
- );
181
- }
182
-
183
- return (
184
- <Layout
185
- height="fill"
186
- contentWidth={1200}
187
- style={fillViewport}
188
- start={
189
- isNarrow ? undefined : (
190
- <LayoutPanel hasDivider padding={0}>
191
- {navList}
192
- </LayoutPanel>
193
- )
194
- }
195
- content={
196
- <LayoutContent padding={6}>
197
- <VStack gap={0}>
198
- {/* Mobile detail view: a back button sits beside the section title
95
+ const isNarrow = useMediaQuery("(max-width: 768px)");
96
+ // Mobile is a master→detail drill-down: 'nav' shows the menu, 'detail' shows
97
+ // the selected section with a back button. Desktop shows both side-by-side.
98
+ const [mobileView, setMobileView] = useState<"nav" | "detail">("nav");
99
+ const [activeNav, setActiveNav] = useState("Personal information");
100
+ const [activeTab, setActiveTab] = useState("login");
101
+ const [readReceipts, setReadReceipts] = useState(true);
102
+ const [searchEngines, setSearchEngines] = useState(true);
103
+ const [showCity, setShowCity] = useState(true);
104
+ const [showTripType, setShowTripType] = useState(true);
105
+ const [showStayLength, setShowStayLength] = useState(true);
106
+ const [showServices, setShowServices] = useState(true);
107
+ const [aiFeatures, setAiFeatures] = useState(true);
108
+ const [emailNotif, setEmailNotif] = useState(true);
109
+ const [pushNotif, setPushNotif] = useState(true);
110
+ const [workTrips, setWorkTrips] = useState(false);
111
+
112
+ const [expandedRow, setExpandedRow] = useState<string | null>(null);
113
+ const [language, setLanguage] = useState("en-CA");
114
+ const [currency, setCurrency] = useState("CAD");
115
+ const [timezone, setTimezone] = useState("ET");
116
+
117
+ const [legalName, setLegalName] = useState("Vlad Dracul");
118
+ const [preferredName, setPreferredName] = useState("");
119
+ const [email, setEmail] = useState("v***d@castle-dracula.ro");
120
+ const [phone, setPhone] = useState("+1 ***-***-0123");
121
+ const [address, setAddress] = useState("");
122
+ const [mailingAddress, setMailingAddress] = useState("");
123
+ const [emergencyContact, setEmergencyContact] = useState("Provided");
124
+
125
+ // Selecting a nav item also drills into the detail view on mobile.
126
+ const selectNav = (label: string) => {
127
+ setActiveNav(label);
128
+ setMobileView("detail");
129
+ };
130
+
131
+ const navList = (
132
+ <VStack gap={4} paddingBlock={4} paddingInline={3}>
133
+ <Text type="label" style={sideNavHeading}>
134
+ Account settings
135
+ </Text>
136
+ <List density="spacious" edge="flush">
137
+ {NAV_ITEMS.map((item) => (
138
+ <ListItem
139
+ key={item.label}
140
+ label={item.label}
141
+ startContent={<Icon icon={item.icon} />}
142
+ endContent={
143
+ isNarrow ? <Icon icon={ChevronRight} size="sm" color="secondary" /> : undefined
144
+ }
145
+ isSelected={!isNarrow && activeNav === item.label}
146
+ onClick={() => selectNav(item.label)}
147
+ />
148
+ ))}
149
+ </List>
150
+ <Divider />
151
+ <List density="spacious" edge="flush">
152
+ <ListItem label="Professional hosting tools" startContent={<Icon icon={Wrench} />} />
153
+ </List>
154
+ </VStack>
155
+ );
156
+
157
+ // Mobile, nav view: show only the menu (full width, no sidebar slot).
158
+ if (isNarrow && mobileView === "nav") {
159
+ return (
160
+ <Layout
161
+ height="fill"
162
+ style={fillViewport}
163
+ // padding={0}, not padding={2}. This wraps the SAME navList element
164
+ // the desktop LayoutPanel renders with padding={0}, and the navList's
165
+ // own paddingBlock/paddingInline props supply the 16px/12px inset.
166
+ // The extra 8px here is what made the same nav list measure 20px on
167
+ // mobile against 12px on desktop -- and against 24px further down,
168
+ // disagreed with each other too. The container owns no gutter; the
169
+ // list owns its own.
170
+ content={<LayoutContent padding={0}>{navList}</LayoutContent>}
171
+ />
172
+ );
173
+ }
174
+
175
+ return (
176
+ <Layout
177
+ height="fill"
178
+ contentWidth={1200}
179
+ style={fillViewport}
180
+ start={
181
+ isNarrow ? undefined : (
182
+ <LayoutPanel hasDivider padding={0}>
183
+ {navList}
184
+ </LayoutPanel>
185
+ )
186
+ }
187
+ content={
188
+ <LayoutContent padding={6}>
189
+ <VStack gap={0}>
190
+ {/* Mobile detail view: a back button sits beside the section title
199
191
  (the per-section headings below are hidden on mobile). Toolbar's
200
192
  start slot edge-compensates the ghost button so its icon aligns
201
193
  flush with the content edge. */}
202
- {isNarrow && (
203
- <Toolbar
204
- label={`Back to Account settings: ${activeNav === HOSTING_TOOLS_TITLE ? 'Hosting tools' : activeNav}`}
205
- gap={2}
206
- startContent={
207
- <>
208
- <Button
209
- label="Back to Account settings"
210
- variant="ghost"
211
- size="sm"
212
- isIconOnly
213
- icon={<Icon icon={ArrowLeft} size="sm" />}
214
- onClick={() => setMobileView('nav')}
215
- />
216
- <Heading level={1}>{activeNav === HOSTING_TOOLS_TITLE ? 'Hosting tools' : activeNav}</Heading>
217
- </>
218
- }
219
- />
220
- )}
221
- {activeNav === 'Login & security' && (
222
- <VStack gap={6}>
223
- {!isNarrow && <Heading level={1}>Login &amp; security</Heading>}
224
-
225
- <TabList value={activeTab} onChange={setActiveTab} hasDivider>
226
- <Tab value="login" label="Login" />
227
- <Tab value="shared" label="Shared access" />
228
- </TabList>
229
-
230
- {activeTab === 'login' && (
231
- <VStack gap={8}>
232
- <VStack gap={2}>
233
- <Heading level={2}>Login</Heading>
234
- {LOGIN_ROWS.map(row => (
235
- <InfoRowItem
236
- key={row.label}
237
- {...row}
238
- hasDivider={false}
239
- style={rowPadding}
240
- />
241
- ))}
242
- </VStack>
243
-
244
- <VStack gap={2}>
245
- <Heading level={2}>Social accounts</Heading>
246
- {SOCIAL_ROWS.map(row => (
247
- <InfoRowItem
248
- key={row.label}
249
- {...row}
250
- hasDivider={false}
251
- style={rowPadding}
252
- />
253
- ))}
254
- </VStack>
255
-
256
- <VStack gap={2}>
257
- <Heading level={2}>Device history</Heading>
258
- {DEVICE_ROWS.map((device) => (
259
- <HStack
260
- key={device.location}
261
- gap={3}
262
- vAlign="start"
263
- style={rowPadding}>
264
- <Icon icon={Monitor} />
265
- <StackItem size="fill">
266
- <VStack gap={1}>
267
- <HStack gap={2} vAlign="center" wrap="wrap">
268
- <Text type="body" weight="semibold">
269
- {device.label}
270
- </Text>
271
- {device.isCurrent && (
272
- <HStack gap={1} vAlign="center">
273
- <StatusDot
274
- variant="success"
275
- label="Current session"
276
- />
277
- <Text type="supporting" color="secondary">
278
- Current session
279
- </Text>
280
- </HStack>
281
- )}
282
- </HStack>
283
- <Text
284
- type="supporting"
285
- color="secondary"
286
- display="block"
287
- hasTabularNumbers>
288
- {device.location}
289
- </Text>
290
- </VStack>
291
- </StackItem>
292
- {device.action && (
293
- <Button
294
- label={device.action}
295
- variant="secondary"
296
- size="sm"
297
- onClick={() => {}}
298
- style={actionNoWrap}
299
- />
300
- )}
301
- </HStack>
302
- ))}
303
- </VStack>
304
-
305
- <VStack gap={2}>
306
- <Heading level={2}>Account</Heading>
307
- <HStack
308
- hAlign="between"
309
- vAlign="start"
310
- style={rowPadding}>
311
- <VStack gap={1}>
312
- <Text type="body" weight="semibold" display="block">
313
- Deactivate your account
314
- </Text>
315
- <Text
316
- type="supporting"
317
- color="secondary"
318
- display="block">
319
- This action cannot be undone
320
- </Text>
321
- </VStack>
322
- <Button
323
- label="Deactivate"
324
- variant="secondary"
325
- size="sm"
326
- onClick={() => {}}
327
- style={actionNoWrap}
328
- />
329
- </HStack>
330
- </VStack>
331
- </VStack>
332
- )}
333
-
334
- {activeTab === 'shared' && (
335
- <VStack gap={8}>
336
- <VStack gap={2}>
337
- <Heading level={2}>Shared access</Heading>
338
- <Text type="body" color="secondary">
339
- Review each request carefully before approving access.
340
- We&apos;ll email your kin or coven-mate a 4-digit
341
- code that lets them enter your crypt from their
342
- trusted device.
343
- </Text>
344
- </VStack>
345
-
346
- <Card variant="muted">
347
- <HStack gap={4} vAlign="start">
348
- <Center width={48} height={48} style={iconBox}>
349
- <Icon icon={Lock} />
350
- </Center>
351
- <VStack gap={1}>
352
- <Text type="body">
353
- Adding devices from people you trust
354
- </Text>
355
- <Text type="body" color="secondary">
356
- When you approve a request, you grant someone full
357
- access to your account. They&apos;ll be able to
358
- change reservations and send messages on your
359
- behalf.
360
- </Text>
361
- </VStack>
362
- </HStack>
363
- </Card>
364
- </VStack>
365
- )}
366
- </VStack>
367
- )}
368
-
369
- {activeNav === 'Languages & currency' && (
370
- <VStack gap={6}>
371
- {!isNarrow && (
372
- <Heading level={1}>Languages &amp; currency</Heading>
373
- )}
374
- <VStack gap={0}>
375
- <ExpandableRow
376
- label="Preferred language"
377
- value={
378
- LANGUAGES.find(l => l.value === language)?.label ??
379
- language
380
- }
381
- isExpanded={expandedRow === 'language'}
382
- onEdit={() => setExpandedRow('language')}
383
- onCancel={() => setExpandedRow(null)}
384
- onSave={() => setExpandedRow(null)}
385
- hasDivider={false}
386
- style={rowPadding}>
387
- <Selector
388
- label="Language"
389
- isLabelHidden
390
- size="lg"
391
- value={language}
392
- onChange={setLanguage}
393
- options={LANGUAGES}
394
- />
395
- </ExpandableRow>
396
- <ExpandableRow
397
- label="Preferred currency"
398
- value={
399
- CURRENCIES.find(c => c.value === currency)?.label ??
400
- currency
401
- }
402
- isExpanded={expandedRow === 'currency'}
403
- onEdit={() => setExpandedRow('currency')}
404
- onCancel={() => setExpandedRow(null)}
405
- onSave={() => setExpandedRow(null)}
406
- hasDivider={false}
407
- style={rowPadding}>
408
- <Selector
409
- label="Currency"
410
- isLabelHidden
411
- size="lg"
412
- value={currency}
413
- onChange={setCurrency}
414
- options={CURRENCIES}
415
- />
416
- </ExpandableRow>
417
- <ExpandableRow
418
- label="Time zone"
419
- value={
420
- TIMEZONES.find(t => t.value === timezone)?.label ??
421
- timezone
422
- }
423
- isExpanded={expandedRow === 'timezone'}
424
- onEdit={() => setExpandedRow('timezone')}
425
- onCancel={() => setExpandedRow(null)}
426
- onSave={() => setExpandedRow(null)}
427
- hasDivider={false}
428
- style={rowPadding}>
429
- <Selector
430
- label="Time zone"
431
- isLabelHidden
432
- size="lg"
433
- value={timezone}
434
- onChange={setTimezone}
435
- options={TIMEZONES}
436
- />
437
- </ExpandableRow>
438
- </VStack>
439
- </VStack>
440
- )}
441
-
442
- {activeNav === 'Personal information' && (
443
- <VStack gap={6}>
444
- {!isNarrow && (
445
- <Heading level={1}>Personal information</Heading>
446
- )}
447
- <VStack gap={0}>
448
- <ExpandableRow
449
- label="Legal name"
450
- value={legalName}
451
- isExpanded={expandedRow === 'legalName'}
452
- onEdit={() => setExpandedRow('legalName')}
453
- onCancel={() => setExpandedRow(null)}
454
- onSave={() => setExpandedRow(null)}
455
- hasDivider={false}
456
- style={rowPadding}>
457
- <TextInput
458
- label="Legal name"
459
- isLabelHidden
460
- value={legalName}
461
- onChange={setLegalName}
462
- />
463
- </ExpandableRow>
464
- <ExpandableRow
465
- label="Preferred first name"
466
- value={preferredName || 'Not provided'}
467
- isExpanded={expandedRow === 'preferredName'}
468
- onEdit={() => setExpandedRow('preferredName')}
469
- onCancel={() => setExpandedRow(null)}
470
- onSave={() => setExpandedRow(null)}
471
- hasDivider={false}
472
- style={rowPadding}>
473
- <TextInput
474
- label="Preferred first name"
475
- isLabelHidden
476
- value={preferredName}
477
- onChange={setPreferredName}
478
- />
479
- </ExpandableRow>
480
- <ExpandableRow
481
- label="Email address"
482
- value={email}
483
- isExpanded={expandedRow === 'email'}
484
- onEdit={() => setExpandedRow('email')}
485
- onCancel={() => setExpandedRow(null)}
486
- onSave={() => setExpandedRow(null)}
487
- hasDivider={false}
488
- style={rowPadding}>
489
- <TextInput
490
- label="Email address"
491
- isLabelHidden
492
- value={email}
493
- onChange={setEmail}
494
- />
495
- </ExpandableRow>
496
- <ExpandableRow
497
- label="Phone number"
498
- value={phone}
499
- isExpanded={expandedRow === 'phone'}
500
- onEdit={() => setExpandedRow('phone')}
501
- onCancel={() => setExpandedRow(null)}
502
- onSave={() => setExpandedRow(null)}
503
- hasDivider={false}
504
- style={rowPadding}>
505
- <TextInput
506
- label="Phone number"
507
- isLabelHidden
508
- value={phone}
509
- onChange={setPhone}
510
- />
511
- </ExpandableRow>
512
- <InfoRowItem
513
- label="Identity verification"
514
- value="Verified"
515
- action=""
516
- hasDivider={false}
517
- style={rowPadding}
518
- />
519
- <ExpandableRow
520
- label="Residential address"
521
- value={address || 'Not provided'}
522
- isExpanded={expandedRow === 'address'}
523
- onEdit={() => setExpandedRow('address')}
524
- onCancel={() => setExpandedRow(null)}
525
- onSave={() => setExpandedRow(null)}
526
- hasDivider={false}
527
- style={rowPadding}>
528
- <TextInput
529
- label="Residential address"
530
- isLabelHidden
531
- value={address}
532
- onChange={setAddress}
533
- />
534
- </ExpandableRow>
535
- <ExpandableRow
536
- label="Mailing address"
537
- value={mailingAddress || 'Not provided'}
538
- isExpanded={expandedRow === 'mailingAddress'}
539
- onEdit={() => setExpandedRow('mailingAddress')}
540
- onCancel={() => setExpandedRow(null)}
541
- onSave={() => setExpandedRow(null)}
542
- hasDivider={false}
543
- style={rowPadding}>
544
- <TextInput
545
- label="Mailing address"
546
- isLabelHidden
547
- value={mailingAddress}
548
- onChange={setMailingAddress}
549
- />
550
- </ExpandableRow>
551
- <ExpandableRow
552
- label="Emergency contact"
553
- value={emergencyContact}
554
- isExpanded={expandedRow === 'emergencyContact'}
555
- onEdit={() => setExpandedRow('emergencyContact')}
556
- onCancel={() => setExpandedRow(null)}
557
- onSave={() => setExpandedRow(null)}
558
- hasDivider={false}
559
- style={rowPadding}>
560
- <TextInput
561
- label="Emergency contact"
562
- isLabelHidden
563
- value={emergencyContact}
564
- onChange={setEmergencyContact}
565
- />
566
- </ExpandableRow>
567
- </VStack>
568
-
569
- <Card padding={4}>
570
- <VStack gap={4}>
571
- {INFO_TILES.map(tile => (
572
- <HStack key={tile.title} gap={3} vAlign="start">
573
- <Center width={48} height={48} style={iconBox}>
574
- <Icon icon={tile.icon} />
575
- </Center>
576
- <VStack gap={1}>
577
- <Text type="body" weight="semibold" display="block">
578
- {tile.title}
579
- </Text>
580
- <Text type="body" color="secondary" display="block">
581
- {tile.body}
582
- </Text>
583
- </VStack>
584
- </HStack>
585
- ))}
586
- </VStack>
587
- </Card>
588
- </VStack>
589
- )}
590
-
591
- {activeNav === 'Privacy' && (
592
- <VStack gap={6}>
593
- {!isNarrow && <Heading level={1}>Privacy</Heading>}
594
-
595
- <VStack gap={8}>
596
- <VStack gap={2}>
597
- <Heading level={2}>Messages</Heading>
598
- <VStack style={rowPadding}>
599
- <Switch
600
- label="Show people when I've read their messages."
601
- value={readReceipts}
602
- onChange={setReadReceipts}
603
- labelPosition="start"
604
- labelSpacing="spread"
605
- />
606
- </VStack>
607
- <HStack hAlign="between" vAlign="center" style={rowPadding}>
608
- <Text type="body" weight="semibold">
609
- Blocked people
610
- </Text>
611
- <Link href={SELF_HASH}>View</Link>
612
- </HStack>
613
- </VStack>
614
-
615
- <VStack gap={2}>
616
- <Heading level={2}>Listings</Heading>
617
- <VStack style={rowPadding}>
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
- </VStack>
628
-
629
- <VStack gap={4}>
630
- <Heading level={2}>Reviews</Heading>
631
- <Text type="body" color="secondary">
632
- Choose what&apos;s shared when you write a review.{' '}
633
- <Link href={SELF_HASH} type="body" hasUnderline>
634
- Learn more
635
- </Link>
636
- </Text>
637
- <VStack gap={4}>
638
- <Switch
639
- label="Show my home haunt and country"
640
- description="Ex: City and country"
641
- value={showCity}
642
- onChange={setShowCity}
643
- labelPosition="start"
644
- labelSpacing="spread"
645
- />
646
- <Switch
647
- label="Show my journey type"
648
- description="Ex: Rested with kin or familiars"
649
- value={showTripType}
650
- onChange={setShowTripType}
651
- labelPosition="start"
652
- labelSpacing="spread"
653
- />
654
- <Switch
655
- label="Show my length of stay"
656
- description="Ex: A few nights, about a week, etc."
657
- value={showStayLength}
658
- onChange={setShowStayLength}
659
- labelPosition="start"
660
- labelSpacing="spread"
661
- />
662
- <Switch
663
- label="Show my booked revels"
664
- description="Ex: Midnight feasts and tasting rituals"
665
- value={showServices}
666
- onChange={setShowServices}
667
- labelPosition="start"
668
- labelSpacing="spread"
669
- />
670
- </VStack>
671
- </VStack>
672
-
673
- <VStack gap={4}>
674
- <Heading level={2}>Data privacy</Heading>
675
- <Card>
676
- <HStack hAlign="between" vAlign="center">
677
- <Text type="body">Request my personal data</Text>
678
- <Button
679
- label="Request"
680
- variant="secondary"
681
- size="sm"
682
- onClick={() => {}}
683
- style={actionNoWrap}
684
- />
685
- </HStack>
686
- </Card>
687
- <Switch
688
- label="Help improve AI-powered features"
689
- description="When this is on, we use your data to develop and improve AI models."
690
- value={aiFeatures}
691
- onChange={setAiFeatures}
692
- labelPosition="start"
693
- labelSpacing="spread"
694
- />
695
- <Card>
696
- <HStack hAlign="between" vAlign="center">
697
- <Text type="body">Delete my account</Text>
698
- <Button
699
- label="Delete"
700
- variant="secondary"
701
- size="sm"
702
- onClick={() => {}}
703
- style={actionNoWrap}
704
- />
705
- </HStack>
706
- </Card>
707
- <Card variant="muted">
708
- <HStack gap={4} vAlign="start">
709
- <Center width={48} height={48} style={iconBox}>
710
- <Icon icon={ShieldCheck} />
711
- </Center>
712
- <VStack gap={1}>
713
- <Text type="body">
714
- Committed to privacy
715
- </Text>
716
- <Text type="body" color="secondary">
717
- We&apos;re committed to keeping your data protected.
718
- See details in our{' '}
719
- <Link
720
- href={SELF_HASH}
721
- type="supporting"
722
- hasUnderline>
723
- Privacy Policy
724
- </Link>
725
- .
726
- </Text>
727
- </VStack>
728
- </HStack>
729
- </Card>
730
- </VStack>
731
- </VStack>
732
- </VStack>
733
- )}
734
-
735
- {activeNav === 'Notifications' && (
736
- <VStack gap={6}>
737
- {!isNarrow && <Heading level={1}>Notifications</Heading>}
738
- <VStack gap={2}>
739
- <Heading level={2}>Messages</Heading>
740
- <VStack gap={4} style={rowPadding}>
741
- <Switch
742
- label="Email notifications"
743
- description="Coven updates, journey reminders, and crypt activity."
744
- value={emailNotif}
745
- onChange={setEmailNotif}
746
- labelPosition="start"
747
- labelSpacing="spread"
748
- />
749
- <Switch
750
- label="Push notifications"
751
- description="Swift ravens for messages and booking requests."
752
- value={pushNotif}
753
- onChange={setPushNotif}
754
- labelPosition="start"
755
- labelSpacing="spread"
756
- />
757
- </VStack>
758
- </VStack>
759
- </VStack>
760
- )}
761
-
762
- {activeNav === 'Taxes' && (
763
- <VStack gap={6}>
764
- {!isNarrow && <Heading level={1}>Taxes</Heading>}
765
- <VStack gap={2}>
766
- <Heading level={2}>Tax scrolls</Heading>
767
- {TAX_ROWS.map(row => (
768
- <InfoRowItem
769
- key={row.label}
770
- {...row}
771
- hasDivider={false}
772
- style={rowPadding}
773
- />
774
- ))}
775
- </VStack>
776
- </VStack>
777
- )}
778
-
779
- {activeNav === 'Payments' && (
780
- <VStack gap={6}>
781
- {!isNarrow && <Heading level={1}>Payments</Heading>}
782
- <VStack gap={2}>
783
- <Heading level={2}>Payout crypt</Heading>
784
- {PAYOUT_ROWS.map(row => (
785
- <InfoRowItem
786
- key={row.label}
787
- {...row}
788
- hasDivider={false}
789
- style={rowPadding}
790
- />
791
- ))}
792
- </VStack>
793
- </VStack>
794
- )}
795
-
796
- {activeNav === 'Travel for work' && (
797
- <VStack gap={6}>
798
- {!isNarrow && <Heading level={1}>Travel for work</Heading>}
799
- <VStack gap={2}>
800
- <Heading level={2}>Work haunts</Heading>
801
- <VStack style={rowPadding}>
802
- <Switch
803
- label="Show night-errand options at checkout"
804
- description="Add a coven address to expense night errands and unlock travel-ready wares."
805
- value={workTrips}
806
- onChange={setWorkTrips}
807
- labelPosition="start"
808
- labelSpacing="spread"
809
- />
810
- </VStack>
811
- </VStack>
812
- </VStack>
813
- )}
814
-
815
- {activeNav === 'Professional hosting tools' && (
816
- <VStack gap={6}>
817
- {!isNarrow && (
818
- <Heading level={1}>Professional hosting tools</Heading>
819
- )}
820
- <Card variant="muted">
821
- <HStack gap={4} vAlign="start">
822
- <Center width={48} height={48} style={iconBox}>
823
- <Icon icon={Wrench} />
824
- </Center>
825
- <VStack gap={1}>
826
- <Text type="body">
827
- Hosting grimoire
828
- </Text>
829
- <Text type="body" color="secondary">
830
- Manage multiple listings, route tasks to co-hosts, and
831
- review consolidated payouts from one place.
832
- </Text>
833
- </VStack>
834
- </HStack>
835
- </Card>
836
- </VStack>
837
- )}
838
- </VStack>
839
- </LayoutContent>
840
- }
841
- />
842
- );
194
+ {isNarrow && (
195
+ <Toolbar
196
+ label={`Back to Account settings: ${activeNav === HOSTING_TOOLS_TITLE ? "Hosting tools" : activeNav}`}
197
+ gap={2}
198
+ startContent={
199
+ <>
200
+ <Button
201
+ label="Back to Account settings"
202
+ variant="ghost"
203
+ size="sm"
204
+ isIconOnly
205
+ icon={<Icon icon={ArrowLeft} size="sm" />}
206
+ onClick={() => setMobileView("nav")}
207
+ />
208
+ <Heading level={1}>
209
+ {activeNav === HOSTING_TOOLS_TITLE ? "Hosting tools" : activeNav}
210
+ </Heading>
211
+ </>
212
+ }
213
+ />
214
+ )}
215
+ {activeNav === "Login & security" && (
216
+ <VStack gap={6}>
217
+ {!isNarrow && <Heading level={1}>Login &amp; security</Heading>}
218
+
219
+ <TabList value={activeTab} onChange={setActiveTab} hasDivider>
220
+ <Tab value="login" label="Login" />
221
+ <Tab value="shared" label="Shared access" />
222
+ </TabList>
223
+
224
+ {activeTab === "login" && (
225
+ <VStack gap={8}>
226
+ <VStack gap={2}>
227
+ <Heading level={2}>Login</Heading>
228
+ {LOGIN_ROWS.map((row) => (
229
+ <InfoRowItem
230
+ key={row.label}
231
+ {...row}
232
+ hasDivider={false}
233
+ style={rowPadding}
234
+ />
235
+ ))}
236
+ </VStack>
237
+
238
+ <VStack gap={2}>
239
+ <Heading level={2}>Social accounts</Heading>
240
+ {SOCIAL_ROWS.map((row) => (
241
+ <InfoRowItem
242
+ key={row.label}
243
+ {...row}
244
+ hasDivider={false}
245
+ style={rowPadding}
246
+ />
247
+ ))}
248
+ </VStack>
249
+
250
+ <VStack gap={2}>
251
+ <Heading level={2}>Device history</Heading>
252
+ {DEVICE_ROWS.map((device) => (
253
+ <HStack key={device.location} gap={3} vAlign="start" style={rowPadding}>
254
+ <Icon icon={Monitor} />
255
+ <StackItem size="fill">
256
+ <VStack gap={1}>
257
+ <HStack gap={2} vAlign="center" wrap="wrap">
258
+ <Text type="body" weight="semibold">
259
+ {device.label}
260
+ </Text>
261
+ {device.isCurrent && (
262
+ <HStack gap={1} vAlign="center">
263
+ <StatusDot variant="success" label="Current session" />
264
+ <Text type="supporting" color="secondary">
265
+ Current session
266
+ </Text>
267
+ </HStack>
268
+ )}
269
+ </HStack>
270
+ <Text
271
+ type="supporting"
272
+ color="secondary"
273
+ display="block"
274
+ hasTabularNumbers
275
+ >
276
+ {device.location}
277
+ </Text>
278
+ </VStack>
279
+ </StackItem>
280
+ {device.action && (
281
+ <Button
282
+ label={device.action}
283
+ variant="secondary"
284
+ size="sm"
285
+ onClick={() => {}}
286
+ style={actionNoWrap}
287
+ />
288
+ )}
289
+ </HStack>
290
+ ))}
291
+ </VStack>
292
+
293
+ <VStack gap={2}>
294
+ <Heading level={2}>Account</Heading>
295
+ <HStack hAlign="between" vAlign="start" style={rowPadding}>
296
+ <VStack gap={1}>
297
+ <Text type="body" weight="semibold" display="block">
298
+ Deactivate your account
299
+ </Text>
300
+ <Text type="supporting" color="secondary" display="block">
301
+ This action cannot be undone
302
+ </Text>
303
+ </VStack>
304
+ <Button
305
+ label="Deactivate"
306
+ variant="secondary"
307
+ size="sm"
308
+ onClick={() => {}}
309
+ style={actionNoWrap}
310
+ />
311
+ </HStack>
312
+ </VStack>
313
+ </VStack>
314
+ )}
315
+
316
+ {activeTab === "shared" && (
317
+ <VStack gap={8}>
318
+ <VStack gap={2}>
319
+ <Heading level={2}>Shared access</Heading>
320
+ <Text type="body" color="secondary">
321
+ Review each request carefully before approving access. We&apos;ll email your
322
+ kin or coven-mate a 4-digit code that lets them enter your crypt from their
323
+ trusted device.
324
+ </Text>
325
+ </VStack>
326
+
327
+ <Card variant="muted">
328
+ <HStack gap={4} vAlign="start">
329
+ <Center width={48} height={48} style={iconBox}>
330
+ <Icon icon={Lock} />
331
+ </Center>
332
+ <VStack gap={1}>
333
+ <Text type="body">Adding devices from people you trust</Text>
334
+ <Text type="body" color="secondary">
335
+ When you approve a request, you grant someone full access to your
336
+ account. They&apos;ll be able to change reservations and send messages
337
+ on your behalf.
338
+ </Text>
339
+ </VStack>
340
+ </HStack>
341
+ </Card>
342
+ </VStack>
343
+ )}
344
+ </VStack>
345
+ )}
346
+
347
+ {activeNav === "Languages & currency" && (
348
+ <VStack gap={6}>
349
+ {!isNarrow && <Heading level={1}>Languages &amp; currency</Heading>}
350
+ <VStack gap={0}>
351
+ <ExpandableRow
352
+ label="Preferred language"
353
+ value={LANGUAGES.find((l) => l.value === language)?.label ?? language}
354
+ isExpanded={expandedRow === "language"}
355
+ onEdit={() => setExpandedRow("language")}
356
+ onCancel={() => setExpandedRow(null)}
357
+ onSave={() => setExpandedRow(null)}
358
+ hasDivider={false}
359
+ style={rowPadding}
360
+ >
361
+ <Selector
362
+ label="Language"
363
+ isLabelHidden
364
+ size="lg"
365
+ value={language}
366
+ onChange={setLanguage}
367
+ options={LANGUAGES}
368
+ />
369
+ </ExpandableRow>
370
+ <ExpandableRow
371
+ label="Preferred currency"
372
+ value={CURRENCIES.find((c) => c.value === currency)?.label ?? currency}
373
+ isExpanded={expandedRow === "currency"}
374
+ onEdit={() => setExpandedRow("currency")}
375
+ onCancel={() => setExpandedRow(null)}
376
+ onSave={() => setExpandedRow(null)}
377
+ hasDivider={false}
378
+ style={rowPadding}
379
+ >
380
+ <Selector
381
+ label="Currency"
382
+ isLabelHidden
383
+ size="lg"
384
+ value={currency}
385
+ onChange={setCurrency}
386
+ options={CURRENCIES}
387
+ />
388
+ </ExpandableRow>
389
+ <ExpandableRow
390
+ label="Time zone"
391
+ value={TIMEZONES.find((t) => t.value === timezone)?.label ?? timezone}
392
+ isExpanded={expandedRow === "timezone"}
393
+ onEdit={() => setExpandedRow("timezone")}
394
+ onCancel={() => setExpandedRow(null)}
395
+ onSave={() => setExpandedRow(null)}
396
+ hasDivider={false}
397
+ style={rowPadding}
398
+ >
399
+ <Selector
400
+ label="Time zone"
401
+ isLabelHidden
402
+ size="lg"
403
+ value={timezone}
404
+ onChange={setTimezone}
405
+ options={TIMEZONES}
406
+ />
407
+ </ExpandableRow>
408
+ </VStack>
409
+ </VStack>
410
+ )}
411
+
412
+ {activeNav === "Personal information" && (
413
+ <VStack gap={6}>
414
+ {!isNarrow && <Heading level={1}>Personal information</Heading>}
415
+ <VStack gap={0}>
416
+ <ExpandableRow
417
+ label="Legal name"
418
+ value={legalName}
419
+ isExpanded={expandedRow === "legalName"}
420
+ onEdit={() => setExpandedRow("legalName")}
421
+ onCancel={() => setExpandedRow(null)}
422
+ onSave={() => setExpandedRow(null)}
423
+ hasDivider={false}
424
+ style={rowPadding}
425
+ >
426
+ <TextInput
427
+ label="Legal name"
428
+ isLabelHidden
429
+ value={legalName}
430
+ onChange={setLegalName}
431
+ />
432
+ </ExpandableRow>
433
+ <ExpandableRow
434
+ label="Preferred first name"
435
+ value={preferredName || "Not provided"}
436
+ isExpanded={expandedRow === "preferredName"}
437
+ onEdit={() => setExpandedRow("preferredName")}
438
+ onCancel={() => setExpandedRow(null)}
439
+ onSave={() => setExpandedRow(null)}
440
+ hasDivider={false}
441
+ style={rowPadding}
442
+ >
443
+ <TextInput
444
+ label="Preferred first name"
445
+ isLabelHidden
446
+ value={preferredName}
447
+ onChange={setPreferredName}
448
+ />
449
+ </ExpandableRow>
450
+ <ExpandableRow
451
+ label="Email address"
452
+ value={email}
453
+ isExpanded={expandedRow === "email"}
454
+ onEdit={() => setExpandedRow("email")}
455
+ onCancel={() => setExpandedRow(null)}
456
+ onSave={() => setExpandedRow(null)}
457
+ hasDivider={false}
458
+ style={rowPadding}
459
+ >
460
+ <TextInput
461
+ label="Email address"
462
+ isLabelHidden
463
+ value={email}
464
+ onChange={setEmail}
465
+ />
466
+ </ExpandableRow>
467
+ <ExpandableRow
468
+ label="Phone number"
469
+ value={phone}
470
+ isExpanded={expandedRow === "phone"}
471
+ onEdit={() => setExpandedRow("phone")}
472
+ onCancel={() => setExpandedRow(null)}
473
+ onSave={() => setExpandedRow(null)}
474
+ hasDivider={false}
475
+ style={rowPadding}
476
+ >
477
+ <TextInput
478
+ label="Phone number"
479
+ isLabelHidden
480
+ value={phone}
481
+ onChange={setPhone}
482
+ />
483
+ </ExpandableRow>
484
+ <InfoRowItem
485
+ label="Identity verification"
486
+ value="Verified"
487
+ action=""
488
+ hasDivider={false}
489
+ style={rowPadding}
490
+ />
491
+ <ExpandableRow
492
+ label="Residential address"
493
+ value={address || "Not provided"}
494
+ isExpanded={expandedRow === "address"}
495
+ onEdit={() => setExpandedRow("address")}
496
+ onCancel={() => setExpandedRow(null)}
497
+ onSave={() => setExpandedRow(null)}
498
+ hasDivider={false}
499
+ style={rowPadding}
500
+ >
501
+ <TextInput
502
+ label="Residential address"
503
+ isLabelHidden
504
+ value={address}
505
+ onChange={setAddress}
506
+ />
507
+ </ExpandableRow>
508
+ <ExpandableRow
509
+ label="Mailing address"
510
+ value={mailingAddress || "Not provided"}
511
+ isExpanded={expandedRow === "mailingAddress"}
512
+ onEdit={() => setExpandedRow("mailingAddress")}
513
+ onCancel={() => setExpandedRow(null)}
514
+ onSave={() => setExpandedRow(null)}
515
+ hasDivider={false}
516
+ style={rowPadding}
517
+ >
518
+ <TextInput
519
+ label="Mailing address"
520
+ isLabelHidden
521
+ value={mailingAddress}
522
+ onChange={setMailingAddress}
523
+ />
524
+ </ExpandableRow>
525
+ <ExpandableRow
526
+ label="Emergency contact"
527
+ value={emergencyContact}
528
+ isExpanded={expandedRow === "emergencyContact"}
529
+ onEdit={() => setExpandedRow("emergencyContact")}
530
+ onCancel={() => setExpandedRow(null)}
531
+ onSave={() => setExpandedRow(null)}
532
+ hasDivider={false}
533
+ style={rowPadding}
534
+ >
535
+ <TextInput
536
+ label="Emergency contact"
537
+ isLabelHidden
538
+ value={emergencyContact}
539
+ onChange={setEmergencyContact}
540
+ />
541
+ </ExpandableRow>
542
+ </VStack>
543
+
544
+ <Card padding={4}>
545
+ <VStack gap={4}>
546
+ {INFO_TILES.map((tile) => (
547
+ <HStack key={tile.title} gap={3} vAlign="start">
548
+ <Center width={48} height={48} style={iconBox}>
549
+ <Icon icon={tile.icon} />
550
+ </Center>
551
+ <VStack gap={1}>
552
+ <Text type="body" weight="semibold" display="block">
553
+ {tile.title}
554
+ </Text>
555
+ <Text type="body" color="secondary" display="block">
556
+ {tile.body}
557
+ </Text>
558
+ </VStack>
559
+ </HStack>
560
+ ))}
561
+ </VStack>
562
+ </Card>
563
+ </VStack>
564
+ )}
565
+
566
+ {activeNav === "Privacy" && (
567
+ <VStack gap={6}>
568
+ {!isNarrow && <Heading level={1}>Privacy</Heading>}
569
+
570
+ <VStack gap={8}>
571
+ <VStack gap={2}>
572
+ <Heading level={2}>Messages</Heading>
573
+ <VStack style={rowPadding}>
574
+ <Switch
575
+ label="Show people when I've read their messages."
576
+ value={readReceipts}
577
+ onChange={setReadReceipts}
578
+ labelPosition="start"
579
+ labelSpacing="spread"
580
+ />
581
+ </VStack>
582
+ <HStack hAlign="between" vAlign="center" style={rowPadding}>
583
+ <Text type="body" weight="semibold">
584
+ Blocked people
585
+ </Text>
586
+ <Link href={SELF_HASH}>View</Link>
587
+ </HStack>
588
+ </VStack>
589
+
590
+ <VStack gap={2}>
591
+ <Heading level={2}>Listings</Heading>
592
+ <VStack style={rowPadding}>
593
+ <Switch
594
+ label="List my wares in the scrying mirrors"
595
+ description="Turning this on means scrying engines, like Google, will show your wares to seekers."
596
+ value={searchEngines}
597
+ onChange={setSearchEngines}
598
+ labelPosition="start"
599
+ labelSpacing="spread"
600
+ />
601
+ </VStack>
602
+ </VStack>
603
+
604
+ <VStack gap={4}>
605
+ <Heading level={2}>Reviews</Heading>
606
+ <Text type="body" color="secondary">
607
+ Choose what&apos;s shared when you write a review.{" "}
608
+ <Link href={SELF_HASH} type="body" hasUnderline>
609
+ Learn more
610
+ </Link>
611
+ </Text>
612
+ <VStack gap={4}>
613
+ <Switch
614
+ label="Show my home haunt and country"
615
+ description="Ex: City and country"
616
+ value={showCity}
617
+ onChange={setShowCity}
618
+ labelPosition="start"
619
+ labelSpacing="spread"
620
+ />
621
+ <Switch
622
+ label="Show my journey type"
623
+ description="Ex: Rested with kin or familiars"
624
+ value={showTripType}
625
+ onChange={setShowTripType}
626
+ labelPosition="start"
627
+ labelSpacing="spread"
628
+ />
629
+ <Switch
630
+ label="Show my length of stay"
631
+ description="Ex: A few nights, about a week, etc."
632
+ value={showStayLength}
633
+ onChange={setShowStayLength}
634
+ labelPosition="start"
635
+ labelSpacing="spread"
636
+ />
637
+ <Switch
638
+ label="Show my booked revels"
639
+ description="Ex: Midnight feasts and tasting rituals"
640
+ value={showServices}
641
+ onChange={setShowServices}
642
+ labelPosition="start"
643
+ labelSpacing="spread"
644
+ />
645
+ </VStack>
646
+ </VStack>
647
+
648
+ <VStack gap={4}>
649
+ <Heading level={2}>Data privacy</Heading>
650
+ <Card>
651
+ <HStack hAlign="between" vAlign="center">
652
+ <Text type="body">Request my personal data</Text>
653
+ <Button
654
+ label="Request"
655
+ variant="secondary"
656
+ size="sm"
657
+ onClick={() => {}}
658
+ style={actionNoWrap}
659
+ />
660
+ </HStack>
661
+ </Card>
662
+ <Switch
663
+ label="Help improve AI-powered features"
664
+ description="When this is on, we use your data to develop and improve AI models."
665
+ value={aiFeatures}
666
+ onChange={setAiFeatures}
667
+ labelPosition="start"
668
+ labelSpacing="spread"
669
+ />
670
+ <Card>
671
+ <HStack hAlign="between" vAlign="center">
672
+ <Text type="body">Delete my account</Text>
673
+ <Button
674
+ label="Delete"
675
+ variant="secondary"
676
+ size="sm"
677
+ onClick={() => {}}
678
+ style={actionNoWrap}
679
+ />
680
+ </HStack>
681
+ </Card>
682
+ <Card variant="muted">
683
+ <HStack gap={4} vAlign="start">
684
+ <Center width={48} height={48} style={iconBox}>
685
+ <Icon icon={ShieldCheck} />
686
+ </Center>
687
+ <VStack gap={1}>
688
+ <Text type="body">Committed to privacy</Text>
689
+ <Text type="body" color="secondary">
690
+ We&apos;re committed to keeping your data protected. See details in our{" "}
691
+ <Link href={SELF_HASH} type="supporting" hasUnderline>
692
+ Privacy Policy
693
+ </Link>
694
+ .
695
+ </Text>
696
+ </VStack>
697
+ </HStack>
698
+ </Card>
699
+ </VStack>
700
+ </VStack>
701
+ </VStack>
702
+ )}
703
+
704
+ {activeNav === "Notifications" && (
705
+ <VStack gap={6}>
706
+ {!isNarrow && <Heading level={1}>Notifications</Heading>}
707
+ <VStack gap={2}>
708
+ <Heading level={2}>Messages</Heading>
709
+ <VStack gap={4} style={rowPadding}>
710
+ <Switch
711
+ label="Email notifications"
712
+ description="Coven updates, journey reminders, and crypt activity."
713
+ value={emailNotif}
714
+ onChange={setEmailNotif}
715
+ labelPosition="start"
716
+ labelSpacing="spread"
717
+ />
718
+ <Switch
719
+ label="Push notifications"
720
+ description="Swift ravens for messages and booking requests."
721
+ value={pushNotif}
722
+ onChange={setPushNotif}
723
+ labelPosition="start"
724
+ labelSpacing="spread"
725
+ />
726
+ </VStack>
727
+ </VStack>
728
+ </VStack>
729
+ )}
730
+
731
+ {activeNav === "Taxes" && (
732
+ <VStack gap={6}>
733
+ {!isNarrow && <Heading level={1}>Taxes</Heading>}
734
+ <VStack gap={2}>
735
+ <Heading level={2}>Tax scrolls</Heading>
736
+ {TAX_ROWS.map((row) => (
737
+ <InfoRowItem key={row.label} {...row} hasDivider={false} style={rowPadding} />
738
+ ))}
739
+ </VStack>
740
+ </VStack>
741
+ )}
742
+
743
+ {activeNav === "Payments" && (
744
+ <VStack gap={6}>
745
+ {!isNarrow && <Heading level={1}>Payments</Heading>}
746
+ <VStack gap={2}>
747
+ <Heading level={2}>Payout crypt</Heading>
748
+ {PAYOUT_ROWS.map((row) => (
749
+ <InfoRowItem key={row.label} {...row} hasDivider={false} style={rowPadding} />
750
+ ))}
751
+ </VStack>
752
+ </VStack>
753
+ )}
754
+
755
+ {activeNav === "Travel for work" && (
756
+ <VStack gap={6}>
757
+ {!isNarrow && <Heading level={1}>Travel for work</Heading>}
758
+ <VStack gap={2}>
759
+ <Heading level={2}>Work haunts</Heading>
760
+ <VStack style={rowPadding}>
761
+ <Switch
762
+ label="Show night-errand options at checkout"
763
+ description="Add a coven address to expense night errands and unlock travel-ready wares."
764
+ value={workTrips}
765
+ onChange={setWorkTrips}
766
+ labelPosition="start"
767
+ labelSpacing="spread"
768
+ />
769
+ </VStack>
770
+ </VStack>
771
+ </VStack>
772
+ )}
773
+
774
+ {activeNav === "Professional hosting tools" && (
775
+ <VStack gap={6}>
776
+ {!isNarrow && <Heading level={1}>Professional hosting tools</Heading>}
777
+ <Card variant="muted">
778
+ <HStack gap={4} vAlign="start">
779
+ <Center width={48} height={48} style={iconBox}>
780
+ <Icon icon={Wrench} />
781
+ </Center>
782
+ <VStack gap={1}>
783
+ <Text type="body">Hosting grimoire</Text>
784
+ <Text type="body" color="secondary">
785
+ Manage multiple listings, route tasks to co-hosts, and review consolidated
786
+ payouts from one place.
787
+ </Text>
788
+ </VStack>
789
+ </HStack>
790
+ </Card>
791
+ </VStack>
792
+ )}
793
+ </VStack>
794
+ </LayoutContent>
795
+ }
796
+ />
797
+ );
843
798
  }