@tristan2828/ui-foundation 4.38.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.
|
@@ -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
|
|
7
|
-
- [The
|
|
8
|
-
- [
|
|
9
|
-
- [
|
|
10
|
-
|
|
11
|
-
- [
|
|
12
|
-
- [
|
|
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
|
|
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';
|
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.
|
|
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": {
|