@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,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../family.schema.json",
|
|
3
|
+
"family": "profile-header",
|
|
4
|
+
"name": "ProfileHeader",
|
|
5
|
+
"description": "Identity block at the top of a profile detail screen — a full-bleed cover band, an overlapping circular [Thumbnail](../thumbnail/thumbnail.md) avatar, an entity name + visibility/follower meta row, and a trailing follow [Toggle Button](../button/toggle.md). Same 'profile' contract as [Profile carousel](../carousel/profile.md) (channel, user, company) — the carousel surfaces a fixed-shape card in a curated rail; the header is the page-level identity rung the rail's `See all` lands on. Composes under a transparent overlay [Navigation bar](../navigation-bar/sub.md) when the screen is a drill-in route.",
|
|
6
|
+
"useCases": [
|
|
7
|
+
"channel detail header",
|
|
8
|
+
"profile detail header",
|
|
9
|
+
"company channel detail header",
|
|
10
|
+
"follow / following on a profile page",
|
|
11
|
+
"cover + avatar identity block"
|
|
12
|
+
],
|
|
13
|
+
"visualReuse": "open",
|
|
14
|
+
"layoutInset": "full-bleed",
|
|
15
|
+
"wrapperGuidance": "Owns its inline padding internally on the meta row, and the cover band stretches edge-to-edge through the page-shell content box. Place as a direct child of the page-shell <main>. Do NOT wrap in a padding-inline div, className=\"px-*\", or style={{ padding }} — 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 — see AGENTS.md § Composition rules.",
|
|
16
|
+
"spec": "profile-header.md",
|
|
17
|
+
"usage": {
|
|
18
|
+
"note": "Paints its own overlay NavigationBar — don't wrap one yourself; opt out with nav={false}. value props are strings.",
|
|
19
|
+
"example": "<ProfileHeader name=\"…\" avatar={{ src, alt }} cover={{ src, alt }} visibility=\"public\" followers=\"…\" />"
|
|
20
|
+
},
|
|
21
|
+
"subcomponents": [
|
|
22
|
+
{
|
|
23
|
+
"slug": "profile-header",
|
|
24
|
+
"spec": "profile-header.spec.json",
|
|
25
|
+
"md": "profile-header.md",
|
|
26
|
+
"default": true
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Profile header
|
|
2
|
+
|
|
3
|
+
Identity block at the top of a profile detail screen — a full-bleed cover band, an overlapping circular [Thumbnail](../thumbnail/thumbnail.md) avatar, an entity name, a visibility + follower meta row, and a trailing follow [Toggle Button](../button/toggle.md). Same `profile` contract as [Profile carousel](../carousel/profile.md) (channel topic, user, company channel) — the carousel surfaces a fixed-shape card in a curated rail; the header is the page-level identity rung the rail's `See all` lands on.
|
|
4
|
+
|
|
5
|
+
**Reach for this when** a profile detail route opens on a followable entity that needs a cover, an avatar, a single page-level heading, and a follow affordance. **Skip when** the screen is a feed list (use [Navigation bar/home](../navigation-bar/main.md) + [Feed](../feed/feed.md)), a settings or account drill-in (use [Navigation bar/page](../navigation-bar/sub.md) + [Nav card](../nav-card/nav-card.md)), or a curated profile rail (use [Carousel](../carousel/carousel.md) + [Profile carousel](../carousel/profile.md)).
|
|
6
|
+
|
|
7
|
+
**Layout inset.** `full-bleed` — sits as a direct child of the page shell at the top of the route. The cover stretches edge-to-edge inside the page-shell content box; the identity row pays its own `16px inline / 16px block` padding via `layout.container.*`. 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
|
+
**Overlay nav.** ProfileHeader paints an overlay [Navigation bar/page](../navigation-bar/sub.md) at `appearance="overlay"` absolutely positioned at the cover's top edge — transparent fill, fixed-white icons. Defaults to a back-arrow leading and a search-icon trailing; consumer wires `onBack` / `onSearch`. Opt out with `nav={false}` when the host route owns its own top chrome.
|
|
10
|
+
|
|
11
|
+
**Status bar.** Pass `statusBar` to paint an iOS-style app status bar (time + cellular / Wi-Fi / battery glyphs) above the overlay nav, at the very top of the cover — transparent fill, fixed-white glyphs, so the cover image shows through the OS-chrome zone (an edge-to-edge / immersive screen). Off by default; pass `statusBar={{ time: '…' }}` to override the canonical 9:41.
|
|
12
|
+
|
|
13
|
+
## Default
|
|
14
|
+
|
|
15
|
+
A public channel topic with a cover photo, a circular avatar, the entity name, and a `Follow` toggle.
|
|
16
|
+
|
|
17
|
+
```preview
|
|
18
|
+
profile-header/default
|
|
19
|
+
---
|
|
20
|
+
import { ProfileHeader } from '@teamblind-chorus/ui';
|
|
21
|
+
|
|
22
|
+
<ProfileHeader
|
|
23
|
+
name="General Topic"
|
|
24
|
+
avatar={{ src: '/placeholder.png', alt: 'General Topic' }}
|
|
25
|
+
cover={{ src: 'https://images.unsplash.com/photo-1497436072909-60f360e1d4b1?w=640&h=200&fit=crop&auto=format&q=80', alt: 'Forest skyline at dusk' }}
|
|
26
|
+
visibility="public"
|
|
27
|
+
followers="999 followers"
|
|
28
|
+
/>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Use cases
|
|
32
|
+
|
|
33
|
+
### Following
|
|
34
|
+
|
|
35
|
+
Active state — the trailing button has flipped to `Following` (`surfaceContainerHigh` + hairline outline) so the followed state recedes.
|
|
36
|
+
|
|
37
|
+
```preview
|
|
38
|
+
profile-header/following
|
|
39
|
+
---
|
|
40
|
+
import { ProfileHeader } from '@teamblind-chorus/ui';
|
|
41
|
+
|
|
42
|
+
<ProfileHeader
|
|
43
|
+
name="Plant People"
|
|
44
|
+
avatar={{ src: '/placeholder.png', alt: 'Plant People' }}
|
|
45
|
+
cover={{ src: 'https://images.unsplash.com/photo-1463320726281-696a485928c7?w=640&h=200&fit=crop&auto=format&q=80', alt: 'Sunlit greenhouse foliage' }}
|
|
46
|
+
visibility="public"
|
|
47
|
+
followers="21.7K followers"
|
|
48
|
+
followed
|
|
49
|
+
/>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Private
|
|
53
|
+
|
|
54
|
+
Private visibility — paints the [LockIcon](../../icons/svg/Lock.svg) on the meta row in place of the globe. Reach for it when the entity is gated (members-only channel, locked company channel).
|
|
55
|
+
|
|
56
|
+
```preview
|
|
57
|
+
profile-header/private
|
|
58
|
+
---
|
|
59
|
+
import { ProfileHeader } from '@teamblind-chorus/ui';
|
|
60
|
+
|
|
61
|
+
<ProfileHeader
|
|
62
|
+
name="Compensation"
|
|
63
|
+
avatar={{ src: '/placeholder.png', alt: 'Compensation' }}
|
|
64
|
+
cover={{ src: 'https://images.unsplash.com/photo-1554224155-6726b3ff858f?w=640&h=200&fit=crop&auto=format&q=80', alt: 'Offer letters on a desk' }}
|
|
65
|
+
visibility="private"
|
|
66
|
+
followers="8.1K followers"
|
|
67
|
+
/>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### With cover image
|
|
71
|
+
|
|
72
|
+
A custom cover photo overrides the placeholder. The image is `object-fit: cover` — aspect ratio preserved, cropped to fill the `375 / 120` cover band (scales with the host column).
|
|
73
|
+
|
|
74
|
+
```preview
|
|
75
|
+
profile-header/with-cover
|
|
76
|
+
---
|
|
77
|
+
import { ProfileHeader } from '@teamblind-chorus/ui';
|
|
78
|
+
|
|
79
|
+
<ProfileHeader
|
|
80
|
+
name="Sourdough Bakers"
|
|
81
|
+
avatar={{ src: '/placeholder.png', alt: 'Sourdough Bakers' }}
|
|
82
|
+
cover={{ src: 'https://images.unsplash.com/photo-1509440159596-0249088772ff?w=640&auto=format&q=80', alt: 'Sliced loaf with open crumb' }}
|
|
83
|
+
visibility="public"
|
|
84
|
+
followers="12.4K followers"
|
|
85
|
+
/>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### With status bar
|
|
89
|
+
|
|
90
|
+
An edge-to-edge / immersive screen — `statusBar` paints an iOS-style app status bar (time + cellular / Wi-Fi / battery) above the overlay nav, at the very top of the cover. Its fill is transparent so the cover image shows through the OS-chrome zone; the glyphs are fixed white, matching the overlay nav. Reach for it when the route renders the cover full-bleed under a translucent system status bar.
|
|
91
|
+
|
|
92
|
+
```preview
|
|
93
|
+
profile-header/with-status-bar
|
|
94
|
+
---
|
|
95
|
+
import { ProfileHeader } from '@teamblind-chorus/ui';
|
|
96
|
+
|
|
97
|
+
<ProfileHeader
|
|
98
|
+
name="General Topic"
|
|
99
|
+
avatar={{ src: '/placeholder.png', alt: 'General Topic' }}
|
|
100
|
+
cover={{ src: 'https://images.unsplash.com/photo-1497436072909-60f360e1d4b1?w=640&h=200&fit=crop&auto=format&q=80', alt: 'Forest skyline at dusk' }}
|
|
101
|
+
visibility="public"
|
|
102
|
+
followers="999 followers"
|
|
103
|
+
statusBar
|
|
104
|
+
/>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Slots
|
|
108
|
+
|
|
109
|
+
- **container** — outer `<section>`. Vertical stack: cover band above, identity column below. `sys.color.surface` fill; no outer padding.
|
|
110
|
+
- **cover** — full-bleed image band at the top; `aspect-ratio: 375 / 120` (mobile screen-width / cover-band proportion — scales with the host column). Image-area slot (same contract as [Thumbnail](../thumbnail/thumbnail.md) and [Profile carousel](../carousel/profile.md) cover) — falls back to `/placeholder.png` over a `surfaceContainerHigh` underlay when no `cover.src` is supplied. Hosts the overlay nav at its top edge.
|
|
111
|
+
- **nav** *(optional)* — [Navigation bar/page](../navigation-bar/sub.md) at `appearance="overlay"`. Absolutely positioned at the cover's top edge; transparent fill, fixed-white icons (back chevron leading, search trailing by default). Pays its own `env(safe-area-inset-top)`. Pass `nav={false}` to opt out.
|
|
112
|
+
- **statusBar** *(optional)* — iOS-style app status bar (time + cellular / Wi-Fi / battery glyphs) stacked above the nav at the cover's top edge. Transparent fill, fixed-white (`ref.palette.white.1000`) glyphs — the cover image shows through. Pays its own `env(safe-area-inset-top)` (and collapses the nav's, so the two never double-inset). Decorative OS-chrome (`aria-hidden`), intentionally off-token — raw px + `-apple-system` font. Off by default; pass `statusBar` (canonical 9:41) or `statusBar={{ time }}`.
|
|
113
|
+
- **avatar** — [Thumbnail](../thumbnail/thumbnail.md) `size={56}` with [`outlined={true}`](../thumbnail/thumbnail.md#with-surface-outline) overlapping the cover band's bottom edge by half its diameter (`margin-top: -28px`). The 2-token (`sys.borderWidth.thin`) `surface`-tone outset halo separating the circle from the cover is owned by Thumbnail's outlined case — the header forwards the prop instead of painting a halo on its own wrapper.
|
|
114
|
+
- **identity** — vertical column under the cover. Pays `sys.layout.container.md` (16px) inline / block-end padding, no block-start padding (the action row's avatar lifts into the cover from y=0). `sys.layout.stack.xs` (8px) stack gap between the action row and the heading sub-group.
|
|
115
|
+
- **actionRow** — top row of the identity column. Avatar leads (overlapping the cover); follow [Toggle Button](../button/toggle.md) trails. The toggle sits `sys.layout.stack.md` (16px) below the cover bottom — independent of the avatar's lower edge — so it reads with its own breathing room.
|
|
116
|
+
- **heading** — sub-group bundling the name and meta row at `sys.layout.stack.2xs` (4px) gap. Sits below the action row.
|
|
117
|
+
- **name** — entity name. `<h1>` at `sys.typo.heading.lg` (24 / Semibold) / `sys.color.onSurface`. Single line; truncates with ellipsis.
|
|
118
|
+
- **meta** — visibility + follower row. `[visibility icon] [visibility label] · [followers]` in `sys.typo.body.sm` / `sys.color.onSurfaceVariant`. Single line.
|
|
119
|
+
- **followAction** — trailing [Toggle Button](../button/toggle.md) (`variant={'toggle'}`). `Follow` (inactive) → `Following` (active). Intrinsic width.
|
|
120
|
+
|
|
121
|
+
## Anatomy
|
|
122
|
+
|
|
123
|
+
| Slot | Token bindings |
|
|
124
|
+
|---------------|----------------|
|
|
125
|
+
| container | `sys.color.surface` fill, full-bleed inline, vertical stack |
|
|
126
|
+
| cover | `aspect-ratio: 375 / 120` (W × H — mobile-viewport ratio; scales with the host column), `sys.color.surfaceContainerHigh` underlay, `/placeholder.png` background-image, `object-fit: cover` on the inline `<img>`; `position: relative` so the overlay nav pins to its bounds |
|
|
127
|
+
| nav | [Navigation bar/page](../navigation-bar/sub.md) `appearance="overlay"`, stacked in the overlay column `inset: 0 0 auto 0` on the cover, transparent fill, `ref.palette.white.1000` icons |
|
|
128
|
+
| statusBar | *(optional)* iOS-style app status bar above the nav in the overlay column; transparent fill, `ref.palette.white.1000` glyphs; `padding-block-start: env(safe-area-inset-top)`; off-token OS-chrome (raw px / `-apple-system`), `aria-hidden` |
|
|
129
|
+
| avatar | [Thumbnail](../thumbnail/thumbnail.md) `size={56}` `outlined`, `margin-top: -28px` (vertical centre on cover bottom edge). The 2-token surface halo is painted by Thumbnail's `outlined` case (outset `box-shadow: 0 0 0 sys.borderWidth.thin sys.color.surface`) — wrapper has no halo of its own |
|
|
130
|
+
| identity | Flex column, `padding-block: 0 sys.layout.container.md`, `padding-inline: sys.layout.container.md`, `sys.layout.stack.xs` (8) gap |
|
|
131
|
+
| actionRow | Flex row, `space-between` justify, `flex-start` align; follow button carries `margin-top: sys.layout.stack.md` (16) so it sits 16px below cover bottom |
|
|
132
|
+
| heading | Flex column, `sys.layout.stack.2xs` (4) gap between name and meta |
|
|
133
|
+
| name | `<h1>`, `sys.typo.heading.lg` (24 / Semibold), `sys.color.onSurface`, single-line ellipsis |
|
|
134
|
+
| meta | Flex row, `sys.layout.inline.sm` (4) gap, `sys.typo.body.sm` / Regular, `sys.color.onSurfaceVariant`; `·` separator before followers |
|
|
135
|
+
| meta icon | `sys.icon.md` (16 × 16), `sys.color.onSurfaceVariant` |
|
|
136
|
+
| followAction | [Toggle Button](../button/toggle.md) (Toolbar-Button footprint); state tokens delegate to Toggle Button |
|
|
137
|
+
|
|
138
|
+
## States
|
|
139
|
+
|
|
140
|
+
ProfileHeader itself has no lifecycle states. The trailing follow Toggle Button carries its own state per [Toggle Button](../button/toggle.md).
|
|
141
|
+
|
|
142
|
+
## Behavior
|
|
143
|
+
|
|
144
|
+
- **Page-level heading.** `name` renders as `<h1>` — the host route should not paint a second `<h1>` elsewhere on the page.
|
|
145
|
+
- **Cover is image-area.** Missing or failed `<img>` resolves to `/placeholder.png` over `surfaceContainerHigh`. Pass `cover.src` explicitly in design-time scaffolds so the contract reads in JSX.
|
|
146
|
+
- **Avatar overlap is structural.** The avatar's `margin-top: -40px` (half its diameter) lifts it so the vertical centre lands on the cover bottom edge. The follow Toggle Button carries its own `margin-top: sys.layout.stack.md` (16) so it sits 16px below the cover regardless of where the avatar lands.
|
|
147
|
+
- **Follow toggle commits in place.** Tapping `Follow` flips to `Following` and stays. State is owned by the consumer via `followed` + `onFollowChange`.
|
|
148
|
+
- **Overlay nav.** Defaults to an opt-in overlay [Navigation bar/page](../navigation-bar/sub.md) at `appearance="overlay"` — transparent fill, fixed-white icons. Pass `nav={false}` to opt out when the host route already paints its own top chrome.
|
|
149
|
+
- **Status bar.** Pass `statusBar` to paint an iOS-style app status bar above the nav at the cover's top edge for an edge-to-edge / immersive screen — transparent fill so the cover image shows through, fixed-white glyphs. When present, the status bar pays `env(safe-area-inset-top)` and the overlay nav collapses its own so the two sit flush. Decorative (`aria-hidden`); the time defaults to 9:41 and is overridable via `statusBar={{ time }}`.
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../spec.schema.json",
|
|
3
|
+
"name": "ProfileHeader",
|
|
4
|
+
"family": "profile-header",
|
|
5
|
+
"subcomponent": "profile-header",
|
|
6
|
+
"description": "Identity block at the top of a profile detail screen — a full-bleed cover band, an overlapping 56-rung circular [Thumbnail](../thumbnail/thumbnail.md) avatar, an entity name (`heading.lg`), a visibility-icon + visibility-label · follower-count meta row, and a trailing follow [Toggle Button](../button/toggle.md). The cover bleeds edge-to-edge inside the page-shell content box; the identity row pays the standard `sys.layout.container.md` (16px) inline padding so name / meta / follow sit on the page rail.",
|
|
7
|
+
"element": "section",
|
|
8
|
+
"props": {
|
|
9
|
+
"name": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"required": true,
|
|
12
|
+
"description": "Entity name — channel topic, person, or company. Renders as the page-level `<h1>` at `sys.typo.heading.lg` / Semibold / `onSurface`. Single line; truncates with ellipsis at narrow widths."
|
|
13
|
+
},
|
|
14
|
+
"avatar": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"required": true,
|
|
17
|
+
"description": "Forwarded to [Thumbnail](../thumbnail/thumbnail.md) verbatim at `size={56}` with `outlined={true}`. Object: `{ src, alt, updateDot?, logoBadge? }`. The avatar overlaps the cover band's bottom edge by half its height; the 2-token `surface`-tone halo that separates the circle from the cover comes from Thumbnail's `outlined` case (see [Thumbnail § With surface outline](../thumbnail/thumbnail.md#with-surface-outline))."
|
|
18
|
+
},
|
|
19
|
+
"cover": {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"optional": true,
|
|
22
|
+
"description": "{ src?, alt? } — image-area override for the cover band. `src` accepts any URL; falls back to `/placeholder.png` (the universal Chorus image-area placeholder) when omitted. Same contract as `Thumbnail.src` / `ProfileCarousel.cover` — `object-fit: cover` preserves aspect ratio and crops to the band's intrinsic `375 / 120` ratio.",
|
|
23
|
+
"assetType": "image",
|
|
24
|
+
"placeholder": "/placeholder.png"
|
|
25
|
+
},
|
|
26
|
+
"visibility": {
|
|
27
|
+
"type": "literal",
|
|
28
|
+
"values": ["public", "private"],
|
|
29
|
+
"default": "public",
|
|
30
|
+
"description": "Visibility class. `public` paints the [GlobeIcon](../../icons/svg/Globe.svg) + the `visibilityLabel` (default 'Public'); `private` paints the [LockIcon](../../icons/svg/Lock.svg) + the `visibilityLabel` (default 'Private'). Consumers override the label text via `visibilityLabel`."
|
|
31
|
+
},
|
|
32
|
+
"visibilityLabel": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"optional": true,
|
|
35
|
+
"description": "Override the visibility label text — used for localisation (e.g. '공개' / '비공개'). When omitted, defaults to 'Public' / 'Private' based on `visibility`."
|
|
36
|
+
},
|
|
37
|
+
"followers": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"required": true,
|
|
40
|
+
"description": "Follower count line — formatted by the consumer so the unit / locale stays in their hands (e.g. '999 followers', '12.4K followers', '999 팔로워'). Paints in `sys.typo.body.sm` / `sys.color.onSurfaceVariant` to the right of a bullet separator on the meta row."
|
|
41
|
+
},
|
|
42
|
+
"followed": {
|
|
43
|
+
"type": "boolean",
|
|
44
|
+
"default": false,
|
|
45
|
+
"description": "Whether the viewer is following this entity. Drives the trailing [Toggle Button](../button/toggle.md): false → 'Follow' (primary fill); true → 'Following' (transparent fill + hairline outline so the committed state recedes against any host surface tier)."
|
|
46
|
+
},
|
|
47
|
+
"onFollowChange": {
|
|
48
|
+
"type": "function",
|
|
49
|
+
"optional": true,
|
|
50
|
+
"description": "Fires when the follow toggle commits — receives the next `followed` boolean. Consumer owns persistence."
|
|
51
|
+
},
|
|
52
|
+
"followLabel": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"optional": true,
|
|
55
|
+
"description": "Inactive follow-button label. Defaults to 'Follow'."
|
|
56
|
+
},
|
|
57
|
+
"followingLabel": {
|
|
58
|
+
"type": "string",
|
|
59
|
+
"optional": true,
|
|
60
|
+
"description": "Active follow-button label. Defaults to 'Following'."
|
|
61
|
+
},
|
|
62
|
+
"nav": {
|
|
63
|
+
"type": "boolean",
|
|
64
|
+
"default": true,
|
|
65
|
+
"description": "Whether to render the overlay [Navigation bar/page](../navigation-bar/sub.md) at the cover's top edge. Defaults to `true` (back-arrow leading + search trailing, transparent + fixed-white icons). Pass `false` when the host route already paints its own top chrome and the cover should be clean."
|
|
66
|
+
},
|
|
67
|
+
"statusBar": {
|
|
68
|
+
"type": "boolean | object",
|
|
69
|
+
"default": false,
|
|
70
|
+
"description": "Paint an iOS-style app status bar (time + cellular / Wi-Fi / battery glyphs) above the overlay nav at the cover's top edge — transparent fill, fixed-white (`ref.palette.white.1000`) glyphs, so the cover image shows through the OS-chrome zone (edge-to-edge / immersive screen). Off by default. Pass `true` for the canonical 9:41 time, or `statusBar={{ time: '…' }}` to override. Decorative OS-chrome mimicry: `aria-hidden`, intentionally off-token (raw px + `-apple-system` font). When present it pays `env(safe-area-inset-top)` and the overlay nav collapses its own inset so the two never double-stack below the notch."
|
|
71
|
+
},
|
|
72
|
+
"onBack": {
|
|
73
|
+
"type": "function",
|
|
74
|
+
"optional": true,
|
|
75
|
+
"description": "Click handler for the overlay nav's leading back chevron."
|
|
76
|
+
},
|
|
77
|
+
"onSearch": {
|
|
78
|
+
"type": "function",
|
|
79
|
+
"optional": true,
|
|
80
|
+
"description": "Click handler for the overlay nav's trailing search icon."
|
|
81
|
+
},
|
|
82
|
+
"backLabel": {
|
|
83
|
+
"type": "string",
|
|
84
|
+
"optional": true,
|
|
85
|
+
"description": "Accessible label for the overlay nav's back chevron. Defaults to 'Back'."
|
|
86
|
+
},
|
|
87
|
+
"searchLabel": {
|
|
88
|
+
"type": "string",
|
|
89
|
+
"optional": true,
|
|
90
|
+
"description": "Accessible label for the overlay nav's search icon. Defaults to 'Search'."
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
"slots": {
|
|
94
|
+
"container": {
|
|
95
|
+
"required": true,
|
|
96
|
+
"description": "Outer `<section>`. Vertical stack — cover band first, identity row second. `sys.color.surface` fill; no outer padding (the cover is full-bleed; the identity row pays its own inline padding).",
|
|
97
|
+
"intrinsic": true
|
|
98
|
+
},
|
|
99
|
+
"cover": {
|
|
100
|
+
"required": true,
|
|
101
|
+
"description": "Full-bleed image band at the top — `aspect-ratio: 375 / 120` (mobile screen-width / cover-band ratio; the band scales proportionally as the host column resizes). **Image-area slot, same contract as `.chorus-thumbnail` and `.chorus-profile-carousel__cover`**: an `<img>` paints the band, falling back to the universal Chorus placeholder PNG (`/placeholder.png`) when no `cover.src` is supplied. The placeholder is *also* wired as the runtime CSS `background-image` on the band, so a missing / failed `<img>` still resolves to the placeholder rather than an empty surface tone. `object-fit: cover` preserves aspect ratio and crops to fill the band.",
|
|
102
|
+
"assetType": "image",
|
|
103
|
+
"placeholder": "/placeholder.png",
|
|
104
|
+
"intrinsic": true
|
|
105
|
+
},
|
|
106
|
+
"avatar": {
|
|
107
|
+
"required": true,
|
|
108
|
+
"description": "Circular avatar overlapping the cover band's bottom edge by half its diameter. Rendered via the [Thumbnail](../thumbnail/thumbnail.md) component at `size={56}` and `outlined={true}` — the 2-token `surface`-tone halo that separates the avatar from the cover band is owned by Thumbnail's outlined case (see [Thumbnail § With surface outline](../thumbnail/thumbnail.md#with-surface-outline)), not by the header's own wrapper. Every other Thumbnail prop is forwarded verbatim; the header does not paint its own crop or image-area fallback.",
|
|
109
|
+
"accepts": ["thumbnail"],
|
|
110
|
+
"rendersAs": "thumbnail:56"
|
|
111
|
+
},
|
|
112
|
+
"identity": {
|
|
113
|
+
"required": true,
|
|
114
|
+
"description": "Vertical column below the cover. Pays `sys.layout.container.md` (16px) inline padding and `sys.layout.stack.xs` (8px) stack gap between the action-row, the name, and the meta row.",
|
|
115
|
+
"intrinsic": true
|
|
116
|
+
},
|
|
117
|
+
"nav": {
|
|
118
|
+
"required": false,
|
|
119
|
+
"description": "Overlay [Navigation bar/page](../navigation-bar/sub.md) at `appearance=\"overlay\"` — stacked in the overlay column at the cover's top edge with a transparent fill and fixed-white icons (back chevron leading, search trailing by default). Floats over the cover image; the host wires `onBack` / `onSearch`. Opt out with `nav={false}`.",
|
|
120
|
+
"intrinsic": true,
|
|
121
|
+
"rendersAs": "navigation-bar:page (appearance='overlay')"
|
|
122
|
+
},
|
|
123
|
+
"statusBar": {
|
|
124
|
+
"required": false,
|
|
125
|
+
"description": "iOS-style app status bar (time + cellular / Wi-Fi / battery glyphs) stacked above the nav at the cover's top edge — transparent fill, fixed-white (`ref.palette.white.1000`) glyphs so the cover image shows through. Pays its own `env(safe-area-inset-top)` (and collapses the nav's so the two never double-inset). Decorative OS-chrome mimicry — `aria-hidden`, intentionally off-token (raw px + `-apple-system` font). Opt in with `statusBar` / `statusBar={{ time }}`.",
|
|
126
|
+
"intrinsic": true
|
|
127
|
+
},
|
|
128
|
+
"actionRow": {
|
|
129
|
+
"required": true,
|
|
130
|
+
"description": "Top row of the identity column. The avatar sits at the leading edge (overlapping the cover by half its diameter); the trailing [Toggle Button](../button/toggle.md) follow action sits at the trailing edge, **16px (`sys.layout.stack.md`) below the cover bottom**, so the affordance reads as its own breathing-room rather than aligning to the avatar's lower edge. `space-between` justify pins the two affordances to opposite edges.",
|
|
131
|
+
"intrinsic": true
|
|
132
|
+
},
|
|
133
|
+
"heading": {
|
|
134
|
+
"required": true,
|
|
135
|
+
"description": "Sub-group bundling the name and meta row. Vertical flex with `sys.layout.stack.2xs` (4px) gap so the page-level title and its meta line read as one heading block, set apart from the action row above by the identity column's `sys.layout.stack.xs` (8px) parent gap.",
|
|
136
|
+
"intrinsic": true
|
|
137
|
+
},
|
|
138
|
+
"name": {
|
|
139
|
+
"required": true,
|
|
140
|
+
"description": "Entity name. Renders as `<h1>` at `sys.typo.heading.lg` (24 / Semibold) / `sys.color.onSurface`. Single line; truncates with ellipsis. Sits inside the `heading` sub-group.",
|
|
141
|
+
"accepts": ["text"]
|
|
142
|
+
},
|
|
143
|
+
"meta": {
|
|
144
|
+
"required": true,
|
|
145
|
+
"description": "Visibility + follower meta row. Composed as: [visibility icon] [visibility label] [bullet separator] [follower count]. `sys.typo.body.sm` (14 / Regular) / `sys.color.onSurfaceVariant`. Single line. Sits inside the `heading` sub-group, 4px below the name.",
|
|
146
|
+
"intrinsic": true
|
|
147
|
+
},
|
|
148
|
+
"followAction": {
|
|
149
|
+
"required": true,
|
|
150
|
+
"description": "Trailing [Toggle Button](../button/toggle.md) (`variant={'toggle'}`). `Follow` (inactive — `primary` fill) ↔ `Following` (active — `transparent` fill + hairline outline so the committed state reads as a ghost outline against any host surface tier). Sits inside the action row, intrinsic width.",
|
|
151
|
+
"accepts": ["button"],
|
|
152
|
+
"rendersAs": "button:toggle"
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
"sizing": {
|
|
156
|
+
"containerFill": "sys.color.surface",
|
|
157
|
+
"coverAspectRatio": "375 / 120 (W × H — the mobile-viewport / cover-band proportion; the band scales with the host column instead of locking to a hard pixel height)",
|
|
158
|
+
"coverFill": "sys.color.surfaceContainerHigh (background underlay — the placeholder image paints on top via `object-fit: cover`)",
|
|
159
|
+
"coverImageSource": "Same `/placeholder.png` asset every Chorus image-area slot falls back to. Decorative — `aria-hidden`. Consumers override via the `cover.src` prop.",
|
|
160
|
+
"avatarSize": 56,
|
|
161
|
+
"avatarOverlap": "Avatar's vertical center sits on the cover band's bottom edge — the top half overlaps the cover, the bottom half sits on the identity surface (-28px margin-top). The 2-token `surface`-tone halo that separates the circle from the cover is owned by Thumbnail's `outlined={true}` case — see [Thumbnail § With surface outline](../thumbnail/thumbnail.md#with-surface-outline). The header does not paint a halo of its own on the wrapper; the Thumbnail's outset `box-shadow` is the contract.",
|
|
162
|
+
"identityPaddingBlock": "0 sys.layout.container.md",
|
|
163
|
+
"identityPaddingInline": "sys.layout.container.md",
|
|
164
|
+
"identityStackGap": "sys.layout.stack.xs",
|
|
165
|
+
"headingStackGap": "sys.layout.stack.2xs",
|
|
166
|
+
"actionRowAlign": "flex-start",
|
|
167
|
+
"actionRowJustify": "space-between",
|
|
168
|
+
"avatarMarginTop": "calc(-1 * 28px)",
|
|
169
|
+
"avatarMarginTopNote": "Negative margin lifts the avatar so its vertical centre lands on the cover bottom edge (half-overlap).",
|
|
170
|
+
"followMarginTop": "sys.layout.stack.md",
|
|
171
|
+
"followMarginTopNote": "Follow Toggle Button sits 16px below the cover bottom edge so the affordance reads with its own breathing room rather than aligning to the avatar's lower edge.",
|
|
172
|
+
"nameTypo": "sys.typo.heading.lg",
|
|
173
|
+
"nameColor": "sys.color.onSurface",
|
|
174
|
+
"metaTypo": "sys.typo.body.sm",
|
|
175
|
+
"metaColor": "sys.color.onSurfaceVariant",
|
|
176
|
+
"metaIconSize": "sys.icon.md",
|
|
177
|
+
"metaIconColor": "sys.color.onSurfaceVariant",
|
|
178
|
+
"metaGap": "sys.layout.inline.sm",
|
|
179
|
+
"metaSeparator": "·",
|
|
180
|
+
"followActionRendersAs": "Button variant='toggle' — Toolbar-Button footprint. State tokens delegate entirely to the Toggle Button (Chip-toggle) contract."
|
|
181
|
+
},
|
|
182
|
+
"states": {
|
|
183
|
+
"note": "ProfileHeader itself is not interactive — commit lives in the follow Toggle Button. The button follows its own spec's state contract."
|
|
184
|
+
},
|
|
185
|
+
"behavior": {
|
|
186
|
+
"coverIsImageArea": "Same image-area fallback contract as Thumbnail / Profile carousel cover: missing or failed `<img>` resolves to the bundled `/placeholder.png` over a `surfaceContainerHigh` underlay so the slot still reads as an image. Design-time scaffolds should pass `cover.src` explicitly.",
|
|
187
|
+
"avatarOverlapsCover": "Avatar carries `margin-top: -28px` so its vertical centre lands on the cover bottom edge — half-overlap (half of the 56-rung avatar). The follow Toggle Button sits in the same action row but with `margin-top: sys.layout.stack.md` (16) so it floats 16px below the cover bottom, independent of the avatar's lower edge.",
|
|
188
|
+
"followToggleCommitsInPlace": "Tapping `Follow` flips the trailing Toggle Button to `Following` and stays there. Consumer owns state via `followed` + `onFollowChange`.",
|
|
189
|
+
"nameSemantics": "`name` renders as `<h1>` — the page-level heading. The host route should not paint a separate `<h1>` elsewhere on the page. Use `as=\"div\"` on a downstream Header if a labelled-region heading is composed below.",
|
|
190
|
+
"overlayNav": "ProfileHeader paints an overlay [Navigation bar/page](../navigation-bar/sub.md) at `appearance=\"overlay\"` absolutely positioned at the cover's top edge — transparent fill, fixed-white icons, the cover image underneath provides the contrast. The bar pays its own `env(safe-area-inset-top)`. Defaults: back chevron leading, search icon trailing; consumer wires `onBack` / `onSearch`. Opt out with `nav={false}` when the host route prefers no overlay chrome.",
|
|
191
|
+
"statusBar": "Pass `statusBar` to stack an iOS-style app status bar (time + cellular / Wi-Fi / battery glyphs) above the overlay nav at the cover's top edge for an edge-to-edge / immersive screen — transparent fill so the cover image shows through, fixed-white glyphs. When present, the status bar pays `env(safe-area-inset-top)` and the overlay nav collapses its own top inset so the two sit flush in the overlay column. Decorative OS-chrome (`aria-hidden`), off-token by design. Time defaults to iOS's canonical 9:41; override via `statusBar={{ time }}`."
|
|
192
|
+
},
|
|
193
|
+
"forbidden": [
|
|
194
|
+
"ProfileHeader wrapped in a horizontal-padding div — the family is full-bleed by declaration; the identity row pays inline padding internally",
|
|
195
|
+
"cover painted as an inline SVG — cover uses /placeholder.png via the image-area contract, object-fit: cover",
|
|
196
|
+
"follow affordance rendered as a Standard Button or Text Button — Follow is the canonical Toggle Button shape; the Toolbar-Button footprint is non-negotiable",
|
|
197
|
+
"avatar rendered at a size other than 56 — the family fixes the avatar rung so the cover-overlap geometry stays consistent",
|
|
198
|
+
"second `<h1>` painted elsewhere on the host route — ProfileHeader owns the page-level heading semantics"
|
|
199
|
+
]
|
|
200
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../family.schema.json",
|
|
3
|
+
"family": "progress",
|
|
4
|
+
"name": "Progress",
|
|
5
|
+
"description": "Linear progress bar — a slim horizontal track that previews how far a long-running task has advanced. Determinate only: a filled segment parks at the value's ratio. One appearance, no emphasis axis: the track paints `sys.color.scrimSubtle` (a faint inverse-tone scrim that reads on any host surface tier) and the indicator paints `sys.color.inverseSurface` so the filled segment always contrasts against the bare track in either theme. A single 8px fully-rounded rung. If a screen needs a higher-attention progress mark, the emphasis belongs in the surrounding copy (e.g. a Banner), never in a chromatic indicator tone. Single-spec family.",
|
|
6
|
+
"useCases": [
|
|
7
|
+
"upload / download progress",
|
|
8
|
+
"onboarding step progress",
|
|
9
|
+
"background sync indicator",
|
|
10
|
+
"long-running task progress on a screen header"
|
|
11
|
+
],
|
|
12
|
+
"visualReuse": "open",
|
|
13
|
+
"layoutInset": "inline",
|
|
14
|
+
"spec": "progress.md",
|
|
15
|
+
"usage": {
|
|
16
|
+
"note": "value is a 0–1 ratio, not a 0–100 percent. Determinate only; the label lives on the host surface.",
|
|
17
|
+
"example": "<Progress value={0.4} aria-label=\"…\" />"
|
|
18
|
+
},
|
|
19
|
+
"subcomponents": [
|
|
20
|
+
{
|
|
21
|
+
"slug": "progress",
|
|
22
|
+
"spec": "progress.spec.json",
|
|
23
|
+
"md": "progress.md",
|
|
24
|
+
"default": true
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Progress
|
|
2
|
+
|
|
3
|
+
A single visual rung — 8px tall, fully rounded — that previews how far a long-running task has advanced. Determinate only: a filled indicator parks at the value's ratio. No emphasis axis: track paints with `sys.color.scrimSubtle` (the translucent inverse-tone scrim — ~8% black light, ~8% white dark); indicator paints in `inverseSurface` so the filled segment contrasts against the track regardless of theme.
|
|
4
|
+
|
|
5
|
+
**Reach for this when** a screen holds a task long enough that the user would otherwise wonder if anything is happening — file uploads, onboarding step counters, background syncs, account migrations. **Skip when** the task resolves under 300ms, the wait is purely opaque (use [Skeleton](../skeleton/skeleton.md) for content placeholders, busy spinners for short opaque waits), or the metric is primary content rather than chrome (use a chart).
|
|
6
|
+
|
|
7
|
+
**Layout inset.** `inline` — Progress ships no padding of its own. Stretches to fill whichever host column it sits in. Pair with a label / supporting line via the host surface, not Progress itself.
|
|
8
|
+
|
|
9
|
+
## Default
|
|
10
|
+
|
|
11
|
+
A determinate progress bar at 40%. 8px tall, `radius.full` corners, `inverseSurface` indicator on a Banner-style scrim track. Transitions over 200ms when `value` changes.
|
|
12
|
+
|
|
13
|
+
```preview
|
|
14
|
+
progress/default
|
|
15
|
+
---
|
|
16
|
+
import { Progress } from '@teamblind-chorus/ui';
|
|
17
|
+
|
|
18
|
+
<Progress value={0.4} aria-label="Uploading file" />
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Slots
|
|
22
|
+
|
|
23
|
+
- **track** — fully-rounded background block. 8px tall, `sys.color.scrimSubtle` fill (translucent inverse-tone scrim — black 8% light, white 8% dark), no stroke. Carries `role="progressbar"` and the aria-value attributes.
|
|
24
|
+
- **indicator** *(decorative)* — inner filled segment painted in `sys.color.inverseSurface`, `translateX`'d so the trailing edge lands at the value's ratio.
|
|
25
|
+
|
|
26
|
+
## Anatomy
|
|
27
|
+
|
|
28
|
+
| Slot | Token bindings |
|
|
29
|
+
|--------------|----------------|
|
|
30
|
+
| track | `sys.color.scrimSubtle` fill (translucent inverse-tone scrim), `sys.radius.full`, 8px (`sys.layout.container.xs`) tall |
|
|
31
|
+
| indicator | `sys.color.inverseSurface` fill, fully rounded, `transform: translateX(…)` driven |
|
|
32
|
+
| transition | 200ms `ease-out` on indicator transform as `value` changes |
|
|
33
|
+
|
|
34
|
+
## Behavior
|
|
35
|
+
|
|
36
|
+
- **Determinate transition.** `value` jumps animate over 200ms `ease-out` — a 30% → 60% step reads as movement, not a pop. Under `prefers-reduced-motion: reduce`, the transition is suppressed.
|
|
37
|
+
- **ARIA.** `role="progressbar"`; `aria-valuemin=0`, `aria-valuemax=100`, `aria-valuenow=<rounded percent>`.
|
|
38
|
+
- **No label, size, or emphasis slot.** The label (step counter, percent, task name) lives on the host surface. The family carries a single visual rung (8px) and a single appearance (inverseSurface on a scrim track).
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../spec.schema.json",
|
|
3
|
+
"name": "Progress",
|
|
4
|
+
"family": "progress",
|
|
5
|
+
"description": "Linear progress bar. A single visual rung — 8px tall, fully rounded — that previews how far a long-running task has advanced. The track paints with `sys.color.scrimSubtle` (the Banner-style inverse-tone scrim — ~8% black in light, ~8% white in dark) so the bar reads cleanly on any host surface tier without colliding with a fixed surface-container step. The indicator paints in `sys.color.inverseSurface` so the filled segment always contrasts against the scrim track regardless of theme. The track owns no width of its own — it stretches to fill its host column. Determinate only — `role='progressbar'` with `aria-valuemin / max / now` reflects the value's ratio.",
|
|
6
|
+
"element": "div",
|
|
7
|
+
"props": {
|
|
8
|
+
"value": {
|
|
9
|
+
"type": "number",
|
|
10
|
+
"default": 0,
|
|
11
|
+
"description": "Progress value. When `max` is omitted, this is the ratio 0..1; with `max`, it is the raw count and the ratio is `value / max`. Clamped to 0..1."
|
|
12
|
+
},
|
|
13
|
+
"max": {
|
|
14
|
+
"type": "number",
|
|
15
|
+
"default": 1,
|
|
16
|
+
"description": "Upper bound for `value`. Defaults to 1 so a `value` of 0.4 reads as 40%."
|
|
17
|
+
},
|
|
18
|
+
"aria-label": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"optional": true,
|
|
21
|
+
"description": "Accessible label describing what is in progress. Required when the bar is not paired with a visible label next to it."
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"slots": {
|
|
25
|
+
"track": {
|
|
26
|
+
"required": true,
|
|
27
|
+
"description": "Fully-rounded background block. 8px tall, `sys.color.scrimSubtle` fill (translucent inverse-tone scrim — black 8% light / white 8% dark), no stroke. Carries `role='progressbar'` and the aria-value attributes.",
|
|
28
|
+
"intrinsic": true
|
|
29
|
+
},
|
|
30
|
+
"indicator": {
|
|
31
|
+
"required": true,
|
|
32
|
+
"description": "Inner filled segment painted in `sys.color.inverseSurface`, translated horizontally so its trailing edge sits at the value's ratio.",
|
|
33
|
+
"intrinsic": true
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"sizing": {
|
|
37
|
+
"height": "sys.layout.container.xs",
|
|
38
|
+
"trackBackground": "sys.color.scrimSubtle",
|
|
39
|
+
"trackRadius": "sys.radius.full",
|
|
40
|
+
"indicatorBackground": "sys.color.inverseSurface",
|
|
41
|
+
"indicatorRadius": "inherit",
|
|
42
|
+
"transitionDuration": "200ms",
|
|
43
|
+
"transitionTiming": "ease-out"
|
|
44
|
+
},
|
|
45
|
+
"appearances": {
|
|
46
|
+
"default": {
|
|
47
|
+
"track": "sys.color.scrimSubtle",
|
|
48
|
+
"indicator": "sys.color.inverseSurface",
|
|
49
|
+
"note": "The only appearance. Track paints `sys.color.scrimSubtle` — a faint inverse-tone scrim (~8% black light / ~8% white dark) visible against every surface tier in either theme. Indicator paints in `inverseSurface` so the filled segment always contrasts against the bare track regardless of theme. Progress has no emphasis axis; if a screen needs a higher-attention progress mark, it belongs as a Banner copy nearby, not as a chromatic indicator tone."
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"states": {
|
|
53
|
+
"default": { "note": "Progress carries no lifecycle states — the value is its visual state." }
|
|
54
|
+
},
|
|
55
|
+
"behavior": {
|
|
56
|
+
"determinateTransition": "The indicator's transform transitions over 200ms `ease-out` as `value` changes, so jumps to higher ratios animate rather than snap. Under `prefers-reduced-motion: reduce` the transition is suppressed.",
|
|
57
|
+
"ariaContract": "Container is `role='progressbar'` with `aria-valuemin=0`, `aria-valuemax=100`, and `aria-valuenow` reflecting the rounded percent of the value's ratio."
|
|
58
|
+
},
|
|
59
|
+
"forbidden": [
|
|
60
|
+
"Progress used as a permanent zero-data placeholder — that role is `skeleton`; Progress communicates active work, not absence",
|
|
61
|
+
"Progress painted thicker than 8px — taller bars read as a UI control, not as chrome. For thick-bar metric visualisation, use a chart instead",
|
|
62
|
+
"Progress nested inside a Toast — Toast is for transient confirmations of completed actions; in-progress states belong inline on the surface that owns the work",
|
|
63
|
+
"track painted with a fixed surface-container tone (e.g. `surfaceContainerHighest`) — the track MUST be the mode-aware scrim recipe so it reads on any host surface tier",
|
|
64
|
+
"indicator painted with a chromatic primary / accent / brand tone — the family has no emphasis axis; emphasis belongs in the surrounding copy, not in the bar itself",
|
|
65
|
+
"track stroke painted with a `border:` — track is a fully-rounded block with no stroke; the affordance is the fill contrast between the scrim and the indicator"
|
|
66
|
+
]
|
|
67
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../family.schema.json",
|
|
3
|
+
"family": "side-sheet",
|
|
4
|
+
"name": "SideSheet",
|
|
5
|
+
"description": "Off-canvas content column anchored to the leading or trailing edge of the viewport. Pairs with BottomSheet as the Sheet family's other anchor: BottomSheet for committed-sheet flows, SideSheet for off-canvas navigation columns, settings panes, channel directories, filter rails. The composition is free-form — the canonical fill is a Header (size=\"medium\") column heading + an embedded [list/entry](../list/entry.md) directory stack (40 avatar + label + inline count Badge + optional trailing icon toggle), optionally followed by another Header + List(entry) pair and a pinned footer commit.",
|
|
6
|
+
"useCases": [
|
|
7
|
+
"navigation drawer",
|
|
8
|
+
"channel directory",
|
|
9
|
+
"off-canvas filter rail",
|
|
10
|
+
"settings panel",
|
|
11
|
+
"subscription / favorites column",
|
|
12
|
+
"sub-navigation overlay"
|
|
13
|
+
],
|
|
14
|
+
"visualReuse": "open",
|
|
15
|
+
"layoutInset": "bounded-surface",
|
|
16
|
+
"wrapperGuidance": "Renders into a body portal — its own modal-like surface. Treat the same as BottomSheet / Dialog: do not wrap the `<SideSheet>` call in any layout container; place it as a sibling of the page shell so the portal mounts at body root. Full-bleed children inside the sheet body negate the body's inline padding via the negative-margin opt-out (see AGENTS.md § Composition rules).",
|
|
17
|
+
"spec": "side-sheet.md",
|
|
18
|
+
"usage": {
|
|
19
|
+
"note": "Portal-rendered; controlled via open/onClose. Free-form children — group canonical Header + List(entry) stacks in SideSheetGroup.",
|
|
20
|
+
"example": "<SideSheet open onClose={close} aria-label=\"…\" footer={…}><SideSheetGroup>…</SideSheetGroup></SideSheet>"
|
|
21
|
+
},
|
|
22
|
+
"subcomponents": [
|
|
23
|
+
{
|
|
24
|
+
"slug": "side-sheet",
|
|
25
|
+
"spec": "side-sheet.spec.json",
|
|
26
|
+
"md": "side-sheet.md",
|
|
27
|
+
"default": true
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|