@tristan2828/ui-foundation 4.37.0 → 4.39.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.
@@ -25,15 +25,15 @@
25
25
  - [15. Yes/no you flip in the row](#15-yesno-you-flip-in-the-row) · lines 538–647
26
26
  - [A variant: a pressed icon inside another value's cell](#a-variant-a-pressed-icon-inside-another-values-cell) · lines 564–647
27
27
  - [16. Title linking to the record's view](#16-title-linking-to-the-records-view) · lines 649–684
28
- - [17. Quick action on the view](#17-quick-action-on-the-view) · lines 686–753
29
- - [18. Editable value](#18-editable-value) · lines 755–1170
30
- - [A list of sub-items](#a-list-of-sub-items) · lines 1014–1071
31
- - [In a table's rows](#in-a-tables-rows) · lines 1073–1158
32
- - [A table of a few short columns: `width="content"`](#a-table-of-a-few-short-columns-widthcontent) · lines 1160–1170
33
- - [19. Stage circle for an ordered status](#19-stage-circle-for-an-ordered-status) · lines 1172–1238
34
- - [20. Count linking to the related records](#20-count-linking-to-the-related-records) · lines 1240–1304
35
- - [21. Rows grouped under headings, and a pill filter](#21-rows-grouped-under-headings-and-a-pill-filter) · lines 1306–1371
36
- - [22. A highlighted row, and rows that are done](#22-a-highlighted-row-and-rows-that-are-done) · lines 1373–1401
28
+ - [17. Quick action on the view](#17-quick-action-on-the-view) · lines 686–759
29
+ - [18. Editable value](#18-editable-value) · lines 761–1176
30
+ - [A list of sub-items](#a-list-of-sub-items) · lines 1020–1077
31
+ - [In a table's rows](#in-a-tables-rows) · lines 1079–1164
32
+ - [A table of a few short columns: `width="content"`](#a-table-of-a-few-short-columns-widthcontent) · lines 1166–1176
33
+ - [19. Stage circle for an ordered status](#19-stage-circle-for-an-ordered-status) · lines 1178–1244
34
+ - [20. Count linking to the related records](#20-count-linking-to-the-related-records) · lines 1246–1310
35
+ - [21. Rows grouped under headings, and a pill filter](#21-rows-grouped-under-headings-and-a-pill-filter) · lines 1312–1377
36
+ - [22. A highlighted row, and rows that are done](#22-a-highlighted-row-and-rows-that-are-done) · lines 1379–1407
37
37
 
38
38
  <!-- /index -->
39
39
 
@@ -731,6 +731,12 @@ const save = useSaveWidgetField(widget.id) // one per control
731
731
  page. When the save answers, the view takes the record the server
732
732
  returned, so a server-side effect shows (a computed field, a flag the
733
733
  change cleared). Never write a quick action's mutation by hand.
734
+ - **A refresh the save always needs goes on the hook**: another query
735
+ the change affects (a summary, a related list) is invalidated in
736
+ `useRecordUpdate`'s own `onSuccess`, never `mutate(change, { onSuccess })`,
737
+ which TanStack skips once the control unmounts (a popover closed
738
+ mid-save). Per-call effects (navigate, a toast in the call's words)
739
+ stay on `mutate()`.
734
740
  - **A list is sent whole, built from the latest record**: pass a function
735
741
  of the record (`(current) => ({ checklist: ... })`), not a list read from
736
742
  props. Saves of one record run one at a time and each request is built
@@ -3,13 +3,14 @@
3
3
  <!-- index: written by `npm run docs:index` in the package from the headings below; never edit it by hand -->
4
4
  ## Index
5
5
 
6
- - [The view](#the-view) · lines 34–69
7
- - [The clock](#the-clock) · lines 71–101
8
- - [Sound](#sound) · lines 103–150
9
- - [Background tabs](#background-tabs) · lines 152–206
10
- - [Interruptions](#interruptions) · lines 186–206
11
- - [Motion](#motion) · lines 208–212
12
- - [Testing a timed screen in Playwright](#testing-a-timed-screen-in-playwright) · lines 214–368
6
+ - [The view](#the-view) · lines 36–109
7
+ - [The dark stage](#the-dark-stage) · lines 73–109
8
+ - [The clock](#the-clock) · lines 111–141
9
+ - [Sound](#sound) · lines 143–190
10
+ - [Background tabs](#background-tabs) · lines 192–246
11
+ - [Interruptions](#interruptions) · lines 226–246
12
+ - [Motion](#motion) · lines 248–252
13
+ - [Testing a timed screen in Playwright](#testing-a-timed-screen-in-playwright) · lines 254–408
13
14
 
14
15
  <!-- /index -->
15
16
 
@@ -19,7 +20,8 @@ package has the parts, so a screen like this is assembled, not rebuilt:
19
20
 
20
21
  | Part | What it does |
21
22
  |---|---|
22
- | `SessionView` | The whole viewport over the app shell: a modal named by its title, focus kept inside, Esc closes it. Optional real fullscreen and a held wake lock |
23
+ | `SessionView` | The whole viewport over the app shell: a modal named by its title, focus kept inside, Esc closes it. Optional real fullscreen, a held wake lock, and `dark` for the dark stage |
24
+ | `SessionStage`, `sessionStageLit`, `sessionStageMain`, `sessionStageQuiet` | The stage under the header: a tone, a glow of it behind the centre, the time lit in it, quiet outline controls |
23
25
  | `useSessionClock` | A clock that doesn't drift, with pause, resume and seek: `idle`, `running`, `done` |
24
26
  | `useWakeLock` | Keeps the screen on while a session runs (`SessionView`'s `keepAwake` uses it) |
25
27
  | `unlockAudio`, `chime`, `sequence`, `cancelCues`, `say`, `hush`, `vibrate` | Sound cues with no audio files, booked ahead where they must be on time |
@@ -29,7 +31,7 @@ package has the parts, so a screen like this is assembled, not rebuilt:
29
31
 
30
32
  All of them are imported from `@tristan2828/ui-foundation`. The package's
31
33
  Storybook has the whole thing working together: `app/SessionView`, the
32
- Timer story.
34
+ Timer story, and the same run on the dark stage, DarkStage.
33
35
 
34
36
  ## The view
35
37
 
@@ -68,6 +70,44 @@ const primary = useRef<HTMLButtonElement>(null)
68
70
  piece by piece. That fights the primitive's own classes on every
69
71
  upgrade, and gets the details above wrong.
70
72
 
73
+ ### The dark stage
74
+
75
+ For a screen followed in a dim room: always dark while it runs, whatever
76
+ the app's theme. A black stage, the time lit in the phase's colour with a
77
+ soft glow of it behind, everything else dimmed, the controls quiet
78
+ outlines.
79
+
80
+ ```tsx
81
+ <SessionView open onOpenChange={close} title="Timer" dark={darkStage}>
82
+ <SessionStage tone={darkStage ? phaseTone : 'neutral'} glow={darkStage}>
83
+ <div role="timer" className={cn('font-mono text-[17cqi]', darkStage && sessionStageLit)}>{time}</div>
84
+ <Button variant="outline" className={cn(darkStage && sessionStageQuiet)}>Skip</Button>
85
+ </SessionStage>
86
+ </SessionView>
87
+ ```
88
+
89
+ - **`dark` scopes the dark tokens to the view.** It puts theme.css's
90
+ `.dark` on the view itself, so every token and `dark:` variant inside
91
+ is the dark theme's and the page under it keeps its own. A popup opened
92
+ from the view (a menu, a select) portals to `<body>` and keeps the app's
93
+ theme.
94
+ - **`SessionStage`** is the box the run draws in, filling the view below
95
+ the header. `tone` is the phase's (`neutral`, `success`, `info`,
96
+ `warning`, `destructive`); `glow` washes it behind the centre, faintly
97
+ for `neutral`. It sets `--stage-tone` and `--stage-tone-text` for
98
+ what's inside.
99
+ - **`sessionStageLit`** on the big time: the tone's text shade (white
100
+ for `neutral`) with a glow around the figures. **`sessionStageQuiet`**
101
+ on an outline `Button`: no fill, a faint border, dimmed text that
102
+ brightens on hover. **`sessionStageMain`** on the main control (Start,
103
+ Done, `variant="outline"`): an outline too, in the foreground colour.
104
+ - **Contrast is checked in dark only**, since the stage is never light:
105
+ the package's axe run covers the DarkStage story in both app themes. An
106
+ app's own Playwright runs axe on its screen with the stage on, under a
107
+ light and a dark app theme.
108
+ - **Make it a setting**, remembered per device like Mute, when the app's
109
+ own look is the default: off, the view and stage are as before.
110
+
71
111
  ## The clock
72
112
 
73
113
  `useSessionClock({ running?, endsAt? })` returns
@@ -13,7 +13,23 @@ export type SessionViewProps = {
13
13
  fullscreenControl?: boolean;
14
14
  /** What takes focus on open (default: the first focusable control, which is the header's). */
15
15
  initialFocus?: DialogPrimitive.Popup.Props['initialFocus'];
16
+ /** Render the view with the dark theme's tokens, whatever the app's theme. */
17
+ dark?: boolean;
16
18
  /** On the area below the header, which holds `children`. */
17
19
  className?: string;
18
20
  };
19
- export declare function SessionView({ open, onOpenChange, title, children, keepAwake, fullscreenControl, initialFocus, className, }: SessionViewProps): React.JSX.Element;
21
+ export declare function SessionView({ open, onOpenChange, title, children, keepAwake, fullscreenControl, initialFocus, dark, className, }: SessionViewProps): React.JSX.Element;
22
+ export type SessionStageTone = 'neutral' | 'success' | 'info' | 'warning' | 'destructive';
23
+ export type SessionStageProps = React.ComponentProps<'div'> & {
24
+ /** The phase's tone: what the glow and `sessionStageLit` take. */
25
+ tone?: SessionStageTone;
26
+ /** A soft wash of the tone behind the centre of the stage. */
27
+ glow?: boolean;
28
+ };
29
+ export declare function SessionStage({ tone, glow, className, style, ...props }: SessionStageProps): React.JSX.Element;
30
+ /** The big time on a stage: the tone's text shade, with a glow of the tone around the figures. */
31
+ export declare const sessionStageLit = "text-[var(--stage-tone-text)] [text-shadow:0_0_0.3em_color-mix(in_oklab,var(--stage-tone)_55%,transparent)]";
32
+ /** A quiet outline control on a stage: no fill, a faint border, dimmed text that brightens on hover. */
33
+ export declare const sessionStageQuiet = "border-foreground/25 bg-transparent text-muted-foreground shadow-none hover:border-foreground hover:bg-transparent hover:text-foreground dark:border-foreground/25 dark:bg-transparent dark:hover:border-foreground dark:hover:bg-transparent";
34
+ /** The main control on a stage (Start, Done): an outline too, in the foreground colour. */
35
+ export declare const sessionStageMain = "border-foreground bg-transparent text-foreground shadow-none hover:bg-foreground/10 hover:text-foreground dark:border-foreground dark:bg-transparent dark:hover:bg-foreground/10";
@@ -8,6 +8,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
8
  // Optional: a "Full screen" control that also asks the browser for real
9
9
  // fullscreen and quietly isn't there where the browser can't (iOS Safari
10
10
  // on a phone), and keepAwake, which holds a screen wake lock while open.
11
+ // `dark` renders the view in the dark theme whatever the app's (the dark
12
+ // stage, #253): the dark tokens are scoped to the view, so the page under
13
+ // it keeps its own. SessionStage, below, is the stage under the header.
11
14
  // Recipes for its states in Playwright: docs/foundation/timed-screens.md.
12
15
  import * as React from 'react';
13
16
  import { Dialog as DialogPrimitive } from '@base-ui/react/dialog';
@@ -16,10 +19,12 @@ import { cn } from 'cn';
16
19
  import { Button } from '../ui/button.js';
17
20
  import { Dialog, DialogClose, DialogPortal, DialogTitle } from '../ui/dialog.js';
18
21
  import { useWakeLock } from '../../hooks/use-wake-lock.js';
19
- export function SessionView({ open, onOpenChange, title, children, keepAwake = false, fullscreenControl = false, initialFocus, className, }) {
22
+ export function SessionView({ open, onOpenChange, title, children, keepAwake = false, fullscreenControl = false, initialFocus, dark = false, className, }) {
20
23
  useWakeLock(open && keepAwake);
21
24
  const fullscreen = useDocumentFullscreen(open);
22
- return (_jsx(Dialog, { open: open, onOpenChange: (next) => onOpenChange(next), children: _jsx(DialogPortal, { children: _jsxs(DialogPrimitive.Popup, { "data-slot": "session-view", initialFocus: initialFocus, className: cn('fixed inset-0 z-50 flex flex-col bg-background text-foreground outline-none',
25
+ return (_jsx(Dialog, { open: open, onOpenChange: (next) => onOpenChange(next), children: _jsx(DialogPortal, { children: _jsxs(DialogPrimitive.Popup, { "data-slot": "session-view", "data-look": dark ? 'dark' : undefined, initialFocus: initialFocus, className: cn('fixed inset-0 z-50 flex flex-col bg-background text-foreground outline-none',
26
+ // theme.css's .dark block remaps the tokens for this subtree only.
27
+ dark && 'dark scheme-dark',
23
28
  // The insets are 0 unless the page's viewport meta has viewport-fit=cover.
24
29
  'pt-[env(safe-area-inset-top)] pr-[env(safe-area-inset-right)] pb-[env(safe-area-inset-bottom)] pl-[env(safe-area-inset-left)]', 'duration-150 data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0'), children: [_jsxs("header", { className: "flex items-center gap-1 px-4 py-2", children: [_jsx(DialogTitle, { className: "min-w-0 flex-1 truncate type-section-title", children: title }), fullscreenControl && fullscreen.supported && (_jsx(Button, { variant: "ghost", size: "icon", "aria-label": "Full screen", "aria-pressed": fullscreen.active, onClick: fullscreen.toggle, children: fullscreen.active ? _jsx(Minimize2Icon, {}) : _jsx(Maximize2Icon, {}) })), _jsx(DialogClose, { render: _jsx(Button, { variant: "ghost", size: "icon", "aria-label": "Close" }), children: _jsx(XIcon, {}) })] }), _jsx("div", { className: cn('flex min-h-0 flex-1 flex-col overflow-auto px-4 pb-4', className), children: children })] }) }) }));
25
30
  }
@@ -66,3 +71,29 @@ function useDocumentFullscreen(open) {
66
71
  };
67
72
  return { supported, active, toggle };
68
73
  }
74
+ // Each tone's solid fill (the glow) and its text shade (the lit time).
75
+ // Neutral glows with the foreground, faintly, and lights in it.
76
+ const STAGE_TONES = {
77
+ neutral: { solid: 'var(--foreground)', text: 'var(--foreground)', wash: '8%' },
78
+ success: { solid: 'var(--success)', text: 'var(--success-text)', wash: '24%' },
79
+ info: { solid: 'var(--info)', text: 'var(--info-text)', wash: '24%' },
80
+ warning: { solid: 'var(--warning)', text: 'var(--warning-text)', wash: '26%' },
81
+ destructive: { solid: 'var(--destructive)', text: 'var(--destructive-text)', wash: '24%' },
82
+ };
83
+ // The stage: the area a timed screen draws its run in, filling the view
84
+ // below the header. It sets --stage-tone (the tone's solid) and
85
+ // --stage-tone-text (its text shade) for what's inside, and with `glow` a
86
+ // radial wash of the tone on the page colour, fading out to the edges. Made
87
+ // for a dark SessionView, where the wash reads as light; on a light page it
88
+ // is a faint tint.
89
+ export function SessionStage({ tone = 'neutral', glow = false, className, style, ...props }) {
90
+ const t = STAGE_TONES[tone];
91
+ return (_jsx("div", { "data-slot": "session-stage", "data-tone": tone, "data-glow": glow || undefined, className: cn('relative flex min-h-0 flex-1 flex-col', glow &&
92
+ 'bg-[radial-gradient(ellipse_70%_60%_at_50%_52%,color-mix(in_oklab,var(--stage-tone)_var(--stage-wash),var(--background))_0,var(--background)_75%)]', className), style: { '--stage-tone': t.solid, '--stage-tone-text': t.text, '--stage-wash': t.wash, ...style }, ...props }));
93
+ }
94
+ /** The big time on a stage: the tone's text shade, with a glow of the tone around the figures. */
95
+ export const sessionStageLit = 'text-[var(--stage-tone-text)] [text-shadow:0_0_0.3em_color-mix(in_oklab,var(--stage-tone)_55%,transparent)]';
96
+ /** A quiet outline control on a stage: no fill, a faint border, dimmed text that brightens on hover. */
97
+ export const sessionStageQuiet = 'border-foreground/25 bg-transparent text-muted-foreground shadow-none hover:border-foreground hover:bg-transparent hover:text-foreground dark:border-foreground/25 dark:bg-transparent dark:hover:border-foreground dark:hover:bg-transparent';
98
+ /** The main control on a stage (Start, Done): an outline too, in the foreground colour. */
99
+ export const sessionStageMain = 'border-foreground bg-transparent text-foreground shadow-none hover:bg-foreground/10 hover:text-foreground dark:border-foreground dark:bg-transparent dark:hover:bg-foreground/10';
@@ -35,6 +35,17 @@ export type RecordUpdateOptions<TRecord extends {
35
35
  * `AppError`, and `refusalReason` words it.
36
36
  */
37
37
  toastOnError?: boolean;
38
+ /**
39
+ * After a save the server accepted, once the caches hold its record:
40
+ * a refresh the save always needs (another query it changes). Runs
41
+ * even when the calling component has unmounted, unlike a `mutate()`
42
+ * callback; keep per-call effects (navigate, a toast) on `mutate()`.
43
+ */
44
+ onSuccess?: (saved: TRecord, change: RecordChange<TRecord, TUpdate>) => unknown;
45
+ /** After a refused save, once it has rolled back (and toasted, unless `toastOnError: false`). */
46
+ onError?: (error: AppError, change: RecordChange<TRecord, TUpdate>) => unknown;
47
+ /** After every save, accepted or refused, following `onSuccess` or `onError`. */
48
+ onSettled?: (saved: TRecord | undefined, error: AppError | null, change: RecordChange<TRecord, TUpdate>) => unknown;
38
49
  };
39
50
  type Entry<TRecord, TUpdate> = {
40
51
  change: RecordChange<TRecord, TUpdate>;
@@ -52,7 +63,7 @@ export declare function refusalReason(error: AppError, sent?: object): string;
52
63
  */
53
64
  export declare function recordUpdateMutationOptions<TRecord extends {
54
65
  id: RecordId;
55
- }, TUpdate extends Partial<TRecord>>(queryClient: QueryClient, { id, detailKey, listsKey, update, name, optimistic, toastOnError }: RecordUpdateOptions<TRecord, TUpdate>): MutationOptions<TRecord, AppError, RecordChange<TRecord, TUpdate>, {
66
+ }, TUpdate extends Partial<TRecord>>(queryClient: QueryClient, { id, detailKey, listsKey, update, name, optimistic, toastOnError, onSuccess, onError, onSettled, }: RecordUpdateOptions<TRecord, TUpdate>): MutationOptions<TRecord, AppError, RecordChange<TRecord, TUpdate>, {
56
67
  entry: Entry<TRecord, TUpdate>;
57
68
  }>;
58
69
  /**
@@ -61,7 +72,8 @@ export declare function recordUpdateMutationOptions<TRecord extends {
61
72
  * with the entity's query keys and its gateway PATCH, then
62
73
  * `mutate({ field: value })` or `mutate((record) => ({ field: ... }))`.
63
74
  * `isPending` is that control's own save in flight. A refusal rolls back
64
- * and shows a toast by itself.
75
+ * and shows a toast by itself. A refresh every save needs goes in the
76
+ * options' `onSuccess`, which runs even if the control has unmounted.
65
77
  */
66
78
  export declare function useRecordUpdate<TRecord extends {
67
79
  id: RecordId;
@@ -30,6 +30,13 @@
30
30
  // `toastOnError: false`: nothing changes on screen until the server
31
31
  // agrees, and a refusal comes back to the field (`mutateAsync` rejects)
32
32
  // instead of a toast.
33
+ //
34
+ // A follow-up the save needs once the server answers (refetching a
35
+ // summary or a related list) goes in the hook's own `onSuccess`,
36
+ // `onError` or `onSettled`, not `mutate(change, { onSuccess })`: TanStack
37
+ // runs a `mutate()` callback only while the calling component is still
38
+ // mounted, so a popover closed mid-save would skip it. These run on the
39
+ // mutation, after the package's own cache work, either way (#250).
33
40
  import { hashKey, useMutation, useQueryClient, } from '@tanstack/react-query';
34
41
  import { toast } from 'sonner';
35
42
  const states = new WeakMap();
@@ -62,7 +69,7 @@ export function refusalReason(error, sent) {
62
69
  * The mutation options behind `useRecordUpdate`, for a given QueryClient.
63
70
  * Exported for tests and for code outside React; screens use the hook.
64
71
  */
65
- export function recordUpdateMutationOptions(queryClient, { id, detailKey, listsKey, update, name, optimistic = true, toastOnError = true }) {
72
+ export function recordUpdateMutationOptions(queryClient, { id, detailKey, listsKey, update, name, optimistic = true, toastOnError = true, onSuccess, onError, onSettled, }) {
66
73
  const key = hashKey(detailKey);
67
74
  const lists = { queryKey: listsKey };
68
75
  const map = recordStates(queryClient);
@@ -133,25 +140,27 @@ export function recordUpdateMutationOptions(queryClient, { id, detailKey, listsK
133
140
  render(state);
134
141
  return { entry };
135
142
  },
136
- onError: (error, _change, context) => {
143
+ onError: (error, change, context) => {
137
144
  const state = map.get(key);
138
- if (!state || !context)
139
- return;
140
- // What was sent: the change built on the record it was sent against.
141
- const record = state.confirmed;
142
- const { change } = context.entry;
143
- const sent = record ? resolve(change, record) : typeof change === 'function' ? undefined : change;
144
- settle(state, context.entry);
145
- if (toastOnError)
146
- toast.error(`Couldn't update ${record ? name(record) : 'this record'}: ${refusalReason(error, sent)}`);
145
+ if (state && context) {
146
+ // What was sent: the change built on the record it was sent against.
147
+ const record = state.confirmed;
148
+ const sent = record ? resolve(change, record) : typeof change === 'function' ? undefined : change;
149
+ settle(state, context.entry);
150
+ if (toastOnError)
151
+ toast.error(`Couldn't update ${record ? name(record) : 'this record'}: ${refusalReason(error, sent)}`);
152
+ }
153
+ return onError?.(error, change);
147
154
  },
148
- onSuccess: (saved, _change, context) => {
155
+ onSuccess: (saved, change, context) => {
149
156
  const state = map.get(key);
150
- if (!state || !context)
151
- return;
152
- state.confirmed = saved;
153
- settle(state, context.entry);
157
+ if (state && context) {
158
+ state.confirmed = saved;
159
+ settle(state, context.entry);
160
+ }
161
+ return onSuccess?.(saved, change);
154
162
  },
163
+ onSettled: (saved, error, change) => onSettled?.(saved, error, change),
155
164
  };
156
165
  }
157
166
  /**
@@ -160,7 +169,8 @@ export function recordUpdateMutationOptions(queryClient, { id, detailKey, listsK
160
169
  * with the entity's query keys and its gateway PATCH, then
161
170
  * `mutate({ field: value })` or `mutate((record) => ({ field: ... }))`.
162
171
  * `isPending` is that control's own save in flight. A refusal rolls back
163
- * and shows a toast by itself.
172
+ * and shows a toast by itself. A refresh every save needs goes in the
173
+ * options' `onSuccess`, which runs even if the control has unmounted.
164
174
  */
165
175
  export function useRecordUpdate(options) {
166
176
  const queryClient = useQueryClient();
package/dist/index.d.ts CHANGED
@@ -25,7 +25,7 @@ export { RichTextEditor, type RichTextEditorProps } from './components/app/rich-
25
25
  export { RouteErrorBoundary } from './components/app/route-error-boundary.js';
26
26
  export { PillFilter, type PillFilterOption, type PillFilterProps } from './components/app/pill-filter.js';
27
27
  export { SecondaryFilters, type ActiveFilter, type SecondaryFiltersProps } from './components/app/secondary-filters.js';
28
- export { SessionView, type SessionViewProps } from './components/app/session-view.js';
28
+ export { SessionStage, SessionView, sessionStageLit, sessionStageMain, sessionStageQuiet, type SessionStageProps, type SessionStageTone, type SessionViewProps, } from './components/app/session-view.js';
29
29
  export { AllowReminders, type AllowRemindersProps } from './components/app/allow-reminders.js';
30
30
  export { StageCircle, type StageCircleProps, type StageCircleStage, type StageCircleTone, } from './components/app/stage-circle.js';
31
31
  export { Notice, type NoticeProps, type NoticeTone } from './components/app/notice.js';
package/dist/index.js CHANGED
@@ -28,7 +28,7 @@ export { RichTextEditor } from './components/app/rich-text-editor.js';
28
28
  export { RouteErrorBoundary } from './components/app/route-error-boundary.js';
29
29
  export { PillFilter } from './components/app/pill-filter.js';
30
30
  export { SecondaryFilters } from './components/app/secondary-filters.js';
31
- export { SessionView } from './components/app/session-view.js';
31
+ export { SessionStage, SessionView, sessionStageLit, sessionStageMain, sessionStageQuiet, } from './components/app/session-view.js';
32
32
  export { AllowReminders } from './components/app/allow-reminders.js';
33
33
  export { StageCircle, } from './components/app/stage-circle.js';
34
34
  export { Notice } from './components/app/notice.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tristan2828/ui-foundation",
3
- "version": "4.37.0",
3
+ "version": "4.39.0",
4
4
  "description": "Shared UI foundation for contract-first, database-backed React apps: shadcn primitives, DataTable/EntityForm composites, auth, the gateway error seam, design tokens, lint rules and the AI playbook.",
5
5
  "license": "MIT",
6
6
  "repository": {