@teamblind-chorus/ui 1.0.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/LICENSE +21 -0
- package/README.md +112 -0
- package/agents/AGENTS.md +143 -0
- package/agents/DESIGN.md +1311 -0
- package/agents/LOVABLE.md +472 -0
- package/agents/anti-patterns.md +533 -0
- package/agents/catalog.md +232 -0
- package/agents/components/avatar-rail/avatar-rail.family.json +46 -0
- package/agents/components/avatar-rail/avatar-rail.md +103 -0
- package/agents/components/avatar-rail/avatar-rail.spec.json +160 -0
- package/agents/components/badge/badge.family.json +45 -0
- package/agents/components/badge/badge.md +10 -0
- package/agents/components/badge/role.md +100 -0
- package/agents/components/badge/role.spec.json +75 -0
- package/agents/components/badge/update.md +132 -0
- package/agents/components/badge/update.spec.json +114 -0
- package/agents/components/banner/banner.family.json +28 -0
- package/agents/components/banner/banner.md +136 -0
- package/agents/components/banner/banner.spec.json +136 -0
- package/agents/components/bottom-sheet/bottom-sheet.family.json +29 -0
- package/agents/components/bottom-sheet/bottom-sheet.md +176 -0
- package/agents/components/bottom-sheet/bottom-sheet.spec.json +168 -0
- package/agents/components/bubble/bubble.family.json +29 -0
- package/agents/components/bubble/bubble.md +134 -0
- package/agents/components/bubble/bubble.spec.json +91 -0
- package/agents/components/button/button.family.json +76 -0
- package/agents/components/button/button.md +31 -0
- package/agents/components/button/check.md +138 -0
- package/agents/components/button/check.spec.json +161 -0
- package/agents/components/button/fab.md +161 -0
- package/agents/components/button/fab.spec.json +106 -0
- package/agents/components/button/icon.md +141 -0
- package/agents/components/button/icon.spec.json +164 -0
- package/agents/components/button/standard.md +219 -0
- package/agents/components/button/standard.spec.json +205 -0
- package/agents/components/button/text.md +186 -0
- package/agents/components/button/text.spec.json +215 -0
- package/agents/components/button/toggle.md +108 -0
- package/agents/components/button/toggle.spec.json +124 -0
- package/agents/components/button/toolbar.md +189 -0
- package/agents/components/button/toolbar.spec.json +109 -0
- package/agents/components/carousel/carousel.family.json +41 -0
- package/agents/components/carousel/carousel.md +40 -0
- package/agents/components/carousel/post.md +148 -0
- package/agents/components/carousel/post.spec.json +229 -0
- package/agents/components/carousel/profile.md +184 -0
- package/agents/components/carousel/profile.spec.json +219 -0
- package/agents/components/chip/chip.family.json +37 -0
- package/agents/components/chip/chip.md +10 -0
- package/agents/components/chip/filter.md +212 -0
- package/agents/components/chip/filter.spec.json +124 -0
- package/agents/components/chip/tag.md +137 -0
- package/agents/components/chip/tag.spec.json +104 -0
- package/agents/components/dialog/dialog.family.json +29 -0
- package/agents/components/dialog/dialog.md +113 -0
- package/agents/components/dialog/dialog.spec.json +156 -0
- package/agents/components/directory-list/directory-list.family.json +46 -0
- package/agents/components/directory-list/directory-list.md +87 -0
- package/agents/components/directory-list/directory-list.spec.json +104 -0
- package/agents/components/divider/divider.family.json +28 -0
- package/agents/components/divider/divider.md +78 -0
- package/agents/components/divider/divider.spec.json +51 -0
- package/agents/components/feed/ad.md +108 -0
- package/agents/components/feed/ad.spec.json +187 -0
- package/agents/components/feed/feed.family.json +48 -0
- package/agents/components/feed/feed.md +30 -0
- package/agents/components/feed/post.md +240 -0
- package/agents/components/feed/post.spec.json +361 -0
- package/agents/components/form-field/form-field.family.json +50 -0
- package/agents/components/form-field/form-field.md +11 -0
- package/agents/components/form-field/input.md +198 -0
- package/agents/components/form-field/input.spec.json +202 -0
- package/agents/components/form-field/search.md +81 -0
- package/agents/components/form-field/search.spec.json +135 -0
- package/agents/components/form-field/select.md +101 -0
- package/agents/components/form-field/select.spec.json +194 -0
- package/agents/components/form-field/textarea.md +89 -0
- package/agents/components/form-field/textarea.spec.json +176 -0
- package/agents/components/header/header.family.json +43 -0
- package/agents/components/header/header.md +18 -0
- package/agents/components/header/main.md +101 -0
- package/agents/components/header/main.spec.json +117 -0
- package/agents/components/header/sub.md +129 -0
- package/agents/components/header/sub.spec.json +81 -0
- package/agents/components/list/accordion.md +183 -0
- package/agents/components/list/accordion.spec.json +201 -0
- package/agents/components/list/entry.md +280 -0
- package/agents/components/list/entry.spec.json +237 -0
- package/agents/components/list/list.family.json +75 -0
- package/agents/components/list/list.md +24 -0
- package/agents/components/list/radio.md +144 -0
- package/agents/components/list/radio.spec.json +186 -0
- package/agents/components/list/standard.md +262 -0
- package/agents/components/list/standard.spec.json +221 -0
- package/agents/components/metadata/compact.md +69 -0
- package/agents/components/metadata/compact.spec.json +69 -0
- package/agents/components/metadata/metadata.family.json +42 -0
- package/agents/components/metadata/metadata.md +26 -0
- package/agents/components/metadata/standard.md +104 -0
- package/agents/components/metadata/standard.spec.json +152 -0
- package/agents/components/nav-card/nav-card.family.json +29 -0
- package/agents/components/nav-card/nav-card.md +179 -0
- package/agents/components/nav-card/nav-card.spec.json +161 -0
- package/agents/components/nav-list/nav-list.family.json +46 -0
- package/agents/components/nav-list/nav-list.md +91 -0
- package/agents/components/nav-list/nav-list.spec.json +107 -0
- package/agents/components/navigation-bar/main.md +201 -0
- package/agents/components/navigation-bar/main.spec.json +109 -0
- package/agents/components/navigation-bar/navigation-bar.family.json +44 -0
- package/agents/components/navigation-bar/navigation-bar.md +21 -0
- package/agents/components/navigation-bar/search.md +96 -0
- package/agents/components/navigation-bar/search.spec.json +142 -0
- package/agents/components/navigation-bar/sub.md +174 -0
- package/agents/components/navigation-bar/sub.spec.json +123 -0
- package/agents/components/page-shell/page-shell.family.json +22 -0
- package/agents/components/page-shell/page-shell.md +51 -0
- package/agents/components/profile-header/profile-header.family.json +29 -0
- package/agents/components/profile-header/profile-header.md +149 -0
- package/agents/components/profile-header/profile-header.spec.json +200 -0
- package/agents/components/progress/progress.family.json +27 -0
- package/agents/components/progress/progress.md +38 -0
- package/agents/components/progress/progress.spec.json +67 -0
- package/agents/components/side-sheet/side-sheet.family.json +30 -0
- package/agents/components/side-sheet/side-sheet.md +154 -0
- package/agents/components/side-sheet/side-sheet.spec.json +109 -0
- package/agents/components/skeleton/skeleton.family.json +28 -0
- package/agents/components/skeleton/skeleton.md +123 -0
- package/agents/components/skeleton/skeleton.spec.json +73 -0
- package/agents/components/status-tag/status-tag.family.json +26 -0
- package/agents/components/status-tag/status-tag.md +114 -0
- package/agents/components/status-tag/status-tag.spec.json +69 -0
- package/agents/components/suggestion-list/suggestion-list.family.json +46 -0
- package/agents/components/suggestion-list/suggestion-list.md +91 -0
- package/agents/components/suggestion-list/suggestion-list.spec.json +178 -0
- package/agents/components/switch/switch.family.json +27 -0
- package/agents/components/switch/switch.md +114 -0
- package/agents/components/switch/switch.spec.json +123 -0
- package/agents/components/tab-bar/tab-bar.family.json +27 -0
- package/agents/components/tab-bar/tab-bar.md +178 -0
- package/agents/components/tab-bar/tab-bar.spec.json +184 -0
- package/agents/components/tabs/rounded.md +150 -0
- package/agents/components/tabs/rounded.spec.json +140 -0
- package/agents/components/tabs/segmented.md +114 -0
- package/agents/components/tabs/segmented.spec.json +100 -0
- package/agents/components/tabs/tabs.family.json +59 -0
- package/agents/components/tabs/tabs.md +18 -0
- package/agents/components/tabs/underline.md +147 -0
- package/agents/components/tabs/underline.spec.json +139 -0
- package/agents/components/thumbnail/thumbnail.family.json +28 -0
- package/agents/components/thumbnail/thumbnail.md +152 -0
- package/agents/components/thumbnail/thumbnail.spec.json +172 -0
- package/agents/components/toast/toast.family.json +28 -0
- package/agents/components/toast/toast.md +133 -0
- package/agents/components/toast/toast.spec.json +89 -0
- package/agents/components/tooltip/tooltip.family.json +29 -0
- package/agents/components/tooltip/tooltip.md +139 -0
- package/agents/components/tooltip/tooltip.spec.json +110 -0
- package/agents/compose.md +240 -0
- package/agents/icons.json +831 -0
- package/agents/images.md +66 -0
- package/agents/manifest.json +87 -0
- package/agents/patterns/README.md +59 -0
- package/agents/patterns/actions.md +50 -0
- package/agents/patterns/browsing.md +52 -0
- package/agents/patterns/communications.md +56 -0
- package/agents/patterns/layout.md +72 -0
- package/agents/patterns/modals.md +50 -0
- package/agents/patterns/visual.md +55 -0
- package/agents/reconstruct.md +55 -0
- package/agents/scoped-adoption.md +111 -0
- package/agents/tokens.usage.json +1657 -0
- package/agents/usage.json +422 -0
- package/dist/icons/index.cjs +1332 -0
- package/dist/icons/index.cjs.map +1 -0
- package/dist/icons/index.d.cts +228 -0
- package/dist/icons/index.d.ts +228 -0
- package/dist/icons/index.js +1114 -0
- package/dist/icons/index.js.map +1 -0
- package/dist/index.cjs +5905 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +896 -0
- package/dist/index.d.ts +896 -0
- package/dist/index.js +5847 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +5765 -0
- package/eslint/README.md +79 -0
- package/eslint/index.js +78 -0
- package/eslint/rules.js +472 -0
- package/eslint/test.mjs +135 -0
- package/package.json +96 -0
- package/placeholder.png +0 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Nav list
|
|
2
|
+
|
|
3
|
+
A vertical label-only nav list — labelled block where each row carries a label (and an optional supporting line) plus a trailing chevron Icon Button, and routes via `href` / `onClick`. Bundles a [Header](../header/header.md) over a label-only [List](../list/entry.md) `variant="entry"` so the section title and the route group come as one composition. Anatomy is intentionally label-only — `thumbnail` is omitted on every row, the leading column collapses, and the trailing rail carries a default [Icon Button](../button/icon.md) with a right-pointing chevron.
|
|
4
|
+
|
|
5
|
+
**Reach for this when** the screen shows a category index, settings menu, or any "pick a sub-page" set where each row is purely a route target. **Skip when** the rows need a leading thumbnail (use [DirectoryList](../directory-list/directory-list.md)) or the rows commit in place rather than route (use [List](../list/standard.md) `variant="standard"`).
|
|
6
|
+
|
|
7
|
+
**Layout inset.** `full-bleed` — sits as a direct child of the page shell (or any host that pays the gutter). Container pays its own `24px block / 16px inline` padding; each row keeps the list/entry native `16px inline padding` for the tap target and pulls its inline margin by `-16` so the visible label lines up with the header label at 16 from the surface. Do **not** wrap in another `padding-inline` / `px-*` / `style={{ padding: … }}` div. Inside a bounded surface (Card / Dialog / BottomSheet / Sheet), apply the negative-margin opt-out — see [`AGENTS.md` § Composition rules](../../../AGENTS.md#composition-rules).
|
|
8
|
+
|
|
9
|
+
## Default
|
|
10
|
+
|
|
11
|
+
Header label only, nine category rows with trailing chevrons.
|
|
12
|
+
|
|
13
|
+
```preview
|
|
14
|
+
nav-list/default
|
|
15
|
+
---
|
|
16
|
+
import { NavList } from '@teamblind-chorus/ui';
|
|
17
|
+
|
|
18
|
+
<NavList
|
|
19
|
+
label="Category"
|
|
20
|
+
items={[
|
|
21
|
+
{ value: 'location', label: 'Location', href: '/category/location' },
|
|
22
|
+
{ value: 'job', label: 'Job Function', href: '/category/job-function' },
|
|
23
|
+
{ value: 'learning', label: 'Learning & Advising', href: '/category/learning' },
|
|
24
|
+
{ value: 'money', label: 'Money', href: '/category/money' },
|
|
25
|
+
{ value: 'industry', label: 'Industry', href: '/category/industry' },
|
|
26
|
+
{ value: 'worklife', label: 'Work Life', href: '/category/work-life' },
|
|
27
|
+
{ value: 'entertainment', label: 'Entertainment', href: '/category/entertainment' },
|
|
28
|
+
{ value: 'relationships', label: 'Relationships & Social',href: '/category/relationships' },
|
|
29
|
+
{ value: 'culture', label: 'Culture', href: '/category/culture' },
|
|
30
|
+
]}
|
|
31
|
+
/>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Use cases
|
|
35
|
+
|
|
36
|
+
### With header action
|
|
37
|
+
|
|
38
|
+
Extends the header with a trailing `accent` Text Button when the screen has a broader index page or "Manage" route.
|
|
39
|
+
|
|
40
|
+
```preview
|
|
41
|
+
nav-list/with-header-action
|
|
42
|
+
---
|
|
43
|
+
import { NavList } from '@teamblind-chorus/ui';
|
|
44
|
+
|
|
45
|
+
<NavList
|
|
46
|
+
label="Settings"
|
|
47
|
+
headerAction={{ label: 'Manage', href: '/settings/manage' }}
|
|
48
|
+
items={[
|
|
49
|
+
{ value: 'account', label: 'Account', href: '/settings/account' },
|
|
50
|
+
{ value: 'notifications',label: 'Notifications', href: '/settings/notifications' },
|
|
51
|
+
{ value: 'privacy', label: 'Privacy', href: '/settings/privacy' },
|
|
52
|
+
{ value: 'appearance', label: 'Appearance', href: '/settings/appearance' },
|
|
53
|
+
{ value: 'language', label: 'Language', href: '/settings/language' },
|
|
54
|
+
]}
|
|
55
|
+
/>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Slots
|
|
59
|
+
|
|
60
|
+
- **container** — `surface` block with 24px block / 16px inline padding. Holds the header above the vertical nav list.
|
|
61
|
+
- **header** — [Header](../header/header.md) `size="large"`. Section label leading, optional accent Text Button trailing. Anchored above the list.
|
|
62
|
+
- **list** — embedded [List](../list/entry.md) `variant="entry"` carrying the rows. `thumbnail` is omitted on every row (leading column collapses); the row's `trailingIcon` slot is filled with a default chevron Icon Button.
|
|
63
|
+
- **row** — single nav row rendered by [List](../list/entry.md) as a label-only entry. The label is the primary content; the trailing Icon Button signals drill-in.
|
|
64
|
+
- **trailingChevron** — default [Icon Button](../button/icon.md) (`variant="icon"`, `size="medium"`) filled with a right-pointing `ChevronRightIcon`. Wired to the row's `href` / `onClick` so the trailing slot is a separate hit target.
|
|
65
|
+
|
|
66
|
+
## Anatomy
|
|
67
|
+
|
|
68
|
+
| Slot | Token bindings |
|
|
69
|
+
|----------------|----------------|
|
|
70
|
+
| container | `surface` fill, 24px block / 16px inline padding, vertical stack |
|
|
71
|
+
| header | [Header](../header/header.md) `size="large"`. Container stack (`sys.layout.stack.md` = 16px) separates from list. |
|
|
72
|
+
| label | `heading.md` / Semibold / `onSurface` |
|
|
73
|
+
| headerAction | `xsmall` [Text Button](../button/text.md), `accent` appearance |
|
|
74
|
+
| list | [List](../list/entry.md) `variant="entry"`. `embedded` (wrapper section owns the rail). Label-only — `thumbnail` is omitted on every row so the leading column collapses and the label sits flush at the 16 inline rail. |
|
|
75
|
+
| row | [list/entry](../list/entry.md)-shaped label-only row — label.md primary / label.sm `description` (optional). Keeps the list/entry native `container.md` inline padding and adds `margin-inline: -container.md` so the label lines up at the section rail (16 from the surface). Divider falls back to the default 16/16 inset (no avatar column to anchor against). |
|
|
76
|
+
| trailingChevron| Default [Icon Button](../button/icon.md) — `variant="icon"`, `size="medium"`, `icon={<ChevronRightIcon />}`. Glyph at `sys.icon.md` / `onSurfaceVariant`. |
|
|
77
|
+
|
|
78
|
+
## States
|
|
79
|
+
|
|
80
|
+
Each row body is the interactive route target — hover / pressed / focused / disabled tokens delegate to [List](../list/entry.md). The trailing chevron Icon Button paints its own state per [Icon Button](../button/icon.md) and wires the same route — clicks on the icon stop propagating before reaching the row, so the trailing slot is its own hit target. The **headerAction** is an `xsmall` Text Button (rendered as `<a>` when `href` is set).
|
|
81
|
+
|
|
82
|
+
## Focus indicator
|
|
83
|
+
|
|
84
|
+
Row body takes the family-wide Inward focus ring (per [List](../list/entry.md)); the trailing chevron Icon Button paints its own Outward ring; headerAction paints its own Outward ring.
|
|
85
|
+
|
|
86
|
+
## Behavior
|
|
87
|
+
|
|
88
|
+
- **Header is required.** Every NavList carries a `label`; the optional `headerAction` extends the header with a trailing `accent` Text Button when there's an index page to route to.
|
|
89
|
+
- **Vertical scroll, no pager.** The full list scrolls in normal document flow.
|
|
90
|
+
- **Each row routes.** Rows render as `<a>` when `href` is set; otherwise `onClick` fires. The trailing chevron Icon Button mirrors the same route so the trailing slot is a redundant — but independently focusable — affordance.
|
|
91
|
+
- **Label-only by design.** The wrapper does not expose a `thumbnail` prop on `items`; the underlying [list/entry](../list/entry.md) keeps the leading slot collapsed for every row. Reach for [DirectoryList](../directory-list/directory-list.md) when a leading avatar is needed.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../spec.schema.json",
|
|
3
|
+
"name": "NavList",
|
|
4
|
+
"family": "nav-list",
|
|
5
|
+
"description": "Vertical label-only nav list — labelled block where each row carries a label and a trailing chevron [Icon Button](../button/icon.md), and routes via `href` / `onClick`. Bundles a [Header](../header/header.md) over a label-only [List](../list/entry.md) `variant=\"entry\"` (every row drops `thumbnail` so the leading column collapses). Reach for it on category indexes, settings menus, and 'pick a sub-page' surfaces.",
|
|
6
|
+
"element": "section",
|
|
7
|
+
"props": {
|
|
8
|
+
"embedded": {
|
|
9
|
+
"type": "boolean",
|
|
10
|
+
"default": false,
|
|
11
|
+
"description": "Composition mode flag. When `true` (or when NavList is a direct child of `.chorus-carousel` / `.chorus-feed`), the list enters **embedded mode**: zeroes its own `background` + `padding` so chrome defers to the host."
|
|
12
|
+
},
|
|
13
|
+
"label": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"required": true,
|
|
16
|
+
"description": "Section title."
|
|
17
|
+
},
|
|
18
|
+
"headerAction": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"optional": true,
|
|
21
|
+
"description": "{ label, href?, onClick? } — trailing accent Text Button in the header. Extend the header when there's an index page to route to."
|
|
22
|
+
},
|
|
23
|
+
"items": {
|
|
24
|
+
"type": "node",
|
|
25
|
+
"required": true,
|
|
26
|
+
"description": "Array of nav row descriptors forwarded to [List](../list/entry.md) `variant=\"entry\"` as label-only rows: { value, label, supportingText?, href?, onClick?, trailingIcon? }. `supportingText` is mapped to the entry sub's `description` slot (caption-tone supporting line under the label). `thumbnail` is intentionally NOT exposed — the leading column is always collapsed."
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"slots": {
|
|
30
|
+
"container": {
|
|
31
|
+
"required": true,
|
|
32
|
+
"description": "Surface block holding the header above the vertical nav list.",
|
|
33
|
+
"intrinsic": true
|
|
34
|
+
},
|
|
35
|
+
"header": {
|
|
36
|
+
"required": true,
|
|
37
|
+
"description": "[Header](../header/header.md) `size=\"large\"` — section label leading, optional `accent` Text Button trailing.",
|
|
38
|
+
"accepts": ["text", "button"]
|
|
39
|
+
},
|
|
40
|
+
"list": {
|
|
41
|
+
"required": true,
|
|
42
|
+
"description": "[List](../list/entry.md) `variant=\"entry\"`. Embedded so the wrapper section owns the rail. Every row drops `thumbnail` so the leading column collapses (label-only). Each row is the interactive route target; the row's `trailingIcon` slot is filled with a default chevron [Icon Button](../button/icon.md).",
|
|
43
|
+
"intrinsic": true
|
|
44
|
+
},
|
|
45
|
+
"row": {
|
|
46
|
+
"required": true,
|
|
47
|
+
"intrinsic": true,
|
|
48
|
+
"description": "Single nav row rendered by [List](../list/entry.md) as a label-only entry. The label is the primary content; the trailing chevron Icon Button (`variant=\"icon\"`, `size=\"medium\"`, `icon={<ChevronRightIcon />}`) signals drill-in. Each row routes via `href` or `onClick`.",
|
|
49
|
+
"accepts": ["row"]
|
|
50
|
+
},
|
|
51
|
+
"trailingChevron": {
|
|
52
|
+
"required": true,
|
|
53
|
+
"intrinsic": true,
|
|
54
|
+
"description": "Default [Icon Button](../button/icon.md) (`variant=\"icon\"`, `size=\"medium\"`) filled with `<ChevronRightIcon />`. Wired to the row's `href` / `onClick`. Clicks stop propagating before reaching the row's primary handler — the trailing slot is its own hit target with its own Outward focus ring."
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"sizing": {
|
|
58
|
+
"containerFill": "sys.color.surface",
|
|
59
|
+
"containerPaddingBlock": "sys.layout.container.lg",
|
|
60
|
+
"containerPaddingInline": "sys.layout.container.md",
|
|
61
|
+
"headerToListGap": "sys.layout.stack.md",
|
|
62
|
+
"labelTypo": "sys.typo.heading.md",
|
|
63
|
+
"labelColor": "sys.color.onSurface",
|
|
64
|
+
"headerActionRendersAs": "Button variant='text' size='xsmall' appearance='accent' — link-affordance accent rule.",
|
|
65
|
+
"rowComposition": "list/entry label-only row (no thumbnail) — label.md primary, optional label.sm description line, family-default min-height (ref.space.600 = 48), leading column collapsed (0 leading-to-text gap), trailing slot filled with a default Icon Button (variant='icon', size='medium', icon=<ChevronRightIcon />).",
|
|
66
|
+
"rowInlinePaddingNote": "Each row keeps the list/entry native sys.layout.container.md inline padding (the tap target reaches the surface edge) and adds margin-inline: calc(-1 * sys.layout.container.md) so the visible label lines up with the section's content rail (aligned with the header label at 16 from the surface).",
|
|
67
|
+
"dividerWidth": "sys.borderWidth.hairline",
|
|
68
|
+
"dividerColor": "sys.color.outlineVariant",
|
|
69
|
+
"dividerInset": "list/entry default — 16 / 16 inset from both row edges. Label-only rows always take the default inset regardless of `size` (no avatar column to anchor against)."
|
|
70
|
+
},
|
|
71
|
+
"rowProps": {
|
|
72
|
+
"value": { "type": "string", "required": true },
|
|
73
|
+
"label": { "type": "string", "required": true },
|
|
74
|
+
"supportingText": {
|
|
75
|
+
"type": "string",
|
|
76
|
+
"optional": true,
|
|
77
|
+
"description": "Caption-tone supporting line under the label. Mapped to the entry sub's `description` slot — single-line, truncated with ellipsis."
|
|
78
|
+
},
|
|
79
|
+
"href": { "type": "string", "optional": true },
|
|
80
|
+
"onClick": { "type": "function", "optional": true },
|
|
81
|
+
"trailingIcon": {
|
|
82
|
+
"type": "node",
|
|
83
|
+
"optional": true,
|
|
84
|
+
"description": "Override the default trailing chevron Icon Button with a custom node (e.g. an external-link Icon Button). Should remain a `<Button variant=\"icon\">` so the trailing slot keeps its own Outward focus ring."
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"states": {
|
|
88
|
+
"note": "Each row body is the interactive route target — hover / pressed / focused / disabled tokens delegate to [List](../list/entry.md). The trailing chevron Icon Button paints its own state per [Icon Button](../button/icon.md); its clicks stop propagating before reaching the row's primary handler. The headerAction is an accent Text Button (its own state contract). Container has no interactive state."
|
|
89
|
+
},
|
|
90
|
+
"focusIndicator": {
|
|
91
|
+
"description": "Row body takes the family-wide Inward focus ring; the trailing chevron Icon Button paints its own Outward ring; headerAction paints its own Outward ring.",
|
|
92
|
+
"composition": "inward",
|
|
93
|
+
"compositionReason": "Rows tile the column with a hairline divider; an outward ring on the row body would overlap divider and neighbour row.",
|
|
94
|
+
"trigger": ":focus-visible"
|
|
95
|
+
},
|
|
96
|
+
"behavior": {
|
|
97
|
+
"headerRequired": "Every NavList carries a label; the optional headerAction extends the header with a trailing accent Text Button.",
|
|
98
|
+
"verticalScroll": "No pager. The full list scrolls in normal document flow.",
|
|
99
|
+
"rowRoutes": "Each row routes via `href` (rendered as `<a>`) or `onClick`. The trailing chevron Icon Button mirrors the same route — independent hit target, independent Outward focus ring.",
|
|
100
|
+
"labelOnly": "The wrapper does not expose a `thumbnail` prop on `items`; the underlying [list/entry](../list/entry.md) keeps the leading slot collapsed for every row."
|
|
101
|
+
},
|
|
102
|
+
"forbidden": [
|
|
103
|
+
"leading thumbnail or icon — NavList rows are intentionally label-only; reach for DirectoryList when a leading visual is needed",
|
|
104
|
+
"swapping the trailing chevron for a non-Button node — the trailing slot must stay a `<Button variant=\"icon\">` so it keeps its own Outward focus ring",
|
|
105
|
+
"header omitted — every NavList carries a label"
|
|
106
|
+
]
|
|
107
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Main
|
|
2
|
+
|
|
3
|
+
The landing-screen top bar — anchored to a tab root (feed, inbox, profile). A leading menu glyph plus left-aligned page name sit at the start; **up to four** trailing icon actions (conventionally search, chat, profile) sit at the end. Title carries the system's largest page-level rung (`typo.heading.lg`, 24/Semibold). The same row also serves a **drill-in (content-detail) screen** — pass `onBack` instead of `onMenuClick` and the leading glyph swaps to a back chevron, with the trailing cluster carrying up to four actions (see [Use cases](#use-cases)).
|
|
4
|
+
|
|
5
|
+
**Reach for this when** the screen is a tab root and needs the menu drawer plus a small set of global affordances, **or** when it's a content-detail drill-in (a post / article reached from a feed) needing back navigation plus a share-and-save cluster. **Skip when** you need a centred title with a single action (use [Sub](./sub.md)) or are on a dedicated search page (use [Search](./search.md)).
|
|
6
|
+
|
|
7
|
+
**Layout inset.** `full-bleed` — direct child of the page shell. The bar pays its own `16px inline / 8px block` padding via `layout.container.*`; do **not** wrap it in another `padding-inline` / `px-*` / `style={{ padding: … }}` div. Inside a bounded surface (Card / Dialog / BottomSheet / Sheet), apply the negative-margin opt-out — see [`AGENTS.md` § Composition rules](../../../AGENTS.md#composition-rules).
|
|
8
|
+
|
|
9
|
+
## Default
|
|
10
|
+
|
|
11
|
+
Menu glyph, brand logotype at 24px tall, three trailing actions.
|
|
12
|
+
|
|
13
|
+
```preview
|
|
14
|
+
navigation-bar/main/default
|
|
15
|
+
---
|
|
16
|
+
import { NavigationBar } from '@teamblind-chorus/ui';
|
|
17
|
+
import { SearchIcon, ChatIcon, ProfileIcon } from '@teamblind-chorus/ui/icons';
|
|
18
|
+
|
|
19
|
+
<NavigationBar
|
|
20
|
+
variant="main"
|
|
21
|
+
title={
|
|
22
|
+
<img
|
|
23
|
+
src="/blind_logotype_black.svg"
|
|
24
|
+
alt="Chorus"
|
|
25
|
+
style={{ height: 24, width: 'auto', display: 'block' }}
|
|
26
|
+
/>
|
|
27
|
+
}
|
|
28
|
+
onMenuClick={() => {}}
|
|
29
|
+
trailingActions={[
|
|
30
|
+
{ icon: <SearchIcon />, 'aria-label': 'Search' },
|
|
31
|
+
{ icon: <ChatIcon />, 'aria-label': 'Messages' },
|
|
32
|
+
{ icon: <ProfileIcon />, 'aria-label': 'Profile' },
|
|
33
|
+
]}
|
|
34
|
+
/>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Use cases
|
|
38
|
+
|
|
39
|
+
### With a text title in place of the logotype
|
|
40
|
+
|
|
41
|
+
Names the screen in words. Plain text at `typo.heading.lg` (24/Semibold) `onSurface`; same 24-tall rhythm as the logotype, ellipsis on narrow.
|
|
42
|
+
|
|
43
|
+
```preview
|
|
44
|
+
navigation-bar/main/default--text-title
|
|
45
|
+
---
|
|
46
|
+
import { NavigationBar } from '@teamblind-chorus/ui';
|
|
47
|
+
import { SearchIcon, ChatIcon, ProfileIcon } from '@teamblind-chorus/ui/icons';
|
|
48
|
+
|
|
49
|
+
<NavigationBar
|
|
50
|
+
variant="main"
|
|
51
|
+
title="Home"
|
|
52
|
+
onMenuClick={() => {}}
|
|
53
|
+
trailingActions={[
|
|
54
|
+
{ icon: <SearchIcon />, 'aria-label': 'Search' },
|
|
55
|
+
{ icon: <ChatIcon />, 'aria-label': 'Messages' },
|
|
56
|
+
{ icon: <ProfileIcon />, 'aria-label': 'Profile' },
|
|
57
|
+
]}
|
|
58
|
+
/>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### With one trailing action
|
|
62
|
+
|
|
63
|
+
Single trailing affordance — e.g. search on an Inbox screen.
|
|
64
|
+
|
|
65
|
+
```preview
|
|
66
|
+
navigation-bar/main/single-action
|
|
67
|
+
---
|
|
68
|
+
import { NavigationBar } from '@teamblind-chorus/ui';
|
|
69
|
+
import { SearchIcon } from '@teamblind-chorus/ui/icons';
|
|
70
|
+
|
|
71
|
+
<NavigationBar
|
|
72
|
+
variant="main"
|
|
73
|
+
title="Inbox"
|
|
74
|
+
trailingActions={[
|
|
75
|
+
{ icon: <SearchIcon />, 'aria-label': 'Search' },
|
|
76
|
+
]}
|
|
77
|
+
/>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Drill-in detail screen (back chevron)
|
|
81
|
+
|
|
82
|
+
A content-detail screen reached from a feed / list (a post, an article, a saved item). Pass `onBack` — the leading glyph becomes a back chevron — and the trailing cluster carries up to four actions (share / notify / bookmark / more). Same row, same 56-tall geometry; only the entry point differs.
|
|
83
|
+
|
|
84
|
+
```preview
|
|
85
|
+
navigation-bar/main/detail
|
|
86
|
+
---
|
|
87
|
+
import { NavigationBar } from '@teamblind-chorus/ui';
|
|
88
|
+
import { ShareIcon, BellOffIcon, BookmarkIcon, EllipsisHorizontalIcon } from '@teamblind-chorus/ui/icons';
|
|
89
|
+
|
|
90
|
+
<NavigationBar
|
|
91
|
+
variant="main"
|
|
92
|
+
title={
|
|
93
|
+
<img
|
|
94
|
+
src="/blind_logotype_black.svg"
|
|
95
|
+
alt="Chorus"
|
|
96
|
+
style={{ height: 24, width: 'auto', display: 'block' }}
|
|
97
|
+
/>
|
|
98
|
+
}
|
|
99
|
+
onBack={() => {}}
|
|
100
|
+
trailingActions={[
|
|
101
|
+
{ icon: <ShareIcon />, 'aria-label': 'Share' },
|
|
102
|
+
{ icon: <BellOffIcon />, 'aria-label': 'Turn off notifications' },
|
|
103
|
+
{ icon: <BookmarkIcon />, 'aria-label': 'Save' },
|
|
104
|
+
{ icon: <EllipsisHorizontalIcon />, 'aria-label': 'More' },
|
|
105
|
+
]}
|
|
106
|
+
/>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Drill-in with a text title and a smaller cluster
|
|
110
|
+
|
|
111
|
+
Names the source — the channel or author the post belongs to. The cluster is `1..4` actions; drop to just the essentials (share + more) when the screen has fewer affordances.
|
|
112
|
+
|
|
113
|
+
```preview
|
|
114
|
+
navigation-bar/main/detail-text-title
|
|
115
|
+
---
|
|
116
|
+
import { NavigationBar } from '@teamblind-chorus/ui';
|
|
117
|
+
import { ShareIcon, EllipsisHorizontalIcon } from '@teamblind-chorus/ui/icons';
|
|
118
|
+
|
|
119
|
+
<NavigationBar
|
|
120
|
+
variant="main"
|
|
121
|
+
title="Sourdough Bakers"
|
|
122
|
+
onBack={() => {}}
|
|
123
|
+
trailingActions={[
|
|
124
|
+
{ icon: <ShareIcon />, 'aria-label': 'Share' },
|
|
125
|
+
{ icon: <EllipsisHorizontalIcon />, 'aria-label': 'More' },
|
|
126
|
+
]}
|
|
127
|
+
/>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Truncation (safety net)
|
|
131
|
+
|
|
132
|
+
Long page name truncates with ellipsis. Author concise titles (*Home*, *Inbox*) so the bar never resorts to ellipsis.
|
|
133
|
+
|
|
134
|
+
```preview
|
|
135
|
+
navigation-bar/main/truncation
|
|
136
|
+
---
|
|
137
|
+
import { NavigationBar } from '@teamblind-chorus/ui';
|
|
138
|
+
import { SearchIcon, ChatIcon, ProfileIcon } from '@teamblind-chorus/ui/icons';
|
|
139
|
+
|
|
140
|
+
<NavigationBar
|
|
141
|
+
variant="main"
|
|
142
|
+
title="A very long screen title that should truncate"
|
|
143
|
+
onMenuClick={() => {}}
|
|
144
|
+
trailingActions={[
|
|
145
|
+
{ icon: <SearchIcon />, 'aria-label': 'Search' },
|
|
146
|
+
{ icon: <ChatIcon />, 'aria-label': 'Messages' },
|
|
147
|
+
{ icon: <ProfileIcon />, 'aria-label': 'Profile' },
|
|
148
|
+
]}
|
|
149
|
+
/>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Slots
|
|
153
|
+
|
|
154
|
+
- **leadingIcon** *(intrinsic)* — the menu / hamburger ([`MenuIcon`](../../icons/svg/Menu.svg)) on a tab root (the hook into the app's primary drawer), or a back chevron ([`ChevronLeftIcon`](../../icons/svg/ChevronLeft.svg), wired to `onBack`) on a drill-in / content-detail screen. Never an arbitrary glyph.
|
|
155
|
+
- **title** — screen identity. Required. Default is the brand logotype at fixed 24px height (ratio preserved); a string may be passed instead — renders as `typo.heading.lg` (24/Semibold) `onSurface`, ellipsis on narrow.
|
|
156
|
+
- **trailingActions** *(optional)* — up to four icon actions (three conventional on a tab root: Search, Chat, Profile; up to four for the drill-in cluster: Share, Notify, Bookmark, More). Laid left-to-right with no inter-icon gap — the capsules' 8px padding provides visible separation. The component slices a fifth.
|
|
157
|
+
|
|
158
|
+
## Anatomy
|
|
159
|
+
|
|
160
|
+
| Slot | Container | Color |
|
|
161
|
+
|-----------------------|--------------------|------------------------------------------|
|
|
162
|
+
| **Bar container** | `sys.color.surface` fill, 8px block / 16px inline padding, no border, no shadow at rest. | — |
|
|
163
|
+
| **Leading icon** | Transparent capsule, 24px glyph centred. | `sys.color.onSurface` |
|
|
164
|
+
| **Title** | Brand logotype `<img>` at 24px tall (width auto) by default; plain-text fallback at `heading.lg`. Not interactive. | `sys.color.onSurface` (text fallback) |
|
|
165
|
+
| **Trailing icon(s)** | Transparent capsule, 24px glyph centred. Capsules sit flush, no inter-icon gap. | `sys.color.onSurface` |
|
|
166
|
+
|
|
167
|
+
## Sizes
|
|
168
|
+
|
|
169
|
+
A single fixed rung.
|
|
170
|
+
|
|
171
|
+
| Property | Value | Token |
|
|
172
|
+
|-----------------------------------|----------------------|-------------------------------------|
|
|
173
|
+
| Container padding (block × inline)| 8 × 16 | `sys.layout.container.xs` × `sys.layout.container.md` |
|
|
174
|
+
| Min-height | 56px | raw ‡ |
|
|
175
|
+
| Slot gap (leading ↔ title) | 16px | `sys.layout.inline.xl` |
|
|
176
|
+
| Slot gap (title ↔ trailing group) | 16px | `sys.layout.inline.xl` |
|
|
177
|
+
| Slot gap (between trailing icons) | 16px | `sys.layout.inline.xl` † |
|
|
178
|
+
| Icon-capsule padding | 8px | `sys.layout.container.xs` |
|
|
179
|
+
| Title — brand logotype (default) | 24px tall, width auto| raw — fixed pixel height |
|
|
180
|
+
| Title — plain text (fallback) | 24 / Semibold | `sys.typo.heading.lg` |
|
|
181
|
+
| Leading icon | 24px | `sys.icon.lg` |
|
|
182
|
+
| Trailing icon | 24px | `sys.icon.lg` |
|
|
183
|
+
|
|
184
|
+
‡ Floor of 8 + 40 + 8 = 56. Keeps a title-only row from collapsing below 56.
|
|
185
|
+
|
|
186
|
+
† Optical alignment via Icon Button's negative-margin bleed — chrome-to-chrome 16 *is* the visible glyph-to-glyph distance.
|
|
187
|
+
|
|
188
|
+
## States
|
|
189
|
+
|
|
190
|
+
The bar itself has no interactive state. Icon slots inherit [Icon Button](../button/icon.md) states. Title carries no states.
|
|
191
|
+
|
|
192
|
+
## Focus indicator
|
|
193
|
+
|
|
194
|
+
Bar isn't a focus target; icon slots inherit [Icon Button → Outward](../button/icon.md#focus-indicator). Trigger: `:focus-visible`.
|
|
195
|
+
|
|
196
|
+
## Behavior
|
|
197
|
+
|
|
198
|
+
- **Bar is page chrome.** Renders in flow; host pins (`position: sticky`) when needed.
|
|
199
|
+
- **Title truncates.** Long page names truncate; bar height stays 56. Safety net only — page names should be one or two words.
|
|
200
|
+
- **Trailing icon count.** Three is the conventional ceiling on a tab root; the drill-in cluster uses up to four (share / notify / bookmark / more). A fifth belongs in the `more` (•••) overflow — the component slices to four.
|
|
201
|
+
- **Leading affordance.** Menu glyph on a tab root (`onMenuClick`); back chevron on a drill-in (`onBack`). `onBack` wins if both are passed; with neither, the menu is the default.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../spec.schema.json",
|
|
3
|
+
"name": "NavigationBar",
|
|
4
|
+
"family": "navigation-bar",
|
|
5
|
+
"subcomponent": "main",
|
|
6
|
+
"exportAlias": "AppBar",
|
|
7
|
+
"exportAliasNote": "The `AppBar` runtime alias re-exports the family-level `<NavigationBar>` component (not just the main sub) so consumers can drop in `<AppBar variant=\"main|sub|search\">`. Declared on the main sub-spec because Main is the canonical default landing for `AppBar`-style consumers; the family-level export is what actually ships from packages/ui/src/index.js.",
|
|
8
|
+
"description": "Landing-screen top bar — anchored to the top of a tab root (a feed, an inbox, a profile, a discover screen). A leading menu glyph + left-aligned title occupy the start of the row; up to four action icons (search, chat, profile / notifications) sit at the trailing edge. **The title slot defaults to the brand logotype rendered at 24px tall** (width auto, ratio preserved) so the landing rung of every Main tab opens with the product mark rather than a written page name. A string title is the fallback for screens that don't anchor a top-level brand identity (a secondary tab root, a feature surface, a dev screen); the fallback carries the system's largest page-level type rung (heading.lg, 24/Semibold) rather than the smaller drill-in rung used by Sub, and shares the 24-tall rhythm of the logotype so the bar's height is unchanged across the two presentations. **Drill-in (content-detail) case:** the same row serves a content-detail screen reached from a feed / list (a post, an article, a saved item) — pass `onBack` instead of `onMenuClick` and the leading glyph swaps from menu to a back chevron; the trailing cluster then carries up to four actions (share / notify / bookmark / more).",
|
|
9
|
+
"element": "header",
|
|
10
|
+
"props": {
|
|
11
|
+
"variant": {
|
|
12
|
+
"type": "literal",
|
|
13
|
+
"value": "main"
|
|
14
|
+
},
|
|
15
|
+
"title": {
|
|
16
|
+
"type": "node",
|
|
17
|
+
"required": true,
|
|
18
|
+
"description": "Default: brand logotype `<img>` rendered at a fixed 24px height (width auto). Fallback: plain string rendered as `heading.lg` text that truncates with ellipsis."
|
|
19
|
+
},
|
|
20
|
+
"onMenuClick": {
|
|
21
|
+
"type": "function",
|
|
22
|
+
"optional": true,
|
|
23
|
+
"description": "Tab-root case — fires when the leading menu / hamburger is tapped (opens the app drawer). Mutually exclusive with `onBack`."
|
|
24
|
+
},
|
|
25
|
+
"onBack": {
|
|
26
|
+
"type": "function",
|
|
27
|
+
"optional": true,
|
|
28
|
+
"description": "Drill-in (content-detail) case — passing `onBack` swaps the leading menu glyph for a back chevron wired to it (route to the previous screen). Takes precedence over `onMenuClick` when both are passed."
|
|
29
|
+
},
|
|
30
|
+
"backLabel": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"optional": true,
|
|
33
|
+
"description": "Accessible label for the back chevron in the drill-in case. Defaults to \"Back\"."
|
|
34
|
+
},
|
|
35
|
+
"trailingActions": {
|
|
36
|
+
"type": "node",
|
|
37
|
+
"optional": true,
|
|
38
|
+
"description": "Up to four { icon, 'aria-label' } entries; laid out left-to-right with no inter-icon gap. Three is the conventional ceiling on a tab root; the drill-in (content-detail) cluster uses up to four (share / notify / bookmark / more)."
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"slots": {
|
|
42
|
+
"leadingIcon": {
|
|
43
|
+
"required": true,
|
|
44
|
+
"intrinsic": true,
|
|
45
|
+
"description": "Intrinsic — the menu / hamburger glyph (MenuIcon) on a tab root, or a back chevron (ChevronLeftIcon, wired to `onBack`) on a drill-in / content-detail screen. Rendered as a transparent icon capsule with the 24px glyph centred. Never an arbitrary custom glyph.",
|
|
46
|
+
"accepts": [
|
|
47
|
+
"icon"
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
"title": {
|
|
51
|
+
"required": true,
|
|
52
|
+
"description": "Screen's identity. Default rendering is the brand logotype `<img>` at a fixed 24px height (width auto, ratio preserved) so every Main tab opens with the product mark. Falls back to a plain-text string at heading.lg / Semibold / onSurface (truncates with ellipsis) when the screen has no logotype affordance.",
|
|
53
|
+
"accepts": [
|
|
54
|
+
"text",
|
|
55
|
+
"image"
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
"trailingActions": {
|
|
59
|
+
"required": false,
|
|
60
|
+
"description": "Array of up to four icon actions at the end of the row (three conventional on a tab root; up to four for the drill-in action cluster). Each renders as a transparent icon capsule; capsules sit flush with no inter-icon gap.",
|
|
61
|
+
"accepts": [
|
|
62
|
+
"icon",
|
|
63
|
+
"button"
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"sizing": {
|
|
68
|
+
"containerPaddingBlock": "sys.layout.container.xs",
|
|
69
|
+
"containerPaddingBlockTop": "calc(env(safe-area-inset-top, 0px) + sys.layout.container.xs)",
|
|
70
|
+
"containerPaddingInline": "sys.layout.container.md",
|
|
71
|
+
"minHeight": "56px",
|
|
72
|
+
"viewportSafeArea": "Bar's block-top padding stacks env(safe-area-inset-top) on top of container.xs so the surface fill extends through the device status-bar / notch zone — scrolled body content does not bleed through the transparent system chrome. Bar is the owner of the viewport-top inset; page shell MUST NOT re-pay it via its own padding-top.",
|
|
73
|
+
"leadingTitleGap": "sys.layout.inline.xl",
|
|
74
|
+
"titleTrailingGap": "sys.layout.inline.xl",
|
|
75
|
+
"trailingIconGap": "sys.layout.inline.xl",
|
|
76
|
+
"iconCapsulePadding": "sys.layout.container.xs",
|
|
77
|
+
"titleLogotypeHeight": "ref.space.300",
|
|
78
|
+
"titleTypo": "sys.typo.heading.lg",
|
|
79
|
+
"leadingIconSize": "sys.icon.lg",
|
|
80
|
+
"trailingIconSize": "sys.icon.lg"
|
|
81
|
+
},
|
|
82
|
+
"appearance": {
|
|
83
|
+
"containerFill": "sys.color.surface",
|
|
84
|
+
"leadingIconColor": "sys.color.onSurface",
|
|
85
|
+
"titleColor": "sys.color.onSurface",
|
|
86
|
+
"trailingIconColor": "sys.color.onSurface"
|
|
87
|
+
},
|
|
88
|
+
"states": {
|
|
89
|
+
"note": "Bar itself has no interactive state. Icon slots carry the standard Icon Button state recipe — default / hovered (sys.state.hover overlay) / pressed (sys.state.pressed overlay) / disabled / focused (three-layer focus ring). Title carries no states."
|
|
90
|
+
},
|
|
91
|
+
"focusIndicator": {
|
|
92
|
+
"description": "The bar itself isn't a focus target. Its action slots (leading menu, trailing icons) inherit each control's own focus composition — Icon Button → Outward — so the ring belongs to whichever capsule the keyboard lands on. See the contained sub-components for the visual contract.",
|
|
93
|
+
"composition": "delegated",
|
|
94
|
+
"delegatesTo": "../button/icon.spec.json#/focusIndicator",
|
|
95
|
+
"trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
|
|
96
|
+
},
|
|
97
|
+
"behavior": {
|
|
98
|
+
"pageChrome": "Bar renders in the page flow at the top of the screen; the host page is responsible for pinning (position: sticky, etc.).",
|
|
99
|
+
"titleTruncates": "Long page names truncate with ellipsis; the bar height stays at 56 regardless. Treat as a safety net — author concise titles.",
|
|
100
|
+
"trailingIconCeiling": "Three is the conventional ceiling on a tab root; the drill-in (content-detail) action cluster uses up to four (share / notify / bookmark / more). A fifth belongs in the `more` (•••) overflow — the component slices to four."
|
|
101
|
+
},
|
|
102
|
+
"forbidden": [
|
|
103
|
+
"brand color on the title / wordmark — header chrome stays on sys.color.surface; the wordmark paints sys.color.onSurface",
|
|
104
|
+
"more than four trailing actions — the action cluster caps at four; a fifth belongs in the `more` overflow",
|
|
105
|
+
"leading glyph other than the menu (tab root) or back chevron (drill-in, via onBack) — never an arbitrary custom glyph",
|
|
106
|
+
"centred title — the Main title is left-aligned next to the leading glyph (a centred title is the `sub` variant)",
|
|
107
|
+
"title bar height different from 56px — geometry is fixed across all navigation-bar subs"
|
|
108
|
+
]
|
|
109
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../family.schema.json",
|
|
3
|
+
"family": "navigation-bar",
|
|
4
|
+
"name": "NavigationBar",
|
|
5
|
+
"description": "Top-bar family. `main` is the landing-screen bar (menu glyph + left-aligned title + trailing action icons) — and the same bar serves a content-detail drill-in when given `onBack`, which swaps the menu glyph for a back chevron and lets the trailing cluster carry up to four actions (share / notify / bookmark / more); `sub` is the drill-in bar (centred title + leading back / single trailing action); `search` is the search-page bar (leading back + bare-text input filling the row + conditional clear). All ride the same 56px min-height and 8/8 padding so they stay at one geometry across a navigation flow.",
|
|
6
|
+
"useCases": [
|
|
7
|
+
"screen header",
|
|
8
|
+
"landing-screen top bar",
|
|
9
|
+
"post / content detail top bar",
|
|
10
|
+
"drill-in top bar",
|
|
11
|
+
"search top bar",
|
|
12
|
+
"back navigation"
|
|
13
|
+
],
|
|
14
|
+
"visualReuse": "open",
|
|
15
|
+
"layoutInset": "full-bleed",
|
|
16
|
+
"wrapperGuidance": "Owns its inline padding internally. Place as a direct child of the page-shell <main> (or any host that pays the gutter once). Do NOT wrap in a padding-inline div, className=\"px-*\", or style={{ padding }} \u2014 the page rail is paid once at the shell, never on the full-bleed child. Inside a bounded surface (Dialog / BottomSheet / SideSheet), apply the negative-margin opt-out \u2014 see AGENTS.md \u00a7 Composition rules.",
|
|
17
|
+
"spec": "navigation-bar.md",
|
|
18
|
+
"usage": {
|
|
19
|
+
"note": "All three bars are the single `NavigationBar` export selected by the `variant` prop — pass `onBack` to a `main` bar to turn it into a content-detail drill-in. Also exported as the alias `AppBar`.",
|
|
20
|
+
"subs": {
|
|
21
|
+
"main": { "variant": "main", "example": "<NavigationBar variant=\"main\" title=\"Home\" trailingActions={[{ icon, 'aria-label' }]} />" },
|
|
22
|
+
"sub": { "variant": "sub", "example": "<NavigationBar variant=\"sub\" title=\"Edit profile\" leading={{ icon, 'aria-label' }} trailing={<Button variant=\"toolbar\">Save</Button>} />" },
|
|
23
|
+
"search": { "variant": "search", "example": "<NavigationBar variant=\"search\" placeholder=\"Search by keyword\" onBack={() => {}} />" }
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"subcomponents": [
|
|
27
|
+
{
|
|
28
|
+
"slug": "main",
|
|
29
|
+
"spec": "main.spec.json",
|
|
30
|
+
"md": "main.md",
|
|
31
|
+
"default": true
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"slug": "sub",
|
|
35
|
+
"spec": "sub.spec.json",
|
|
36
|
+
"md": "sub.md"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"slug": "search",
|
|
40
|
+
"spec": "search.spec.json",
|
|
41
|
+
"md": "search.md"
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Navigation bar
|
|
2
|
+
|
|
3
|
+
The top app bar — a horizontal strip pinned to the top of a screen that names the screen and exposes its highest-priority actions. Three sub-flavors share this contract: **Main** (landing-screen bar), **Sub** (drill-in bar with centred title), and **Search** (search-page bar with a bare-text input). All three sit at 16px inline / 8px block padding and delegate icon slots to [Icon Button](../button/icon.md).
|
|
4
|
+
|
|
5
|
+
**Layout inset.** `full-bleed` — sits flush at the top of the page shell. The bar owns its own `16px inline / 8px block` padding via `layout.container.*`; do **not** wrap it in another `padding-inline` / `px-*` / `style={{ padding: … }}` div. The bar is pinned chrome, not a `<main>` child — sits *outside* the `<main>` that pays `sys.layout.page.*`. See [`AGENTS.md` § Composition rules](../../../AGENTS.md#composition-rules).
|
|
6
|
+
|
|
7
|
+
**Viewport safe area.** The bar's block padding stacks `env(safe-area-inset-top)` on top of the canonical `container.xs` (8) breathing room — so its `surface` background extends through the device status-bar / notch zone above the 56-tall content row, and scrolled body content does not bleed through the transparent system chrome. On non-mobile viewports `env()` resolves to 0 and the bar collapses to its original 8 / 16 padding. **The page shell MUST NOT add its own `padding-top: env(safe-area-inset-top)` when a NavigationBar is rendered at the top** — the bar already pays it, and stacking would double-inset the row by the notch height.
|
|
8
|
+
|
|
9
|
+
## Cross-sub contract
|
|
10
|
+
|
|
11
|
+
- **Container.** Horizontal strip with **16px inline + 8px block padding** (`sys.layout.container.md` × `sys.layout.container.xs`), `surface` fill, **min-height 56**. Inter-slot gap is `sys.layout.inline.xl` (16).
|
|
12
|
+
- **Slots.** A **leading** slot (icon), a **centre slot** (title text on Main/Sub; bare input on Search), and a **trailing** slot (up to four icons on Main; a single button / link / icon on Sub; conditional clear (×) on Search). Titles truncate with ellipsis; Search input never reflows its leading edge.
|
|
13
|
+
- **Icon slots are [Icon Buttons](../button/icon.md).** Every icon slot renders as a 40 × 40 transparent capsule hosting a 24px (`sys.icon.lg`) glyph.
|
|
14
|
+
- **Title is not interactive.**
|
|
15
|
+
- **Accessibility.** Bar exposes `role="banner"` (Main, as main banner) or no implicit role (Sub — page chrome). Icon slots carry `aria-label`; the page's `<h1>` lives in the page body.
|
|
16
|
+
|
|
17
|
+
## Sub-components
|
|
18
|
+
|
|
19
|
+
- **[Main](./main.md)** — Landing-screen top bar. Left-aligned page name (`typo.heading.lg`, 24/Semibold) preceded by a leading menu icon; up to four trailing action icons. The same bar serves a content-detail drill-in when given `onBack` — the leading glyph becomes a back chevron and the trailing cluster carries up to four actions. Min-height 56.
|
|
20
|
+
- **[Sub](./sub.md)** — Drill-in top bar. Centred page name (`typo.heading.sm`, 16/Semibold); leading back-arrow and a trailing slot ([Toolbar Button](../button/toolbar.md), text link, or single icon). Min-height 56.
|
|
21
|
+
- **[Search](./search.md)** — Search-page top bar. Leading back-arrow Icon Button, a single bare-text input filling the middle column, and a conditional clear (×) Icon Button. No title slot. Min-height 56.
|