@super-ic/product-shell 0.0.0-oidc-bootstrap.0 → 0.1.1

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/README.md CHANGED
@@ -1,3 +1,157 @@
1
1
  # @super-ic/product-shell
2
2
 
3
- This prerelease reserves the package name for initial GitHub Actions OIDC trusted-publisher setup. It contains no runtime, exports, or API. Do not use it in production. Real design-system versions are released separately through the trusted GitHub workflow.
3
+ Provider-neutral React application shell primitives for SuperIC product surfaces: the app
4
+ frame, navigation rails, page and state regions, the top bar, the segmented search pill, and
5
+ the settings switch row.
6
+
7
+ Every component in this package renders from fixture props alone. Nothing here opens a
8
+ network connection, reads a cookie, initialises an SDK, or decides authorisation.
9
+
10
+ ## Install
11
+
12
+ ```sh
13
+ npm install @super-ic/product-shell @super-ic/foundation react react-dom
14
+ ```
15
+
16
+ `react`, `react-dom` and `@super-ic/foundation` are peer dependencies. The declared React
17
+ range is `>=19.0.0`. Installing this package on its own emits a peer warning naming
18
+ `@super-ic/foundation`; that warning is the intended signal, not a defect.
19
+
20
+ ## Required setup
21
+
22
+ The components style themselves from CSS custom properties that only `@super-ic/foundation`
23
+ defines. Import the token sheet once, at the root of the consuming application, before any
24
+ component from this package renders:
25
+
26
+ ```ts
27
+ import "@super-ic/foundation/tokens.css";
28
+ import "@super-ic/product-shell/member-shell.css";
29
+ ```
30
+
31
+ Without that import every surface falls back to the browser default colours: the frame has no
32
+ background, the state regions have no surface, and the switch has no track.
33
+
34
+ The 19 custom properties this package reads are `--accent`, `--background`, `--border`,
35
+ `--control-border`, `--control-elevation`, `--control-elevation-surface`,
36
+ `--control-highlight`, `--elev-card`, `--elev-pill`, `--foreground`, `--muted`,
37
+ `--muted-foreground`, `--primary`, `--primary-foreground`, `--radius`, `--radius-card`,
38
+ `--ring`, `--surface` and `--surface-2`.
39
+
40
+ No Tailwind `@source` line is needed. Existing primitives carry scoped inline CSS. The
41
+ `MemberShell` recipe uses the public ordinary CSS entry shown above, scoped by `data-slot`
42
+ attributes and available in the installed package.
43
+
44
+ ## Usage
45
+
46
+ ```tsx
47
+ import "@super-ic/foundation/tokens.css";
48
+ import "@super-ic/product-shell/member-shell.css";
49
+
50
+ import {
51
+ AppFrame,
52
+ AppRail,
53
+ AppTopBar,
54
+ PageRegion,
55
+ StateRegion,
56
+ } from "@super-ic/product-shell";
57
+
58
+ export function Screen() {
59
+ return (
60
+ <AppFrame
61
+ header={<AppTopBar brand="Example" modes={[{ id: "browse", label: "Browse" }]} activeModeId="browse" />}
62
+ rail={<AppRail items={[{ id: "home", label: "Home", href: "/", active: true }]} />}
63
+ >
64
+ <PageRegion title="Home" description="What is available near you today.">
65
+ <StateRegion
66
+ state="empty"
67
+ title="Nothing here yet"
68
+ description="New items appear as neighbours post them."
69
+ action={{ label: "Refresh", onAction: () => location.reload() }}
70
+ />
71
+ </PageRegion>
72
+ </AppFrame>
73
+ );
74
+ }
75
+ ```
76
+
77
+ For a composed member application, use one navigation array and select the current item by
78
+ id. `MemberShell` accepts zero through five unique destinations. A non-empty navigation
79
+ requires a matching `currentNavigationId`, or explicit `null` for a secondary route
80
+ such as Account. An omitted ID still rejects a populated navigation. A secondary
81
+ route keeps all primary links available without falsely marking Home current; its
82
+ utility link may carry `aria-current="page"`. Empty navigation omits its landmarks.
83
+
84
+ The frame defaults to `100dvh`. Within a parent that has an explicitly resolved
85
+ height, set `--member-shell-block-size: 100%` to fill the available region. A review
86
+ banner belongs in the parent layout and must not push navigation below the viewport.
87
+
88
+ ```tsx
89
+ import "@super-ic/foundation/tokens.css";
90
+ import "@super-ic/product-shell/member-shell.css";
91
+ import { MemberShell } from "@super-ic/product-shell";
92
+
93
+ <MemberShell
94
+ brand="Fixture Share"
95
+ navigation={[{ id: "home", label: "Home", href: "/home" }]}
96
+ currentNavigationId="home"
97
+ pageTitle="Member home"
98
+ >
99
+ {content}
100
+ </MemberShell>;
101
+ ```
102
+
103
+ The product adapter owns route transitions, route-heading focus and scroll restoration. An
104
+ optional `onNavigation` callback receives the unmodified React anchor event and item, so an
105
+ SPA adapter can preserve modifier clicks and other browser link behavior.
106
+
107
+ ## Exports
108
+
109
+ | Export | Kind | Notes |
110
+ | --- | --- | --- |
111
+ | `MemberShell` | recipe | Container-responsive `100dvh` member shell with one main, one h1, skip link and zero-to-five destination invariant. Import `@super-ic/product-shell/member-shell.css`. |
112
+ | `AppFrame` | component | Grid shell. Stamps `data-superic-provider="none"` and injects the frame stylesheet. |
113
+ | `AppRail` | component | Desktop navigation `<nav>`; sets `aria-current="page"` on the active item. |
114
+ | `MobileBottomNavigation` | component | Small-viewport counterpart to `AppRail`. |
115
+ | `PageRegion` | component | The `<main>` landmark, its heading, and its description. |
116
+ | `StateRegion` | component | Blocking and recovery states, each with a live region. |
117
+ | `AppTopBar` | component | Brand plus a single-select mode group using `aria-pressed`. |
118
+ | `SegmentedSearchPill` | component | `role="search"` form with per-segment labels and one submit control. |
119
+ | `SettingsSwitchRow` | component | `role="switch"` row wired to its label and help text. |
120
+ | `StateRegionStates` | const | `waiting`, `empty`, `error`, `offline`, `permission`, `recovery`, `stale`, `success`, plus matrix aliases `loading`, `initial-empty`, `validation-error`, `permission-denied`, `removed`, `retryable-error`, `conflict`, `capability-disabled`, `safe-stop`, `support-entry`. |
121
+ | `ProductMemberRoles`, `ProductMemberCapabilities` | const | Display-only role and capability vocabularies. |
122
+ | `assertProductMemberDisplayProjection` | function | Throws unless `authority === "display-only"`. |
123
+ | `createProviderNeutralFixture`, `assertProviderNeutralFixture` | function | Throws unless `mode === "fixture"` and `provider === "none"`. |
124
+ | `PRODUCT_SHELL_CONTRACT_VERSION` | const | Tracks this package's own version. |
125
+
126
+ ## Contracts worth knowing before you build on this
127
+
128
+ **`ProductMember` is a projection, not an authority.** It carries role labels and capability
129
+ labels for rendering only. `assertProductMemberDisplayProjection` throws if you hand it a
130
+ member whose `authority` is anything other than `"display-only"`. Never branch a permission
131
+ decision on it; ask your own server.
132
+
133
+ **Fixtures may not reach a provider.** `assertProviderNeutralFixture` throws unless the
134
+ fixture declares `mode: "fixture"` and `provider: "none"`. `AppFrame`, `AppTopBar`,
135
+ `SegmentedSearchPill` and `SettingsSwitchRow` all stamp `data-superic-provider="none"` on
136
+ their root so a consumer test can assert the same thing from the DOM.
137
+
138
+ **`PageRegion` labels itself.** When you pass `title` and no `labelledBy`, the heading id is
139
+ generated per instance with `useId`, so two `PageRegion`s on one page never collide on a
140
+ duplicate `id`. Pass `labelledBy` when the heading lives outside the region and you own its
141
+ id; `PageRegion` then uses your id verbatim and does not generate one.
142
+
143
+ **`StateRegion` picks its own ARIA role.** `error` and `permission` render
144
+ `role="alert"` with `aria-live="assertive"`; the other six render `role="status"` with
145
+ `aria-live="polite"`. Do not override this from the outside. The mapping is the contract.
146
+
147
+ **`AppTopBar` allows exactly one pressed mode.** `activeModeId` selects it, and only that one
148
+ button gets `aria-pressed="true"`.
149
+
150
+ **`SegmentedSearchPill` shows a segment's `value` or its `placeholder`, never both.** A
151
+ segment with a `value` renders the value as text; a segment without one renders the
152
+ placeholder.
153
+
154
+ ## Versioning
155
+
156
+ `PRODUCT_SHELL_CONTRACT_VERSION` is the package version, not a separate contract line. Assert
157
+ against it if you need to fail closed on a shell upgrade.
@@ -0,0 +1,338 @@
1
+ [data-slot="member-shell"] {
2
+ --member-shell-gutter: 1rem;
3
+ container-name: superic-member-shell;
4
+ container-type: inline-size;
5
+ display: grid;
6
+ grid-template-rows: auto minmax(0, 1fr) auto;
7
+ block-size: var(--member-shell-block-size, 100dvh);
8
+ min-block-size: 0;
9
+ min-inline-size: 0;
10
+ overflow: hidden;
11
+ color: var(--foreground);
12
+ background: var(--background);
13
+ font-family: var(--type-body-medium-family);
14
+ }
15
+
16
+ [data-slot="member-shell"] *,
17
+ [data-slot="member-shell"] *::before,
18
+ [data-slot="member-shell"] *::after {
19
+ box-sizing: border-box;
20
+ }
21
+
22
+ [data-slot="member-shell-skip-link"] {
23
+ position: fixed;
24
+ inset-block-start: 0.5rem;
25
+ inset-inline-start: 0.5rem;
26
+ z-index: 50;
27
+ inline-size: 1px;
28
+ block-size: 1px;
29
+ min-block-size: 0;
30
+ clip-path: inset(50%);
31
+ overflow: hidden;
32
+ white-space: nowrap;
33
+ border-radius: var(--radius);
34
+ background: var(--foreground);
35
+ color: var(--background);
36
+ padding: 0;
37
+ font-weight: 600;
38
+ text-decoration: none;
39
+ }
40
+
41
+ [data-slot="member-shell-skip-link"]:focus-visible {
42
+ inline-size: auto;
43
+ block-size: auto;
44
+ min-block-size: 2.75rem;
45
+ clip-path: none;
46
+ overflow: visible;
47
+ white-space: normal;
48
+ padding: 0.75rem 1rem;
49
+ outline: 2px solid var(--ring);
50
+ outline-offset: 2px;
51
+ }
52
+
53
+ [data-slot="member-shell-header"] {
54
+ z-index: 2;
55
+ max-block-size: 25dvh;
56
+ overflow: auto;
57
+ border-block-end: 1px solid var(--border);
58
+ background: var(--surface);
59
+ }
60
+
61
+ [data-slot="member-shell-header-inner"] {
62
+ display: flex;
63
+ flex-wrap: wrap;
64
+ align-items: center;
65
+ justify-content: space-between;
66
+ gap: 0.75rem 1rem;
67
+ inline-size: 100%;
68
+ max-inline-size: 72rem;
69
+ min-block-size: 4rem;
70
+ margin-inline: auto;
71
+ padding: 0.5rem var(--member-shell-gutter);
72
+ }
73
+
74
+ [data-slot="member-shell-brand"],
75
+ [data-slot="member-shell-utility-actions"] {
76
+ display: flex;
77
+ align-items: center;
78
+ gap: 0.5rem;
79
+ min-inline-size: 0;
80
+ max-inline-size: 100%;
81
+ overflow-wrap: anywhere;
82
+ }
83
+
84
+ [data-slot="member-shell-brand"] { flex: 0 1 auto; }
85
+ [data-slot="member-shell-brand"] > * { max-inline-size: 100%; }
86
+
87
+ [data-slot="member-shell-utility-actions"] {
88
+ margin-inline-start: auto;
89
+ flex-wrap: wrap;
90
+ justify-content: flex-end;
91
+ }
92
+
93
+ [data-slot="member-shell-utility-actions"] button {
94
+ block-size: auto;
95
+ min-block-size: var(--control-height-lg);
96
+ max-inline-size: 100%;
97
+ white-space: normal;
98
+ }
99
+
100
+ [data-slot="member-shell-body"] {
101
+ display: grid;
102
+ grid-template-columns: minmax(0, 1fr);
103
+ min-block-size: 0;
104
+ min-inline-size: 0;
105
+ }
106
+
107
+ [data-slot="member-shell-rail"] {
108
+ display: none;
109
+ min-block-size: 0;
110
+ overflow: auto;
111
+ border-inline-end: 1px solid var(--border);
112
+ background: var(--surface);
113
+ padding: 1rem;
114
+ }
115
+
116
+ [data-slot="member-shell-main"] {
117
+ min-block-size: 0;
118
+ min-inline-size: 0;
119
+ overflow: auto;
120
+ scroll-padding-block: 1rem;
121
+ }
122
+
123
+ [data-slot="member-shell-main"]:focus {
124
+ outline: none;
125
+ }
126
+
127
+ [data-slot="member-shell-content"] {
128
+ display: grid;
129
+ align-content: start;
130
+ gap: 2rem;
131
+ inline-size: 100%;
132
+ max-inline-size: 72rem;
133
+ margin-inline: auto;
134
+ padding: 1.5rem var(--member-shell-gutter) 2rem;
135
+ }
136
+
137
+ [data-slot="member-shell-page-header"] {
138
+ display: flex;
139
+ flex-wrap: wrap;
140
+ align-items: flex-start;
141
+ justify-content: space-between;
142
+ gap: 1rem 1.5rem;
143
+ }
144
+
145
+ [data-slot="member-shell-page-heading"] {
146
+ display: grid;
147
+ gap: 0.5rem;
148
+ min-inline-size: 0;
149
+ max-inline-size: 70ch;
150
+ overflow-wrap: anywhere;
151
+ }
152
+
153
+ [data-slot="member-shell-page-heading"] h1 {
154
+ margin: 0;
155
+ color: var(--foreground);
156
+ font-family: var(--type-h3-family);
157
+ font-size: 1.5rem;
158
+ font-weight: 600;
159
+ line-height: 1.2;
160
+ letter-spacing: -0.01em;
161
+ }
162
+
163
+ [data-slot="member-shell-page-description"] {
164
+ color: var(--muted-foreground);
165
+ line-height: 1.5;
166
+ }
167
+
168
+ [data-slot="member-shell-page-description"] > :first-child {
169
+ margin-block-start: 0;
170
+ }
171
+
172
+ [data-slot="member-shell-page-description"] > :last-child {
173
+ margin-block-end: 0;
174
+ }
175
+
176
+ [data-slot="member-shell-page-actions"] {
177
+ display: flex;
178
+ flex-wrap: wrap;
179
+ align-items: center;
180
+ gap: 0.5rem;
181
+ }
182
+
183
+ [data-slot="member-shell-navigation-list"] {
184
+ display: grid;
185
+ gap: 0.25rem;
186
+ margin: 0;
187
+ padding: 0;
188
+ list-style: none;
189
+ }
190
+
191
+ [data-slot="member-shell-navigation-list"] a {
192
+ display: flex;
193
+ align-items: center;
194
+ justify-content: flex-start;
195
+ gap: 0.5rem;
196
+ min-block-size: 2.75rem;
197
+ border-radius: var(--radius);
198
+ color: var(--foreground);
199
+ padding: 0.625rem 0.75rem;
200
+ line-height: 1.25;
201
+ text-decoration: none;
202
+ }
203
+
204
+ [data-slot="member-shell-navigation-list"] a:hover {
205
+ background: var(--surface-2);
206
+ }
207
+
208
+ [data-slot="member-shell-navigation-list"] a[aria-current="page"] {
209
+ background: var(--muted);
210
+ font-weight: 600;
211
+ }
212
+
213
+ [data-slot="member-shell-navigation-list"] a:focus-visible,
214
+ [data-slot="member-shell"] button:focus-visible,
215
+ [data-slot="member-shell"] [role="button"]:focus-visible {
216
+ outline: 2px solid var(--ring);
217
+ outline-offset: 2px;
218
+ }
219
+
220
+ [data-slot="member-shell-navigation-icon"] {
221
+ display: grid;
222
+ flex: 0 0 auto;
223
+ place-items: center;
224
+ }
225
+
226
+ [data-slot="member-shell-compact-navigation-region"] {
227
+ max-block-size: 35dvh;
228
+ min-inline-size: 0;
229
+ overflow: auto;
230
+ overscroll-behavior: contain;
231
+ border-block-start: 1px solid var(--border);
232
+ background: var(--surface);
233
+ padding: 0.375rem 0.5rem calc(0.375rem + env(safe-area-inset-bottom, 0px));
234
+ }
235
+
236
+ [data-slot="member-shell-compact-navigation"] [data-slot="member-shell-navigation-list"] {
237
+ grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
238
+ gap: 0;
239
+ }
240
+
241
+ [data-slot="member-shell-compact-navigation"] [data-slot="member-shell-navigation-list"] > li {
242
+ min-inline-size: 0;
243
+ }
244
+
245
+ [data-slot="member-shell-compact-navigation"] [data-slot="member-shell-navigation-list"] a {
246
+ flex-direction: column;
247
+ justify-content: center;
248
+ gap: 0.25rem;
249
+ min-inline-size: 0;
250
+ padding-inline: 0;
251
+ font-family: var(--type-caption-family);
252
+ font-size: var(--type-caption-size);
253
+ font-weight: var(--type-caption-weight);
254
+ line-height: var(--type-caption-line-height);
255
+ letter-spacing: var(--type-caption-tracking);
256
+ text-align: center;
257
+ }
258
+
259
+ [data-slot="member-shell-compact-navigation"] [data-slot="member-shell-navigation-list"] a > span:last-child {
260
+ min-inline-size: 0;
261
+ max-inline-size: 100%;
262
+ overflow-wrap: anywhere;
263
+ }
264
+
265
+ @container superic-member-shell (min-width: 64rem) {
266
+ [data-slot="member-shell-header-inner"],
267
+ [data-slot="member-shell-content"] {
268
+ --member-shell-gutter: 1.5rem;
269
+ }
270
+
271
+ [data-slot="member-shell-body"] {
272
+ grid-template-columns: 15rem minmax(0, 1fr);
273
+ }
274
+
275
+ [data-slot="member-shell"][data-has-navigation="false"] [data-slot="member-shell-body"] {
276
+ grid-template-columns: minmax(0, 1fr);
277
+ }
278
+
279
+ [data-slot="member-shell-rail"] {
280
+ display: block;
281
+ }
282
+
283
+ [data-slot="member-shell-compact-navigation-region"] {
284
+ display: none;
285
+ }
286
+ }
287
+
288
+ @media (forced-colors: active) {
289
+ [data-slot="member-shell-navigation-list"] a[aria-current="page"] {
290
+ outline: 2px solid CanvasText;
291
+ }
292
+ }
293
+
294
+ @media (prefers-reduced-motion: reduce) {
295
+ [data-slot="member-shell"] *,
296
+ [data-slot="member-shell"] *::before,
297
+ [data-slot="member-shell"] *::after {
298
+ scroll-behavior: auto;
299
+ transition-duration: 0ms !important;
300
+ animation-duration: 0ms !important;
301
+ }
302
+ }
303
+
304
+ /* Top navigation is the current member recipe; the rail is an explicit legacy option. */
305
+ [data-slot="member-shell-top-navigation"] { display: none; }
306
+ [data-slot="member-shell"][data-navigation-placement="top"] [data-slot="member-shell-page-heading"] h1 {
307
+ font-size: var(--type-h1-size);
308
+ font-family: var(--type-h1-family);
309
+ line-height: var(--type-h1-line-height);
310
+ letter-spacing: -0.035em;
311
+ }
312
+ [data-slot="member-shell"][data-navigation-placement="top"] [data-slot="member-shell-content"] { padding-block-start: 2.5rem; gap: 2.25rem; }
313
+ @container superic-member-shell (min-width: 48rem) {
314
+ [data-slot="member-shell"][data-navigation-placement="top"] [data-slot="member-shell-body"] { grid-template-columns: minmax(0,1fr); }
315
+ [data-slot="member-shell"][data-navigation-placement="top"] [data-slot="member-shell-header-inner"] { min-block-size: 5rem; gap: 2rem; }
316
+ [data-slot="member-shell-top-navigation"] { display: block; margin-inline: auto; }
317
+ [data-slot="member-shell-top-navigation"] [data-slot="member-shell-navigation-list"] { display: flex; flex-wrap: wrap; align-items: center; gap: 1.5rem; }
318
+ [data-slot="member-shell-top-navigation"] [data-slot="member-shell-navigation-list"] a { position: relative; min-block-size: 5rem; border-radius: 0; padding-inline: 0.25rem; color: var(--muted-foreground); background: transparent; }
319
+ [data-slot="member-shell-top-navigation"] [data-slot="member-shell-navigation-list"] a:hover { color: var(--foreground); }
320
+ [data-slot="member-shell-top-navigation"] [data-slot="member-shell-navigation-list"] a[aria-current="page"] { color: var(--foreground); }
321
+ [data-slot="member-shell-top-navigation"] [data-slot="member-shell-navigation-list"] a[aria-current="page"]::after { content: ""; position: absolute; inset-inline: 0; inset-block-end: 0; block-size: 3px; background: var(--foreground); }
322
+ [data-slot="member-shell-top-navigation"] [data-slot="member-shell-navigation-icon"] { display: none; }
323
+ [data-slot="member-shell"][data-navigation-placement="top"] [data-slot="member-shell-compact-navigation-region"] { display: none; }
324
+ }
325
+
326
+ [data-slot="member-shell-page-identity"] { display: flex; align-items: center; gap: 1.25rem; min-inline-size: 0; flex: 1 1 16rem; }
327
+ [data-slot="member-shell"][data-content-layout="workspace"] [data-slot="member-shell-main"] { container: member-main / size; }
328
+ [data-slot="member-shell"][data-content-layout="workspace"] [data-slot="member-shell-content"] { block-size:100%; min-block-size:0; grid-template-rows:auto minmax(0,1fr); align-content:stretch; gap:1rem; padding-block:1rem; }
329
+ [data-slot="member-shell"][data-content-layout="workspace"] [data-slot="member-shell-page-heading"] h1 { font-family:var(--type-h3-family); font-size:var(--type-h3-size); line-height:1.25; letter-spacing:-.025em; }
330
+ @container member-main (max-height: 34rem) {
331
+ [data-slot="member-shell-content"] { block-size:auto !important; min-block-size:100%; grid-template-rows:auto auto !important; }
332
+ }
333
+ [data-slot="member-shell-page-media"] { inline-size: 6rem; block-size: 5rem; border-radius: var(--radius-xl); overflow: hidden; flex: 0 0 auto; background: var(--surface-2); }
334
+ [data-slot="member-shell-page-media"] > img { inline-size: 100%; block-size: 100%; object-fit: cover; }
335
+ @container superic-member-shell (max-width: 30rem) {
336
+ [data-slot="member-shell-page-identity"] { gap: 1rem; }
337
+ [data-slot="member-shell-page-media"] { inline-size: 4.5rem; block-size: 4.5rem; }
338
+ }
@@ -0,0 +1,108 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+ import type { AppNavigationItem } from "./recipes/MemberShell.js";
3
+ export { MEMBER_SHELL_HEADING_ID, MEMBER_SHELL_MAIN_ID, MemberShell, } from "./recipes/MemberShell.js";
4
+ export type { AppNavigationItem, MemberShellNavigationHandler, MemberShellProps, } from "./recipes/MemberShell.js";
5
+ export declare const PRODUCT_SHELL_CONTRACT_VERSION = "0.1.0";
6
+ export declare const ProductMemberRoles: readonly ["member", "requester", "lender", "provider", "organizer", "founder", "steward", "courier"];
7
+ export declare const ProductMemberCapabilities: readonly ["browse_public", "create_ask", "create_offer", "manage_commitment", "manage_plan", "review_community_case", "perform_simulated_courier_task"];
8
+ export type ProductMemberRole = (typeof ProductMemberRoles)[number];
9
+ export type ProductMemberCapability = (typeof ProductMemberCapabilities)[number];
10
+ export type ProductMember = Readonly<{
11
+ id: string;
12
+ displayName: string;
13
+ roleLabels: readonly ProductMemberRole[];
14
+ capabilities: readonly ProductMemberCapability[];
15
+ brandTenantId: string;
16
+ productTenantId: string;
17
+ communityId: string;
18
+ effectiveMembershipId: string | null;
19
+ authority: "display-only";
20
+ }>;
21
+ export declare function assertProductMemberDisplayProjection(member: ProductMember): ProductMember;
22
+ export type ProviderNeutralFixture<T> = Readonly<{
23
+ fixtureId: string;
24
+ mode: "fixture";
25
+ provider: "none";
26
+ data: T;
27
+ }>;
28
+ export declare function createProviderNeutralFixture<T>(fixtureId: string, data: T): ProviderNeutralFixture<T>;
29
+ export declare function assertProviderNeutralFixture<T>(fixture: ProviderNeutralFixture<T>): ProviderNeutralFixture<T>;
30
+ export type AppFrameProps = Readonly<{
31
+ children: ReactNode;
32
+ header?: ReactNode;
33
+ rail?: ReactNode;
34
+ mobileNavigation?: ReactNode;
35
+ className?: string;
36
+ }>;
37
+ export declare function AppFrame({ children, header, rail, mobileNavigation, className }: AppFrameProps): import("react").JSX.Element;
38
+ export type AppRailProps = Readonly<{
39
+ items: readonly AppNavigationItem[];
40
+ label?: string;
41
+ }>;
42
+ export declare function AppRail({ items, label }: AppRailProps): import("react").JSX.Element;
43
+ export type MobileBottomNavigationProps = Readonly<{
44
+ items: readonly AppNavigationItem[];
45
+ label?: string;
46
+ }>;
47
+ export declare function MobileBottomNavigation({ items, label, }: MobileBottomNavigationProps): import("react").JSX.Element;
48
+ export type PageRegionProps = Readonly<{
49
+ children: ReactNode;
50
+ title?: string;
51
+ description?: string;
52
+ labelledBy?: string;
53
+ }>;
54
+ export declare function PageRegion({ children, title, description, labelledBy }: PageRegionProps): import("react").JSX.Element;
55
+ export declare const StateRegionStates: readonly ["waiting", "empty", "error", "offline", "permission", "recovery", "stale", "success", "loading", "initial-empty", "validation-error", "permission-denied", "removed", "retryable-error", "conflict", "capability-disabled", "safe-stop", "support-entry"];
56
+ export type StateRegionState = (typeof StateRegionStates)[number];
57
+ export type StateRegionAction = Readonly<{
58
+ label: string;
59
+ href?: string;
60
+ onAction?: () => void;
61
+ }>;
62
+ export type StateRegionProps = Readonly<{
63
+ state: StateRegionState;
64
+ title: string;
65
+ description: string;
66
+ action?: StateRegionAction;
67
+ icon?: ReactNode;
68
+ children?: ReactNode;
69
+ style?: CSSProperties;
70
+ }>;
71
+ export declare function StateRegion({ state, title, description, action, icon, children, style, }: StateRegionProps): import("react").JSX.Element;
72
+ export type AppTopBarMode = Readonly<{
73
+ id: string;
74
+ label: string;
75
+ icon?: ReactNode;
76
+ }>;
77
+ export type AppTopBarProps = Readonly<{
78
+ brand: ReactNode;
79
+ modes: readonly AppTopBarMode[];
80
+ activeModeId: string;
81
+ onModeChange?: (modeId: string) => void;
82
+ trailing?: ReactNode;
83
+ label?: string;
84
+ modesLabel?: string;
85
+ }>;
86
+ export declare function AppTopBar({ brand, modes, activeModeId, onModeChange, trailing, label, modesLabel, }: AppTopBarProps): import("react").JSX.Element;
87
+ export type SearchPillSegment = Readonly<{
88
+ id: string;
89
+ label: string;
90
+ placeholder: string;
91
+ value?: string;
92
+ }>;
93
+ export type SegmentedSearchPillProps = Readonly<{
94
+ segments: readonly SearchPillSegment[];
95
+ onSubmit?: () => void;
96
+ submitLabel?: string;
97
+ submitIcon?: ReactNode;
98
+ label?: string;
99
+ }>;
100
+ export declare function SegmentedSearchPill({ segments, onSubmit, submitLabel, submitIcon, label, }: SegmentedSearchPillProps): import("react").JSX.Element;
101
+ export type SettingsSwitchRowProps = Readonly<{
102
+ label: string;
103
+ help?: string;
104
+ checked: boolean;
105
+ onCheckedChange?: (checked: boolean) => void;
106
+ disabled?: boolean;
107
+ }>;
108
+ export declare function SettingsSwitchRow({ label, help, checked, onCheckedChange, disabled, }: SettingsSwitchRowProps): import("react").JSX.Element;
package/dist/index.js ADDED
@@ -0,0 +1,544 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Fragment, useId } from "react";
3
+ export { MEMBER_SHELL_HEADING_ID, MEMBER_SHELL_MAIN_ID, MemberShell, } from "./recipes/MemberShell.js";
4
+ export const PRODUCT_SHELL_CONTRACT_VERSION = "0.1.0";
5
+ export const ProductMemberRoles = [
6
+ "member",
7
+ "requester",
8
+ "lender",
9
+ "provider",
10
+ "organizer",
11
+ "founder",
12
+ "steward",
13
+ "courier",
14
+ ];
15
+ export const ProductMemberCapabilities = [
16
+ "browse_public",
17
+ "create_ask",
18
+ "create_offer",
19
+ "manage_commitment",
20
+ "manage_plan",
21
+ "review_community_case",
22
+ "perform_simulated_courier_task",
23
+ ];
24
+ export function assertProductMemberDisplayProjection(member) {
25
+ if (member.authority !== "display-only") {
26
+ throw new Error("ProductMember is a display projection and cannot grant authority");
27
+ }
28
+ return member;
29
+ }
30
+ export function createProviderNeutralFixture(fixtureId, data) {
31
+ return { fixtureId, mode: "fixture", provider: "none", data };
32
+ }
33
+ export function assertProviderNeutralFixture(fixture) {
34
+ if (fixture.mode !== "fixture" || fixture.provider !== "none") {
35
+ throw new Error("accepted fixtures cannot initialize a provider");
36
+ }
37
+ return fixture;
38
+ }
39
+ const appFrameCss = `
40
+ [data-slot="superic-app-frame"] {
41
+ display: grid;
42
+ min-block-size: 100%;
43
+ color: var(--foreground);
44
+ background: var(--background);
45
+ }
46
+ [data-slot="superic-app-frame-header"] {
47
+ position: sticky;
48
+ inset-block-start: 0;
49
+ z-index: 1;
50
+ border-block-end: 1px solid var(--border);
51
+ background: var(--surface);
52
+ }
53
+ [data-slot="superic-app-frame-body"] {
54
+ display: grid;
55
+ min-inline-size: 0;
56
+ }
57
+ [data-slot="superic-app-rail"] {
58
+ display: grid;
59
+ align-content: start;
60
+ gap: 0.25rem;
61
+ border-inline-end: 1px solid var(--border);
62
+ background: var(--surface);
63
+ padding: 1rem;
64
+ }
65
+ [data-slot="superic-app-rail"] a,
66
+ [data-slot="superic-mobile-bottom-navigation"] a {
67
+ display: flex;
68
+ min-block-size: 44px;
69
+ align-items: center;
70
+ gap: 0.5rem;
71
+ border-radius: var(--radius);
72
+ color: var(--foreground);
73
+ padding-inline: 0.75rem;
74
+ text-decoration: none;
75
+ }
76
+ [data-slot="superic-app-rail"] a[aria-current="page"],
77
+ [data-slot="superic-mobile-bottom-navigation"] a[aria-current="page"] {
78
+ background: var(--muted);
79
+ font-weight: 600;
80
+ }
81
+ [data-slot="superic-app-rail"] a:focus-visible,
82
+ [data-slot="superic-mobile-bottom-navigation"] a:focus-visible,
83
+ [data-slot="superic-state-region-action"]:focus-visible {
84
+ outline: 2px solid var(--ring);
85
+ outline-offset: 2px;
86
+ }
87
+ [data-slot="superic-page-region"] {
88
+ display: grid;
89
+ align-content: start;
90
+ gap: 1rem;
91
+ min-inline-size: 0;
92
+ padding: clamp(1rem, 3vw, 2rem);
93
+ }
94
+ [data-slot="superic-page-region-heading"] {
95
+ display: grid;
96
+ gap: 0.5rem;
97
+ max-inline-size: 70ch;
98
+ }
99
+ [data-slot="superic-state-region"] {
100
+ display: grid;
101
+ gap: 0.75rem;
102
+ max-inline-size: 48rem;
103
+ border-radius: var(--radius);
104
+ background: var(--surface);
105
+ box-shadow: var(--elev-card);
106
+ padding: clamp(1rem, 3vw, 1.5rem);
107
+ }
108
+ [data-slot="superic-state-region-icon"] {
109
+ display: grid;
110
+ inline-size: 2.75rem;
111
+ block-size: 2.75rem;
112
+ place-items: center;
113
+ border-radius: 999px;
114
+ background: var(--muted);
115
+ color: var(--primary);
116
+ }
117
+ [data-slot="superic-state-region-action"] {
118
+ inline-size: fit-content;
119
+ min-block-size: 44px;
120
+ border: 1px solid var(--border);
121
+ border-radius: var(--radius);
122
+ background: var(--primary);
123
+ color: var(--primary-foreground);
124
+ padding-inline: 1rem;
125
+ text-decoration: none;
126
+ }
127
+ [data-slot="superic-mobile-bottom-navigation"] {
128
+ display: grid;
129
+ grid-auto-flow: column;
130
+ grid-auto-columns: minmax(0, 1fr);
131
+ border-block-start: 1px solid var(--border);
132
+ background: var(--surface);
133
+ padding-block-end: env(safe-area-inset-bottom, 0px);
134
+ }
135
+ @media (max-width: 47.99rem) {
136
+ [data-slot="superic-app-rail"] { display: none; }
137
+ [data-slot="superic-app-frame-body"] { grid-template-columns: minmax(0, 1fr); }
138
+ }
139
+ @media (min-width: 48rem) {
140
+ [data-slot="superic-app-frame-body"] { grid-template-columns: minmax(13rem, 17rem) minmax(0, 1fr); }
141
+ [data-slot="superic-mobile-bottom-navigation"] { display: none; }
142
+ }
143
+ @media (forced-colors: active) {
144
+ [data-slot="superic-app-rail"] a[aria-current="page"],
145
+ [data-slot="superic-mobile-bottom-navigation"] a[aria-current="page"] {
146
+ outline: 2px solid CanvasText;
147
+ }
148
+ }
149
+ @media (prefers-reduced-motion: reduce) {
150
+ [data-slot="superic-app-frame"] *,
151
+ [data-slot="superic-app-frame"] *::before,
152
+ [data-slot="superic-app-frame"] *::after {
153
+ scroll-behavior: auto;
154
+ transition-duration: 0ms;
155
+ animation-duration: 0ms;
156
+ }
157
+ }
158
+ `;
159
+ function AppFrameStyles() {
160
+ return _jsx("style", { children: appFrameCss });
161
+ }
162
+ export function AppFrame({ children, header, rail, mobileNavigation, className }) {
163
+ return (_jsxs("div", { className: className, "data-slot": "superic-app-frame", "data-superic-provider": "none", children: [_jsx(AppFrameStyles, {}), header ? _jsx("header", { "data-slot": "superic-app-frame-header", children: header }) : null, _jsxs("div", { "data-slot": "superic-app-frame-body", children: [rail, children] }), mobileNavigation] }));
164
+ }
165
+ export function AppRail({ items, label = "Application navigation" }) {
166
+ return (_jsx("nav", { "aria-label": label, "data-slot": "superic-app-rail", children: items.map((item) => (_jsxs("a", { "aria-current": item.active ? "page" : undefined, href: item.href, children: [item.icon ? _jsx("span", { "aria-hidden": "true", children: item.icon }) : null, _jsx("span", { children: item.label })] }, item.id))) }));
167
+ }
168
+ export function MobileBottomNavigation({ items, label = "Mobile application navigation", }) {
169
+ return (_jsx("nav", { "aria-label": label, "data-slot": "superic-mobile-bottom-navigation", children: items.map((item) => (_jsxs("a", { "aria-current": item.active ? "page" : undefined, href: item.href, children: [item.icon ? _jsx("span", { "aria-hidden": "true", children: item.icon }) : null, _jsx("span", { children: item.label })] }, item.id))) }));
170
+ }
171
+ export function PageRegion({ children, title, description, labelledBy }) {
172
+ const generatedHeadingId = useId();
173
+ const headingId = labelledBy ?? (title ? `${generatedHeadingId}superic-page-region-title` : undefined);
174
+ return (_jsxs("main", { "aria-labelledby": headingId, "data-slot": "superic-page-region", children: [title ? (_jsxs("header", { "data-slot": "superic-page-region-heading", children: [_jsx("h1", { id: headingId, children: title }), description ? _jsx("p", { children: description }) : null] })) : null, children] }));
175
+ }
176
+ export const StateRegionStates = [
177
+ "waiting",
178
+ "empty",
179
+ "error",
180
+ "offline",
181
+ "permission",
182
+ "recovery",
183
+ "stale",
184
+ "success",
185
+ "loading",
186
+ "initial-empty",
187
+ "validation-error",
188
+ "permission-denied",
189
+ "removed",
190
+ "retryable-error",
191
+ "conflict",
192
+ "capability-disabled",
193
+ "safe-stop",
194
+ "support-entry",
195
+ ];
196
+ const regionRoles = {
197
+ waiting: "status",
198
+ empty: "status",
199
+ error: "alert",
200
+ offline: "status",
201
+ permission: "alert",
202
+ recovery: "status",
203
+ stale: "status",
204
+ success: "status",
205
+ loading: "status",
206
+ "initial-empty": "status",
207
+ "validation-error": "alert",
208
+ "permission-denied": "alert",
209
+ removed: "status",
210
+ "retryable-error": "alert",
211
+ conflict: "alert",
212
+ "capability-disabled": "status",
213
+ "safe-stop": "alert",
214
+ "support-entry": "status",
215
+ };
216
+ export function StateRegion({ state, title, description, action, icon, children, style, }) {
217
+ const role = regionRoles[state];
218
+ const actionNode = action?.href
219
+ ? _jsx("a", { "data-slot": "superic-state-region-action", href: action.href, children: action.label })
220
+ : action
221
+ ? _jsx("button", { "data-slot": "superic-state-region-action", onClick: action.onAction, type: "button", children: action.label })
222
+ : null;
223
+ return (_jsxs("section", { "aria-live": role === "alert" ? "assertive" : "polite", "data-slot": "superic-state-region", "data-state": state, role: role, style: style, children: [icon ? _jsx("span", { "aria-hidden": "true", "data-slot": "superic-state-region-icon", children: icon }) : null, _jsxs("div", { children: [_jsx("h2", { children: title }), _jsx("p", { children: description })] }), children, actionNode] }));
224
+ }
225
+ const appTopBarCss = `
226
+ [data-slot="superic-app-top-bar"] {
227
+ display: grid;
228
+ grid-template-areas: "brand modes trailing";
229
+ grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
230
+ align-items: center;
231
+ column-gap: 1rem;
232
+ row-gap: 0.5rem;
233
+ min-block-size: 4rem;
234
+ border-block-end: 1px solid var(--border);
235
+ background: var(--surface);
236
+ color: var(--foreground);
237
+ padding-block: 0.5rem;
238
+ padding-inline: clamp(1rem, 3vw, 2rem);
239
+ }
240
+ [data-slot="superic-app-top-bar-brand"] {
241
+ grid-area: brand;
242
+ display: flex;
243
+ align-items: center;
244
+ gap: 0.5rem;
245
+ min-inline-size: 0;
246
+ font-weight: 600;
247
+ }
248
+ [data-slot="superic-app-top-bar-modes"] {
249
+ grid-area: modes;
250
+ justify-self: center;
251
+ display: flex;
252
+ align-items: center;
253
+ gap: 0.5rem;
254
+ border: 1px solid var(--border);
255
+ border-radius: 999px;
256
+ background: var(--surface);
257
+ box-shadow: var(--control-elevation-surface);
258
+ padding: 0.25rem;
259
+ }
260
+ [data-slot="superic-app-top-bar-mode"] {
261
+ display: inline-flex;
262
+ align-items: center;
263
+ justify-content: center;
264
+ gap: 0.5rem;
265
+ min-block-size: 2.75rem;
266
+ border: 1px solid transparent;
267
+ border-radius: 999px;
268
+ background: transparent;
269
+ color: var(--muted-foreground);
270
+ padding-inline: 1rem;
271
+ font: inherit;
272
+ font-weight: 500;
273
+ cursor: pointer;
274
+ }
275
+ [data-slot="superic-app-top-bar-mode"]:hover {
276
+ color: var(--foreground);
277
+ background: var(--accent);
278
+ }
279
+ [data-slot="superic-app-top-bar-mode"][aria-pressed="true"] {
280
+ color: var(--primary-foreground);
281
+ border-color: var(--control-border);
282
+ background: var(--primary);
283
+ background-image: var(--control-highlight);
284
+ box-shadow: var(--control-elevation);
285
+ }
286
+ [data-slot="superic-app-top-bar-mode"]:focus-visible {
287
+ outline: 2px solid var(--ring);
288
+ outline-offset: 2px;
289
+ }
290
+ [data-slot="superic-app-top-bar-trailing"] {
291
+ grid-area: trailing;
292
+ justify-self: end;
293
+ display: flex;
294
+ align-items: center;
295
+ gap: 0.5rem;
296
+ min-inline-size: 0;
297
+ }
298
+ @media (max-width: 47.99rem) {
299
+ [data-slot="superic-app-top-bar"] {
300
+ grid-template-areas: "brand trailing" "modes modes";
301
+ grid-template-columns: minmax(0, 1fr) auto;
302
+ }
303
+ }
304
+ @media (forced-colors: active) {
305
+ [data-slot="superic-app-top-bar-mode"][aria-pressed="true"] {
306
+ outline: 2px solid CanvasText;
307
+ }
308
+ }
309
+ `;
310
+ function AppTopBarStyles() {
311
+ return _jsx("style", { children: appTopBarCss });
312
+ }
313
+ export function AppTopBar({ brand, modes, activeModeId, onModeChange, trailing, label = "Product top bar", modesLabel = "Mode", }) {
314
+ return (_jsxs("header", { "aria-label": label, "data-slot": "superic-app-top-bar", "data-superic-provider": "none", children: [_jsx(AppTopBarStyles, {}), _jsx("div", { "data-slot": "superic-app-top-bar-brand", children: brand }), _jsx("div", { "aria-label": modesLabel, "data-slot": "superic-app-top-bar-modes", role: "group", children: modes.map((mode) => {
315
+ const active = mode.id === activeModeId;
316
+ return (_jsxs("button", { "aria-pressed": active, "data-slot": "superic-app-top-bar-mode", onClick: onModeChange ? () => onModeChange(mode.id) : undefined, type: "button", children: [mode.icon ? _jsx("span", { "aria-hidden": "true", children: mode.icon }) : null, _jsx("span", { children: mode.label })] }, mode.id));
317
+ }) }), trailing ? _jsx("div", { "data-slot": "superic-app-top-bar-trailing", children: trailing }) : null] }));
318
+ }
319
+ const searchPillCss = `
320
+ [data-slot="superic-search-pill"] {
321
+ display: flex;
322
+ align-items: stretch;
323
+ inline-size: fit-content;
324
+ max-inline-size: 100%;
325
+ border: 1px solid var(--border);
326
+ border-radius: 999px;
327
+ background: var(--surface);
328
+ color: var(--foreground);
329
+ box-shadow: var(--elev-pill);
330
+ }
331
+ [data-slot="superic-search-pill-segment"] {
332
+ display: grid;
333
+ align-content: center;
334
+ gap: 0.25rem;
335
+ min-inline-size: 0;
336
+ min-block-size: 2.75rem;
337
+ border: 0;
338
+ border-radius: 999px;
339
+ background: transparent;
340
+ color: inherit;
341
+ text-align: start;
342
+ padding-block: 0.5rem;
343
+ padding-inline: 1.5rem;
344
+ font: inherit;
345
+ cursor: pointer;
346
+ }
347
+ [data-slot="superic-search-pill-segment"]:hover {
348
+ background: var(--accent);
349
+ }
350
+ [data-slot="superic-search-pill-segment"]:focus-visible,
351
+ [data-slot="superic-search-pill-submit"]:focus-visible {
352
+ outline: 2px solid var(--ring);
353
+ outline-offset: 2px;
354
+ }
355
+ [data-slot="superic-search-pill-segment-label"] {
356
+ font-size: 0.75rem;
357
+ font-weight: 600;
358
+ line-height: 1rem;
359
+ }
360
+ [data-slot="superic-search-pill-segment-value"] {
361
+ font-size: 0.875rem;
362
+ line-height: 1.25rem;
363
+ color: var(--muted-foreground);
364
+ overflow: hidden;
365
+ text-overflow: ellipsis;
366
+ white-space: nowrap;
367
+ }
368
+ [data-slot="superic-search-pill-segment-value"][data-filled="true"] {
369
+ color: var(--foreground);
370
+ }
371
+ [data-slot="superic-search-pill-divider"] {
372
+ align-self: center;
373
+ inline-size: 1px;
374
+ block-size: 2rem;
375
+ background: var(--border);
376
+ }
377
+ [data-slot="superic-search-pill-submit"] {
378
+ display: inline-flex;
379
+ align-items: center;
380
+ justify-content: center;
381
+ gap: 0.5rem;
382
+ align-self: center;
383
+ min-inline-size: 2.75rem;
384
+ min-block-size: 2.75rem;
385
+ margin-block: 0.5rem;
386
+ margin-inline: 0.25rem 0.5rem;
387
+ border: 1px solid var(--control-border);
388
+ border-radius: 999px;
389
+ background: var(--primary);
390
+ background-image: var(--control-highlight);
391
+ color: var(--primary-foreground);
392
+ box-shadow: var(--control-elevation);
393
+ padding-inline: 1rem;
394
+ font: inherit;
395
+ font-weight: 600;
396
+ cursor: pointer;
397
+ }
398
+ [data-slot="superic-search-pill-submit"][data-icon-only="true"] {
399
+ inline-size: 2.75rem;
400
+ padding-inline: 0;
401
+ }
402
+ [data-slot="superic-search-pill"] [data-slot="superic-visually-hidden"] {
403
+ position: absolute;
404
+ inline-size: 1px;
405
+ block-size: 1px;
406
+ margin: -1px;
407
+ padding: 0;
408
+ overflow: hidden;
409
+ clip-path: inset(50%);
410
+ white-space: nowrap;
411
+ border: 0;
412
+ }
413
+ @media (max-width: 47.99rem) {
414
+ [data-slot="superic-search-pill"] {
415
+ flex-direction: column;
416
+ align-items: stretch;
417
+ inline-size: 100%;
418
+ border-radius: var(--radius-card);
419
+ }
420
+ [data-slot="superic-search-pill-divider"] {
421
+ align-self: stretch;
422
+ inline-size: auto;
423
+ block-size: 1px;
424
+ margin-inline: 1.5rem;
425
+ }
426
+ [data-slot="superic-search-pill-submit"] {
427
+ align-self: stretch;
428
+ margin: 0.5rem;
429
+ }
430
+ }
431
+ `;
432
+ function SegmentedSearchPillStyles() {
433
+ return _jsx("style", { children: searchPillCss });
434
+ }
435
+ export function SegmentedSearchPill({ segments, onSubmit, submitLabel = "Search", submitIcon, label = "Search", }) {
436
+ const handleSubmit = (event) => {
437
+ event.preventDefault();
438
+ onSubmit?.();
439
+ };
440
+ return (_jsxs("form", { "aria-label": label, "data-slot": "superic-search-pill", "data-superic-provider": "none", onSubmit: handleSubmit, role: "search", children: [_jsx(SegmentedSearchPillStyles, {}), segments.map((segment, index) => (_jsxs(Fragment, { children: [index > 0 ? _jsx("span", { "aria-hidden": "true", "data-slot": "superic-search-pill-divider" }) : null, _jsxs("button", { "data-slot": "superic-search-pill-segment", type: "button", children: [_jsx("span", { "data-slot": "superic-search-pill-segment-label", children: segment.label }), _jsx("span", { "data-filled": segment.value ? "true" : undefined, "data-slot": "superic-search-pill-segment-value", children: segment.value ?? segment.placeholder })] })] }, segment.id))), _jsxs("button", { "data-icon-only": submitIcon ? "true" : undefined, "data-slot": "superic-search-pill-submit", type: "submit", children: [submitIcon ? _jsx("span", { "aria-hidden": "true", children: submitIcon }) : null, _jsx("span", { "data-slot": submitIcon ? "superic-visually-hidden" : undefined, children: submitLabel })] })] }));
441
+ }
442
+ const settingsSwitchRowCss = `
443
+ [data-slot="superic-settings-switch-row"] {
444
+ display: flex;
445
+ align-items: center;
446
+ justify-content: space-between;
447
+ gap: 1rem;
448
+ border: 1px solid var(--border);
449
+ border-radius: var(--radius);
450
+ background: var(--surface);
451
+ color: var(--foreground);
452
+ box-shadow: var(--elev-card);
453
+ padding-block: 0.75rem;
454
+ padding-inline: 1rem;
455
+ }
456
+ [data-slot="superic-settings-switch-row"][data-disabled="true"] {
457
+ opacity: 0.6;
458
+ }
459
+ [data-slot="superic-settings-switch-row-copy"] {
460
+ display: grid;
461
+ gap: 0.25rem;
462
+ min-inline-size: 0;
463
+ }
464
+ [data-slot="superic-settings-switch-row-label"] {
465
+ font-weight: 600;
466
+ }
467
+ [data-slot="superic-settings-switch-row-help"] {
468
+ margin: 0;
469
+ font-size: 0.875rem;
470
+ line-height: 1.25rem;
471
+ color: var(--muted-foreground);
472
+ }
473
+ [data-slot="superic-settings-switch"] {
474
+ display: grid;
475
+ place-items: center;
476
+ flex: none;
477
+ min-inline-size: 2.75rem;
478
+ min-block-size: 2.75rem;
479
+ border: 0;
480
+ border-radius: 999px;
481
+ background: transparent;
482
+ padding: 0;
483
+ cursor: pointer;
484
+ }
485
+ [data-slot="superic-settings-switch"]:disabled {
486
+ cursor: not-allowed;
487
+ }
488
+ [data-slot="superic-settings-switch"]:focus-visible {
489
+ outline: 2px solid var(--ring);
490
+ outline-offset: 2px;
491
+ }
492
+ [data-slot="superic-settings-switch-track"] {
493
+ display: flex;
494
+ align-items: center;
495
+ inline-size: 2.75rem;
496
+ block-size: 1.75rem;
497
+ border: 1px solid var(--border);
498
+ border-radius: 999px;
499
+ background: var(--surface-2);
500
+ padding-inline: 0.125rem;
501
+ transition: background 150ms ease, border-color 150ms ease;
502
+ }
503
+ [data-slot="superic-settings-switch-thumb"] {
504
+ inline-size: 1.375rem;
505
+ block-size: 1.375rem;
506
+ border: 1px solid var(--border);
507
+ border-radius: 999px;
508
+ background: var(--surface);
509
+ box-shadow: var(--elev-card);
510
+ transition: transform 150ms ease;
511
+ }
512
+ [data-slot="superic-settings-switch"][aria-checked="true"] [data-slot="superic-settings-switch-track"] {
513
+ border-color: var(--control-border);
514
+ background: var(--primary);
515
+ }
516
+ [data-slot="superic-settings-switch"][aria-checked="true"] [data-slot="superic-settings-switch-thumb"] {
517
+ transform: translateX(1rem);
518
+ border-color: var(--control-border);
519
+ background: var(--primary-foreground);
520
+ }
521
+ @media (forced-colors: active) {
522
+ [data-slot="superic-settings-switch-track"] {
523
+ border: 1px solid CanvasText;
524
+ }
525
+ [data-slot="superic-settings-switch-thumb"] {
526
+ border: 1px solid CanvasText;
527
+ background: CanvasText;
528
+ }
529
+ }
530
+ @media (prefers-reduced-motion: reduce) {
531
+ [data-slot="superic-settings-switch-track"],
532
+ [data-slot="superic-settings-switch-thumb"] {
533
+ transition-duration: 0ms;
534
+ }
535
+ }
536
+ `;
537
+ function SettingsSwitchRowStyles() {
538
+ return _jsx("style", { children: settingsSwitchRowCss });
539
+ }
540
+ export function SettingsSwitchRow({ label, help, checked, onCheckedChange, disabled = false, }) {
541
+ const labelId = useId();
542
+ const helpId = useId();
543
+ return (_jsxs("div", { "data-disabled": disabled ? "true" : undefined, "data-slot": "superic-settings-switch-row", "data-superic-provider": "none", children: [_jsx(SettingsSwitchRowStyles, {}), _jsxs("div", { "data-slot": "superic-settings-switch-row-copy", children: [_jsx("span", { "data-slot": "superic-settings-switch-row-label", id: labelId, children: label }), help ? _jsx("p", { "data-slot": "superic-settings-switch-row-help", id: helpId, children: help }) : null] }), _jsx("button", { "aria-checked": checked, "aria-describedby": help ? helpId : undefined, "aria-labelledby": labelId, "data-slot": "superic-settings-switch", disabled: disabled, onClick: onCheckedChange ? () => onCheckedChange(!checked) : undefined, role: "switch", type: "button", children: _jsx("span", { "aria-hidden": "true", "data-slot": "superic-settings-switch-track", children: _jsx("span", { "data-slot": "superic-settings-switch-thumb" }) }) })] }));
544
+ }
@@ -0,0 +1,37 @@
1
+ import type { MouseEventHandler, ReactNode } from "react";
2
+ export declare const MEMBER_SHELL_MAIN_ID = "superic-member-shell-main";
3
+ export declare const MEMBER_SHELL_HEADING_ID = "superic-member-shell-heading";
4
+ export type AppNavigationItem = Readonly<{
5
+ id: string;
6
+ label: string;
7
+ href: string;
8
+ icon?: ReactNode;
9
+ /** Legacy AppRail state. MemberShell derives current state from currentNavigationId. */
10
+ active?: boolean;
11
+ }>;
12
+ export type MemberShellNavigationHandler = (event: Parameters<MouseEventHandler<HTMLAnchorElement>>[0], item: AppNavigationItem) => void;
13
+ export type MemberShellProps = Readonly<{
14
+ brand: ReactNode;
15
+ navigation: readonly AppNavigationItem[];
16
+ pageTitle: string;
17
+ /** Use null for a secondary route, such as Account, with no current primary destination. */
18
+ currentNavigationId?: string | null;
19
+ /** Top navigation keeps neighborhood discovery and coordination in one frame. */
20
+ navigationPlacement?: "top" | "rail";
21
+ /** Workspace fills the remaining pane with one controlled child, such as MemberInbox. */
22
+ contentLayout?: "document" | "workspace";
23
+ pageDescription?: ReactNode;
24
+ /** Optional object or person media beside the page title, with consumer-owned alt text. */
25
+ pageMedia?: ReactNode;
26
+ pageActions?: ReactNode;
27
+ utilityActions?: ReactNode;
28
+ children: ReactNode;
29
+ onNavigation?: MemberShellNavigationHandler;
30
+ className?: string;
31
+ }>;
32
+ /**
33
+ * Responsive member application frame. Products retain route transitions,
34
+ * route-heading focus and scroll restoration; this component owns landmarks,
35
+ * reading order and available-space layout only.
36
+ */
37
+ export declare function MemberShell({ brand, navigation, pageTitle, currentNavigationId, navigationPlacement, contentLayout, pageDescription, pageMedia, pageActions, utilityActions, children, onNavigation, className, }: MemberShellProps): import("react").JSX.Element;
@@ -0,0 +1,38 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export const MEMBER_SHELL_MAIN_ID = "superic-member-shell-main";
3
+ export const MEMBER_SHELL_HEADING_ID = "superic-member-shell-heading";
4
+ function assertMemberShellNavigation(navigation, currentNavigationId) {
5
+ if (navigation.length > 5) {
6
+ throw new Error("MemberShell invariant: navigation supports zero through five destinations.");
7
+ }
8
+ const ids = navigation.map((item) => item.id);
9
+ if (new Set(ids).size !== ids.length) {
10
+ throw new Error("MemberShell invariant: navigation destination ids must be unique.");
11
+ }
12
+ if (navigation.length === 0) {
13
+ if (currentNavigationId !== undefined && currentNavigationId !== null) {
14
+ throw new Error("MemberShell invariant: currentNavigationId must be omitted when navigation is empty.");
15
+ }
16
+ return;
17
+ }
18
+ if (currentNavigationId === null)
19
+ return;
20
+ if (!currentNavigationId || !ids.includes(currentNavigationId)) {
21
+ throw new Error("MemberShell invariant: navigation requires one matching currentNavigationId, or explicit null for a secondary route.");
22
+ }
23
+ }
24
+ function MemberNavigation({ compact, currentNavigationId, items, onNavigation, }) {
25
+ return (_jsx("nav", { "aria-label": compact ? "Primary navigation" : "Primary navigation", "data-slot": compact
26
+ ? "member-shell-compact-navigation"
27
+ : "member-shell-rail-navigation", children: _jsx("ul", { "data-slot": "member-shell-navigation-list", children: items.map((item) => (_jsx("li", { children: _jsxs("a", { "aria-current": item.id === currentNavigationId ? "page" : undefined, href: item.href, onClick: onNavigation ? (event) => onNavigation(event, item) : undefined, children: [item.icon ? (_jsx("span", { "aria-hidden": "true", "data-slot": "member-shell-navigation-icon", children: item.icon })) : null, _jsx("span", { children: item.label })] }) }, item.id))) }) }));
28
+ }
29
+ /**
30
+ * Responsive member application frame. Products retain route transitions,
31
+ * route-heading focus and scroll restoration; this component owns landmarks,
32
+ * reading order and available-space layout only.
33
+ */
34
+ export function MemberShell({ brand, navigation, pageTitle, currentNavigationId, navigationPlacement = "top", contentLayout = "document", pageDescription, pageMedia, pageActions, utilityActions, children, onNavigation, className, }) {
35
+ assertMemberShellNavigation(navigation, currentNavigationId);
36
+ const hasNavigation = navigation.length > 0;
37
+ return (_jsxs("div", { className: className, "data-has-navigation": hasNavigation ? "true" : "false", "data-navigation-placement": navigationPlacement, "data-content-layout": contentLayout, "data-slot": "member-shell", "data-superic-provider": "none", children: [_jsxs("header", { "data-slot": "member-shell-header", children: [_jsx("a", { "data-slot": "member-shell-skip-link", href: `#${MEMBER_SHELL_MAIN_ID}`, children: "Skip to main content" }), _jsxs("div", { "data-slot": "member-shell-header-inner", children: [_jsx("div", { "data-slot": "member-shell-brand", children: brand }), hasNavigation && navigationPlacement === "top" ? (_jsx("div", { "data-slot": "member-shell-top-navigation", children: _jsx(MemberNavigation, { compact: false, currentNavigationId: currentNavigationId ?? null, items: navigation, onNavigation: onNavigation }) })) : null, utilityActions ? (_jsx("div", { "data-slot": "member-shell-utility-actions", children: utilityActions })) : null] })] }), _jsxs("div", { "data-slot": "member-shell-body", children: [hasNavigation && navigationPlacement === "rail" ? (_jsx("div", { "data-slot": "member-shell-rail", children: _jsx(MemberNavigation, { compact: false, currentNavigationId: currentNavigationId ?? null, items: navigation, onNavigation: onNavigation }) })) : null, _jsx("main", { "aria-labelledby": MEMBER_SHELL_HEADING_ID, "data-slot": "member-shell-main", id: MEMBER_SHELL_MAIN_ID, tabIndex: -1, children: _jsxs("div", { "data-slot": "member-shell-content", children: [_jsxs("header", { "data-slot": "member-shell-page-header", children: [_jsxs("div", { "data-slot": "member-shell-page-identity", children: [pageMedia ? (_jsx("div", { "data-slot": "member-shell-page-media", children: pageMedia })) : null, _jsxs("div", { "data-slot": "member-shell-page-heading", children: [_jsx("h1", { id: MEMBER_SHELL_HEADING_ID, children: pageTitle }), pageDescription ? (_jsx("div", { "data-slot": "member-shell-page-description", children: pageDescription })) : null] })] }), pageActions ? (_jsx("div", { "data-slot": "member-shell-page-actions", children: pageActions })) : null] }), children] }) })] }), hasNavigation ? (_jsx("div", { "data-slot": "member-shell-compact-navigation-region", children: _jsx(MemberNavigation, { compact: true, currentNavigationId: currentNavigationId ?? null, items: navigation, onNavigation: onNavigation }) })) : null] }));
38
+ }
package/package.json CHANGED
@@ -1,19 +1,42 @@
1
1
  {
2
2
  "name": "@super-ic/product-shell",
3
- "version": "0.0.0-oidc-bootstrap.0",
4
- "description": "One-time OIDC name reservation only. No runtime or API. Do not use in production.",
3
+ "version": "0.1.1",
4
+ "description": "Provider-neutral React application shell primitives: app frame, rails, page and state regions, top bar, segmented search pill, and settings switch row.",
5
5
  "license": "UNLICENSED",
6
+ "type": "module",
7
+ "sideEffects": [
8
+ "./css/*.css"
9
+ ],
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
6
12
  "files": [
13
+ "dist",
14
+ "css",
7
15
  "README.md"
8
16
  ],
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "default": "./dist/index.js"
21
+ },
22
+ "./member-shell.css": "./css/member-shell.css"
23
+ },
24
+ "scripts": {
25
+ "build": "tsc -b tsconfig.json --force",
26
+ "prepack": "node ../../scripts/prepack-check.mjs"
27
+ },
28
+ "peerDependencies": {
29
+ "@super-ic/foundation": "0.2.4",
30
+ "react": ">=19.0.0",
31
+ "react-dom": ">=19.0.0"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public",
35
+ "registry": "https://registry.npmjs.org/"
36
+ },
9
37
  "repository": {
10
38
  "type": "git",
11
39
  "url": "git+https://github.com/superic-hq/superic-hq.git",
12
40
  "directory": "ds/packages/product-shell"
13
- },
14
- "publishConfig": {
15
- "access": "public",
16
- "registry": "https://registry.npmjs.org/",
17
- "tag": "oidc-bootstrap"
18
41
  }
19
42
  }