@sesamehr/react-design-system 3.4.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Button/index.d.ts +1 -1
- package/dist/Display/AiTag/AiTag.d.ts +33 -0
- package/dist/Display/AiTag/index.d.ts +21 -0
- package/dist/Display/Avatar/index.d.ts +2 -2
- package/dist/Display/Badge/index.d.ts +1 -1
- package/dist/Display/BrandLogo/BrandLogo.d.ts +53 -0
- package/dist/Display/BrandLogo/brandLogoArt.d.ts +29 -0
- package/dist/Display/BrandLogo/index.d.ts +17 -0
- package/dist/Display/FileCard/FileCard.d.ts +102 -41
- package/dist/Display/FileCard/index.d.ts +31 -0
- package/dist/Display/ImageAttachment/ImageAttachment.d.ts +58 -0
- package/dist/Display/ImageAttachment/index.d.ts +2 -0
- package/dist/Display/LinkPreview/LinkPreview.d.ts +65 -0
- package/dist/Display/LinkPreview/index.d.ts +1 -0
- package/dist/Display/LocationAttachment/LocationAttachment.d.ts +71 -0
- package/dist/Display/LocationAttachment/index.d.ts +1 -0
- package/dist/Display/VideoAttachment/VideoAttachment.d.ts +78 -0
- package/dist/Display/VideoAttachment/index.d.ts +1 -0
- package/dist/Display/VoiceMessage/VoiceMessage.d.ts +103 -0
- package/dist/Display/VoiceMessage/index.d.ts +2 -0
- package/dist/Display/VoiceMessage/toWaveformPeaks.d.ts +58 -0
- package/dist/Display/VoiceMessage/useVoiceWaveform.d.ts +90 -0
- package/dist/Feedback/Dialog/Dialog.d.ts +13 -1
- package/dist/Feedback/Dialog/DialogOverlay.d.ts +12 -1
- package/dist/Feedback/Drawer/Drawer.d.ts +68 -0
- package/dist/Feedback/Drawer/DrawerBody.d.ts +18 -0
- package/dist/Feedback/Drawer/DrawerContent.d.ts +36 -0
- package/dist/Feedback/Drawer/DrawerDescription.d.ts +10 -0
- package/dist/Feedback/Drawer/DrawerExpand.d.ts +23 -0
- package/dist/Feedback/Drawer/DrawerTitle.d.ts +13 -0
- package/dist/Feedback/Drawer/index.d.ts +7 -0
- package/dist/Feedback/Drawer/useDrawer.d.ts +30 -0
- package/dist/Feedback/Progress/index.d.ts +1 -1
- package/dist/Forms/Inputs/AiSearch/AiSearch.d.ts +67 -0
- package/dist/Forms/Inputs/AiSearch/AiSearchAsk.d.ts +34 -0
- package/dist/Forms/Inputs/AiSearch/AiSearchPanel.d.ts +50 -0
- package/dist/Forms/Inputs/AiSearch/AiSearchSend.d.ts +30 -0
- package/dist/Forms/Inputs/AiSearch/AiSearchShortcut.d.ts +23 -0
- package/dist/Forms/Inputs/AiSearch/AiSearchSuggestion.d.ts +27 -0
- package/dist/Forms/Inputs/AiSearch/index.d.ts +6 -0
- package/dist/Forms/Inputs/AiSearch/useAiSearch.d.ts +37 -0
- package/dist/Forms/Inputs/TimePicker/TimePicker.d.ts +64 -0
- package/dist/Forms/Inputs/TimePicker/TimePickerField.d.ts +35 -0
- package/dist/Forms/Inputs/TimePicker/TimePickerList.d.ts +23 -0
- package/dist/Forms/Inputs/TimePicker/TimePickerWheel.d.ts +26 -0
- package/dist/Forms/Inputs/TimePicker/TimePickerWheelColumn.d.ts +23 -0
- package/dist/Forms/Inputs/TimePicker/index.d.ts +6 -0
- package/dist/Forms/Inputs/TimePicker/time.d.ts +41 -0
- package/dist/Forms/Inputs/TimePicker/useTimeField.d.ts +62 -0
- package/dist/Forms/Inputs/TimePicker/useTimeList.d.ts +53 -0
- package/dist/Forms/Inputs/TimePicker/useTimePicker.d.ts +35 -0
- package/dist/Forms/Inputs/TimePicker/useWheelColumn.d.ts +71 -0
- package/dist/Forms/Inputs/index.d.ts +1 -0
- package/dist/Navigation/Tabs/Tabs.d.ts +18 -0
- package/dist/Navigation/Tabs/TabsContent.d.ts +7 -0
- package/dist/Navigation/Tabs/TabsList.d.ts +10 -0
- package/dist/Navigation/Tabs/TabsTrigger.d.ts +20 -0
- package/dist/Navigation/Tabs/index.d.ts +4 -0
- package/dist/internal/AttachmentMedia/AttachmentMedia.d.ts +38 -0
- package/dist/internal/AttachmentMedia/index.d.ts +36 -0
- package/dist/internal/index.d.ts +1 -0
- package/dist/main.d.ts +12 -2
- package/dist/react-design-system.css +1 -1
- package/dist/react-design-system.js +12581 -10218
- package/dist/react-design-system.umd.cjs +108 -74
- package/package.json +3 -2
- package/src/assets/styles/theme-v3.css +99 -2
- package/src/assets/styles/theme-v3.test.ts +5 -5
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { AttachmentImage, AttachmentSurface } from '../../internal/AttachmentMedia/index.ts';
|
|
3
|
+
/**
|
|
4
|
+
* A video attached to a chat message: its poster frame, a play affordance and
|
|
5
|
+
* however long it runs.
|
|
6
|
+
* Mirrors `@sesame/orxata-core` VideoAttachment.
|
|
7
|
+
*
|
|
8
|
+
* No primitive underneath. shadcn has no video card and Radix has no primitive
|
|
9
|
+
* in this shape — there is no open state, no roving focus and no value to
|
|
10
|
+
* coordinate, only a single activation. What is left is one `<button>`, which
|
|
11
|
+
* is what Radix's `Primitive` would have rendered anyway, and
|
|
12
|
+
* `OxImageAttachment` reached the same place one cell at a time.
|
|
13
|
+
*
|
|
14
|
+
* The picture, its three states and the `Surface` tint are `AttachmentMedia`'s,
|
|
15
|
+
* shared with the other four attachments. This file adds the two things that
|
|
16
|
+
* are only a video's: the circle and the pill over it.
|
|
17
|
+
*
|
|
18
|
+
* ### The whole thumbnail is the button
|
|
19
|
+
*
|
|
20
|
+
* The behaviour board puts the full-screen player behind a click on the
|
|
21
|
+
* thumbnail, not on the circle, and nothing inside is interactive, so there is
|
|
22
|
+
* no control to nest — the opposite of `OxFileCard`, whose root refuses to be
|
|
23
|
+
* one for exactly that reason. The circle is therefore decoration and
|
|
24
|
+
* `aria-hidden`; the button is named by the poster's `alt` and the duration,
|
|
25
|
+
* both of which sit inside it. That is the accessibility board's "the duration
|
|
26
|
+
* is announced beside the button, not as loose text".
|
|
27
|
+
*
|
|
28
|
+
* The board also asks the button to be labelled "Play video, duration m:ss".
|
|
29
|
+
* This package ships no strings and has no catalogue to add one to, so the
|
|
30
|
+
* wording is the application's; `alt` plus the duration is the honest name a
|
|
31
|
+
* design system can assemble.
|
|
32
|
+
*
|
|
33
|
+
* ### `onPlay`, and why it displaces the DOM's own
|
|
34
|
+
*
|
|
35
|
+
* The Vue original emits `play`, so the port names the handler `onPlay` —
|
|
36
|
+
* which on a `<button>` collides with React's media event of the same name.
|
|
37
|
+
* The media one is omitted rather than renamed: a `<button>` never fires it, so
|
|
38
|
+
* nothing real is lost, and keeping the Vue name is what lets a consumer move
|
|
39
|
+
* between the two packages without relearning the API. A caller's own
|
|
40
|
+
* `onClick` still arrives and runs first.
|
|
41
|
+
*
|
|
42
|
+
* ### Why the duration is a node and not a string
|
|
43
|
+
*
|
|
44
|
+
* `OxLinkPreview` and `OxLocationAttachment` carry a title and a description,
|
|
45
|
+
* names the composition rule does not allow as props at all, so every sibling
|
|
46
|
+
* takes its text as a node and none of them takes it a second way. The pill's
|
|
47
|
+
* shape, position and colour stay the component's; only the string is the
|
|
48
|
+
* application's, and it has no opinion on whether that reads `0:42`,
|
|
49
|
+
* `1:03:20` or `12 min`.
|
|
50
|
+
*
|
|
51
|
+
* ### It has no size
|
|
52
|
+
*
|
|
53
|
+
* The width is the bubble's and the poster keeps its 3:2 shape at whatever
|
|
54
|
+
* width that turns out to be, which is `AttachmentMedia`'s aspect ratio rather
|
|
55
|
+
* than the 240×160 the board prints.
|
|
56
|
+
*/
|
|
57
|
+
export interface VideoAttachmentProps extends Omit<React.ComponentPropsWithoutRef<'button'>, 'onPlay'> {
|
|
58
|
+
/**
|
|
59
|
+
* The poster frame, if there is one. Without it the box keeps its tint and
|
|
60
|
+
* shows a glyph, which is also what a poster that fails to load falls back
|
|
61
|
+
* to.
|
|
62
|
+
*/
|
|
63
|
+
image?: AttachmentImage;
|
|
64
|
+
/**
|
|
65
|
+
* Which side of the bubble this sits in: `brand` inside the reader's own
|
|
66
|
+
* message, `default` inside anyone else's.
|
|
67
|
+
*/
|
|
68
|
+
surface?: AttachmentSurface;
|
|
69
|
+
/**
|
|
70
|
+
* How long the video runs, drawn as the pill over the poster. Ports the Vue
|
|
71
|
+
* `#duration` slot; omitted, there is no pill at all.
|
|
72
|
+
*/
|
|
73
|
+
duration?: ReactNode;
|
|
74
|
+
/** The thumbnail was activated. Open the player; nothing plays in place. */
|
|
75
|
+
onPlay?: () => void;
|
|
76
|
+
dataTestid: string;
|
|
77
|
+
}
|
|
78
|
+
export declare const VideoAttachment: import('react').ForwardRefExoticComponent<VideoAttachmentProps & import('react').RefAttributes<HTMLButtonElement>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { VideoAttachment, type VideoAttachmentProps, } from './VideoAttachment.tsx';
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { AttachmentSurface } from '../../internal/AttachmentMedia/index.ts';
|
|
3
|
+
/**
|
|
4
|
+
* A voice note in a chat message: a play button, the recording's waveform and
|
|
5
|
+
* how long it runs.
|
|
6
|
+
* Mirrors `@sesame/orxata-core` VoiceMessage.
|
|
7
|
+
*
|
|
8
|
+
* No primitive underneath, and this one is a decision rather than an absence.
|
|
9
|
+
* shadcn has no audio player; Radix has `Slider`, which is the right behaviour
|
|
10
|
+
* but the wrong DOM — Radix puts `role="slider"` on a *thumb*, and the board
|
|
11
|
+
* draws no thumb at all. Introducing one would invent a control the design does
|
|
12
|
+
* not have and a focus state nobody drew, so the gesture and the ARIA are
|
|
13
|
+
* implemented here against Radix's own conventions: one `role="slider"`
|
|
14
|
+
* element, `aria-valuemin`/`max`/`now`, arrows and Home/End, pointer capture on
|
|
15
|
+
* drag. The bars inside it are presentational children of a slider, which is
|
|
16
|
+
* what keeps a 32-bar waveform from announcing itself 32 times.
|
|
17
|
+
*
|
|
18
|
+
* ### It does not decode audio, and it does not own an `<audio>` element
|
|
19
|
+
*
|
|
20
|
+
* `peaks` arrives as data. Real peaks are computed server-side at upload, which
|
|
21
|
+
* is what WhatsApp, Telegram and Slack do and why their waveforms are visible
|
|
22
|
+
* before playback starts — decoding in the browser means downloading and
|
|
23
|
+
* decoding the whole file before anything can be drawn. Whether the numbers are
|
|
24
|
+
* real or decorative is the application's business, exactly as `OxFileCard`
|
|
25
|
+
* does not upload files.
|
|
26
|
+
*
|
|
27
|
+
* `toWaveformPeaks` converts the shapes a backend is likely to emit into the
|
|
28
|
+
* contract. It is a separate exported function the application calls at its own
|
|
29
|
+
* boundary rather than a guess made inside this component: sniffing the shape
|
|
30
|
+
* here would type `peaks` as `unknown` and make every new upstream format a
|
|
31
|
+
* change to a component.
|
|
32
|
+
*
|
|
33
|
+
* ### Controlled, with no fallback
|
|
34
|
+
*
|
|
35
|
+
* `playing` and `progress` are plain props with plain handlers, the same shape
|
|
36
|
+
* as `OxSwitch` and for the same reason: there is one player per conversation
|
|
37
|
+
* and one `<audio>` element behind it, both of which live in the application.
|
|
38
|
+
* An uncontrolled path here would let the component seed itself from the first
|
|
39
|
+
* value and carry on playing a position the real audio had left behind — so
|
|
40
|
+
* there deliberately is not one, and the Vue original has no `defineModel`
|
|
41
|
+
* either.
|
|
42
|
+
*
|
|
43
|
+
* So nothing moves unless the parent moves it. A `useState` in the caller,
|
|
44
|
+
* wired to `onPlayingChange` and `onProgressChange`, is the ordinary way.
|
|
45
|
+
*
|
|
46
|
+
* ### How many bars, and why the component decides
|
|
47
|
+
*
|
|
48
|
+
* The length of `peaks` is unconstrained because the number of bars is a visual
|
|
49
|
+
* decision and a backend cannot make it. `audiowaveform` emits whatever its
|
|
50
|
+
* samples-per-pixel gives it; how many 3px bars fit is a function of the
|
|
51
|
+
* bubble's width. The track is measured and the peaks are bucketed down to fit
|
|
52
|
+
* — see `useVoiceWaveform` for the maximum-per-bucket argument. Fewer peaks
|
|
53
|
+
* than bars draws short and left-aligned, which is what the board shows.
|
|
54
|
+
*
|
|
55
|
+
* ### Two nodes, because the package ships no strings
|
|
56
|
+
*
|
|
57
|
+
* `label` is the accessible name of the pair of controls — visually hidden, and
|
|
58
|
+
* the one thing a design system cannot supply, since it has no catalogue to add
|
|
59
|
+
* "Voice message" to. The button then carries state through `aria-pressed`
|
|
60
|
+
* rather than a name that changes, which is the only version of play/pause that
|
|
61
|
+
* needs no second string.
|
|
62
|
+
*
|
|
63
|
+
* `duration` is the visible one, and a node for the same reason
|
|
64
|
+
* `OxVideoAttachment`'s is: the pill's shape is the component's and the string
|
|
65
|
+
* is the application's, which has no opinion on `0:18` against `18 s`. With no
|
|
66
|
+
* `label` the duration names the controls instead, so the component is never
|
|
67
|
+
* nameless.
|
|
68
|
+
*/
|
|
69
|
+
export interface VoiceMessageProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
70
|
+
/**
|
|
71
|
+
* The waveform, as magnitudes normalised to 0–1 — one per sample, any
|
|
72
|
+
* length. Run a backend's own shape through `toWaveformPeaks` first.
|
|
73
|
+
*/
|
|
74
|
+
peaks: number[];
|
|
75
|
+
/**
|
|
76
|
+
* Whether the note is playing. Controlled: the glyph follows this and
|
|
77
|
+
* nothing else.
|
|
78
|
+
*/
|
|
79
|
+
playing?: boolean;
|
|
80
|
+
/** How far through the note the playhead is, 0–1. Controlled. */
|
|
81
|
+
progress?: number;
|
|
82
|
+
/**
|
|
83
|
+
* Which side of the bubble this sits in: `brand` inside the reader's own
|
|
84
|
+
* message, `default` inside anyone else's.
|
|
85
|
+
*/
|
|
86
|
+
surface?: AttachmentSurface;
|
|
87
|
+
/**
|
|
88
|
+
* The accessible name of both controls, read and not seen. Ports the Vue
|
|
89
|
+
* `#label` slot; without it the duration names them instead.
|
|
90
|
+
*/
|
|
91
|
+
label?: ReactNode;
|
|
92
|
+
/** How long the note runs, drawn beside the waveform. Ports `#duration`. */
|
|
93
|
+
duration?: ReactNode;
|
|
94
|
+
/** The button was pressed. Start or stop the audio. */
|
|
95
|
+
onPlayingChange?: (playing: boolean) => void;
|
|
96
|
+
/**
|
|
97
|
+
* The listener scrubbed the waveform. Move the playhead to this 0–1
|
|
98
|
+
* position.
|
|
99
|
+
*/
|
|
100
|
+
onProgressChange?: (progress: number) => void;
|
|
101
|
+
dataTestid: string;
|
|
102
|
+
}
|
|
103
|
+
export declare const VoiceMessage: import('react').ForwardRefExoticComponent<VoiceMessageProps & import('react').RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BBC `audiowaveform`'s JSON, as `audiowaveform -i in.mp3 -o out.json` writes
|
|
3
|
+
* it.
|
|
4
|
+
*
|
|
5
|
+
* Field names are the tool's own and are therefore `snake_case`, which is why
|
|
6
|
+
* this interface is the one place in the package that spells props that way:
|
|
7
|
+
* renaming them would mean the application had to translate before it could
|
|
8
|
+
* call the translator.
|
|
9
|
+
*
|
|
10
|
+
* `version` is 1 or 2 and `channels` exists only in 2. Everything else is
|
|
11
|
+
* present in both.
|
|
12
|
+
*
|
|
13
|
+
* @see https://github.com/bbc/audiowaveform/blob/master/doc/DataFormat.md
|
|
14
|
+
*/
|
|
15
|
+
export interface AudiowaveformJson {
|
|
16
|
+
version?: number;
|
|
17
|
+
/** Version 2 only. Absent means one channel. */
|
|
18
|
+
channels?: number;
|
|
19
|
+
sample_rate?: number;
|
|
20
|
+
samples_per_pixel?: number;
|
|
21
|
+
/** 8 or 16. Decides the scale `data` is written at. */
|
|
22
|
+
bits: number;
|
|
23
|
+
/** Minimum/maximum pairs per channel, which is `data.length / (2 * channels)`. */
|
|
24
|
+
length?: number;
|
|
25
|
+
/** Minimum and maximum pairs, interleaved, and across channels too. */
|
|
26
|
+
data: readonly number[];
|
|
27
|
+
}
|
|
28
|
+
/** Anything `toWaveformPeaks` knows how to read. */
|
|
29
|
+
export type WaveformSource = AudiowaveformJson | readonly number[];
|
|
30
|
+
/**
|
|
31
|
+
* Turns what a backend has into what `OxVoiceMessage` draws: magnitudes
|
|
32
|
+
* normalised to 0…1, one per sample.
|
|
33
|
+
*
|
|
34
|
+
* **A function the application calls at its own boundary, not something the
|
|
35
|
+
* component does.** Sniffing the shape inside the component would mean typing
|
|
36
|
+
* its `peaks` prop as `unknown` and losing the only check there is that the
|
|
37
|
+
* numbers are numbers; it would also mean every new upstream format was a
|
|
38
|
+
* change to a component rather than a case in a function. Here, a new format is
|
|
39
|
+
* one more branch and `OxVoiceMessage` never moves.
|
|
40
|
+
*
|
|
41
|
+
* It reads three things:
|
|
42
|
+
*
|
|
43
|
+
* - **BBC `audiowaveform` JSON**, the tool that precomputes this server-side.
|
|
44
|
+
* Minimum/maximum pairs, interleaved across channels, at 8 or 16 bits.
|
|
45
|
+
* - **A flat array in an arbitrary range** — `−1…1`, `0…255`, or anything
|
|
46
|
+
* else: it is normalised by its own loudest value.
|
|
47
|
+
* - **A flat array already inside 0…1**, which comes back unchanged.
|
|
48
|
+
*
|
|
49
|
+
* Anything else **throws**, naming what arrived. A waveform is decoration that
|
|
50
|
+
* nobody checks against the audio, so a silently wrong one would survive every
|
|
51
|
+
* review; an exception at the boundary is the only place the mistake is still
|
|
52
|
+
* cheap.
|
|
53
|
+
*
|
|
54
|
+
* ```ts
|
|
55
|
+
* const peaks = toWaveformPeaks(await (await fetch(waveformUrl)).json());
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export declare const toWaveformPeaks: (source: WaveformSource) => number[];
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bar and the gap the board draws, in pixels, and the only numbers in this
|
|
3
|
+
* file that also live in the stylesheet.
|
|
4
|
+
*
|
|
5
|
+
* They have to: how many bars fit is arithmetic, and CSS cannot bucket an
|
|
6
|
+
* array. `VoiceMessage.test.tsx` asserts the stylesheet still says `w-0.75` and
|
|
7
|
+
* `gap-0.5`, so changing one half fails there rather than quietly drawing a
|
|
8
|
+
* waveform at the wrong pitch.
|
|
9
|
+
*/
|
|
10
|
+
export declare const VOICE_WAVEFORM_BAR_PX = 3;
|
|
11
|
+
export declare const VOICE_WAVEFORM_GAP_PX = 2;
|
|
12
|
+
/**
|
|
13
|
+
* How many bars to draw before the track has ever been measured — on the
|
|
14
|
+
* server, and on the first frame in a browser.
|
|
15
|
+
*
|
|
16
|
+
* It is the count that fits the board's own track: 240px wide, less the 36px
|
|
17
|
+
* button, the two 10px gaps and the duration, leaves 162px, and 162px holds 32
|
|
18
|
+
* bars at a 5px pitch. So an unmeasured waveform is drawn at the width the
|
|
19
|
+
* design was drawn at, which is the closest thing to a right answer available
|
|
20
|
+
* before layout.
|
|
21
|
+
*/
|
|
22
|
+
export declare const VOICE_WAVEFORM_FALLBACK_BARS = 32;
|
|
23
|
+
/** One bar: how tall it is, and whether the playhead has passed it. */
|
|
24
|
+
export interface VoiceWaveformBar {
|
|
25
|
+
key: number;
|
|
26
|
+
peak: number;
|
|
27
|
+
played: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* `peaks` reduced to exactly `count` values, or left as it is when there are
|
|
31
|
+
* fewer peaks than bars.
|
|
32
|
+
*
|
|
33
|
+
* **Maximum per bucket, not RMS.** Two reasons, and the first is the contract:
|
|
34
|
+
* what arrives is already magnitudes, so the sign is gone and a root mean
|
|
35
|
+
* square over them is no longer the signal's energy — it is just a smoother
|
|
36
|
+
* average wearing the name of a measurement. The second is what a waveform is
|
|
37
|
+
* for: it is read to find the loud bit, and a maximum is the only reduction
|
|
38
|
+
* that keeps a transient visible once 400 samples have to fit in 32 bars. RMS
|
|
39
|
+
* would flatten a clap between two silences into an unremarkable bar.
|
|
40
|
+
*
|
|
41
|
+
* Buckets are cut by proportion rather than by a fixed size, so a length that
|
|
42
|
+
* does not divide evenly spreads the remainder across the track instead of
|
|
43
|
+
* dropping the tail.
|
|
44
|
+
*/
|
|
45
|
+
export declare const bucketPeaks: (peaks: readonly number[], count: number) => number[];
|
|
46
|
+
export interface UseVoiceWaveformOptions {
|
|
47
|
+
/**
|
|
48
|
+
* The waveform. A plain value where the Vue original takes a getter: a
|
|
49
|
+
* composable is set up once and has to defer reading its props, while a hook
|
|
50
|
+
* re-runs on every render and so is handed the current value directly.
|
|
51
|
+
*/
|
|
52
|
+
peaks: readonly number[];
|
|
53
|
+
progress: number;
|
|
54
|
+
/** Called with a 0…1 position whenever the listener scrubs. */
|
|
55
|
+
seek: (fraction: number) => void;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The waveform's two jobs, which share one element and so share one hook:
|
|
59
|
+
* deciding how many bars to draw, and turning a gesture on them into a
|
|
60
|
+
* position. Port of `core/src/Display/VoiceMessage/useVoiceWaveform.ts`.
|
|
61
|
+
*
|
|
62
|
+
* **How many bars is a question only the element can answer.** The backend
|
|
63
|
+
* sends however many peaks it computed — `audiowaveform` at a given
|
|
64
|
+
* samples-per-pixel will happily produce hundreds — and how many bars fit is a
|
|
65
|
+
* function of the bubble's width, which nothing knows until layout has run. So
|
|
66
|
+
* the track is measured and the peaks are bucketed down to what fits, at the
|
|
67
|
+
* board's fixed 3px/2px pitch; drawing all of them instead would either overflow
|
|
68
|
+
* or squeeze the bars to hairlines, and clipping them would hide the end of the
|
|
69
|
+
* recording.
|
|
70
|
+
*
|
|
71
|
+
* Fewer peaks than bars is left alone rather than stretched: the board's own
|
|
72
|
+
* waveform is left-aligned in a track wider than its bars, so a short recording
|
|
73
|
+
* drawing short is what the design already shows.
|
|
74
|
+
*
|
|
75
|
+
* The handlers are rebuilt on every render rather than memoised. They close
|
|
76
|
+
* over `progress` and `seek`, both of which change as the note plays, so a
|
|
77
|
+
* `useCallback` keyed on them would rebuild anyway — and one keyed on less than
|
|
78
|
+
* them would scrub against a position the audio had already left behind.
|
|
79
|
+
* `dragging` is the one thing that must survive a render, which is why it is a
|
|
80
|
+
* ref: holding it in state would restart the gesture mid-drag.
|
|
81
|
+
*/
|
|
82
|
+
export declare const useVoiceWaveform: (options: UseVoiceWaveformOptions) => {
|
|
83
|
+
track: import('react').RefObject<HTMLDivElement | null>;
|
|
84
|
+
bars: VoiceWaveformBar[];
|
|
85
|
+
percent: number;
|
|
86
|
+
onPointerDown: (event: React.PointerEvent<HTMLDivElement>) => void;
|
|
87
|
+
onPointerMove: (event: React.PointerEvent<HTMLDivElement>) => void;
|
|
88
|
+
onPointerUp: (event: React.PointerEvent<HTMLDivElement>) => void;
|
|
89
|
+
onKeyDown: (event: React.KeyboardEvent<HTMLDivElement>) => void;
|
|
90
|
+
};
|
|
@@ -5,7 +5,19 @@ export interface DialogProps {
|
|
|
5
5
|
onOpenChange?: (open: boolean) => void;
|
|
6
6
|
modal?: boolean;
|
|
7
7
|
}
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Dialog root — built on `@radix-ui/react-dialog`.
|
|
10
|
+
*
|
|
11
|
+
* `open` is destructured with **no default**, and that is deliberate. Radix
|
|
12
|
+
* decides whether the caller owns the state by testing `open === undefined`,
|
|
13
|
+
* so a default of `false` here would read as "the caller controls this and says
|
|
14
|
+
* closed" and `defaultOpen` would be ignored — the panel would render nothing
|
|
15
|
+
* on mount while still opening from its trigger. `OxDialog` on the Vue side has
|
|
16
|
+
* the same hazard through a different door, since Vue casts an absent `Boolean`
|
|
17
|
+
* prop to `false` unless the prop declares a default; `Dialog.vue` carries the
|
|
18
|
+
* matching note. Nothing casts anything here, so leaving `open` alone is the
|
|
19
|
+
* whole fix.
|
|
20
|
+
*/
|
|
9
21
|
export declare const Dialog: {
|
|
10
22
|
({ children, defaultOpen, open, onOpenChange, modal, }: DialogProps): import("react/jsx-runtime").JSX.Element;
|
|
11
23
|
displayName: string;
|
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
|
2
2
|
export type DialogOverlayProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>;
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* DialogOverlay — the glass behind dialog content.
|
|
5
|
+
*
|
|
6
|
+
* Radix's primitive owns the portal, the presence and the pointer handling; the
|
|
7
|
+
* look comes from `ox-overlay`, shared with whatever surface lands next — see
|
|
8
|
+
* `../overlay.css`.
|
|
9
|
+
*
|
|
10
|
+
* `data-slot="dialog-overlay"` is unchanged and remains the hook to target. The
|
|
11
|
+
* class was `styles.oxDialogOverlay` while the declarations lived in this
|
|
12
|
+
* component's module, which means it only ever reached the DOM hashed; the
|
|
13
|
+
* shared name is the first stable one, and it is the same string `core` applies.
|
|
14
|
+
*/
|
|
4
15
|
export declare const DialogOverlay: import('react').ForwardRefExoticComponent<Omit<DialogPrimitive.DialogOverlayProps & import('react').RefAttributes<HTMLDivElement>, "ref"> & import('react').RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export interface DrawerProps {
|
|
2
|
+
children?: React.ReactNode;
|
|
3
|
+
/** The controlled open state. Pair it with `onOpenChange`. */
|
|
4
|
+
open?: boolean;
|
|
5
|
+
defaultOpen?: boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Whether the drawer blocks the page behind it. `true` draws the glass and
|
|
8
|
+
* locks body scroll; `false` leaves the list operable while the panel is open,
|
|
9
|
+
* which the board draws as `Show overlay: No`.
|
|
10
|
+
*/
|
|
11
|
+
modal?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Which edge the panel is anchored to.
|
|
14
|
+
*
|
|
15
|
+
* Two values, not four. The board draws the right edge only — the component
|
|
16
|
+
* set has a single `Size` axis and its description says "panel lateral
|
|
17
|
+
* derecho" — and a top or bottom panel is a Sheet, which is mobile and is not
|
|
18
|
+
* this component.
|
|
19
|
+
*/
|
|
20
|
+
side?: 'left' | 'right';
|
|
21
|
+
onOpenChange?: (open: boolean) => void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Drawer root — mirrors `@sesame/orxata-core` Drawer, built on
|
|
25
|
+
* `@radix-ui/react-dialog`.
|
|
26
|
+
*
|
|
27
|
+
* **A sibling of `OxDialog`, not a shape of it.** Both stand on the dialog
|
|
28
|
+
* primitive, which is where the focus trap, the body scroll lock, the Escape
|
|
29
|
+
* handling, the portal, `role="dialog"` and the `aria-labelledby` /
|
|
30
|
+
* `aria-describedby` wiring come from — hand-rolling any of that is how a
|
|
31
|
+
* surface ships unusable by keyboard. What makes a drawer its own component is
|
|
32
|
+
* everything above the primitive: it is anchored to an edge rather than centred,
|
|
33
|
+
* it is full height, it slides instead of scaling, it can be non-modal so the
|
|
34
|
+
* list behind stays operable, and it grows. None of that is a content shape.
|
|
35
|
+
*
|
|
36
|
+
* Note that modality arrives as `aria-hidden` on everything else rather than as
|
|
37
|
+
* `aria-modal` on the panel — Radix's choice, reka's too, and the current APG
|
|
38
|
+
* advice, since `aria-modal` leaves the rest of the page reachable by a screen
|
|
39
|
+
* reader's own navigation in several implementations. `Drawer.test.tsx` asserts
|
|
40
|
+
* the former, so nobody goes looking for the latter and concludes it is missing.
|
|
41
|
+
*
|
|
42
|
+
* **Why this root holds the open state rather than leaving it to the
|
|
43
|
+
* primitive.** `DrawerContent` has to know whether the panel is open: it drives
|
|
44
|
+
* the slide from `OxTransitionSlideX` rather than from the primitive's own
|
|
45
|
+
* presence, so it is what decides when the panel mounts. On the Vue side it asks
|
|
46
|
+
* reka, through `injectDialogRootContext()`. Radix exports no such hook, so the
|
|
47
|
+
* state lives here and `Dialog.Root` is always controlled from it. Everything
|
|
48
|
+
* else about the primitive is untouched.
|
|
49
|
+
*
|
|
50
|
+
* That is also why `defaultOpen` is not forwarded. Radix decides who owns the
|
|
51
|
+
* state by testing `open === undefined`, and this always passes it a boolean —
|
|
52
|
+
* so handing it `defaultOpen` as well would be giving it an initial value it is
|
|
53
|
+
* no longer in a position to use. It seeds the state below instead, which is the
|
|
54
|
+
* same thing one layer out. `core`'s `Drawer.vue` carries the matching note from
|
|
55
|
+
* the other direction: there, `open: undefined` in `withDefaults` is
|
|
56
|
+
* load-bearing, because Vue would otherwise cast an absent `Boolean` prop to
|
|
57
|
+
* `false` and reka would read that as "the caller says closed".
|
|
58
|
+
*
|
|
59
|
+
* `open` without `onOpenChange` pins the panel, exactly as it does on a bare
|
|
60
|
+
* `Dialog.Root` or a reka `DialogRoot`: a caller that takes the value over is
|
|
61
|
+
* expected to listen. This is not one of the `defineModel` ports that gate on
|
|
62
|
+
* the handler — `Drawer.vue` is a plain prop plus an emit, which is
|
|
63
|
+
* controlled-only, and so is this.
|
|
64
|
+
*/
|
|
65
|
+
export declare const Drawer: {
|
|
66
|
+
({ children, open, defaultOpen, modal, side, onOpenChange, }: DrawerProps): import("react/jsx-runtime").JSX.Element;
|
|
67
|
+
displayName: string;
|
|
68
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface DrawerBodyProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
2
|
+
dataTestid: string;
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* DrawerBody — mirrors `@sesame/orxata-core` DrawerBody. The scrolling region of
|
|
6
|
+
* the panel, the board's `Content`, "con Slot y scroll interno".
|
|
7
|
+
*
|
|
8
|
+
* The header and the footer stay put; this is the only part that moves once the
|
|
9
|
+
* content is taller than the panel. It carries no styling of its own: the
|
|
10
|
+
* declarations that make it the flexible, scrolling item live on `DrawerContent`
|
|
11
|
+
* next to the `top`/`bottom` pair they depend on, since a body is only a scroll
|
|
12
|
+
* region because the panel is pinned to both edges and splitting that pair
|
|
13
|
+
* across two files is how one of them gets changed alone.
|
|
14
|
+
*
|
|
15
|
+
* `data-slot` rather than position, so it can sit anywhere among the panel's
|
|
16
|
+
* children.
|
|
17
|
+
*/
|
|
18
|
+
export declare const DrawerBody: import('react').ForwardRefExoticComponent<DrawerBodyProps & import('react').RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
|
2
|
+
export interface DrawerContentProps extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {
|
|
3
|
+
showCloseButton?: boolean;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* DrawerContent — mirrors `@sesame/orxata-core` DrawerContent. The panel itself:
|
|
7
|
+
* the portal, the glass, the slide and the dialog content in one place.
|
|
8
|
+
*
|
|
9
|
+
* **Why `OxTransitionSlideX` and not `OxTransitionReveal`.** The panel's whole
|
|
10
|
+
* movement is along the axis it is anchored to, and `SlideX` is the only one of
|
|
11
|
+
* the two that moves on X at all: `Reveal` rises 26px on Y, which would read as
|
|
12
|
+
* a card appearing rather than a panel entering. `SlideX` also takes
|
|
13
|
+
* `direction` / `leaveDirection` in the same `'left' | 'right'` vocabulary as
|
|
14
|
+
* our own `side`, so the two cannot drift.
|
|
15
|
+
*
|
|
16
|
+
* **One prop of `core`'s is missing, and it could not be kept.** `DrawerContent`
|
|
17
|
+
* on the Vue side takes `trapFocus` and hands it through, because reka's
|
|
18
|
+
* `DialogContent` spreads its `$attrs` into `DialogContentModal`, which declares
|
|
19
|
+
* `trapFocus` as a real prop — so the pass-through works even though reka omits
|
|
20
|
+
* it from `DialogContentProps`. Radix closes both doors: `DialogContentTypeProps`
|
|
21
|
+
* omits `trapFocus`, and `DialogContentModal` sets `trapFocus={context.open}`
|
|
22
|
+
* *after* spreading the caller's props, so anything passed is overwritten.
|
|
23
|
+
* Focus trapping here follows `modal` and nothing else. Do not add the prop
|
|
24
|
+
* back as a no-op to make the two prop lists match — a dead prop is worse than
|
|
25
|
+
* an absent one, and props are the part of the API the parity rules
|
|
26
|
+
* deliberately do not compare.
|
|
27
|
+
*
|
|
28
|
+
* **No portal-target prop.** `OxToaster` has a `to` because a toast viewport is
|
|
29
|
+
* genuinely a region of the application and tests want it somewhere else. A
|
|
30
|
+
* drawer is the opposite case: it is anchored to the viewport, and a portal
|
|
31
|
+
* anywhere but the document root puts it back inside the `overflow` and
|
|
32
|
+
* `transform` ancestors the portal exists to escape — so a `container` here
|
|
33
|
+
* would be an escape hatch whose main use is to break the component. Radix's
|
|
34
|
+
* `Dialog.Portal` is one import away for anybody who really needs it.
|
|
35
|
+
*/
|
|
36
|
+
export declare const DrawerContent: import('react').ForwardRefExoticComponent<DrawerContentProps & import('react').RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
|
2
|
+
export type DrawerDescriptionProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>;
|
|
3
|
+
/**
|
|
4
|
+
* DrawerDescription — mirrors `@sesame/orxata-core` DrawerDescription.
|
|
5
|
+
*
|
|
6
|
+
* The board's `Subtitle`, bound to `Text/sm` (14/20) and
|
|
7
|
+
* `Semantic-Colors/muted-foreground`. The primitive registers the id
|
|
8
|
+
* `aria-describedby` points at.
|
|
9
|
+
*/
|
|
10
|
+
export declare const DrawerDescription: import('react').ForwardRefExoticComponent<Omit<DialogPrimitive.DialogDescriptionProps & import('react').RefAttributes<HTMLParagraphElement>, "ref"> & import('react').RefAttributes<HTMLParagraphElement>>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type DrawerExpandProps = React.ButtonHTMLAttributes<HTMLButtonElement>;
|
|
2
|
+
/**
|
|
3
|
+
* DrawerExpand — mirrors `@sesame/orxata-core` DrawerExpand.
|
|
4
|
+
*
|
|
5
|
+
* The header's grow/shrink toggle. The board draws it on every variant —
|
|
6
|
+
* `icon-maximize-diagonal` while the panel is at its resting width,
|
|
7
|
+
* `icon-minimize-diagonal` once it has grown (nodes I1463:199;1483:88 on
|
|
8
|
+
* `Size=md` and on `Size=xl`).
|
|
9
|
+
*
|
|
10
|
+
* A component rather than a prop on `DrawerContent`, because it is a control the
|
|
11
|
+
* consumer places: a drawer whose detail does not benefit from the extra width
|
|
12
|
+
* simply does not include it, and no `showExpandButton` has to exist.
|
|
13
|
+
*
|
|
14
|
+
* `aria-pressed` and not a second button: it is one control with two states,
|
|
15
|
+
* which is what a toggle button is. The accessible name changes with it, so a
|
|
16
|
+
* screen reader hears what pressing it will do rather than what it did.
|
|
17
|
+
*
|
|
18
|
+
* Positioned against the panel like the close glyph beside it, and for the same
|
|
19
|
+
* reason — see `DrawerContent`'s `.oxDrawerClose`. It therefore renders
|
|
20
|
+
* correctly wherever it sits among the panel's children; next to the title is
|
|
21
|
+
* where it reads.
|
|
22
|
+
*/
|
|
23
|
+
export declare const DrawerExpand: import('react').ForwardRefExoticComponent<DrawerExpandProps & import('react').RefAttributes<HTMLButtonElement>>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
|
2
|
+
export type DrawerTitleProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>;
|
|
3
|
+
/**
|
|
4
|
+
* DrawerTitle — mirrors `@sesame/orxata-core` DrawerTitle.
|
|
5
|
+
*
|
|
6
|
+
* The panel's heading, and what `aria-labelledby` points at — the board's
|
|
7
|
+
* accessibility note asks for exactly that pairing, and the primitive is what
|
|
8
|
+
* registers the id, so this is not interchangeable with an `<h2>`.
|
|
9
|
+
*
|
|
10
|
+
* `text-xl` and not `OxDialogTitle`'s `text-lg`: the board's `Title` is bound to
|
|
11
|
+
* `Text/xl` (20/28) where the Modal's is `lg`.
|
|
12
|
+
*/
|
|
13
|
+
export declare const DrawerTitle: import('react').ForwardRefExoticComponent<Omit<DialogPrimitive.DialogTitleProps & import('react').RefAttributes<HTMLHeadingElement>, "ref"> & import('react').RefAttributes<HTMLHeadingElement>>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { Drawer, type DrawerProps } from './Drawer.tsx';
|
|
2
|
+
export { DrawerContent, type DrawerContentProps } from './DrawerContent.tsx';
|
|
3
|
+
export { DrawerBody, type DrawerBodyProps } from './DrawerBody.tsx';
|
|
4
|
+
export { DrawerTitle, type DrawerTitleProps } from './DrawerTitle.tsx';
|
|
5
|
+
export { DrawerDescription, type DrawerDescriptionProps, } from './DrawerDescription.tsx';
|
|
6
|
+
export { DrawerExpand, type DrawerExpandProps } from './DrawerExpand.tsx';
|
|
7
|
+
export { Trigger as DrawerTrigger, Close as DrawerClose, } from '@radix-ui/react-dialog';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the drawer's parts need from its root, and nothing else.
|
|
3
|
+
*
|
|
4
|
+
* **`open` is here, where `core`'s `DrawerContext` deliberately leaves it out.**
|
|
5
|
+
* That is not drift; it is the one thing the two primitives genuinely do
|
|
6
|
+
* differently. reka publishes its dialog root's state as
|
|
7
|
+
* `injectDialogRootContext()`, so `DrawerContent.vue` reads `open` from the
|
|
8
|
+
* primitive and a second copy in that context would be a second thing able to
|
|
9
|
+
* disagree with it. Radix publishes no equivalent — its `useDialogContext` is
|
|
10
|
+
* module-private and nothing exported reaches it — so the root here owns the
|
|
11
|
+
* state, hands it to `Dialog.Root` as a controlled value and publishes it
|
|
12
|
+
* through this context. Still one source of truth, just a different one.
|
|
13
|
+
*/
|
|
14
|
+
export interface DrawerContextValue {
|
|
15
|
+
/** Whether the panel is open. The root owns this — see above. */
|
|
16
|
+
open: boolean;
|
|
17
|
+
/** Which edge the panel is anchored to. */
|
|
18
|
+
side: 'left' | 'right';
|
|
19
|
+
/** Whether the panel has grown to its inset limit. */
|
|
20
|
+
expanded: boolean;
|
|
21
|
+
toggleExpanded: () => void;
|
|
22
|
+
}
|
|
23
|
+
export declare const DrawerProvider: import('react').Provider<DrawerContextValue | null>;
|
|
24
|
+
/**
|
|
25
|
+
* Throws rather than returning a default: a part that cannot find its root is a
|
|
26
|
+
* composition mistake, and a silent fallback would let `OxDrawerExpand` render a
|
|
27
|
+
* button that moves nothing. The message is `core`'s word for word, so the two
|
|
28
|
+
* packages fail identically.
|
|
29
|
+
*/
|
|
30
|
+
export declare const useDrawer: () => DrawerContextValue;
|
|
@@ -14,6 +14,6 @@ export type { ProgressProps } from './Progress.tsx';
|
|
|
14
14
|
* The variant is the caller's to say.
|
|
15
15
|
*/
|
|
16
16
|
export declare const progressVariants: (props?: ({
|
|
17
|
-
variant?: "success" | "
|
|
17
|
+
variant?: "success" | "brand" | "error" | "warning" | null | undefined;
|
|
18
18
|
} & import('class-variance-authority/types').ClassProp) | undefined) => string;
|
|
19
19
|
export type ProgressVariants = VariantProps<typeof progressVariants>;
|