@sableui/react 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +3 -1
- package/CHANGELOG.md +120 -0
- package/CONTRIBUTING.md +8 -0
- package/README.md +1 -0
- package/bin/sable-lint.mjs +0 -0
- package/bin/sable.mjs +68 -26
- package/dist/{_contract-CYScOdf5.d.ts → _contract-o3tUc8Ig.d.ts} +1 -1
- package/dist/blocks/index.d.ts +396 -0
- package/dist/blocks/index.js +1997 -0
- package/dist/chunk-6LCIBMMG.js +4908 -0
- package/dist/{chunk-J23ELQQZ.js → chunk-CKNM7KSB.js} +1664 -4270
- package/dist/chunk-K6LII3OE.js +5093 -0
- package/dist/{chunk-RWP4XBSK.js → chunk-KFZ43RKU.js} +1 -1
- package/dist/{chunk-PAOZBZME.js → chunk-NGBDP3BM.js} +1 -1
- package/dist/{chunk-5Z5KLOAU.js → chunk-TH563WLW.js} +14 -1
- package/dist/{chunk-JI7WJP2L.js → chunk-X276MF2W.js} +5 -0
- package/dist/foundations/index.js +1 -1
- package/dist/icon/index.js +2 -2
- package/dist/icon-sets/reicon.js +1 -1
- package/dist/index.d.ts +624 -133
- package/dist/index.js +767 -4964
- package/dist/ios/index.d.ts +14 -2
- package/dist/ios/index.js +10 -9
- package/dist/ios/preview.d.ts +11 -1
- package/dist/ios/preview.js +14 -9
- package/dist/ios/styles.css +26 -6
- package/dist/{meta-6dUzgC6M.d.ts → meta-CcE_SeFu.d.ts} +37 -1
- package/dist/patterns/index.d.ts +1 -1
- package/dist/patterns/index.js +1684 -471
- package/dist/presets/index.d.ts +1 -1
- package/dist/presets/index.js +1 -1
- package/dist/styles.css +238 -40
- package/dist/{theme-provider-BWcizhAt.d.ts → theme-provider-BE-KqUG-.d.ts} +1 -1
- package/dist/themes/atelier/atelier.css +3 -0
- package/dist/themes/atelier/index.d.ts +1 -1
- package/dist/themes/base/base.css +34 -4
- package/dist/themes/base/index.d.ts +1 -1
- package/dist/themes/ember/ember.css +3 -0
- package/dist/themes/ember/index.d.ts +1 -1
- package/dist/themes/index.d.ts +2 -2
- package/dist/themes/index.js +2 -2
- package/dist/themes/meridian/index.d.ts +1 -1
- package/dist/themes/meridian/meridian.css +10 -5
- package/dist/themes/poppy/index.d.ts +1 -1
- package/dist/themes/poppy/poppy.css +16 -0
- package/dist/themes/prism/index.d.ts +1 -1
- package/dist/themes/prism/prism.css +4 -1
- package/dist/themes/verdant/index.d.ts +1 -1
- package/dist/themes/verdant/verdant.css +7 -2
- package/llms/blocks/api-keys-block.md +79 -0
- package/llms/blocks/chat-thread-block.md +51 -0
- package/llms/blocks/command-palette-block.md +48 -0
- package/llms/blocks/cta-band-block.md +62 -0
- package/llms/blocks/danger-zone-block.md +69 -0
- package/llms/blocks/device-comparison-block.md +91 -0
- package/llms/blocks/event-card-block.md +84 -0
- package/llms/blocks/faq-block.md +72 -0
- package/llms/blocks/feature-grid-block.md +75 -0
- package/llms/blocks/feedback-form-block.md +66 -0
- package/llms/blocks/goal-progress-block.md +54 -0
- package/llms/blocks/hero-block.md +65 -0
- package/llms/blocks/invite-team-block.md +61 -0
- package/llms/blocks/invoice-history-block.md +84 -0
- package/llms/blocks/kpi-sparkline-block.md +57 -0
- package/llms/blocks/media-player-block.md +100 -0
- package/llms/blocks/newsletter-block.md +64 -0
- package/llms/blocks/not-found-block.md +56 -0
- package/llms/blocks/notification-list-block.md +63 -0
- package/llms/blocks/notification-settings-block.md +54 -0
- package/llms/blocks/order-tracking-block.md +62 -0
- package/llms/blocks/pricing-card-block.md +82 -0
- package/llms/blocks/pricing-table-block.md +129 -0
- package/llms/blocks/product-card-block.md +69 -0
- package/llms/blocks/profile-card-block.md +103 -0
- package/llms/blocks/profile-form-block.md +114 -0
- package/llms/blocks/revenue-breakdown-block.md +70 -0
- package/llms/blocks/sign-in-block.md +64 -0
- package/llms/blocks/sign-up-block.md +78 -0
- package/llms/blocks/site-footer-block.md +96 -0
- package/llms/blocks/stat-row-block.md +59 -0
- package/llms/blocks/steps-block.md +73 -0
- package/llms/blocks/task-list-block.md +81 -0
- package/llms/blocks/team-members-block.md +91 -0
- package/llms/blocks/testimonial-block.md +78 -0
- package/llms/blocks/traffic-sources-block.md +63 -0
- package/llms/blocks/traffic-trend-block.md +95 -0
- package/llms/blocks/upload-list-block.md +66 -0
- package/llms/blocks/verify-code-block.md +59 -0
- package/llms/blocks/wallet-block.md +84 -0
- package/llms/components/area-chart.md +112 -0
- package/llms/components/bar-chart.md +131 -0
- package/llms/components/bubble.md +5 -5
- package/llms/components/button.md +1 -1
- package/llms/components/card.md +2 -2
- package/llms/components/checkbox-card.md +1 -1
- package/llms/components/context-card.md +2 -2
- package/llms/components/context-menu.md +1 -1
- package/llms/components/date-tile.md +79 -0
- package/llms/components/dialog.md +1 -1
- package/llms/components/empty-state.md +4 -4
- package/llms/components/entity.md +5 -3
- package/llms/components/fieldset.md +1 -1
- package/llms/components/kbd.md +18 -6
- package/llms/components/line-chart.md +101 -0
- package/llms/components/menu.md +1 -1
- package/llms/components/{dots-menu.md → overflow-menu.md} +9 -9
- package/llms/components/pie-chart.md +103 -0
- package/llms/components/pill.md +1 -1
- package/llms/components/radar-chart.md +106 -0
- package/llms/components/radial-chart.md +102 -0
- package/llms/components/segmented-bar.md +102 -0
- package/llms/components/settings-menu.md +2 -2
- package/llms/components/side-nav.md +1 -1
- package/llms/components/skeleton.md +1 -1
- package/llms/components/sparkline.md +80 -0
- package/llms/components/table.md +1 -1
- package/llms/components/theme-selector.md +1 -1
- package/llms/foundations/shape.md +1 -1
- package/llms/patterns/auth-page.md +132 -42
- package/llms/patterns/brand-landing-page.md +271 -175
- package/llms/patterns/chat-panel.md +120 -39
- package/llms/patterns/dashboard-page.md +276 -65
- package/llms/patterns/feed-page.md +245 -64
- package/llms/patterns/landing-page.md +473 -75
- package/llms/patterns/list-detail-page.md +377 -68
- package/llms/patterns/settings-page.md +268 -67
- package/llms/reference/token-intent.md +4 -2
- package/llms/reference/utilities.md +20 -5
- package/llms/tasks/building-an-ios-prototype.md +4 -1
- package/llms/tasks/choosing-a-component.md +2 -2
- package/llms-full.txt +6428 -892
- package/llms.txt +67 -16
- package/package.json +5 -1
- package/registry.json +3217 -552
- package/targets/ios.json +88 -18
- package/llms/components/description.md +0 -96
- package/dist/{chunk-L6Y46C62.js → chunk-MXJX3NZI.js} +1 -1
|
@@ -17,7 +17,7 @@ For a cluster of related settings that a reader checks often and changes occasio
|
|
|
17
17
|
- Do: Use `MenuRadioGroup` + `MenuRadioItem` inside a row for single-choice settings, so the current option is checked rather than merely remembered.
|
|
18
18
|
- Do: Reach for `variant="subtle"` when the trigger sits on a surface where a ghost control would float unanchored — inside a ChatComposer, for instance.
|
|
19
19
|
- Don't: Do not use it for a single setting. One choice is a `Select`; the nesting only earns its cost when there are several related settings to summarise.
|
|
20
|
-
- Don't: Do not put actions in it. Rows change state; a menu that mixes "Effort ›" with "Delete conversation" makes destructive commands look like preferences. That is `Menu` or `
|
|
20
|
+
- Don't: Do not put actions in it. Rows change state; a menu that mixes "Effort ›" with "Delete conversation" makes destructive commands look like preferences. That is `Menu` or `OverflowMenu`.
|
|
21
21
|
- Don't: Do not nest more than one level deep. Past that the reader loses the path back, and the thing you have is a settings dialog wearing a menu.
|
|
22
22
|
- Don't: Do not override the upward, end-aligned default without reason. Its home is the bottom of a composer or toolbar, where a downward menu opens off-screen.
|
|
23
23
|
|
|
@@ -69,7 +69,7 @@ For a cluster of related settings that a reader checks often and changes occasio
|
|
|
69
69
|
|
|
70
70
|
## Related
|
|
71
71
|
|
|
72
|
-
- Components: Menu,
|
|
72
|
+
- Components: Menu, OverflowMenu, Select, ChatComposer, Popover
|
|
73
73
|
- Patterns: ChatPanel
|
|
74
74
|
|
|
75
75
|
## Examples
|
|
@@ -15,7 +15,7 @@ Use SideNav for vertical navigation inside a settings page, documentation area,
|
|
|
15
15
|
- Do: Use aria-label on SideNav when a page contains more than one nav landmark.
|
|
16
16
|
- Do: Group related items with SideNavGroup when the rail has more than one conceptual area.
|
|
17
17
|
- Do: Use size='lg' and activeVariant='sidebar' when the nav is part of persistent app chrome.
|
|
18
|
-
- Do:
|
|
18
|
+
- Do: Rows are rounded-sm, which nests in the usual rounded-lg p-2 frame. In any other rounded, padded frame give each item rounded-concentric-<outer>/<inset> — a same-radius row inset into a frame reads as a second system.
|
|
19
19
|
- Don't: Use SideNav for bottom mobile navigation; use BottomNav in AppShell's bottomNav slot.
|
|
20
20
|
- Don't: Mix content and sidebar active variants in the same rail unless individual items intentionally override the group.
|
|
21
21
|
|
|
@@ -32,7 +32,7 @@ Use Skeleton to reserve space and hint at the shape of content while data is loa
|
|
|
32
32
|
- **arrival** · Root · _no motion_ — A skeleton stands in for content that has not arrived; fading the placeholder in adds a wait before the wait.
|
|
33
33
|
- **response** · Root · _no motion_ — Nothing here is interactive. A skeleton that reacted to the pointer would invite a click on a thing that cannot be clicked.
|
|
34
34
|
- **transition** · Root · _no motion_ — The skeleton has one state. Swapping it for real content is the parent's job, and the parent decides whether that swap is animated.
|
|
35
|
-
- **activity** · Root · `motion-shimmer` — The sweep is the entire point of the component: it is what separates 'still loading' from 'loaded, and empty'. A static grey block is indistinguishable from a broken layout, which is why users reload the page.
|
|
35
|
+
- **activity** · Root · `motion-shimmer` — The sweep is the entire point of the component: it is what separates 'still loading' from 'loaded, and empty'. A static grey block is indistinguishable from a broken layout, which is why users reload the page. It is one band of light against the viewport, so every skeleton in a loading layout lights in sequence as a single wave rather than each block flashing at its own speed; each pass eases in, glides and settles, then rests off-screen before the next.
|
|
36
36
|
|
|
37
37
|
## Accessibility
|
|
38
38
|
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Sparkline
|
|
2
|
+
|
|
3
|
+
**Status:** beta
|
|
4
|
+
|
|
5
|
+
A word-sized trend with no axes — a line, a line over a soft fill, or columns — sized by its container, for stat cards, table cells and sentences. Draws on as it scrolls into view.
|
|
6
|
+
|
|
7
|
+
**Keywords:** sparkline, trend, mini chart, inline chart, micro chart, kpi, stat
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
Use Sparkline next to a number to show where it has been — a KPI's last 30 days in a stat card, a row's trend in a table. It has no axes, tooltip or values to read off; when the reader needs those, use AreaChart or LineChart.
|
|
12
|
+
|
|
13
|
+
### Best practices
|
|
14
|
+
|
|
15
|
+
- Do: Pair it with the current value and its change in text; the sparkline shows only the shape.
|
|
16
|
+
- Do: Give it a label that names the measure and the period — the spoken summary adds the range and latest value.
|
|
17
|
+
- Don't: Put two sparklines on one shared axis to compare them — each scales to its own data; use LineChart.
|
|
18
|
+
- Don't: Use it as the only place a number appears; a shape without a value cannot be read off.
|
|
19
|
+
|
|
20
|
+
## Props
|
|
21
|
+
|
|
22
|
+
| Name | Type | Default | Description |
|
|
23
|
+
| --- | --- | --- | --- |
|
|
24
|
+
| data | number[] | — | The values, oldest first. |
|
|
25
|
+
| label | string | — | What the line tracks ("Revenue, last 30 days"). The spoken summary adds the first and last values, low and high. |
|
|
26
|
+
| variant | "line" \| "area" \| "bar" | "line" | A line, a line over a soft fill, or columns. |
|
|
27
|
+
| tone | "edition" \| "collectible" \| "standard" \| "info" \| "warning" \| "primary" \| "neutral" \| "success" \| "destructive" | "primary" | Colour by role. |
|
|
28
|
+
| className | string | "h-8 w-24" | Sizes it; the drawing stretches to fit. |
|
|
29
|
+
| animate | boolean | true | Draw on (or rise) when it scrolls into view. |
|
|
30
|
+
| locale | string | runtime locale | BCP 47 locale for default number and date formatting. |
|
|
31
|
+
|
|
32
|
+
## Variants
|
|
33
|
+
|
|
34
|
+
- **variant**: line, area, bar
|
|
35
|
+
|
|
36
|
+
## Tokens used
|
|
37
|
+
|
|
38
|
+
`primary`, `tone-edition`, `tone-collectible`, `tone-standard`, `tone-info`, `tone-warning`, `muted-foreground`, `success`, `destructive`
|
|
39
|
+
|
|
40
|
+
## Motion
|
|
41
|
+
|
|
42
|
+
- **arrival** · Line / bars · _custom (outside the recipe layer)_ — Once on screen the line draws on (or the bars rise, in sequence). On the motion tokens; reduced motion shows the finished drawing.
|
|
43
|
+
- **response** · Sparkline · _no motion_ — Not interactive: it has no tooltip or focus.
|
|
44
|
+
- **transition** · Sparkline · _no motion_ — New data redraws in place; a word-sized chart animating on every update would distract from the number beside it.
|
|
45
|
+
- **activity** · Sparkline · _no motion_ — No indeterminate state.
|
|
46
|
+
|
|
47
|
+
## Accessibility
|
|
48
|
+
|
|
49
|
+
- Rendered role=img with an aria-label built from `label` and the data: "Revenue, last 30 days: from 120 to 184, low 96, high 190".
|
|
50
|
+
- It is not focusable and has no tooltip, so the number it accompanies must be in text beside it.
|
|
51
|
+
|
|
52
|
+
## Related
|
|
53
|
+
|
|
54
|
+
- Components: AreaChart, LineChart, Card, Table
|
|
55
|
+
|
|
56
|
+
## Examples
|
|
57
|
+
|
|
58
|
+
### In a stat card
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
import { Card, CardAction, CardDescription, CardHeader, CardTitle, Sparkline } from "@sableui/react"
|
|
62
|
+
|
|
63
|
+
<Card>
|
|
64
|
+
<CardHeader>
|
|
65
|
+
<CardDescription>Revenue</CardDescription>
|
|
66
|
+
<CardTitle className="text-mono-xl">$18,240</CardTitle>
|
|
67
|
+
<CardAction>
|
|
68
|
+
<Sparkline label="Revenue, last 12 weeks" data={[12, 14, 13, 17, 16, 19, 18, 21, 20, 24, 23, 26]} variant="area" tone="success" />
|
|
69
|
+
</CardAction>
|
|
70
|
+
</CardHeader>
|
|
71
|
+
</Card>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Columns in a table cell
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
import { Sparkline } from "@sableui/react"
|
|
78
|
+
|
|
79
|
+
<Sparkline label="Daily signups, last 14 days" data={[4, 6, 3, 8, 7, 9, 5, 6, 10, 8, 7, 11, 9, 12]} variant="bar" className="h-6 w-20" />
|
|
80
|
+
```
|
package/llms/components/table.md
CHANGED
|
@@ -17,7 +17,7 @@ Use Table for structured data with consistent columns and rows. Use DataTable wh
|
|
|
17
17
|
- Do: Use the built-in horizontal frame for overflow instead of squeezing columns until content becomes unreadable.
|
|
18
18
|
- Do: Put a table in a card with `<CardContent flush>`. The first and last columns then take the card's own padding (`--table-edge-px`), so they line up with the CardHeader at any density. Never hand-set `pl-6`/`pr-6` on edge cells.
|
|
19
19
|
- Don't: Use Table for heterogeneous items with no stable columns. Use Card, Entity, or a list composition instead.
|
|
20
|
-
- Don't: Put every possible action in every row. Use one primary row action,
|
|
20
|
+
- Don't: Put every possible action in every row. Use one primary row action, an OverflowMenu, or row detail navigation.
|
|
21
21
|
|
|
22
22
|
### Anatomy
|
|
23
23
|
|
|
@@ -12,7 +12,7 @@ Use ThemeSelector wherever a person picks a theme. The default `cards` layout is
|
|
|
12
12
|
|
|
13
13
|
### Best practices
|
|
14
14
|
|
|
15
|
-
- Do: Use layout="list" in a popover, a menu, or a docked panel. Rows sit on the --control-sm ladder and still show each theme's swatch strip, so the choice stays visible rather than described. Rows take `sable-frame-inner`, so host the list in a sable-frame (
|
|
15
|
+
- Do: Use layout="list" in a popover, a menu, or a docked panel. Rows sit on the --control-sm ladder and still show each theme's swatch strip, so the choice stays visible rather than described. Rows take `sable-frame-inner`, so host the list in a sable-frame (`sable-frame sable-surface sable-surface-popover sable-frame-popover` — PopoverContent is not one; give it that class list, or its p-4 leaves the rows unnested) and their corners nest concentrically.
|
|
16
16
|
- Do: Pass mode={useSableTheme().mode} so every preview renders in the colour mode actually in use, rather than in each theme's manifest default.
|
|
17
17
|
- Do: Load each theme's fonts in the host app. Every card carries a type specimen in that theme's own face so people can compare typography as well as color. A theme whose fonts are not loaded falls back to system-ui, and its specimen stops telling the truth.
|
|
18
18
|
- Don't: Add a viewport breakpoint to force a column count — `className="sm:grid-cols-1"` or similar. `sm:` keys off the VIEWPORT, not this component's container, so on a wide screen it does nothing for a narrow parent. The cards grid already reflows on its own width via auto-fit.
|
|
@@ -30,7 +30,7 @@ Each step is 4px apart, so common insets land on a real step. Themes and the cus
|
|
|
30
30
|
## Rules
|
|
31
31
|
|
|
32
32
|
1. **Nested corners are concentric: inner radius = outer radius − inset. Write it as rounded-concentric-<outer>/<inset>, not a hardcoded step.** A 12px popover with 4px padding takes 8px items. The utility computes the subtraction from the live tokens, so the pair stays concentric when a theme or the customizer rescales the radius ladder; a hardcoded rounded-sm drifts.
|
|
33
|
-
2. **A list of rows inside a rounded popup uses popup-inset on the popup, rounded-popup-item on the rows and popup-bleed on a separator.** All three derive from the popup's radius: the inset grows to a third of it (never below --spacing(1), so density moves it too), the rows take what is left, and a separator bleeds by exactly the inset. A rounder look gets more room instead of rows that round into pills
|
|
33
|
+
2. **A list of rows inside a rounded popup uses popup-inset on the popup, rounded-popup-item on the rows and popup-bleed on a separator.** All three derive from the popup's radius: the inset grows to a third of it (never below --spacing(1), so density moves it too), the rows take what is left after the inset and the 1px border, and a separator bleeds by exactly the inset. A rounder look gets more room instead of rows that round into pills; the default ladder lands on a 4px inset and 7px rows. *Enforced by sable-lint: popup-separator-bleed.*
|
|
34
34
|
3. **A Button, Pill or Badge gets its look from a variant, never from override classes.** border, bg, rounded, ring and shadow classes on a component fight the variant's own hover, selected and focus states, and give one call site a look no other site can ask for. Use the variant that exists (Button variant="floating" over content, "on-color" on a fill; Pill appearance="outline"; Badge appearance and tone) or add one to the component. *Enforced by sable-lint: component-restyle.*
|
|
35
35
|
4. **The outer radius is always at least the inner radius.** If the inset is larger than the outer radius, square the inner element or step the container up.
|
|
36
36
|
5. **Never mix radii within one component.** A component reads as one object when its corners agree.
|
|
@@ -1,67 +1,157 @@
|
|
|
1
1
|
# AuthPage
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Split sign-in page: a form column (brand mark, heading, email + password, primary sign-in, passkey and wallet alternatives, sign-up switch, terms) beside a brand panel painted from the theme's tones with a featured release card. Below lg the panel drops away and the form is the whole screen.
|
|
4
4
|
|
|
5
5
|
**Intent:** Reach for this for sign-in, sign-up, forgot-password, and any single-purpose one-column form route (invite accept, unsubscribe). Not for multi-section forms (use SettingsPage).
|
|
6
6
|
|
|
7
7
|
## Notes
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
9
|
+
- The form column is a `form`-capped Region (480px), so the controls keep the same measure whether the panel is present or not.
|
|
10
|
+
- No card around the form: the page itself is the surface, and the brand panel carries the identity. A card on a blank page reads as a template.
|
|
11
|
+
- The heading is `text-heading-1` in an h1 — it is the page outline, so it takes a heading tier, not a card title.
|
|
12
|
+
- Brand mark, form and terms are a three-part column: mark pinned to the top, form vertically centred in the remaining space, terms pinned to the bottom.
|
|
13
|
+
- Forgot password sits under the password field, right-aligned, before the submit button — so it comes right after the field in tab order. Field has no label-row action slot yet.
|
|
14
|
+
- One primary action. Passkey and wallet are outline buttons under an `or` Separator; switching to sign-up is a quiet `link-underline` link.
|
|
15
|
+
- Validation uses Field `validate` for product copy instead of the browser's generic required message. A rejected sign-in comes back through Form `errors` on the password field; the submit Button shows `loading` while the request is in flight.
|
|
16
|
+
- The brand panel artwork is a mesh of the theme's decorative tones over `--primary`, so it re-paints for every theme and mode. The relative-colour `oklch(from var(--tone-*) …)` lift needs an `inline-color` disable with a reason.
|
|
17
|
+
- The panel is `hidden lg:flex`: on a phone the form is the whole screen, with no decorative scroll above it.
|
|
14
18
|
|
|
15
19
|
## Accessibility
|
|
16
20
|
|
|
17
|
-
- The route renders inside <main> with
|
|
18
|
-
- Field auto-wires labels, describedby, and invalid state onto the inputs.
|
|
19
|
-
- Autocomplete attributes (email
|
|
21
|
+
- The route renders inside <main> with one h1; the brand panel is an <aside> labelled "Featured release".
|
|
22
|
+
- Field auto-wires labels, describedby, and invalid state onto the inputs; on submit, the first invalid field is focused.
|
|
23
|
+
- Autocomplete attributes (`email webauthn`, `current-password`) keep password managers and passkey autofill working.
|
|
24
|
+
- The Forgot password and Create one links use `hit-target` to meet the 44px touch floor without changing layout.
|
|
25
|
+
- The artwork swatches are aria-hidden decoration; the brand link's accessible name is the wordmark.
|
|
20
26
|
|
|
21
27
|
## Source
|
|
22
28
|
|
|
23
29
|
```tsx
|
|
24
|
-
import {
|
|
30
|
+
import { useState } from "react"
|
|
31
|
+
import { Avatar, Badge, Button, Card, CardContent, Entity, Field, Form, Icon, Input, Region, Separator, Stack } from "@sableui/react"
|
|
32
|
+
|
|
33
|
+
// A soft mesh of the theme's decorative tones over its brand colour, so the
|
|
34
|
+
// artwork follows every theme and mode. Each tone is lifted from its text
|
|
35
|
+
// strength (dark, restrained) to an art strength.
|
|
36
|
+
const artwork = [
|
|
37
|
+
// design-lint-disable-next-line inline-color -- relative colour derived from the theme's own --tone token, so it follows every look
|
|
38
|
+
"radial-gradient(in oklab at 18% 22%, oklch(from var(--tone-info) clamp(0.5, calc(l + 0.16), 0.9) max(calc(c * 1.25), 0.1) h), transparent 58%)",
|
|
39
|
+
// design-lint-disable-next-line inline-color -- relative colour derived from the theme's own --tone token, so it follows every look
|
|
40
|
+
"radial-gradient(in oklab at 82% 18%, oklch(from var(--tone-collectible) clamp(0.5, calc(l + 0.16), 0.9) max(calc(c * 1.25), 0.1) h), transparent 55%)",
|
|
41
|
+
// design-lint-disable-next-line inline-color -- relative colour derived from the theme's own --tone token, so it follows every look
|
|
42
|
+
"radial-gradient(in oklab at 70% 88%, oklch(from var(--tone-edition) clamp(0.5, calc(l + 0.16), 0.9) max(calc(c * 1.25), 0.1) h), transparent 60%)",
|
|
43
|
+
// design-lint-disable-next-line inline-color -- relative colour derived from the theme's own --tone token, so it follows every look
|
|
44
|
+
"radial-gradient(in oklab at 12% 92%, oklch(from var(--tone-standard) clamp(0.5, calc(l + 0.16), 0.9) max(calc(c * 1.25), 0.1) h), transparent 55%)",
|
|
45
|
+
"var(--primary)",
|
|
46
|
+
].join(", ")
|
|
25
47
|
|
|
26
48
|
export function AuthPage() {
|
|
49
|
+
const [pending, setPending] = useState(false)
|
|
50
|
+
const [errors, setErrors] = useState<Record<string, string>>({})
|
|
51
|
+
|
|
27
52
|
return (
|
|
28
|
-
<main className="grid min-h-dvh
|
|
29
|
-
<Region max="form" safeArea={["top", "bottom"]}>
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
<
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
53
|
+
<main className="grid min-h-dvh bg-background lg:grid-cols-[minmax(0,1fr)_minmax(0,1.1fr)]">
|
|
54
|
+
<Region max="form" safeArea={["top", "bottom"]} className="flex flex-col">
|
|
55
|
+
{/* safeArea owns the region's own padding, so the breathing room sits one level in. */}
|
|
56
|
+
<Stack gap={0} className="flex-1 py-8 sm:py-10">
|
|
57
|
+
<a href="/" className="flex items-center gap-2.5 self-start rounded-sm motion-interactive motion-press">
|
|
58
|
+
{/* The same mark the app shell's sidebar and header carry. */}
|
|
59
|
+
<Icon name="gem" variant="solid" className="text-2xl" />
|
|
60
|
+
<span className="text-title-lg">Atelier</span>
|
|
61
|
+
</a>
|
|
62
|
+
|
|
63
|
+
<Stack gap={4} justify="center" className="flex-1 py-12">
|
|
64
|
+
<Stack gap={1.5}>
|
|
65
|
+
<h1 className="text-heading-1">Welcome back</h1>
|
|
66
|
+
<p className="text-body-lg text-muted-foreground">
|
|
67
|
+
Sign in to collect new releases and manage your editions.
|
|
68
|
+
</p>
|
|
69
|
+
</Stack>
|
|
70
|
+
|
|
71
|
+
<Form
|
|
72
|
+
className="gap-5"
|
|
73
|
+
errors={errors}
|
|
74
|
+
onFormSubmit={() => {
|
|
75
|
+
setPending(true)
|
|
76
|
+
// Replace with your auth call; a rejected sign-in comes back as a field error.
|
|
77
|
+
setTimeout(() => {
|
|
78
|
+
setPending(false)
|
|
79
|
+
setErrors({ password: "That email and password don’t match. Try again or reset your password." })
|
|
80
|
+
}, 900)
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
<Field label="Email" name="email" validate={(value) => (value ? null : "Enter your email address.")}>
|
|
84
|
+
<Input type="email" autoComplete="email webauthn" placeholder="you@example.com" />
|
|
85
|
+
</Field>
|
|
86
|
+
<Stack gap={1.5}>
|
|
87
|
+
<Field label="Password" name="password" validate={(value) => (value ? null : "Enter your password.")}>
|
|
88
|
+
<Input type="password" autoComplete="current-password" />
|
|
46
89
|
</Field>
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
</
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
90
|
+
<a href="/forgot-password" className="hit-target self-end text-body-sm text-muted-foreground link-underline motion-interactive">
|
|
91
|
+
Forgot password?
|
|
92
|
+
</a>
|
|
93
|
+
</Stack>
|
|
94
|
+
<Button type="submit" size="cta" loading={pending} loadingLabel="Signing in" className="w-full">
|
|
95
|
+
Sign in
|
|
96
|
+
</Button>
|
|
97
|
+
</Form>
|
|
98
|
+
|
|
99
|
+
<Separator label="or" />
|
|
100
|
+
|
|
101
|
+
<Stack gap={1.5}>
|
|
102
|
+
<Button variant="outline" size="cta" className="w-full" iconStart={<Icon name="key" />}>
|
|
103
|
+
Sign in with a passkey
|
|
104
|
+
</Button>
|
|
105
|
+
<Button variant="outline" size="cta" className="w-full" iconStart={<Icon name="wallet" />}>
|
|
106
|
+
Connect a wallet
|
|
107
|
+
</Button>
|
|
108
|
+
</Stack>
|
|
109
|
+
|
|
110
|
+
<p className="text-body-md text-muted-foreground">
|
|
111
|
+
No account?{" "}
|
|
112
|
+
<a href="/signup" className="hit-target link-underline motion-interactive text-foreground">
|
|
113
|
+
Create one
|
|
114
|
+
</a>
|
|
115
|
+
</p>
|
|
116
|
+
</Stack>
|
|
117
|
+
|
|
118
|
+
<p className="text-body-xs text-muted-foreground">
|
|
119
|
+
By signing in you agree to the{" "}
|
|
120
|
+
<a href="/terms" className="link-underline motion-interactive">
|
|
121
|
+
Terms
|
|
122
|
+
</a>{" "}
|
|
123
|
+
and{" "}
|
|
124
|
+
<a href="/privacy" className="link-underline motion-interactive">
|
|
125
|
+
Privacy Policy
|
|
61
126
|
</a>
|
|
127
|
+
.
|
|
62
128
|
</p>
|
|
63
129
|
</Stack>
|
|
64
130
|
</Region>
|
|
131
|
+
|
|
132
|
+
{/* The brand panel is decoration: on a phone the form is the whole screen. */}
|
|
133
|
+
<aside aria-label="Featured release" className="hidden p-3 lg:flex">
|
|
134
|
+
<div className="flex flex-1 flex-col justify-end rounded-xl p-8" style={{ background: artwork }}>
|
|
135
|
+
<Card className="max-w-md">
|
|
136
|
+
<CardContent>
|
|
137
|
+
<Stack gap={3}>
|
|
138
|
+
<Badge tone="edition" className="self-start">
|
|
139
|
+
Edition of 50
|
|
140
|
+
</Badge>
|
|
141
|
+
<p className="text-title-lg">
|
|
142
|
+
“Every edition is a conversation with whoever holds it next.”
|
|
143
|
+
</p>
|
|
144
|
+
<Entity
|
|
145
|
+
className="p-0"
|
|
146
|
+
leading={<Avatar fallback="MO" />}
|
|
147
|
+
title="Mara Okafor"
|
|
148
|
+
subtitle="Untitled (gold rush), 2026"
|
|
149
|
+
/>
|
|
150
|
+
</Stack>
|
|
151
|
+
</CardContent>
|
|
152
|
+
</Card>
|
|
153
|
+
</div>
|
|
154
|
+
</aside>
|
|
65
155
|
</main>
|
|
66
156
|
)
|
|
67
157
|
}
|