@mieweb/ui 0.6.1-dev.169 → 0.6.1-dev.171
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/brands/index.cjs +18 -18
- package/dist/brands/index.js +3 -3
- package/dist/chunk-3Q2VWPML.cjs +1802 -0
- package/dist/chunk-3Q2VWPML.cjs.map +1 -0
- package/dist/{chunk-6W3HLFAJ.cjs → chunk-3YRDZBW6.cjs} +3 -3
- package/dist/{chunk-6W3HLFAJ.cjs.map → chunk-3YRDZBW6.cjs.map} +1 -1
- package/dist/{chunk-UVQEHZRF.js → chunk-6VGXX6L6.js} +2 -2
- package/dist/{chunk-UVQEHZRF.js.map → chunk-6VGXX6L6.js.map} +1 -1
- package/dist/chunk-CH26CHPH.js +1776 -0
- package/dist/chunk-CH26CHPH.js.map +1 -0
- package/dist/{chunk-TXRQQMG5.js → chunk-CYMZ2QS5.js} +2 -2
- package/dist/{chunk-TXRQQMG5.js.map → chunk-CYMZ2QS5.js.map} +1 -1
- package/dist/chunk-FGP64Z24.js +131 -0
- package/dist/chunk-FGP64Z24.js.map +1 -0
- package/dist/chunk-FIWKHLWY.js +197 -0
- package/dist/chunk-FIWKHLWY.js.map +1 -0
- package/dist/chunk-GLXTVCHN.cjs +4 -0
- package/dist/chunk-GLXTVCHN.cjs.map +1 -0
- package/dist/chunk-J7KWQJOB.js +14 -0
- package/dist/chunk-J7KWQJOB.js.map +1 -0
- package/dist/chunk-JYZW2PZS.cjs +166 -0
- package/dist/chunk-JYZW2PZS.cjs.map +1 -0
- package/dist/{chunk-XVF472GT.js → chunk-NSLR3B7K.js} +2 -2
- package/dist/{chunk-XVF472GT.js.map → chunk-NSLR3B7K.js.map} +1 -1
- package/dist/chunk-NTYGCBCL.js +543 -0
- package/dist/chunk-NTYGCBCL.js.map +1 -0
- package/dist/chunk-RISZWWEQ.js +3 -0
- package/dist/chunk-RISZWWEQ.js.map +1 -0
- package/dist/chunk-S7JQ3FV5.cjs +16 -0
- package/dist/chunk-S7JQ3FV5.cjs.map +1 -0
- package/dist/{chunk-JWTCEWQ4.cjs → chunk-SNEQXSC5.cjs} +2 -2
- package/dist/{chunk-JWTCEWQ4.cjs.map → chunk-SNEQXSC5.cjs.map} +1 -1
- package/dist/chunk-SQHMGT4W.js +164 -0
- package/dist/chunk-SQHMGT4W.js.map +1 -0
- package/dist/{chunk-JHPFOG2N.cjs → chunk-ST2RE5UB.cjs} +30 -18
- package/dist/chunk-ST2RE5UB.cjs.map +1 -0
- package/dist/chunk-UO3KEUYK.cjs +220 -0
- package/dist/chunk-UO3KEUYK.cjs.map +1 -0
- package/dist/{chunk-FETRH4V5.js → chunk-X763OLF2.js} +3 -3
- package/dist/{chunk-FETRH4V5.js.map → chunk-X763OLF2.js.map} +1 -1
- package/dist/{chunk-DKGUOMBC.cjs → chunk-X7J45FU5.cjs} +2 -2
- package/dist/{chunk-DKGUOMBC.cjs.map → chunk-X7J45FU5.cjs.map} +1 -1
- package/dist/{chunk-I56VBDSC.js → chunk-XDFDVF2O.js} +30 -18
- package/dist/chunk-XDFDVF2O.js.map +1 -0
- package/dist/chunk-Y4ESBDQ5.cjs +552 -0
- package/dist/chunk-Y4ESBDQ5.cjs.map +1 -0
- package/dist/{chunk-MJ7YITLN.cjs → chunk-Y65SK5Y2.cjs} +2 -2
- package/dist/{chunk-MJ7YITLN.cjs.map → chunk-Y65SK5Y2.cjs.map} +1 -1
- package/dist/chunk-YD23ZGRU.cjs +155 -0
- package/dist/chunk-YD23ZGRU.cjs.map +1 -0
- package/dist/components/Markdown/index.cjs +10 -10
- package/dist/components/Markdown/index.js +2 -2
- package/dist/components/MediaEditor/index.cjs +42 -0
- package/dist/components/MediaEditor/index.cjs.map +1 -0
- package/dist/components/MediaEditor/index.d.cts +151 -0
- package/dist/components/MediaEditor/index.d.ts +151 -0
- package/dist/components/MediaEditor/index.js +17 -0
- package/dist/components/MediaEditor/index.js.map +1 -0
- package/dist/components/MediaPlayer/index.cjs +24 -0
- package/dist/components/MediaPlayer/index.cjs.map +1 -0
- package/dist/components/MediaPlayer/index.d.cts +71 -0
- package/dist/components/MediaPlayer/index.d.ts +71 -0
- package/dist/components/MediaPlayer/index.js +7 -0
- package/dist/components/MediaPlayer/index.js.map +1 -0
- package/dist/components/ThemeProvider/index.cjs +7 -7
- package/dist/components/ThemeProvider/index.js +1 -1
- package/dist/components/TranscriptView/index.cjs +21 -0
- package/dist/components/TranscriptView/index.cjs.map +1 -0
- package/dist/components/TranscriptView/index.d.cts +55 -0
- package/dist/components/TranscriptView/index.d.ts +55 -0
- package/dist/components/TranscriptView/index.js +4 -0
- package/dist/components/TranscriptView/index.js.map +1 -0
- package/dist/esheet.cjs +5 -5
- package/dist/esheet.js +2 -2
- package/dist/hooks/index.cjs +49 -11
- package/dist/hooks/index.d.cts +88 -1
- package/dist/hooks/index.d.ts +88 -1
- package/dist/hooks/index.js +3 -1
- package/dist/index.cjs +380 -253
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +87 -40
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/transcript-oaWqY3yr.d.cts +81 -0
- package/dist/transcript-oaWqY3yr.d.ts +81 -0
- package/dist/useTranscriptEdits-BUylQFg0.d.cts +168 -0
- package/dist/useTranscriptEdits-CWYnjbGP.d.ts +168 -0
- package/package.json +1 -1
- package/dist/chunk-I56VBDSC.js.map +0 -1
- package/dist/chunk-JHPFOG2N.cjs.map +0 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transcript schema for @mieweb/ui media components.
|
|
3
|
+
*
|
|
4
|
+
* Provider-agnostic and persistence-agnostic. Canonical time unit is
|
|
5
|
+
* **milliseconds** (consumers with seconds-based data, e.g. osheet, adapt
|
|
6
|
+
* at the boundary).
|
|
7
|
+
*
|
|
8
|
+
* Shared by TranscriptView, MediaEditor, and useTranscriptEdits.
|
|
9
|
+
*/
|
|
10
|
+
/** Type of transcript word - 'word' for spoken content, 'silence' for detected gaps, 'silence-newline' for longer pauses */
|
|
11
|
+
type WordType = 'word' | 'silence' | 'silence-newline';
|
|
12
|
+
interface TranscriptWord {
|
|
13
|
+
text: string;
|
|
14
|
+
startMs: number;
|
|
15
|
+
endMs: number;
|
|
16
|
+
/** References Speaker.id when diarization is available */
|
|
17
|
+
speakerId?: string;
|
|
18
|
+
confidence?: number;
|
|
19
|
+
/** Type of word: 'word' for spoken content, 'silence' for detected gaps. Defaults to 'word' */
|
|
20
|
+
wordType?: WordType;
|
|
21
|
+
}
|
|
22
|
+
/** A diarized segment of contiguous speech (osheet-style granularity) */
|
|
23
|
+
interface TranscriptSegment {
|
|
24
|
+
text: string;
|
|
25
|
+
startMs: number;
|
|
26
|
+
endMs: number;
|
|
27
|
+
/** References Speaker.id when diarization is available */
|
|
28
|
+
speakerId?: string;
|
|
29
|
+
words: TranscriptWord[];
|
|
30
|
+
}
|
|
31
|
+
interface Speaker {
|
|
32
|
+
id: string;
|
|
33
|
+
/** Display name (e.g. 'Clinician', 'Patient'). Hosts may remap via speakerLabels props. */
|
|
34
|
+
name?: string;
|
|
35
|
+
}
|
|
36
|
+
interface Transcript {
|
|
37
|
+
durationMs: number;
|
|
38
|
+
speakers?: Speaker[];
|
|
39
|
+
words: TranscriptWord[];
|
|
40
|
+
segments?: TranscriptSegment[];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* An editable word that references the original transcript word.
|
|
44
|
+
* Used in the edited timeline to track deletions and reordering.
|
|
45
|
+
*/
|
|
46
|
+
interface EditableWord {
|
|
47
|
+
/** Reference to original word in transcript.words */
|
|
48
|
+
originalIndex: number;
|
|
49
|
+
/** The word data (from original transcript) */
|
|
50
|
+
word: TranscriptWord;
|
|
51
|
+
/** Whether this word is marked as deleted */
|
|
52
|
+
deleted: boolean;
|
|
53
|
+
/** Whether this word was inserted (pasted from clipboard) */
|
|
54
|
+
inserted?: boolean;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Represents a contiguous segment of playback from the original media.
|
|
58
|
+
* Used when playing back an edited timeline with potentially reordered/deleted words.
|
|
59
|
+
*/
|
|
60
|
+
interface PlaybackSegment {
|
|
61
|
+
startMs: number;
|
|
62
|
+
endMs: number;
|
|
63
|
+
/** Indices into the editedWords array that this segment covers */
|
|
64
|
+
editedIndices: number[];
|
|
65
|
+
}
|
|
66
|
+
/** Available playback speed multipliers */
|
|
67
|
+
type PlaybackSpeed = 0.5 | 0.75 | 1 | 1.25 | 1.5 | 1.75 | 2;
|
|
68
|
+
/** Array of all available speed options for dropdowns */
|
|
69
|
+
declare const PLAYBACK_SPEEDS: PlaybackSpeed[];
|
|
70
|
+
/**
|
|
71
|
+
* A speed marker applied to a word in the transcript.
|
|
72
|
+
* The speed applies from this word until the next speed marker or end of transcript.
|
|
73
|
+
*/
|
|
74
|
+
interface SpeedMarker {
|
|
75
|
+
/** Index in the editedWords array where the speed marker is placed */
|
|
76
|
+
wordIndex: number;
|
|
77
|
+
/** The playback speed multiplier */
|
|
78
|
+
speed: PlaybackSpeed;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export { type EditableWord as E, type PlaybackSpeed as P, type SpeedMarker as S, type Transcript as T, type WordType as W, type PlaybackSegment as a, type TranscriptWord as b, PLAYBACK_SPEEDS as c, type Speaker as d, type TranscriptSegment as e };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transcript schema for @mieweb/ui media components.
|
|
3
|
+
*
|
|
4
|
+
* Provider-agnostic and persistence-agnostic. Canonical time unit is
|
|
5
|
+
* **milliseconds** (consumers with seconds-based data, e.g. osheet, adapt
|
|
6
|
+
* at the boundary).
|
|
7
|
+
*
|
|
8
|
+
* Shared by TranscriptView, MediaEditor, and useTranscriptEdits.
|
|
9
|
+
*/
|
|
10
|
+
/** Type of transcript word - 'word' for spoken content, 'silence' for detected gaps, 'silence-newline' for longer pauses */
|
|
11
|
+
type WordType = 'word' | 'silence' | 'silence-newline';
|
|
12
|
+
interface TranscriptWord {
|
|
13
|
+
text: string;
|
|
14
|
+
startMs: number;
|
|
15
|
+
endMs: number;
|
|
16
|
+
/** References Speaker.id when diarization is available */
|
|
17
|
+
speakerId?: string;
|
|
18
|
+
confidence?: number;
|
|
19
|
+
/** Type of word: 'word' for spoken content, 'silence' for detected gaps. Defaults to 'word' */
|
|
20
|
+
wordType?: WordType;
|
|
21
|
+
}
|
|
22
|
+
/** A diarized segment of contiguous speech (osheet-style granularity) */
|
|
23
|
+
interface TranscriptSegment {
|
|
24
|
+
text: string;
|
|
25
|
+
startMs: number;
|
|
26
|
+
endMs: number;
|
|
27
|
+
/** References Speaker.id when diarization is available */
|
|
28
|
+
speakerId?: string;
|
|
29
|
+
words: TranscriptWord[];
|
|
30
|
+
}
|
|
31
|
+
interface Speaker {
|
|
32
|
+
id: string;
|
|
33
|
+
/** Display name (e.g. 'Clinician', 'Patient'). Hosts may remap via speakerLabels props. */
|
|
34
|
+
name?: string;
|
|
35
|
+
}
|
|
36
|
+
interface Transcript {
|
|
37
|
+
durationMs: number;
|
|
38
|
+
speakers?: Speaker[];
|
|
39
|
+
words: TranscriptWord[];
|
|
40
|
+
segments?: TranscriptSegment[];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* An editable word that references the original transcript word.
|
|
44
|
+
* Used in the edited timeline to track deletions and reordering.
|
|
45
|
+
*/
|
|
46
|
+
interface EditableWord {
|
|
47
|
+
/** Reference to original word in transcript.words */
|
|
48
|
+
originalIndex: number;
|
|
49
|
+
/** The word data (from original transcript) */
|
|
50
|
+
word: TranscriptWord;
|
|
51
|
+
/** Whether this word is marked as deleted */
|
|
52
|
+
deleted: boolean;
|
|
53
|
+
/** Whether this word was inserted (pasted from clipboard) */
|
|
54
|
+
inserted?: boolean;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Represents a contiguous segment of playback from the original media.
|
|
58
|
+
* Used when playing back an edited timeline with potentially reordered/deleted words.
|
|
59
|
+
*/
|
|
60
|
+
interface PlaybackSegment {
|
|
61
|
+
startMs: number;
|
|
62
|
+
endMs: number;
|
|
63
|
+
/** Indices into the editedWords array that this segment covers */
|
|
64
|
+
editedIndices: number[];
|
|
65
|
+
}
|
|
66
|
+
/** Available playback speed multipliers */
|
|
67
|
+
type PlaybackSpeed = 0.5 | 0.75 | 1 | 1.25 | 1.5 | 1.75 | 2;
|
|
68
|
+
/** Array of all available speed options for dropdowns */
|
|
69
|
+
declare const PLAYBACK_SPEEDS: PlaybackSpeed[];
|
|
70
|
+
/**
|
|
71
|
+
* A speed marker applied to a word in the transcript.
|
|
72
|
+
* The speed applies from this word until the next speed marker or end of transcript.
|
|
73
|
+
*/
|
|
74
|
+
interface SpeedMarker {
|
|
75
|
+
/** Index in the editedWords array where the speed marker is placed */
|
|
76
|
+
wordIndex: number;
|
|
77
|
+
/** The playback speed multiplier */
|
|
78
|
+
speed: PlaybackSpeed;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export { type EditableWord as E, type PlaybackSpeed as P, type SpeedMarker as S, type Transcript as T, type WordType as W, type PlaybackSegment as a, type TranscriptWord as b, PLAYBACK_SPEEDS as c, type Speaker as d, type TranscriptSegment as e };
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { E as EditableWord, T as Transcript, S as SpeedMarker, P as PlaybackSpeed, a as PlaybackSegment, b as TranscriptWord } from './transcript-oaWqY3yr.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useTranscriptEdits — headless transcript-editing state hook.
|
|
5
|
+
*
|
|
6
|
+
* Extracted from pulseclip's TranscriptViewer for @mieweb/ui. Holds the
|
|
7
|
+
* edited word timeline (deletions, insertions, text edits, silence
|
|
8
|
+
* splitting), a single-level undo stack, an internal clipboard, speed
|
|
9
|
+
* markers, and derived playback/statistics data.
|
|
10
|
+
*
|
|
11
|
+
* Controlled + persistence-agnostic: no DOM, no fetches. Selection lives in
|
|
12
|
+
* the UI layer — mutations take explicit indices. State flows out via
|
|
13
|
+
* `onChange` and back in via `initialEditedWords` / `initialUndoStack`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Default filler words offered for removal */
|
|
17
|
+
declare const DEFAULT_FILLER_WORDS: string[];
|
|
18
|
+
/** Default minimum silence duration in milliseconds to detect and insert */
|
|
19
|
+
declare const DEFAULT_MIN_SILENCE_MS = 400;
|
|
20
|
+
/** Default threshold for "newline" silences - longer pauses that indicate paragraph breaks */
|
|
21
|
+
declare const DEFAULT_NL_SILENCE_MS = 1500;
|
|
22
|
+
/** Clipboard data for cut/copy/paste operations */
|
|
23
|
+
interface TranscriptClipboard {
|
|
24
|
+
words: EditableWord[];
|
|
25
|
+
operation: 'cut' | 'copy';
|
|
26
|
+
}
|
|
27
|
+
/** Count and total duration of silences above a given threshold (seconds) */
|
|
28
|
+
interface SilenceThresholdCount {
|
|
29
|
+
threshold: number;
|
|
30
|
+
count: number;
|
|
31
|
+
durationMs: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Builds playback segments from the edited word list.
|
|
35
|
+
* Consecutive words in original order are merged into single segments.
|
|
36
|
+
* Inserted (pasted) words each get their own segment since they may be duplicates.
|
|
37
|
+
*/
|
|
38
|
+
declare function buildPlaybackSegments(editedWords: EditableWord[]): PlaybackSegment[];
|
|
39
|
+
/**
|
|
40
|
+
* Get the effective playback speed at a given word index.
|
|
41
|
+
* Finds the most recent speed marker at or before the word index,
|
|
42
|
+
* or returns the default speed if no marker applies.
|
|
43
|
+
*/
|
|
44
|
+
declare function getSpeedAtIndex(wordIndex: number, speedMarkers: SpeedMarker[], defaultSpeed: PlaybackSpeed): PlaybackSpeed;
|
|
45
|
+
/**
|
|
46
|
+
* Detect silences between words and return a new array with silence pseudo-words inserted.
|
|
47
|
+
* Silences are detected as gaps between the endMs of one word and startMs of the next.
|
|
48
|
+
* Silences >= nlSilenceMs are marked as 'silence-newline' for visual line breaks.
|
|
49
|
+
*/
|
|
50
|
+
declare function insertSilences(words: TranscriptWord[], minSilenceMs?: number, nlSilenceMs?: number): TranscriptWord[];
|
|
51
|
+
/** Initialize editable words from transcript, inserting detected silences */
|
|
52
|
+
declare function initEditableWords(transcript: Transcript, minSilenceMs?: number, nlSilenceMs?: number): EditableWord[];
|
|
53
|
+
interface UseTranscriptEditsOptions {
|
|
54
|
+
/** The source transcript (canonical, immutable) */
|
|
55
|
+
transcript: Transcript;
|
|
56
|
+
/** Initial edited words (from saved edits) */
|
|
57
|
+
initialEditedWords?: EditableWord[];
|
|
58
|
+
/** Initial undo stack (from saved edits) */
|
|
59
|
+
initialUndoStack?: EditableWord[][];
|
|
60
|
+
/** Fired after every mutation (for persistence) */
|
|
61
|
+
onChange?: (editedWords: EditableWord[], undoStack: EditableWord[][]) => void;
|
|
62
|
+
/** Minimum silence duration (ms) to detect and insert. Default 400 */
|
|
63
|
+
minSilenceMs?: number;
|
|
64
|
+
/** Threshold (ms) for "newline" silences (paragraph breaks). Default 1500 */
|
|
65
|
+
nlSilenceMs?: number;
|
|
66
|
+
}
|
|
67
|
+
interface TranscriptEditStats {
|
|
68
|
+
/** Non-deleted spoken words (silences excluded) */
|
|
69
|
+
activeWordCount: number;
|
|
70
|
+
/** Non-deleted silence entries */
|
|
71
|
+
activeSilenceCount: number;
|
|
72
|
+
/** Deleted spoken words */
|
|
73
|
+
deletedWordCount: number;
|
|
74
|
+
/** Deleted silence entries */
|
|
75
|
+
deletedSilenceCount: number;
|
|
76
|
+
/** Total duration of non-deleted entries, adjusted for speed markers */
|
|
77
|
+
editedDurationMs: number;
|
|
78
|
+
}
|
|
79
|
+
interface FillerAnalysis {
|
|
80
|
+
/** Occurrence count per normalized word (fillers and every active word) */
|
|
81
|
+
matchCounts: Map<string, number>;
|
|
82
|
+
/** Total duration (ms) per normalized word */
|
|
83
|
+
durations: Map<string, number>;
|
|
84
|
+
/** Count + total duration of active silences above each analysis threshold */
|
|
85
|
+
silenceCounts: SilenceThresholdCount[];
|
|
86
|
+
/** The built-in filler word list */
|
|
87
|
+
defaultFillerWords: string[];
|
|
88
|
+
}
|
|
89
|
+
interface UseTranscriptEditsResult {
|
|
90
|
+
/** The edited timeline (words + silence pseudo-words) */
|
|
91
|
+
editedWords: EditableWord[];
|
|
92
|
+
/** Undo snapshots, oldest first */
|
|
93
|
+
undoStack: EditableWord[][];
|
|
94
|
+
/** Internal clipboard from the last cut/copy, or null */
|
|
95
|
+
clipboard: TranscriptClipboard | null;
|
|
96
|
+
/** Whether any edit has been made (deletions, insertions, text edits, speed markers) */
|
|
97
|
+
hasEdits: boolean;
|
|
98
|
+
/** Push the current editedWords onto the undo stack (call before external batch mutations) */
|
|
99
|
+
pushUndo: () => void;
|
|
100
|
+
/** Restore the most recent undo snapshot */
|
|
101
|
+
undo: () => void;
|
|
102
|
+
/** Toggle the deleted flag on a single word */
|
|
103
|
+
toggleWordDeleted: (index: number) => void;
|
|
104
|
+
/**
|
|
105
|
+
* Toggle the deleted state of a contiguous range. The first index acts as
|
|
106
|
+
* the anchor: all words are set to the opposite of the anchor's state
|
|
107
|
+
* (delete all if anchor active, restore all if anchor deleted).
|
|
108
|
+
*/
|
|
109
|
+
deleteRange: (indices: number[]) => void;
|
|
110
|
+
/** Cut words at the given indices into the clipboard (marks them deleted) */
|
|
111
|
+
cut: (indices: number[]) => void;
|
|
112
|
+
/** Copy non-deleted words at the given indices into the clipboard */
|
|
113
|
+
copy: (indices: number[]) => void;
|
|
114
|
+
/**
|
|
115
|
+
* Paste the clipboard at a word index. `before` (default true) inserts
|
|
116
|
+
* before the word at `atIndex`; false inserts after it. Returns the index
|
|
117
|
+
* where the first pasted word landed, or null if the clipboard was empty.
|
|
118
|
+
*/
|
|
119
|
+
paste: (atIndex: number, before?: boolean) => number | null;
|
|
120
|
+
/** Replace a word's text (no-op if unchanged) */
|
|
121
|
+
setWordText: (index: number, text: string) => void;
|
|
122
|
+
/** Split a silence at the given index into segments with the given durations (seconds) */
|
|
123
|
+
splitSilence: (index: number, durationsSec: number[]) => void;
|
|
124
|
+
/** Toggle the word at the index deleted (editor "delete" action) */
|
|
125
|
+
deleteWord: (index: number) => void;
|
|
126
|
+
/** Replace the entire edit state from an external source (e.g. an editable script view); undo-safe */
|
|
127
|
+
replaceEditedWords: (next: EditableWord[]) => void;
|
|
128
|
+
/** Mark matching filler words (and silences above the given seconds threshold) deleted */
|
|
129
|
+
removeFillers: (fillerWords: string[], removeSilenceAboveSec: number | null) => void;
|
|
130
|
+
/** Rebuild silence detection with new thresholds, preserving word deletions */
|
|
131
|
+
setSilenceThresholds: (minMs: number, nlMs: number) => void;
|
|
132
|
+
/** Current silence detection thresholds (ms) */
|
|
133
|
+
silenceThresholds: {
|
|
134
|
+
minSilenceMs: number;
|
|
135
|
+
nlSilenceMs: number;
|
|
136
|
+
};
|
|
137
|
+
/** Speed markers, sorted by word index */
|
|
138
|
+
speedMarkers: SpeedMarker[];
|
|
139
|
+
/** Default playback speed when no marker applies */
|
|
140
|
+
defaultSpeed: PlaybackSpeed;
|
|
141
|
+
/** Set the default playback speed */
|
|
142
|
+
setDefaultSpeed: (speed: PlaybackSpeed) => void;
|
|
143
|
+
/** Add/update/remove a speed marker (re-selecting the same speed removes it) */
|
|
144
|
+
toggleSpeedMarker: (index: number, speed: PlaybackSpeed) => void;
|
|
145
|
+
/** Remove the speed marker at a word index, if any */
|
|
146
|
+
removeSpeedMarker: (index: number) => void;
|
|
147
|
+
/** Effective playback speed at a word index */
|
|
148
|
+
getSpeedAtIndex: (index: number) => PlaybackSpeed;
|
|
149
|
+
/** The speed marker at a word index, if any */
|
|
150
|
+
getSpeedMarkerAtIndex: (index: number) => SpeedMarker | undefined;
|
|
151
|
+
/** Contiguous playback segments built from the edited timeline */
|
|
152
|
+
playbackSegments: PlaybackSegment[];
|
|
153
|
+
/** Word/silence counts and speed-adjusted edited duration */
|
|
154
|
+
stats: TranscriptEditStats;
|
|
155
|
+
/** Filler word and silence analysis for removal UIs */
|
|
156
|
+
fillerAnalysis: FillerAnalysis;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Headless transcript-editing hook. See module docs for scope.
|
|
160
|
+
*
|
|
161
|
+
* Undo semantics: each mutation snapshots the full `editedWords` array onto
|
|
162
|
+
* a single-level stack before applying; `undo` pops one snapshot. Speed
|
|
163
|
+
* markers and silence-threshold changes are NOT part of the undo stack
|
|
164
|
+
* (matching the source component).
|
|
165
|
+
*/
|
|
166
|
+
declare function useTranscriptEdits(options: UseTranscriptEditsOptions): UseTranscriptEditsResult;
|
|
167
|
+
|
|
168
|
+
export { DEFAULT_FILLER_WORDS as D, type FillerAnalysis as F, type SilenceThresholdCount as S, type TranscriptClipboard as T, type UseTranscriptEditsOptions as U, DEFAULT_MIN_SILENCE_MS as a, DEFAULT_NL_SILENCE_MS as b, type TranscriptEditStats as c, type UseTranscriptEditsResult as d, buildPlaybackSegments as e, insertSilences as f, getSpeedAtIndex as g, initEditableWords as i, useTranscriptEdits as u };
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { E as EditableWord, T as Transcript, S as SpeedMarker, P as PlaybackSpeed, a as PlaybackSegment, b as TranscriptWord } from './transcript-oaWqY3yr.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useTranscriptEdits — headless transcript-editing state hook.
|
|
5
|
+
*
|
|
6
|
+
* Extracted from pulseclip's TranscriptViewer for @mieweb/ui. Holds the
|
|
7
|
+
* edited word timeline (deletions, insertions, text edits, silence
|
|
8
|
+
* splitting), a single-level undo stack, an internal clipboard, speed
|
|
9
|
+
* markers, and derived playback/statistics data.
|
|
10
|
+
*
|
|
11
|
+
* Controlled + persistence-agnostic: no DOM, no fetches. Selection lives in
|
|
12
|
+
* the UI layer — mutations take explicit indices. State flows out via
|
|
13
|
+
* `onChange` and back in via `initialEditedWords` / `initialUndoStack`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Default filler words offered for removal */
|
|
17
|
+
declare const DEFAULT_FILLER_WORDS: string[];
|
|
18
|
+
/** Default minimum silence duration in milliseconds to detect and insert */
|
|
19
|
+
declare const DEFAULT_MIN_SILENCE_MS = 400;
|
|
20
|
+
/** Default threshold for "newline" silences - longer pauses that indicate paragraph breaks */
|
|
21
|
+
declare const DEFAULT_NL_SILENCE_MS = 1500;
|
|
22
|
+
/** Clipboard data for cut/copy/paste operations */
|
|
23
|
+
interface TranscriptClipboard {
|
|
24
|
+
words: EditableWord[];
|
|
25
|
+
operation: 'cut' | 'copy';
|
|
26
|
+
}
|
|
27
|
+
/** Count and total duration of silences above a given threshold (seconds) */
|
|
28
|
+
interface SilenceThresholdCount {
|
|
29
|
+
threshold: number;
|
|
30
|
+
count: number;
|
|
31
|
+
durationMs: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Builds playback segments from the edited word list.
|
|
35
|
+
* Consecutive words in original order are merged into single segments.
|
|
36
|
+
* Inserted (pasted) words each get their own segment since they may be duplicates.
|
|
37
|
+
*/
|
|
38
|
+
declare function buildPlaybackSegments(editedWords: EditableWord[]): PlaybackSegment[];
|
|
39
|
+
/**
|
|
40
|
+
* Get the effective playback speed at a given word index.
|
|
41
|
+
* Finds the most recent speed marker at or before the word index,
|
|
42
|
+
* or returns the default speed if no marker applies.
|
|
43
|
+
*/
|
|
44
|
+
declare function getSpeedAtIndex(wordIndex: number, speedMarkers: SpeedMarker[], defaultSpeed: PlaybackSpeed): PlaybackSpeed;
|
|
45
|
+
/**
|
|
46
|
+
* Detect silences between words and return a new array with silence pseudo-words inserted.
|
|
47
|
+
* Silences are detected as gaps between the endMs of one word and startMs of the next.
|
|
48
|
+
* Silences >= nlSilenceMs are marked as 'silence-newline' for visual line breaks.
|
|
49
|
+
*/
|
|
50
|
+
declare function insertSilences(words: TranscriptWord[], minSilenceMs?: number, nlSilenceMs?: number): TranscriptWord[];
|
|
51
|
+
/** Initialize editable words from transcript, inserting detected silences */
|
|
52
|
+
declare function initEditableWords(transcript: Transcript, minSilenceMs?: number, nlSilenceMs?: number): EditableWord[];
|
|
53
|
+
interface UseTranscriptEditsOptions {
|
|
54
|
+
/** The source transcript (canonical, immutable) */
|
|
55
|
+
transcript: Transcript;
|
|
56
|
+
/** Initial edited words (from saved edits) */
|
|
57
|
+
initialEditedWords?: EditableWord[];
|
|
58
|
+
/** Initial undo stack (from saved edits) */
|
|
59
|
+
initialUndoStack?: EditableWord[][];
|
|
60
|
+
/** Fired after every mutation (for persistence) */
|
|
61
|
+
onChange?: (editedWords: EditableWord[], undoStack: EditableWord[][]) => void;
|
|
62
|
+
/** Minimum silence duration (ms) to detect and insert. Default 400 */
|
|
63
|
+
minSilenceMs?: number;
|
|
64
|
+
/** Threshold (ms) for "newline" silences (paragraph breaks). Default 1500 */
|
|
65
|
+
nlSilenceMs?: number;
|
|
66
|
+
}
|
|
67
|
+
interface TranscriptEditStats {
|
|
68
|
+
/** Non-deleted spoken words (silences excluded) */
|
|
69
|
+
activeWordCount: number;
|
|
70
|
+
/** Non-deleted silence entries */
|
|
71
|
+
activeSilenceCount: number;
|
|
72
|
+
/** Deleted spoken words */
|
|
73
|
+
deletedWordCount: number;
|
|
74
|
+
/** Deleted silence entries */
|
|
75
|
+
deletedSilenceCount: number;
|
|
76
|
+
/** Total duration of non-deleted entries, adjusted for speed markers */
|
|
77
|
+
editedDurationMs: number;
|
|
78
|
+
}
|
|
79
|
+
interface FillerAnalysis {
|
|
80
|
+
/** Occurrence count per normalized word (fillers and every active word) */
|
|
81
|
+
matchCounts: Map<string, number>;
|
|
82
|
+
/** Total duration (ms) per normalized word */
|
|
83
|
+
durations: Map<string, number>;
|
|
84
|
+
/** Count + total duration of active silences above each analysis threshold */
|
|
85
|
+
silenceCounts: SilenceThresholdCount[];
|
|
86
|
+
/** The built-in filler word list */
|
|
87
|
+
defaultFillerWords: string[];
|
|
88
|
+
}
|
|
89
|
+
interface UseTranscriptEditsResult {
|
|
90
|
+
/** The edited timeline (words + silence pseudo-words) */
|
|
91
|
+
editedWords: EditableWord[];
|
|
92
|
+
/** Undo snapshots, oldest first */
|
|
93
|
+
undoStack: EditableWord[][];
|
|
94
|
+
/** Internal clipboard from the last cut/copy, or null */
|
|
95
|
+
clipboard: TranscriptClipboard | null;
|
|
96
|
+
/** Whether any edit has been made (deletions, insertions, text edits, speed markers) */
|
|
97
|
+
hasEdits: boolean;
|
|
98
|
+
/** Push the current editedWords onto the undo stack (call before external batch mutations) */
|
|
99
|
+
pushUndo: () => void;
|
|
100
|
+
/** Restore the most recent undo snapshot */
|
|
101
|
+
undo: () => void;
|
|
102
|
+
/** Toggle the deleted flag on a single word */
|
|
103
|
+
toggleWordDeleted: (index: number) => void;
|
|
104
|
+
/**
|
|
105
|
+
* Toggle the deleted state of a contiguous range. The first index acts as
|
|
106
|
+
* the anchor: all words are set to the opposite of the anchor's state
|
|
107
|
+
* (delete all if anchor active, restore all if anchor deleted).
|
|
108
|
+
*/
|
|
109
|
+
deleteRange: (indices: number[]) => void;
|
|
110
|
+
/** Cut words at the given indices into the clipboard (marks them deleted) */
|
|
111
|
+
cut: (indices: number[]) => void;
|
|
112
|
+
/** Copy non-deleted words at the given indices into the clipboard */
|
|
113
|
+
copy: (indices: number[]) => void;
|
|
114
|
+
/**
|
|
115
|
+
* Paste the clipboard at a word index. `before` (default true) inserts
|
|
116
|
+
* before the word at `atIndex`; false inserts after it. Returns the index
|
|
117
|
+
* where the first pasted word landed, or null if the clipboard was empty.
|
|
118
|
+
*/
|
|
119
|
+
paste: (atIndex: number, before?: boolean) => number | null;
|
|
120
|
+
/** Replace a word's text (no-op if unchanged) */
|
|
121
|
+
setWordText: (index: number, text: string) => void;
|
|
122
|
+
/** Split a silence at the given index into segments with the given durations (seconds) */
|
|
123
|
+
splitSilence: (index: number, durationsSec: number[]) => void;
|
|
124
|
+
/** Toggle the word at the index deleted (editor "delete" action) */
|
|
125
|
+
deleteWord: (index: number) => void;
|
|
126
|
+
/** Replace the entire edit state from an external source (e.g. an editable script view); undo-safe */
|
|
127
|
+
replaceEditedWords: (next: EditableWord[]) => void;
|
|
128
|
+
/** Mark matching filler words (and silences above the given seconds threshold) deleted */
|
|
129
|
+
removeFillers: (fillerWords: string[], removeSilenceAboveSec: number | null) => void;
|
|
130
|
+
/** Rebuild silence detection with new thresholds, preserving word deletions */
|
|
131
|
+
setSilenceThresholds: (minMs: number, nlMs: number) => void;
|
|
132
|
+
/** Current silence detection thresholds (ms) */
|
|
133
|
+
silenceThresholds: {
|
|
134
|
+
minSilenceMs: number;
|
|
135
|
+
nlSilenceMs: number;
|
|
136
|
+
};
|
|
137
|
+
/** Speed markers, sorted by word index */
|
|
138
|
+
speedMarkers: SpeedMarker[];
|
|
139
|
+
/** Default playback speed when no marker applies */
|
|
140
|
+
defaultSpeed: PlaybackSpeed;
|
|
141
|
+
/** Set the default playback speed */
|
|
142
|
+
setDefaultSpeed: (speed: PlaybackSpeed) => void;
|
|
143
|
+
/** Add/update/remove a speed marker (re-selecting the same speed removes it) */
|
|
144
|
+
toggleSpeedMarker: (index: number, speed: PlaybackSpeed) => void;
|
|
145
|
+
/** Remove the speed marker at a word index, if any */
|
|
146
|
+
removeSpeedMarker: (index: number) => void;
|
|
147
|
+
/** Effective playback speed at a word index */
|
|
148
|
+
getSpeedAtIndex: (index: number) => PlaybackSpeed;
|
|
149
|
+
/** The speed marker at a word index, if any */
|
|
150
|
+
getSpeedMarkerAtIndex: (index: number) => SpeedMarker | undefined;
|
|
151
|
+
/** Contiguous playback segments built from the edited timeline */
|
|
152
|
+
playbackSegments: PlaybackSegment[];
|
|
153
|
+
/** Word/silence counts and speed-adjusted edited duration */
|
|
154
|
+
stats: TranscriptEditStats;
|
|
155
|
+
/** Filler word and silence analysis for removal UIs */
|
|
156
|
+
fillerAnalysis: FillerAnalysis;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Headless transcript-editing hook. See module docs for scope.
|
|
160
|
+
*
|
|
161
|
+
* Undo semantics: each mutation snapshots the full `editedWords` array onto
|
|
162
|
+
* a single-level stack before applying; `undo` pops one snapshot. Speed
|
|
163
|
+
* markers and silence-threshold changes are NOT part of the undo stack
|
|
164
|
+
* (matching the source component).
|
|
165
|
+
*/
|
|
166
|
+
declare function useTranscriptEdits(options: UseTranscriptEditsOptions): UseTranscriptEditsResult;
|
|
167
|
+
|
|
168
|
+
export { DEFAULT_FILLER_WORDS as D, type FillerAnalysis as F, type SilenceThresholdCount as S, type TranscriptClipboard as T, type UseTranscriptEditsOptions as U, DEFAULT_MIN_SILENCE_MS as a, DEFAULT_NL_SILENCE_MS as b, type TranscriptEditStats as c, type UseTranscriptEditsResult as d, buildPlaybackSegments as e, insertSilences as f, getSpeedAtIndex as g, initEditableWords as i, useTranscriptEdits as u };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mieweb/ui",
|
|
3
|
-
"version": "0.6.1-dev.
|
|
3
|
+
"version": "0.6.1-dev.171",
|
|
4
4
|
"description": "A themeable, accessible React component library built with Tailwind CSS",
|
|
5
5
|
"author": "Medical Informatics Engineering, Inc.",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/ThemeProvider/ThemeProvider.tsx","../src/components/ThemeProvider/ThemeToggle.tsx"],"names":["jsx","React2"],"mappings":"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","file":"chunk-I56VBDSC.js","sourcesContent":["import * as React from 'react';\nimport { useTheme, type Theme, type ResolvedTheme } from '../../hooks/useTheme';\n\nexport interface ThemeProviderContextValue {\n theme: Theme;\n setTheme: (theme: Theme) => void;\n resolvedTheme: ResolvedTheme;\n}\n\nconst ThemeProviderContext = React.createContext<\n ThemeProviderContextValue | undefined\n>(undefined);\n\nexport interface ThemeProviderProps {\n children: React.ReactNode;\n /** Default theme to use on first load */\n defaultTheme?: Theme;\n /** Storage key for persisting theme */\n storageKey?: string;\n}\n\n/**\n * Provider component that wraps your app to enable theme switching.\n *\n * @example\n * ```tsx\n * function App() {\n * return (\n * <ThemeProvider defaultTheme=\"system\">\n * <YourApp />\n * </ThemeProvider>\n * );\n * }\n * ```\n */\nfunction ThemeProvider({\n children,\n defaultTheme: _defaultTheme = 'system',\n}: ThemeProviderProps) {\n const { theme, setTheme, resolvedTheme } = useTheme();\n\n const value = React.useMemo(\n () => ({ theme, setTheme, resolvedTheme }),\n [theme, setTheme, resolvedTheme]\n );\n\n return (\n <ThemeProviderContext.Provider value={value}>\n {children}\n </ThemeProviderContext.Provider>\n );\n}\n\nThemeProvider.displayName = 'ThemeProvider';\n\n/**\n * Hook to access the theme context.\n *\n * @example\n * ```tsx\n * function ThemeToggle() {\n * const { theme, setTheme, resolvedTheme } = useThemeContext();\n * return (\n * <button onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}>\n * Toggle theme\n * </button>\n * );\n * }\n * ```\n */\nfunction useThemeContext(): ThemeProviderContextValue {\n const context = React.useContext(ThemeProviderContext);\n if (!context) {\n throw new Error('useThemeContext must be used within a ThemeProvider');\n }\n return context;\n}\n\nexport { ThemeProvider, ThemeProviderContext, useThemeContext };\n","import * as React from 'react';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from '../../utils/cn';\nimport { useTheme, type Theme } from '../../hooks/useTheme';\nimport { Tooltip } from '../Tooltip';\n\n// ============================================================================\n// Theme Toggle Variants\n// ============================================================================\n\nconst themeToggleVariants = cva(\n [\n 'inline-flex items-center justify-center',\n 'rounded-md border transition-colors',\n 'focus:outline-none focus:ring-2 focus:ring-primary-500/40',\n ],\n {\n variants: {\n size: {\n sm: 'h-7 w-7',\n md: 'h-8 w-8',\n lg: 'h-9 w-9',\n },\n variant: {\n default: [\n 'border-neutral-300 bg-white text-neutral-600 shadow-sm',\n 'hover:bg-neutral-100',\n 'dark:border-neutral-600 dark:bg-neutral-800 dark:text-neutral-300 dark:hover:bg-neutral-700',\n ],\n ghost: [\n 'border-transparent bg-transparent text-neutral-600',\n 'hover:bg-neutral-100',\n 'dark:text-neutral-300 dark:hover:bg-neutral-700',\n ],\n },\n },\n defaultVariants: {\n size: 'md',\n variant: 'default',\n },\n }\n);\n\nconst themeToggleIconVariants = cva('', {\n variants: {\n size: {\n sm: 'h-3.5 w-3.5',\n md: 'h-4 w-4',\n lg: 'h-5 w-5',\n },\n },\n defaultVariants: {\n size: 'md',\n },\n});\n\n// ============================================================================\n// Icon Components\n// ============================================================================\n\ninterface IconProps {\n className?: string;\n}\n\nconst SunIcon: React.FC<IconProps> = ({ className }) => (\n <svg\n className={className}\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n strokeWidth={2}\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n d=\"M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z\"\n />\n </svg>\n);\n\nconst MoonIcon: React.FC<IconProps> = ({ className }) => (\n <svg\n className={className}\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n strokeWidth={2}\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n d=\"M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z\"\n />\n </svg>\n);\n\nconst SystemIcon: React.FC<IconProps> = ({ className }) => (\n <svg\n className={className}\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n strokeWidth={2}\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n d=\"M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z\"\n />\n </svg>\n);\n\n// ============================================================================\n// ThemeToggle Component\n// ============================================================================\n\nexport interface ThemeToggleProps\n extends\n Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'>,\n VariantProps<typeof themeToggleVariants> {\n /**\n * What themes to cycle through.\n * - 'two-way': Toggle between light and dark only\n * - 'three-way': Cycle through light → dark → system\n * @default 'two-way'\n */\n mode?: 'two-way' | 'three-way';\n /**\n * Whether to show a tooltip with the current/next theme\n * @default true\n */\n showTooltip?: boolean;\n /**\n * Tooltip placement\n * @default 'bottom'\n */\n tooltipPlacement?: 'top' | 'right' | 'bottom' | 'left';\n /**\n * Delay before showing tooltip in ms\n * @default 140\n */\n tooltipDelay?: number;\n /**\n * Custom icon for light theme\n */\n lightIcon?: React.ReactNode;\n /**\n * Custom icon for dark theme\n */\n darkIcon?: React.ReactNode;\n /**\n * Custom icon for system theme (only used in three-way mode)\n */\n systemIcon?: React.ReactNode;\n}\n\n/**\n * An accessible theme toggle button that switches between light/dark modes.\n *\n * Uses the mieweb-ui useTheme hook for state management and persists\n * the user's preference to localStorage.\n *\n * @example\n * ```tsx\n * // Simple two-way toggle (light/dark)\n * <ThemeToggle />\n *\n * // Three-way cycle (light → dark → system)\n * <ThemeToggle mode=\"three-way\" />\n *\n * // With custom styling\n * <ThemeToggle size=\"lg\" variant=\"ghost\" />\n *\n * // Without tooltip\n * <ThemeToggle showTooltip={false} />\n * ```\n */\nconst ThemeToggle = React.forwardRef<HTMLButtonElement, ThemeToggleProps>(\n (\n {\n className,\n size,\n variant,\n mode = 'two-way',\n showTooltip = true,\n tooltipPlacement = 'bottom',\n tooltipDelay = 140,\n lightIcon,\n darkIcon,\n systemIcon,\n ...props\n },\n ref\n ) => {\n const { theme, setTheme, resolvedTheme } = useTheme();\n\n const handleClick = React.useCallback(() => {\n if (mode === 'two-way') {\n // Simple toggle between light and dark\n setTheme(resolvedTheme === 'dark' ? 'light' : 'dark');\n } else {\n // Three-way cycle: light → dark → system\n const nextTheme: Record<Theme, Theme> = {\n light: 'dark',\n dark: 'system',\n system: 'light',\n };\n setTheme(nextTheme[theme]);\n }\n }, [mode, theme, resolvedTheme, setTheme]);\n\n const getLabel = () => {\n if (mode === 'two-way') {\n return resolvedTheme === 'dark'\n ? 'Switch to light mode'\n : 'Switch to dark mode';\n }\n // Three-way mode - show what the next theme will be\n const nextThemeLabels: Record<Theme, string> = {\n light: 'Switch to dark mode',\n dark: 'Switch to system theme',\n system: 'Switch to light mode',\n };\n return nextThemeLabels[theme];\n };\n\n const getCurrentIcon = () => {\n if (mode === 'three-way' && theme === 'system') {\n return (\n systemIcon || (\n <SystemIcon className={themeToggleIconVariants({ size })} />\n )\n );\n }\n\n if (resolvedTheme === 'dark') {\n return (\n darkIcon || <SunIcon className={themeToggleIconVariants({ size })} />\n );\n }\n\n return (\n lightIcon || <MoonIcon className={themeToggleIconVariants({ size })} />\n );\n };\n\n const label = getLabel();\n\n const button = (\n <button\n ref={ref}\n type=\"button\"\n aria-label={label}\n onClick={handleClick}\n className={cn(themeToggleVariants({ size, variant }), className)}\n {...props}\n >\n {getCurrentIcon()}\n </button>\n );\n\n if (showTooltip) {\n return (\n <Tooltip\n content={label}\n placement={tooltipPlacement}\n delay={tooltipDelay}\n >\n {button}\n </Tooltip>\n );\n }\n\n return button;\n }\n);\n\nThemeToggle.displayName = 'ThemeToggle';\n\nexport { ThemeToggle, themeToggleVariants, themeToggleIconVariants };\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/ThemeProvider/ThemeProvider.tsx","../src/components/ThemeProvider/ThemeToggle.tsx"],"names":["React","useTheme","jsx","cva","React2","cn","Tooltip"],"mappings":"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","file":"chunk-JHPFOG2N.cjs","sourcesContent":["import * as React from 'react';\nimport { useTheme, type Theme, type ResolvedTheme } from '../../hooks/useTheme';\n\nexport interface ThemeProviderContextValue {\n theme: Theme;\n setTheme: (theme: Theme) => void;\n resolvedTheme: ResolvedTheme;\n}\n\nconst ThemeProviderContext = React.createContext<\n ThemeProviderContextValue | undefined\n>(undefined);\n\nexport interface ThemeProviderProps {\n children: React.ReactNode;\n /** Default theme to use on first load */\n defaultTheme?: Theme;\n /** Storage key for persisting theme */\n storageKey?: string;\n}\n\n/**\n * Provider component that wraps your app to enable theme switching.\n *\n * @example\n * ```tsx\n * function App() {\n * return (\n * <ThemeProvider defaultTheme=\"system\">\n * <YourApp />\n * </ThemeProvider>\n * );\n * }\n * ```\n */\nfunction ThemeProvider({\n children,\n defaultTheme: _defaultTheme = 'system',\n}: ThemeProviderProps) {\n const { theme, setTheme, resolvedTheme } = useTheme();\n\n const value = React.useMemo(\n () => ({ theme, setTheme, resolvedTheme }),\n [theme, setTheme, resolvedTheme]\n );\n\n return (\n <ThemeProviderContext.Provider value={value}>\n {children}\n </ThemeProviderContext.Provider>\n );\n}\n\nThemeProvider.displayName = 'ThemeProvider';\n\n/**\n * Hook to access the theme context.\n *\n * @example\n * ```tsx\n * function ThemeToggle() {\n * const { theme, setTheme, resolvedTheme } = useThemeContext();\n * return (\n * <button onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}>\n * Toggle theme\n * </button>\n * );\n * }\n * ```\n */\nfunction useThemeContext(): ThemeProviderContextValue {\n const context = React.useContext(ThemeProviderContext);\n if (!context) {\n throw new Error('useThemeContext must be used within a ThemeProvider');\n }\n return context;\n}\n\nexport { ThemeProvider, ThemeProviderContext, useThemeContext };\n","import * as React from 'react';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from '../../utils/cn';\nimport { useTheme, type Theme } from '../../hooks/useTheme';\nimport { Tooltip } from '../Tooltip';\n\n// ============================================================================\n// Theme Toggle Variants\n// ============================================================================\n\nconst themeToggleVariants = cva(\n [\n 'inline-flex items-center justify-center',\n 'rounded-md border transition-colors',\n 'focus:outline-none focus:ring-2 focus:ring-primary-500/40',\n ],\n {\n variants: {\n size: {\n sm: 'h-7 w-7',\n md: 'h-8 w-8',\n lg: 'h-9 w-9',\n },\n variant: {\n default: [\n 'border-neutral-300 bg-white text-neutral-600 shadow-sm',\n 'hover:bg-neutral-100',\n 'dark:border-neutral-600 dark:bg-neutral-800 dark:text-neutral-300 dark:hover:bg-neutral-700',\n ],\n ghost: [\n 'border-transparent bg-transparent text-neutral-600',\n 'hover:bg-neutral-100',\n 'dark:text-neutral-300 dark:hover:bg-neutral-700',\n ],\n },\n },\n defaultVariants: {\n size: 'md',\n variant: 'default',\n },\n }\n);\n\nconst themeToggleIconVariants = cva('', {\n variants: {\n size: {\n sm: 'h-3.5 w-3.5',\n md: 'h-4 w-4',\n lg: 'h-5 w-5',\n },\n },\n defaultVariants: {\n size: 'md',\n },\n});\n\n// ============================================================================\n// Icon Components\n// ============================================================================\n\ninterface IconProps {\n className?: string;\n}\n\nconst SunIcon: React.FC<IconProps> = ({ className }) => (\n <svg\n className={className}\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n strokeWidth={2}\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n d=\"M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z\"\n />\n </svg>\n);\n\nconst MoonIcon: React.FC<IconProps> = ({ className }) => (\n <svg\n className={className}\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n strokeWidth={2}\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n d=\"M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z\"\n />\n </svg>\n);\n\nconst SystemIcon: React.FC<IconProps> = ({ className }) => (\n <svg\n className={className}\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n strokeWidth={2}\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n d=\"M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z\"\n />\n </svg>\n);\n\n// ============================================================================\n// ThemeToggle Component\n// ============================================================================\n\nexport interface ThemeToggleProps\n extends\n Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'>,\n VariantProps<typeof themeToggleVariants> {\n /**\n * What themes to cycle through.\n * - 'two-way': Toggle between light and dark only\n * - 'three-way': Cycle through light → dark → system\n * @default 'two-way'\n */\n mode?: 'two-way' | 'three-way';\n /**\n * Whether to show a tooltip with the current/next theme\n * @default true\n */\n showTooltip?: boolean;\n /**\n * Tooltip placement\n * @default 'bottom'\n */\n tooltipPlacement?: 'top' | 'right' | 'bottom' | 'left';\n /**\n * Delay before showing tooltip in ms\n * @default 140\n */\n tooltipDelay?: number;\n /**\n * Custom icon for light theme\n */\n lightIcon?: React.ReactNode;\n /**\n * Custom icon for dark theme\n */\n darkIcon?: React.ReactNode;\n /**\n * Custom icon for system theme (only used in three-way mode)\n */\n systemIcon?: React.ReactNode;\n}\n\n/**\n * An accessible theme toggle button that switches between light/dark modes.\n *\n * Uses the mieweb-ui useTheme hook for state management and persists\n * the user's preference to localStorage.\n *\n * @example\n * ```tsx\n * // Simple two-way toggle (light/dark)\n * <ThemeToggle />\n *\n * // Three-way cycle (light → dark → system)\n * <ThemeToggle mode=\"three-way\" />\n *\n * // With custom styling\n * <ThemeToggle size=\"lg\" variant=\"ghost\" />\n *\n * // Without tooltip\n * <ThemeToggle showTooltip={false} />\n * ```\n */\nconst ThemeToggle = React.forwardRef<HTMLButtonElement, ThemeToggleProps>(\n (\n {\n className,\n size,\n variant,\n mode = 'two-way',\n showTooltip = true,\n tooltipPlacement = 'bottom',\n tooltipDelay = 140,\n lightIcon,\n darkIcon,\n systemIcon,\n ...props\n },\n ref\n ) => {\n const { theme, setTheme, resolvedTheme } = useTheme();\n\n const handleClick = React.useCallback(() => {\n if (mode === 'two-way') {\n // Simple toggle between light and dark\n setTheme(resolvedTheme === 'dark' ? 'light' : 'dark');\n } else {\n // Three-way cycle: light → dark → system\n const nextTheme: Record<Theme, Theme> = {\n light: 'dark',\n dark: 'system',\n system: 'light',\n };\n setTheme(nextTheme[theme]);\n }\n }, [mode, theme, resolvedTheme, setTheme]);\n\n const getLabel = () => {\n if (mode === 'two-way') {\n return resolvedTheme === 'dark'\n ? 'Switch to light mode'\n : 'Switch to dark mode';\n }\n // Three-way mode - show what the next theme will be\n const nextThemeLabels: Record<Theme, string> = {\n light: 'Switch to dark mode',\n dark: 'Switch to system theme',\n system: 'Switch to light mode',\n };\n return nextThemeLabels[theme];\n };\n\n const getCurrentIcon = () => {\n if (mode === 'three-way' && theme === 'system') {\n return (\n systemIcon || (\n <SystemIcon className={themeToggleIconVariants({ size })} />\n )\n );\n }\n\n if (resolvedTheme === 'dark') {\n return (\n darkIcon || <SunIcon className={themeToggleIconVariants({ size })} />\n );\n }\n\n return (\n lightIcon || <MoonIcon className={themeToggleIconVariants({ size })} />\n );\n };\n\n const label = getLabel();\n\n const button = (\n <button\n ref={ref}\n type=\"button\"\n aria-label={label}\n onClick={handleClick}\n className={cn(themeToggleVariants({ size, variant }), className)}\n {...props}\n >\n {getCurrentIcon()}\n </button>\n );\n\n if (showTooltip) {\n return (\n <Tooltip\n content={label}\n placement={tooltipPlacement}\n delay={tooltipDelay}\n >\n {button}\n </Tooltip>\n );\n }\n\n return button;\n }\n);\n\nThemeToggle.displayName = 'ThemeToggle';\n\nexport { ThemeToggle, themeToggleVariants, themeToggleIconVariants };\n"]}
|