sonahang-ui 1.0.3 → 1.0.5

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
@@ -88,6 +88,8 @@ Navigation: `Tabs`, `Breadcrumb`, `Pagination`, `Stepper`, `Accordion`
88
88
 
89
89
  Data display: `Avatar`, `Tag`, `Chip`, `Carousel`
90
90
 
91
+ Media: `AudioPlayer`, `AudioPlaylist`
92
+
91
93
  Every component, every prop, and every state is documented with live controls
92
94
  in [Storybook](https://github.com/sonangrai/sonahang-ui) — that's the real
93
95
  reference, this file is the summary.
@@ -0,0 +1,48 @@
1
+ import type { AudioHTMLAttributes, ReactNode, Ref } from "react";
2
+ import "./AudioPlayer.css";
3
+ export type AudioPlayerLabels = {
4
+ play?: string;
5
+ pause?: string;
6
+ previous?: string;
7
+ next?: string;
8
+ seek?: string;
9
+ mute?: string;
10
+ unmute?: string;
11
+ volume?: string;
12
+ /** Shown in place of the controls when the source fails to load. */
13
+ error?: string;
14
+ };
15
+ export type AudioPlayerProps = {
16
+ /** URL of the audio file. */
17
+ src: string;
18
+ /** Track name, shown above the controls. Also names the player region. */
19
+ title?: ReactNode;
20
+ /** Secondary line under the title — an artist, show, or episode. */
21
+ subtitle?: ReactNode;
22
+ /** Hides the volume controls, e.g. where the OS volume is all that matters. */
23
+ hideVolume?: boolean;
24
+ /** Initial volume, 0–1. Defaults to 1. */
25
+ defaultVolume?: number;
26
+ /** Accessible names for the controls — override to localise. */
27
+ labels?: AudioPlayerLabels;
28
+ /** Renders a Previous button beside Play. Omit for a single track. */
29
+ onPrevious?: () => void;
30
+ /** Renders a Next button beside Play. Omit for a single track. */
31
+ onNext?: () => void;
32
+ /** Extra controls, placed in the control row before the volume. */
33
+ actions?: ReactNode;
34
+ /** Class for the wrapper element. */
35
+ className?: string;
36
+ /** Ref to the underlying `<audio>` element, for imperative control. */
37
+ ref?: Ref<HTMLAudioElement>;
38
+ } & Omit<AudioHTMLAttributes<HTMLAudioElement>, "src" | "title" | "className" | "controls">;
39
+ /**
40
+ * Custom controls over a native `<audio>` element.
41
+ *
42
+ * The element stays the source of truth: playing, position, and volume are
43
+ * read back from its media events rather than tracked alongside it, so the UI
44
+ * can't drift from what's actually playing — even when something else drives
45
+ * the element through `ref`. The seek and volume bars are `RangeSlider`s, so
46
+ * arrow keys, Home/End, and screen-reader announcements come from the platform.
47
+ */
48
+ export declare function AudioPlayer({ src, title, subtitle, hideVolume, defaultVolume, labels, onPrevious, onNext, actions, className, ref, onPlay, onPause, onEnded, onTimeUpdate, onLoadedMetadata, onDurationChange, onVolumeChange, onError, ...props }: AudioPlayerProps): import("react").JSX.Element;
@@ -0,0 +1,59 @@
1
+ import type { ReactNode, Ref } from "react";
2
+ import type { AudioPlayerLabels } from "./AudioPlayer";
3
+ import type { PlayMode } from "./playlistOrder";
4
+ import "./AudioPlaylist.css";
5
+ export type AudioTrack = {
6
+ src: string;
7
+ title: ReactNode;
8
+ subtitle?: ReactNode;
9
+ /**
10
+ * Length in seconds, shown in the track list. Optional — the player learns
11
+ * it once a track loads, but the list can't without loading every file.
12
+ */
13
+ duration?: number;
14
+ /** Stable key. Falls back to the index and `src`. */
15
+ id?: string;
16
+ };
17
+ export type AudioPlaylistLabels = AudioPlayerLabels & {
18
+ /** Prefix for the play-mode button's name, e.g. "Play mode: Shuffle". */
19
+ playMode?: string;
20
+ /** Names of the individual modes. */
21
+ modes?: Partial<Record<PlayMode, string>>;
22
+ /** Name of the track list. */
23
+ tracks?: string;
24
+ };
25
+ export type AudioPlaylistProps = {
26
+ tracks: readonly AudioTrack[];
27
+ /** Names the playlist as a whole. Defaults to "Playlist". */
28
+ label?: string;
29
+ /** Current track — makes the playlist's position controlled. */
30
+ trackIndex?: number;
31
+ /** Initial track when uncontrolled. Defaults to the first. */
32
+ defaultTrackIndex?: number;
33
+ /** Fired with the index of the track being moved to. */
34
+ onTrackChange?: (index: number) => void;
35
+ /** Current play mode — makes it controlled. */
36
+ playMode?: PlayMode;
37
+ /** Initial play mode when uncontrolled. Defaults to `sequential`. */
38
+ defaultPlayMode?: PlayMode;
39
+ /** Fired with the next mode when the play-mode button is pressed. */
40
+ onPlayModeChange?: (mode: PlayMode) => void;
41
+ /** Hides the track list, leaving the player and its skip buttons. */
42
+ hideTrackList?: boolean;
43
+ hideVolume?: boolean;
44
+ /** Initial volume, 0–1. Carries across tracks. */
45
+ defaultVolume?: number;
46
+ labels?: AudioPlaylistLabels;
47
+ className?: string;
48
+ /** Ref to the underlying `<audio>` element. */
49
+ ref?: Ref<HTMLAudioElement>;
50
+ };
51
+ /**
52
+ * An `AudioPlayer` that works through a list of tracks, with Previous/Next,
53
+ * a play-mode button cycling through in order, repeat all, repeat one and
54
+ * shuffle, and a track list to jump around in.
55
+ *
56
+ * Volume and mute carry from track to track — it's one `<audio>` element
57
+ * whose source changes, not a player per track.
58
+ */
59
+ export declare function AudioPlaylist({ tracks, label, trackIndex, defaultTrackIndex, onTrackChange, playMode, defaultPlayMode, onPlayModeChange, hideTrackList, hideVolume, defaultVolume, labels, className, ref, }: AudioPlaylistProps): import("react").JSX.Element | null;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Formats a playback position as `m:ss`, or `h:mm:ss` once it passes an hour.
3
+ *
4
+ * Anything that isn't a finite, non-negative number comes back as `--:--`.
5
+ * That covers the `NaN` a media element reports before its metadata loads and
6
+ * the `Infinity` it reports for a live stream — neither has a length to show.
7
+ */
8
+ export declare function formatAudioTime(seconds: number): string;
@@ -0,0 +1,7 @@
1
+ export { AudioPlayer } from "./AudioPlayer";
2
+ export type { AudioPlayerProps, AudioPlayerLabels } from "./AudioPlayer";
3
+ export { AudioPlaylist } from "./AudioPlaylist";
4
+ export type { AudioPlaylistProps, AudioPlaylistLabels, AudioTrack } from "./AudioPlaylist";
5
+ export { formatAudioTime } from "./formatAudioTime";
6
+ export { playModes, getNextTrack, getPreviousTrack, createShuffleOrder, } from "./playlistOrder";
7
+ export type { PlayMode, PlaylistAdvance, PlaylistPosition, PlaylistStep, } from "./playlistOrder";
@@ -0,0 +1,45 @@
1
+ export declare const playModes: readonly ["sequential", "repeat-all", "repeat-one", "shuffle"];
2
+ export type PlayMode = (typeof playModes)[number];
3
+ /** What moved the playlist on: the track finishing, or the Next button. */
4
+ export type PlaylistAdvance = "ended" | "next";
5
+ export type PlaylistPosition = {
6
+ /** Index of the current track. */
7
+ index: number;
8
+ /** Number of tracks in the playlist. */
9
+ count: number;
10
+ mode: PlayMode;
11
+ /**
12
+ * Shuffled play order, as track indices. Only read in `shuffle` mode; an
13
+ * order that no longer fits the playlist is replaced.
14
+ */
15
+ order?: readonly number[];
16
+ /** Source of randomness, for deterministic tests. Defaults to `Math.random`. */
17
+ random?: () => number;
18
+ };
19
+ export type PlaylistStep = {
20
+ /** Track to move to, or `null` to stop where we are. */
21
+ index: number | null;
22
+ /** The shuffle order to keep for the next step. */
23
+ order: number[];
24
+ };
25
+ /**
26
+ * A random permutation of `0..count-1` that starts with `first`, so turning
27
+ * shuffle on never jumps away from the track that's playing.
28
+ */
29
+ export declare function createShuffleOrder(count: number, first: number, random?: () => number): number[];
30
+ /**
31
+ * Where the playlist goes after the current track.
32
+ *
33
+ * - `sequential` stops after the last track when it ends; Next wraps.
34
+ * - `repeat-all` wraps either way.
35
+ * - `repeat-one` replays the track when it ends; Next still moves on, since
36
+ * pressing it is asking for a different track.
37
+ * - `shuffle` walks a shuffled order and reshuffles once every track has
38
+ * played, never starting the new round on the track that just finished.
39
+ */
40
+ export declare function getNextTrack(position: PlaylistPosition, advance: PlaylistAdvance): PlaylistStep;
41
+ /**
42
+ * Where Previous goes. In `shuffle` it retraces the shuffled order and stays
43
+ * on the first track of the round; otherwise it steps back and wraps.
44
+ */
45
+ export declare function getPreviousTrack(position: PlaylistPosition): PlaylistStep;
@@ -16,12 +16,19 @@ export type TooltipProps = {
16
16
  /** Tip body. Nothing renders when this is empty. */
17
17
  content?: ReactNode;
18
18
  /**
19
- * Which side of the trigger the tip sits on. Defaults to `top`.
19
+ * Which side of the trigger the tip prefers. Defaults to `top`.
20
20
  *
21
- * `auto` measures the trigger against the viewport each time the tip opens
22
- * and picks a side with room for it.
21
+ * The tip is measured against the viewport each time it opens. If the
22
+ * preferred side has no room, it flips to the opposite side, then to
23
+ * whichever side does. `auto` is the same as `top`.
23
24
  */
24
25
  placement?: TooltipPlacement;
26
+ /**
27
+ * Keeps the tip on screen by flipping it to another side and sliding it
28
+ * away from the viewport edge. Defaults to `true`. Turn it off to pin the
29
+ * tip to `placement` exactly.
30
+ */
31
+ avoidCollisions?: boolean;
25
32
  /** Milliseconds to wait before showing on hover. Defaults to 150. */
26
33
  delay?: number;
27
34
  /** Open state — makes the component controlled. */
@@ -41,4 +48,4 @@ export type TooltipProps = {
41
48
  * needs, because it can't be reached by touch and disappears on the way to
42
49
  * anything else. Use it to name an icon button or expand an abbreviation.
43
50
  */
44
- export declare function Tooltip({ children, content, placement, delay, open: controlledOpen, defaultOpen, onOpenChange, disabled, className, ...props }: TooltipProps): import("react").JSX.Element;
51
+ export declare function Tooltip({ children, content, placement, avoidCollisions, delay, open: controlledOpen, defaultOpen, onOpenChange, disabled, className, ...props }: TooltipProps): import("react").JSX.Element;
@@ -1,4 +1,4 @@
1
1
  export { Tooltip } from "./Tooltip";
2
2
  export type { TooltipProps } from "./Tooltip";
3
- export { resolveTooltipPlacement } from "./resolveTooltipPlacement";
4
- export type { TooltipPlacement, TooltipSide, TooltipRect, ResolveTooltipPlacementOptions, } from "./resolveTooltipPlacement";
3
+ export { resolveTooltipPlacement, resolveTooltipShift } from "./resolveTooltipPlacement";
4
+ export type { TooltipPlacement, TooltipSide, TooltipRect, ResolveTooltipPlacementOptions, ResolveTooltipShiftOptions, } from "./resolveTooltipPlacement";
@@ -23,17 +23,49 @@ export type ResolveTooltipPlacementOptions = {
23
23
  };
24
24
  /** Separation between trigger and tip. Defaults to 8, matching the CSS. */
25
25
  gap?: number;
26
+ /**
27
+ * The side to keep whenever it fits. Defaults to `top`. When it doesn't,
28
+ * the opposite side is tried next, so a tip that can't go above a trigger
29
+ * goes below it rather than off to one side.
30
+ */
31
+ preferred?: TooltipSide;
32
+ };
33
+ export type ResolveTooltipShiftOptions = {
34
+ /** The side the tip is on — from `resolveTooltipPlacement`. */
35
+ side: TooltipSide;
36
+ trigger: TooltipRect;
37
+ tip: {
38
+ width: number;
39
+ height: number;
40
+ };
41
+ viewport: {
42
+ width: number;
43
+ height: number;
44
+ };
45
+ /** Minimum distance kept between the tip and the viewport edge. Defaults to 8. */
46
+ padding?: number;
26
47
  };
27
48
  /**
28
49
  * Picks the side of the trigger with room for the tip.
29
50
  *
30
- * Returns the first side in preference order that fits. When none do — a
31
- * trigger in a cramped scroll container, say — it returns whichever side
32
- * overflows least, so the tip is still mostly readable.
51
+ * Returns the preferred side if it fits, then its opposite, then the first
52
+ * remaining side that fits. When none do — a trigger in a cramped scroll
53
+ * container, say — it returns whichever side overflows least, so the tip is
54
+ * still mostly readable.
55
+ *
56
+ * This looks at the primary axis only. Keeping the tip inside the viewport
57
+ * along the other axis is `resolveTooltipShift`'s job.
58
+ */
59
+ export declare function resolveTooltipPlacement({ trigger, tip, viewport, gap, preferred, }: ResolveTooltipPlacementOptions): TooltipSide;
60
+ /**
61
+ * How far to slide the tip off-centre so it stays inside the viewport.
62
+ *
63
+ * A `top` or `bottom` tip is centred on its trigger, so a trigger near the
64
+ * left or right edge pushes half of it off screen; the result is a horizontal
65
+ * offset in pixels that pulls it back. For `left` and `right` it's vertical.
33
66
  *
34
- * This looks at the primary axis only: a `top` tip is centred horizontally
35
- * and can still overrun the left or right edge of a narrow viewport. Shifting
36
- * along the cross axis would need the tip to be positioned in JavaScript
37
- * rather than by CSS.
67
+ * When the tip is wider than the viewport, its start edge wins, because that's
68
+ * where reading begins. The shift is capped so the arrow always stays on the
69
+ * tip and keeps pointing at the trigger.
38
70
  */
39
- export declare function resolveTooltipPlacement({ trigger, tip, viewport, gap, }: ResolveTooltipPlacementOptions): TooltipSide;
71
+ export declare function resolveTooltipShift({ side, trigger, tip, viewport, padding, }: ResolveTooltipShiftOptions): number;
package/dist/index.d.ts CHANGED
@@ -74,6 +74,8 @@ export { Switch, SwitchGroup } from './components/Switch';
74
74
  export type { SwitchProps, SwitchGroupProps } from './components/Switch';
75
75
  export { SegmentedControl } from './components/SegmentedControl';
76
76
  export type { SegmentedControlProps, SegmentedControlOption, } from './components/SegmentedControl';
77
+ export { AudioPlayer, AudioPlaylist, formatAudioTime, playModes, getNextTrack, getPreviousTrack, createShuffleOrder, } from './components/AudioPlayer';
78
+ export type { AudioPlayerProps, AudioPlayerLabels, AudioPlaylistProps, AudioPlaylistLabels, AudioTrack, PlayMode, PlaylistAdvance, PlaylistPosition, PlaylistStep, } from './components/AudioPlayer';
77
79
  /**
78
80
  * Design tokens. The stylesheet already defines every color as a CSS var
79
81
  * (`--color-accent-600`, `--color-bg-surface`, …) — these are the same