astryx-dracula 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -216,7 +216,9 @@ function PageHeader({
216
216
  <VStack gap={3}>
217
217
  <HStack gap={4} vAlign="start">
218
218
  <StackItem size="fill">
219
- <VStack gap={0}>
219
+ {/* gap={2}: the breadcrumb is a control, not a text-baseline
220
+ pairing, so it needs separation from the H1 rather than 0. */}
221
+ <VStack gap={2}>
220
222
  <Link href="#/templates/detail-page">
221
223
  <HStack gap={1} vAlign="center">
222
224
  <Icon icon={ArrowLeft} size="sm" color="inherit" />
@@ -502,8 +504,8 @@ function TimelineSection() {
502
504
  return (
503
505
  <Section>
504
506
  <VStack gap={4}>
505
- {/* gap={2}, matching the byte-identical header rows at :338 and
506
- :407. Core's Stack has no default gap (Stack.tsx:211, pass-through
507
+ {/* gap={2}, matching the byte-identical header rows at :340 and
508
+ :408. Core's Stack has no default gap (Stack.tsx:211, pass-through
507
509
  at :255/:285), so this rendered 0px -- the heading and the Filters
508
510
  button touched -- while both siblings render 8px. */}
509
511
  <HStack gap={2} vAlign="center">
@@ -582,11 +584,16 @@ function PanelContent() {
582
584
  return (
583
585
  <VStack gap={4}>
584
586
  <Collapsible trigger={<Heading level={3}>Notes</Heading>}>
587
+ {/* "Show more" closes the sentence inside the Text below, so it is
588
+ prose and asks for `hasUnderline` — colour alone is not a
589
+ sufficient cue inline (1.4.1 / F73). `type="inherit"` adopts the
590
+ paragraph's size and leading. The two links in the header are
591
+ navigation, so they pass neither. */}
585
592
  <Text type="body">
586
593
  Customer is a repeat buyer, 3rd order this quarter. Prefers
587
594
  moonlight and sage glazes. Requested gift wrapping for the mug set.
588
595
  Ships to a residential address in CA.{' '}
589
- <Link href="#/templates/detail-page">
596
+ <Link href="#/templates/detail-page" hasUnderline type="inherit">
590
597
  Show more
591
598
  </Link>
592
599
  </Text>
@@ -29,7 +29,7 @@ import {useMediaQuery} from '@astryxdesign/core/hooks';
29
29
  import {Dialog, DialogHeader} from '@astryxdesign/core/Dialog';
30
30
  import {Divider} from '@astryxdesign/core/Divider';
31
31
  import {Tooltip} from '@astryxdesign/core/Tooltip';
32
- import {Table, pixel} from '@astryxdesign/core/Table';
32
+ import {Table, pixel, proportional} from '@astryxdesign/core/Table';
33
33
  import {Icon} from '@astryxdesign/core/Icon';
34
34
  import {Section} from '@astryxdesign/core/Section';
35
35
  import {Center} from '@astryxdesign/core/Center';
@@ -42,6 +42,17 @@ const tabListFlush: CSSProperties = {
42
42
  marginInlineStart: 'calc(var(--spacing-3) * -1)',
43
43
  };
44
44
 
45
+ // The best-practices table lives in the prose column, where the eye tracks a
46
+ // single measure. Table's scroll wrapper bleeds edge-to-edge by reading the
47
+ // container padding vars LayoutContent publishes (32px here), which pushes
48
+ // its dividers 32px past the measure on both sides at every width. This table
49
+ // never overflows, so the bleed buys nothing — zero the vars for this subtree
50
+ // so it sits on the measure with the headings above it.
51
+ const practicesOnMeasure: CSSProperties = ({
52
+ '--container-padding-inline-start': '0px',
53
+ '--container-padding-inline-end': '0px',
54
+ }) as CSSProperties;
55
+
45
56
  const COMPONENT_OUTLINE_ITEMS: OutlineItem[] = [
46
57
  {id: 'usage', label: 'Usage', level: 2},
47
58
  {id: 'best-practices', label: 'Best practices', level: 3},
@@ -635,34 +646,37 @@ function ComponentDetailView({activeNav}: {activeNav: string}) {
635
646
  <Heading id="best-practices" level={3}>
636
647
  Best practices
637
648
  </Heading>
638
- <Table
639
- data={docs.bestPractices as Record<string, unknown>[]}
640
- dividers="none"
641
- hasHover
642
- columns={[
643
- {
644
- key: 'type',
645
- header: 'Guidance',
646
- width: pixel(125),
647
- renderCell: (item: Record<string, unknown>) => (
648
- <Badge
649
- label={item.type === 'do' ? 'Do' : "Don't"}
650
- variant={item.type === 'do' ? 'green' : 'red'}
651
- />
652
- ),
653
- },
654
- {
655
- key: 'text',
656
- header: 'Practices',
657
- renderCell: (item: Record<string, unknown>) => (
658
- <Text type="body" textWrap="wrap">
659
- {item.text as string}
660
- </Text>
661
- ),
662
- },
663
- ]}
664
- density="spacious"
665
- />
649
+ <div style={practicesOnMeasure}>
650
+ <Table
651
+ data={docs.bestPractices as Record<string, unknown>[]}
652
+ dividers="none"
653
+ hasHover
654
+ columns={[
655
+ {
656
+ key: 'type',
657
+ header: 'Guidance',
658
+ width: pixel(125),
659
+ renderCell: (item: Record<string, unknown>) => (
660
+ <Badge
661
+ label={item.type === 'do' ? 'Do' : "Don't"}
662
+ variant={item.type === 'do' ? 'green' : 'red'}
663
+ />
664
+ ),
665
+ },
666
+ {
667
+ key: 'text',
668
+ header: 'Practices',
669
+ width: proportional(1),
670
+ renderCell: (item: Record<string, unknown>) => (
671
+ <Text type="body" textWrap="wrap">
672
+ {item.text as string}
673
+ </Text>
674
+ ),
675
+ },
676
+ ]}
677
+ density="spacious"
678
+ />
679
+ </div>
666
680
  </VStack>
667
681
 
668
682
  <Divider />
@@ -30,7 +30,7 @@ import {outlinePanel} from 'astryx-dracula/shared/chaptered-doc-config';
30
30
  // Same prompt in the copy action and the visible body: one string, so the
31
31
  // two can never drift.
32
32
  const SETUP_PROMPT =
33
- 'Help me get set up with Astryx Dracula. Based on my project, do the following: 1. Install @astryxdesign/core and the StyleX compiler. 2. Wrap my app in ThemeProvider. 3. Replace one existing component with an Astryx equivalent. After setup, suggest relevant next steps based on my project.';
33
+ 'Help me get set up with Astryx Dracula. Based on my project, do the following: 1. Install @astryxdesign/core and the StyleX compiler. 2. Wrap my app in the Theme component. 3. Replace one existing component with an Astryx equivalent. After setup, suggest relevant next steps based on my project.';
34
34
 
35
35
  // Copies the setup prompt in the shape each tool wants to receive it. The
36
36
  // menu cannot navigate anywhere from a static template, so the action says
@@ -138,8 +138,8 @@ export default function DocumentationTechnical() {
138
138
  <Text type="body" color="secondary">
139
139
  Help me get set up with Astryx Dracula. Based on my project,
140
140
  do the following: 1. Install @astryxdesign/core and the
141
- StyleX compiler. 2. Wrap my app in ThemeProvider. 3. Replace
142
- one existing component with an Astryx equivalent.
141
+ StyleX compiler. 2. Wrap my app in the Theme component. 3.
142
+ Replace one existing component with an Astryx equivalent.
143
143
  </Text>
144
144
  </VStack>
145
145
  </Card>
@@ -225,17 +225,19 @@ export default function App() {
225
225
  </Heading>
226
226
  <Text type="body">
227
227
  Astryx ships with a default theme that works out of the box. To
228
- customize colors, typography, and spacing, wrap your app in a
229
- theme provider.
228
+ customize colors, typography, and spacing, wrap your app in the
229
+ Theme component.
230
230
  </Text>
231
231
  <CodeBlock
232
- code={`import { ThemeProvider } from '@astryxdesign/core/Theme';
232
+ code={`import { type ReactNode } from 'react';
233
+ import { Theme } from '@astryxdesign/core/theme';
234
+ import { astryxDraculaTheme } from 'astryx-dracula';
233
235
 
234
- export default function App({ children }) {
236
+ export default function App({ children }: { children: ReactNode }) {
235
237
  return (
236
- <ThemeProvider theme="default">
238
+ <Theme theme={astryxDraculaTheme} mode="dark">
237
239
  {children}
238
- </ThemeProvider>
240
+ </Theme>
239
241
  );
240
242
  }`}
241
243
  language="tsx"
@@ -436,6 +436,15 @@ export default function FileExplorer() {
436
436
  return item?.type === 'file' ? item : null;
437
437
  }, [selectedPath]);
438
438
 
439
+ // Narrow mode is one pane at a time, the same master→detail shape the
440
+ // folder columns already use: the deepest column, or the file preview when
441
+ // the selection ends on a file. Beside each other the column and the
442
+ // preview need 240 + 320 = 560px, so in a 390px pane the preview was cut at
443
+ // the viewport edge with nothing to scroll it into view. "Go back" (:464)
444
+ // pops the selected file off the path, so it is also the way back from the
445
+ // preview to the column.
446
+ const isPreviewPane = isNarrow && selectedFile != null;
447
+
439
448
  const handleSelect = (columnIndex: number, itemId: string) => {
440
449
  setSelectedPath([...selectedPath.slice(0, columnIndex), itemId]);
441
450
  };
@@ -508,81 +517,84 @@ export default function FileExplorer() {
508
517
  File Explorer
509
518
  </VisuallyHidden>
510
519
  <HStack height="100%" style={columnRow}>
511
- {visibleColumns.map((col, colIndex) => {
512
- const trueIndex = columnOffset + colIndex;
513
- const showDivider =
514
- trueIndex < columns.length - 1 || selectedFile != null;
515
- return (
516
- <Section
517
- key={col.id}
518
- width={240}
519
- padding={2}
520
- variant="transparent"
521
- dividers={showDivider ? ['end'] : undefined}
522
- style={{...scrollable, ...fixedColumn}}
523
- role="region"
524
- aria-label={`Folder column ${trueIndex + 1}`}
525
- tabIndex={0}>
526
- {col.items.length === 0 ? (
527
- <EmptyState
528
- title="Quiet in the crypt"
529
- description="This folder holds no scrolls yet."
530
- isCompact
531
- />
532
- ) : (
533
- <List density="compact" hasDividers={false}>
534
- {col.items.map(item => {
535
- const isSelected = col.selectedId === item.id;
536
- const hasChildren =
537
- item.type === 'folder' &&
538
- item.children != null &&
539
- item.children.length > 0;
540
- return (
541
- <ListItem
542
- key={item.id}
543
- label={
544
- <Text type="body" maxLines={1}>
545
- {item.name}
546
- </Text>
547
- }
548
- startContent={
549
- <Icon
550
- icon={
551
- item.type === 'folder'
552
- ? Folder
553
- : FileText
554
- }
555
- color={
556
- item.type === 'folder' ? 'accent' : 'secondary'
557
- }
558
- size="sm"
559
- />
560
- }
561
- endContent={
562
- hasChildren ? (
520
+ {!isPreviewPane &&
521
+ visibleColumns.map((col, colIndex) => {
522
+ const trueIndex = columnOffset + colIndex;
523
+ const showDivider =
524
+ trueIndex < columns.length - 1 || selectedFile != null;
525
+ return (
526
+ <Section
527
+ key={col.id}
528
+ width={240}
529
+ padding={2}
530
+ variant="transparent"
531
+ dividers={showDivider ? ['end'] : undefined}
532
+ style={{...scrollable, ...fixedColumn}}
533
+ role="region"
534
+ aria-label={`Folder column ${trueIndex + 1}`}
535
+ tabIndex={0}>
536
+ {col.items.length === 0 ? (
537
+ <EmptyState
538
+ title="Quiet in the crypt"
539
+ description="This folder holds no scrolls yet."
540
+ isCompact
541
+ />
542
+ ) : (
543
+ <List density="compact" hasDividers={false}>
544
+ {col.items.map(item => {
545
+ const isSelected = col.selectedId === item.id;
546
+ const hasChildren =
547
+ item.type === 'folder' &&
548
+ item.children != null &&
549
+ item.children.length > 0;
550
+ return (
551
+ <ListItem
552
+ key={item.id}
553
+ label={
554
+ <Text type="body" maxLines={1}>
555
+ {item.name}
556
+ </Text>
557
+ }
558
+ startContent={
563
559
  <Icon
564
- icon={ChevronRight}
565
- size="xsm"
566
- color="secondary"
560
+ icon={
561
+ item.type === 'folder'
562
+ ? Folder
563
+ : FileText
564
+ }
565
+ color={
566
+ item.type === 'folder'
567
+ ? 'accent'
568
+ : 'secondary'
569
+ }
570
+ size="sm"
567
571
  />
568
- ) : undefined
569
- }
570
- onClick={() => handleSelect(trueIndex, item.id)}
571
- isSelected={isSelected}
572
- />
573
- );
574
- })}
575
- </List>
576
- )}
577
- </Section>
578
- );
579
- })}
572
+ }
573
+ endContent={
574
+ hasChildren ? (
575
+ <Icon
576
+ icon={ChevronRight}
577
+ size="xsm"
578
+ color="secondary"
579
+ />
580
+ ) : undefined
581
+ }
582
+ onClick={() => handleSelect(trueIndex, item.id)}
583
+ isSelected={isSelected}
584
+ />
585
+ );
586
+ })}
587
+ </List>
588
+ )}
589
+ </Section>
590
+ );
591
+ })}
580
592
  {selectedFile && (
581
593
  <Section
582
- width={320}
583
- // 16px, not 24. The sibling column at :519 is padding={2}
594
+ width={isPreviewPane ? '100%' : 320}
595
+ // 16px, not 24. The sibling column at :529 is padding={2}
584
596
  // (8px) because it holds a dense compact List, and these two
585
- // sit in one HStack (:510) as columns of the same view --
597
+ // sit in one HStack (:519) as columns of the same view --
586
598
  // 24 beside 8 made the compact column look broken rather
587
599
  // than compact. 16 is the outer-inset value this repo already
588
600
  // uses for exactly this (scaffolds.md new-site recipe).
@@ -238,7 +238,7 @@ export default function TwoColumnForm() {
238
238
  <Text type="supporting" color="secondary">
239
239
  {col.label}
240
240
  </Text>
241
- <Link href={`mailto:${col.email}`}>
241
+ <Link href={`mailto:${col.email}`} hasUnderline>
242
242
  {col.email}
243
243
  </Link>
244
244
  </VStack>
package/templates/ide.tsx CHANGED
@@ -226,16 +226,16 @@ export default function IdeWorkspace() {
226
226
 
227
227
  const startPanel = useResizable({
228
228
  defaultSize: 256,
229
- minSizePx: 160,
230
- maxSizePx: 400,
229
+ minSize: 160,
230
+ maxSize: 400,
231
231
  collapsible: true,
232
232
  collapsedSize: 50,
233
233
  });
234
234
 
235
235
  const endPanel = useResizable({
236
236
  defaultSize: 320,
237
- minSizePx: 180,
238
- maxSizePx: 500,
237
+ minSize: 180,
238
+ maxSize: 500,
239
239
  collapsible: true,
240
240
  collapsedSize: 50,
241
241
  });
@@ -244,8 +244,8 @@ export default function IdeWorkspace() {
244
244
  // has no minimum of its own, and an unbounded drag leaves nothing to grab.
245
245
  const bottomPanel = useResizable({
246
246
  defaultSize: 300,
247
- minSizePx: 80,
248
- maxSizePx: 520,
247
+ minSize: 80,
248
+ maxSize: 520,
249
249
  collapsible: true,
250
250
  collapsedSize: 40,
251
251
  });
@@ -1,6 +1,14 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
  // XLE (canonical structure, validated with `bunx astryx layout check`):
3
- // L > (LH > H[g3 a=center] > (H[g2 a=center] > Hd"Night watch"[level=1] + Tx"3 investigating"[t=supporting]) + SegmentedControl"Filter by status" + B"Declare incident") + (LC[p0] > V[g0] > (H > PowerSearch"Search the night watch…") + (List > (ListItem)*7)) + (LayoutPanel[w380 p0] > V[g4] > (V[g2] > (H[g2 a=center] > StatusDot + Tx"INC-2417"[t=supporting] + Token"Investigating") + Hd"Checkout API elevated 5xx rate"[level=2]) + (H[g2] > B.primary"Mark mitigated" + B.secondary"Escalate") + Divider + MetadataList + Divider + (V[g2] > Hd"Timeline"[level=3] + List))
3
+ // NOTE on `t=display-2`: the h1 asked for display-2 from the start, but
4
+ // core 0.3.0's Heading emitted no `data-type`, and the 0.3.0 CLI emitted no
5
+ // `.astryx-heading[data-type]` rule, so --text-display-2-size never reached a
6
+ // heading and the 24px level-1 default leaked through. core+CLI 0.6.3 restored
7
+ // the path, so this title now renders at its declared 35px. Verified by
8
+ // pixel-diffing the pre-upgrade build against this one at 1440 and 390: the
9
+ // only regression was this header detaching at 390, fixed by stacking the
10
+ // title group under `isNarrow`.
11
+ // L > (LH > H[g3 a=center] > (H[g2 a=center] > Hd"Night watch"[level=1 t=display-2] + Tx"3 investigating"[t=supporting]) + SegmentedControl"Filter by status" + B"Declare incident") + (LC[p0] > V[g0] > (H > PowerSearch"Search the night watch…") + (List > (ListItem)*7)) + (LayoutPanel[w380 p0] > V[g4] > (V[g2] > (H[g2 a=center] > StatusDot + Tx"INC-2417"[t=supporting] + Token"Investigating") + Hd"Checkout API elevated 5xx rate"[level=2]) + (H[g2] > B.primary"Mark mitigated" + B.secondary"Escalate") + Divider + MetadataList + Divider + (V[g2] > Hd"Timeline"[level=3] + List))
4
12
 
5
13
  /**
6
14
  * Incident Console — an on-call incident response tool for the night watch. Frame-first layout (see `bunx astryx docs layout`), distilled fro (Frame/responsive/container: see XLE header above.)
@@ -484,8 +492,8 @@ export default function IncidentConsole() {
484
492
 
485
493
  const inspectorPanel = useResizable({
486
494
  defaultSize: 380,
487
- minSizePx: 320,
488
- maxSizePx: 480,
495
+ minSize: 320,
496
+ maxSize: 480,
489
497
  });
490
498
 
491
499
  const visible = useMemo(() => {
@@ -521,7 +529,22 @@ export default function IncidentConsole() {
521
529
  icon={<Icon icon={Plus} size="sm" />}
522
530
  />
523
531
  );
524
- const titleGroup = (
532
+ // Stacks below 1024px. The H1 asks for `display-2` (35px), which is the
533
+ // theme's own declared value but only started rendering in core 0.6.3 —
534
+ // see the note in the XLE header. At 35px the title wraps to two lines on a
535
+ // phone, and the counter beside it then wraps too and detaches into the
536
+ // corner. Stacking keeps the counter on the title's own line at any width
537
+ // the title needs more than one, and costs nothing at desktop.
538
+ const titleGroup = isNarrow ? (
539
+ <VStack gap={1}>
540
+ <Heading level={1} type="display-2">
541
+ Night watch
542
+ </Heading>
543
+ <Text type="supporting" color="secondary" hasTabularNumbers>
544
+ {openCount} investigating
545
+ </Text>
546
+ </VStack>
547
+ ) : (
525
548
  <HStack gap={2} vAlign="center">
526
549
  <Heading level={1} type="display-2">Night watch</Heading>
527
550
  <Text type="supporting" color="secondary" hasTabularNumbers>
@@ -105,7 +105,8 @@ export default function LoginCard() {
105
105
  <VStack hAlign="end">
106
106
  <Link
107
107
  href="#/templates/login-card"
108
- color="secondary">
108
+ color="secondary"
109
+ isStandalone>
109
110
  {AUTH_FORGOT_PASSWORD}
110
111
  </Link>
111
112
  </VStack>
@@ -141,11 +142,13 @@ export default function LoginCard() {
141
142
  />
142
143
  </VStack>
143
144
 
144
- {/* Sign up link */}
145
+ {/* Prose links. A link inside a sentence needs a persistent
146
+ underline — colour alone is not a cue (WCAG 1.4.1 / F73),
147
+ and the kit underlines on hover only. */}
145
148
  <VStack hAlign="center">
146
149
  <Text type="supporting" color="secondary">
147
150
  {AUTH_SIGNUP_PROMPT}{' '}
148
- <Link href="#/templates/login-card" type="supporting">
151
+ <Link href="#/templates/login-card" type="supporting" hasUnderline>
149
152
  {AUTH_SIGNUP_LINK}
150
153
  </Link>
151
154
  </Text>
@@ -157,11 +160,11 @@ export default function LoginCard() {
157
160
  <VStack hAlign="center" width="100%">
158
161
  <Text type="supporting" color="secondary" justify="center">
159
162
  {AUTH_TERMS_PREFIX}{' '}
160
- <Link href="#/templates/login-card" type="supporting">
163
+ <Link href="#/templates/login-card" type="supporting" hasUnderline>
161
164
  {AUTH_TERMS_SERVICE}
162
165
  </Link>{' '}
163
166
  and{' '}
164
- <Link href="#/templates/login-card" type="supporting">
167
+ <Link href="#/templates/login-card" type="supporting" hasUnderline>
165
168
  {AUTH_TERMS_PRIVACY}
166
169
  </Link>
167
170
  .
@@ -1,6 +1,6 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
  // XLE (canonical structure, validated with `bunx astryx layout check`):
3
- // Ctr > V[g=4] > (Ctr.horizontal > C[p=0] > G[c={min:240} g=8 a=stretch] > (S[p=0] > V[g=4] > (H[g=2 a=center] > Ic + Tx"Castle Dracula"[t=body]) + (V[g=4] > (V[g=1] > Hd"Welcome back to the night"[level=1] + Tx"Sign in to your crypt"[t=body]) + (V[g=2] > TI"Email"[t=email] + (V[g=1] > TI"Password"[t=password] + Lk"Forgot password?")) + B.primary"Enter the night" + D"Or continue with" + (G[c={min:200} g=3] > B.secondary"Login with Apple" + B.secondary"Login with Google")) + Tx"New to the castle?"[t=supporting]) + (C[p=0] > AR)) + (V[a=center] > Tx"By clicking continue, you agree to our Terms of service and Privacy policy"[t=supporting])
3
+ // Ctr > V[g=4] > (Ctr.horizontal > C[p=0] > G[c={min:240} g=8 a=stretch] > (S[p=0] > V[g=4] > (H[g=2 a=center] > Ic + Tx"Castle Dracula"[t=body]) + (V[g=4] > (V[g=1] > Hd"Welcome back to the night"[level=1] + Tx"Sign in to your crypt"[t=body]) + (V[g=2] > TI"Email"[t=email] + (V[g=1] > TI"Password"[t=password] + Lk"Forgot password?")) + B.primary"Enter the night" + D"Or continue with" + (G[c={min:200} g=3] > B.secondary"Login with Apple" + B.secondary"Login with Google")) + (V[a=center] > Tx"New to the castle?"[t=supporting] + Lk"Sign up")) + (C[p=0] > AR)) + (V[a=center] > Tx"By clicking continue, you agree to our Terms of service and Privacy policy"[t=supporting])
4
4
 
5
5
  import {useState, useTransition, type CSSProperties} from 'react';
6
6
  import {inputAutoComplete} from 'astryx-dracula/shared/auth-chrome-config';
@@ -191,7 +191,9 @@ export default function LoginSplit() {
191
191
  />
192
192
  {error && (
193
193
  <VStack hAlign="end">
194
- <Link href="#/templates/login-split">
194
+ <Link
195
+ href="#/templates/login-split"
196
+ isStandalone>
195
197
  {AUTH_FORGOT_PASSWORD}
196
198
  </Link>
197
199
  </VStack>
@@ -234,13 +236,20 @@ export default function LoginSplit() {
234
236
  </Center>
235
237
  </StackItem>
236
238
 
239
+ {/* Prose link: colour alone is not a cue inside a sentence
240
+ (WCAG 1.4.1 / F73), so it asks for the underline the kit
241
+ only gives on hover. The VStack centres the line, as the
242
+ other three login templates do — it was sitting flush
243
+ left under a left-aligned form. */}
237
244
  {!isSuccess && (
238
- <Text type="supporting" color="secondary">
239
- {AUTH_SIGNUP_PROMPT}{' '}
240
- <Link href="#/templates/login-split" type="supporting">
241
- {AUTH_SIGNUP_LINK}
242
- </Link>
243
- </Text>
245
+ <VStack hAlign="center">
246
+ <Text type="supporting" color="secondary">
247
+ {AUTH_SIGNUP_PROMPT}{' '}
248
+ <Link href="#/templates/login-split" type="supporting" hasUnderline>
249
+ {AUTH_SIGNUP_LINK}
250
+ </Link>
251
+ </Text>
252
+ </VStack>
244
253
  )}
245
254
  </VStack>
246
255
  </Section>
@@ -259,14 +268,14 @@ export default function LoginSplit() {
259
268
  </Card>
260
269
  </Center>
261
270
 
262
- <VStack hAlign="center">
263
- <Text type="supporting" color="secondary">
271
+ <VStack hAlign="center" width="100%">
272
+ <Text type="supporting" color="secondary" justify="center">
264
273
  {AUTH_TERMS_PREFIX}{' '}
265
- <Link href="#/templates/login-split" type="supporting">
274
+ <Link href="#/templates/login-split" type="supporting" hasUnderline>
266
275
  {AUTH_TERMS_SERVICE}
267
276
  </Link>{' '}
268
277
  and{' '}
269
- <Link href="#/templates/login-split" type="supporting">
278
+ <Link href="#/templates/login-split" type="supporting" hasUnderline>
270
279
  {AUTH_TERMS_PRIVACY}
271
280
  </Link>
272
281
  .