@joewinke/jatui 0.46.0 → 0.48.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.
Files changed (39) hide show
  1. package/README.md +46 -0
  2. package/dist/ambience/README.md +131 -0
  3. package/dist/ambience/breath.d.ts +22 -0
  4. package/dist/ambience/breath.js +43 -0
  5. package/dist/ambience/default.d.ts +24 -0
  6. package/dist/ambience/default.js +34 -0
  7. package/dist/ambience/engine.d.ts +21 -0
  8. package/dist/ambience/engine.js +313 -0
  9. package/dist/ambience/index.d.ts +23 -0
  10. package/dist/ambience/index.js +27 -0
  11. package/dist/ambience/master.d.ts +49 -0
  12. package/dist/ambience/master.js +83 -0
  13. package/dist/ambience/persist.d.ts +11 -0
  14. package/dist/ambience/persist.js +24 -0
  15. package/dist/ambience/registry.d.ts +11 -0
  16. package/dist/ambience/registry.js +16 -0
  17. package/dist/ambience/scapes/focus.d.ts +55 -0
  18. package/dist/ambience/scapes/focus.js +245 -0
  19. package/dist/ambience/scapes/hibiscus.d.ts +103 -0
  20. package/dist/ambience/scapes/hibiscus.js +345 -0
  21. package/dist/ambience/scapes/nature.d.ts +36 -0
  22. package/dist/ambience/scapes/nature.js +325 -0
  23. package/dist/ambience/scapes/noise.d.ts +77 -0
  24. package/dist/ambience/scapes/noise.js +226 -0
  25. package/dist/ambience/scapes/playlist.d.ts +129 -0
  26. package/dist/ambience/scapes/playlist.js +569 -0
  27. package/dist/ambience/types.d.ts +80 -0
  28. package/dist/ambience/types.js +9 -0
  29. package/dist/components/AmbienceToggle.svelte +599 -0
  30. package/dist/components/AmbienceToggle.svelte.d.ts +74 -0
  31. package/dist/components/VideoPlayer.svelte +73 -6
  32. package/dist/components/VideoPlayer.svelte.d.ts +9 -0
  33. package/dist/index.d.ts +6 -0
  34. package/dist/index.js +9 -0
  35. package/dist/video/keys.d.ts +4 -3
  36. package/dist/video/keys.js +5 -4
  37. package/dist/video/settings/remembered.d.ts +9 -0
  38. package/dist/video/settings/remembered.js +9 -0
  39. package/package.json +6 -1
@@ -0,0 +1,74 @@
1
+ /**
2
+ * AmbienceToggle — the audio twin of ThemeToggle (jatui-b7qvr): one small
3
+ * speaker button for the app shell over the headless ambience engine
4
+ * (`createAmbience()` from `src/lib/ambience/`).
5
+ *
6
+ * Interaction contract:
7
+ * - Click (or Space/Enter) = play / stop the last scape. A gesture-gated
8
+ * engine call, made synchronously inside the click handler.
9
+ * - The scape menu (menu button pattern) opens with ArrowDown/ArrowUp,
10
+ * a press-and-hold (~450 ms) or a right-click (Android/Chrome fire
11
+ * contextmenu on touch holds; iOS may cancel the pointer instead —
12
+ * the keyboard path always works). It lists every
13
+ * registered scape grouped meditation / focus / noise / nature / music
14
+ * (`menuitemradio` items, current scape checked), carries a volume
15
+ * slider wired to `engine.setVolume`, and shows each scape's
16
+ * `description` as its secondary line — playlist scapes put their
17
+ * track attribution there.
18
+ *
19
+ * A11y: the trigger is `aria-pressed` while playing and carries the full
20
+ * menu-button wiring (`aria-expanded` / `aria-haspopup` / `aria-controls`).
21
+ * An sr-only description teaches the menu gestures, and an `aria-live`
22
+ * status line announces state changes ("Ambience: Hibiscus, playing").
23
+ * The speaker glyph morphs off↔on on the motion tokens and, while playing,
24
+ * breathes with `engine.onBreath` — ambient motion, so it is static under
25
+ * `prefers-reduced-motion` and `data-motion="calm"`.
26
+ *
27
+ * Token-only theming (DaisyUI + `--jatui-*` + `--motion-*`). SSR-safe: the
28
+ * engine is lazy and nothing here touches `window` at import.
29
+ */
30
+ import type { Ambience, Scape, ScapeGroup } from '../ambience/types';
31
+ /** Canonical menu group order (the Scape contract's groups). */
32
+ export declare const AMBIENCE_GROUP_ORDER: readonly ScapeGroup[];
33
+ /** Human labels for the canonical groups. */
34
+ export declare const GROUP_LABELS: Readonly<Record<ScapeGroup, string>>;
35
+ /** One grouped section of the scape menu. */
36
+ export interface AmbienceScapeGroup {
37
+ group: ScapeGroup;
38
+ label: string;
39
+ scapes: Scape[];
40
+ }
41
+ /**
42
+ * Group scapes for the menu: canonical group order, registration order
43
+ * within a group, empty groups omitted. A group outside the union (a
44
+ * registry from a newer engine) still renders, after the known ones, in
45
+ * first-seen order — a menu never silently drops a registered scape.
46
+ */
47
+ export declare function groupScapes(scapes: Scape[]): AmbienceScapeGroup[];
48
+ /**
49
+ * The live label — "Ambience: Hibiscus, playing" while sounding,
50
+ * "Ambience: Hibiscus, stopped" once a scape has been selected but is not
51
+ * playing, "Ambience: off" before anything was selected.
52
+ */
53
+ export declare function ambienceLiveLabel(currentLabel: string | null, playing: boolean): string;
54
+ /** Transient failure label — a rejected engine.play() (gesture probe,
55
+ * context suspend, unknown scape) must never surface as an unhandled
56
+ * rejection OR as silence: the trigger visibly fails to respond, so the
57
+ * sr-only status explains it. Cleared by the next successful play. */
58
+ export declare const PLAY_ERROR_LABEL = "Ambience: couldn't start \u2014 tap again";
59
+ /**
60
+ * Glyph scale for a breath value (0..1): the speaker grows on the inhale
61
+ * and settles on the exhale — 6% total, clamped against out-of-range input.
62
+ */
63
+ export declare function breathScale(value: number): number;
64
+ type $$ComponentProps = {
65
+ /** The engine instance (from `createAmbience()`). The component never creates one, and never kills it — the host owns the engine's lifecycle. */
66
+ engine: Ambience;
67
+ /** Smaller app-shell footprint: 40px trigger with an 18px glyph (default 44px / 20px). Never below the 40px touch floor. */
68
+ compact?: boolean;
69
+ /** Which way the menu opens from the trigger. Auto-flips vertically when the viewport has no room on that side. */
70
+ placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
71
+ };
72
+ declare const AmbienceToggle: import("svelte").Component<$$ComponentProps, {}, "">;
73
+ type AmbienceToggle = ReturnType<typeof AmbienceToggle>;
74
+ export default AmbienceToggle;
@@ -265,10 +265,12 @@
265
265
  SETTINGS_WRITE_DELAY_MS,
266
266
  VIDEO_SETTINGS,
267
267
  REMEMBERED_MUTED,
268
+ REMEMBERED_TRANSCRIPT,
268
269
  REMEMBERED_VOLUME,
269
270
  type SettingAvailability,
270
271
  type SettingsSession,
271
272
  type SettingValue,
273
+ type TranscriptLayout,
272
274
  type VideoSettingsStore
273
275
  } from '../video/settings';
274
276
  import SettingsPanel from '../video/settings/SettingsPanel.svelte';
@@ -458,6 +460,15 @@
458
460
  talkTime?: VideoSpeakerTalkTime[];
459
461
  /** Seek here once metadata lands (a queued seek never fires on a later source). */
460
462
  startAt?: number | null;
463
+ /**
464
+ * Where the transcript sits when the viewer has not chosen otherwise —
465
+ * `'below'` the player, or beside it (`'side'`) on wide viewports. The
466
+ * viewer's Shift+T cycles the layout: side / below / hidden (E24,
467
+ * jatui-kgsxf) and the choice is remembered per viewer under the
468
+ * `player.transcript` setting — through `settingsStore` when one is
469
+ * given, else this browser's localStorage (every access guarded). A
470
+ * remembered choice wins over this prop.
471
+ */
461
472
  transcriptPosition?: 'below' | 'side';
462
473
  /**
463
474
  * CSS length bounding the transcript pane's height — it scrolls inside
@@ -924,7 +935,14 @@
924
935
  let baseRate = $state(1);
925
936
 
926
937
  // ── Transcript + help overlays ──────────────────────────────────────────
927
- let transcriptVisible = $state(true);
938
+ /**
939
+ * The viewer's transcript layout (E24, jatui-kgsxf): the host's
940
+ * `transcriptPosition` prop is the default; a remembered choice wins over
941
+ * it. The side layout is the beside-the-video grid, below the pane under
942
+ * the player, hidden — no pane at all.
943
+ */
944
+ let transcriptChoice = $state<TranscriptLayout | null>(null);
945
+ const transcriptLayout = $derived(transcriptChoice ?? transcriptPosition);
928
946
  let keysOpen = $state(false);
929
947
 
930
948
  // ── Transcript search (jatui-xmtnv) ────────────────────────────────
@@ -2386,6 +2404,54 @@
2386
2404
  flashOsd(next ? 'Theater mode' : 'Default view');
2387
2405
  }
2388
2406
 
2407
+ // ── Transcript layout cycle (E24, jatui-kgsxf) ─────────────────────────
2408
+ // The extension's T cycles three layouts: beside the video, below, hidden
2409
+ // (library.js:6137, 9346). The host's `transcriptPosition` prop is the
2410
+ // default; a remembered choice wins over it, and survives a reload — the
2411
+ // same store split as the media state (the host's `settingsStore`, else
2412
+ // this browser's localStorage), `settings/remembered.ts` holds the id and
2413
+ // the never-throws coercion.
2414
+ const TRANSCRIPT_CYCLE = ['side', 'below', 'hidden'] as const;
2415
+ const localTranscriptStore = localSettingsStore();
2416
+ let transcriptTouched = false;
2417
+
2418
+ function cycleTranscript() {
2419
+ if (sortedSegments.length === 0) return;
2420
+ const idx = TRANSCRIPT_CYCLE.indexOf(transcriptLayout);
2421
+ const next = TRANSCRIPT_CYCLE[(idx + 1) % TRANSCRIPT_CYCLE.length];
2422
+ transcriptChoice = next;
2423
+ rememberTranscriptLayout(next);
2424
+ flashOsd(
2425
+ next === 'side' ? 'Transcript beside' : next === 'below' ? 'Transcript below' : 'Transcript hidden'
2426
+ );
2427
+ }
2428
+
2429
+ /** A viewer choice from outside the panel (Shift+T) — saved like one. */
2430
+ function rememberTranscriptLayout(v: TranscriptLayout) {
2431
+ transcriptTouched = true;
2432
+ writeRemembered(settingsStore ?? localTranscriptStore, REMEMBERED_TRANSCRIPT.id, v);
2433
+ }
2434
+
2435
+ // The load-time read, once per store identity: a junk or throwing store
2436
+ // reads as nothing stored, and a choice made before the read resolves wins
2437
+ // (the settings session's rule, mirrored here).
2438
+ $effect(() => {
2439
+ const store = settingsStore ?? localTranscriptStore;
2440
+ let cancelled = false;
2441
+ untrack(() => {
2442
+ void Promise.resolve(store.get(REMEMBERED_TRANSCRIPT.id))
2443
+ .then((raw) => {
2444
+ if (cancelled || transcriptTouched) return;
2445
+ const v = REMEMBERED_TRANSCRIPT.coerce(raw);
2446
+ if (v !== undefined) transcriptChoice = v;
2447
+ })
2448
+ .catch(() => {});
2449
+ });
2450
+ return () => {
2451
+ cancelled = true;
2452
+ };
2453
+ });
2454
+
2389
2455
  // ── Remembered volume & mute (E12, jatui-xz6so) ──────────────────────
2390
2456
  // Extension library.js:5768 — the volume level and the mute choice survive
2391
2457
  // a reload, persisted through the same store the settings registry writes
@@ -3160,8 +3226,7 @@
3160
3226
  case 'T':
3161
3227
  if (sortedSegments.length > 0) {
3162
3228
  e.preventDefault();
3163
- transcriptVisible = !transcriptVisible;
3164
- flashOsd(transcriptVisible ? 'Transcript shown' : 'Transcript hidden');
3229
+ cycleTranscript();
3165
3230
  }
3166
3231
  return;
3167
3232
  case '/':
@@ -3317,7 +3382,9 @@
3317
3382
  * it focus once it mounts — the effect below lands it. */
3318
3383
  function openSearch() {
3319
3384
  if (sortedSegments.length === 0) return;
3320
- transcriptVisible = true;
3385
+ // Showing the pane for the search is transient — the viewer's own
3386
+ // layout choice (possibly hidden) is not overwritten.
3387
+ if (transcriptLayout === 'hidden') transcriptChoice = transcriptPosition;
3321
3388
  searchOpen = true;
3322
3389
  searchMatchIdx = -1;
3323
3390
  }
@@ -3447,7 +3514,7 @@
3447
3514
  <div
3448
3515
  class="vp-root {className}"
3449
3516
  bind:this={rootEl}
3450
- class:vp-side={transcriptPosition === 'side' && !theaterOn && sortedSegments.length > 0 && transcriptVisible}
3517
+ class:vp-side={transcriptLayout === 'side' && !theaterOn && sortedSegments.length > 0}
3451
3518
  class:vp-theater={theaterOn}
3452
3519
  data-theater={theaterAvailable ? String(theaterOn) : undefined}
3453
3520
  class:vp-rail-side={railOn}
@@ -4097,7 +4164,7 @@
4097
4164
  <div class="vp-below-stage">{@render belowStage()}</div>
4098
4165
  {/if}
4099
4166
 
4100
- {#if sortedSegments.length > 0 && transcriptVisible}
4167
+ {#if sortedSegments.length > 0 && transcriptLayout !== 'hidden'}
4101
4168
  <!-- Height-bounded (jatui-ap4oz): the pane scrolls itself and never
4102
4169
  the page, so the video stays in frame. Below the player the bound
4103
4170
  is `transcriptMaxHeight` (default min(40vh, 28rem)); beside it, the
@@ -167,6 +167,15 @@ type $$ComponentProps = {
167
167
  talkTime?: VideoSpeakerTalkTime[];
168
168
  /** Seek here once metadata lands (a queued seek never fires on a later source). */
169
169
  startAt?: number | null;
170
+ /**
171
+ * Where the transcript sits when the viewer has not chosen otherwise —
172
+ * `'below'` the player, or beside it (`'side'`) on wide viewports. The
173
+ * viewer's Shift+T cycles the layout: side / below / hidden (E24,
174
+ * jatui-kgsxf) and the choice is remembered per viewer under the
175
+ * `player.transcript` setting — through `settingsStore` when one is
176
+ * given, else this browser's localStorage (every access guarded). A
177
+ * remembered choice wins over this prop.
178
+ */
170
179
  transcriptPosition?: 'below' | 'side';
171
180
  /**
172
181
  * CSS length bounding the transcript pane's height — it scrolls inside
package/dist/index.d.ts CHANGED
@@ -405,3 +405,9 @@ export * from './data/grid/index';
405
405
  export { default as ImageReveal } from './components/ImageReveal.svelte';
406
406
  export { imageRevealPhase, imageRevealBusy, imageRevealInitials, imageRevealSize, imageRevealTone, IMAGE_REVEAL_SIZES, IMAGE_REVEAL_RADIUS } from './components/imageReveal';
407
407
  export type { ImageRevealPhase, ImageRevealShape, ImageRevealSizeName, ImageRevealToneName, ImageRevealState } from './components/imageReveal';
408
+ export { default as AmbienceToggle, AMBIENCE_GROUP_ORDER } from './components/AmbienceToggle.svelte';
409
+ export { createAmbience, createDefaultAmbience, DEFAULT_SCAPES, BreathClock, BREATHS_PER_MINUTE, MASTER, ScapeRegistry, hibiscusScape, focusScape, noiseScapes, brownNoiseScape, pinkNoiseScape, whiteNoiseScape, rain, waves } from './ambience';
410
+ export type { DefaultAmbienceOptions } from './ambience';
411
+ export type { Ambience, AmbienceOptions, AmbienceState, BreathState, Scape, ScapeContext, ScapeGroup, ScapeInstance } from './ambience';
412
+ export { createPlaylistScape } from './ambience';
413
+ export type { PlaylistScape, PlaylistScapeOptions, PlaylistManifest, PlaylistState, PlaylistStatus, PlaylistTrack } from './ambience';
package/dist/index.js CHANGED
@@ -451,3 +451,12 @@ export { default as SettingsRow } from './components/settings/SettingsRow.svelte
451
451
  export * from './data/grid/index';
452
452
  export { default as ImageReveal } from './components/ImageReveal.svelte';
453
453
  export { imageRevealPhase, imageRevealBusy, imageRevealInitials, imageRevealSize, imageRevealTone, IMAGE_REVEAL_SIZES, IMAGE_REVEAL_RADIUS } from './components/imageReveal';
454
+ // Ambience — generative background-sound engine (headless) + the app-shell
455
+ // AmbienceToggle. Mount the engine once at the root layout so sound survives
456
+ // navigation; createDefaultAmbience() pre-registers every shipped scape
457
+ // (hibiscus, focus, brown/pink/white noise, rain, waves).
458
+ export { default as AmbienceToggle, AMBIENCE_GROUP_ORDER } from './components/AmbienceToggle.svelte';
459
+ export { createAmbience, createDefaultAmbience, DEFAULT_SCAPES, BreathClock, BREATHS_PER_MINUTE, MASTER, ScapeRegistry, hibiscusScape, focusScape, noiseScapes, brownNoiseScape, pinkNoiseScape, whiteNoiseScape, rain, waves } from './ambience';
460
+ // Playlist scape (phase 2) — per-host, never in DEFAULT_SCAPES: build with
461
+ // createPlaylistScape() and register on the engine yourself.
462
+ export { createPlaylistScape } from './ambience';
@@ -5,9 +5,10 @@
5
5
  *
6
6
  * Map parity is with extension-library's theater handler (FEATURES.md):
7
7
  * Space play/pause, ←/→ seek, ↑/↓ volume, M mute, −/= speed, Backspace reset
8
- * speed, </> frame step, [/] chapter jumps, B bookmark, Shift+T transcript
9
- * (t is theater mode, YouTube's key — jatui-ee2ty), ?
10
- * shortcuts, Home/End, and `/` transcript search (the pane has none yet).
8
+ * speed, </> frame step, [/] chapter jumps, B bookmark, Shift+T transcript layout
9
+ * cycle (side / below / hidden — jatui-kgsxf), t is theater mode (YouTube's key
10
+ * — jatui-ee2ty), ? shortcuts, Home/End, and `/` transcript search (the pane has
11
+ * none yet).
11
12
  * Deliberately NOT ported: F/M (favourite/file-under — library-app verbs,
12
13
  * not player verbs). N (night cycle) landed with the night module
13
14
  * (mos-5lzns.33) and `,` (settings) with the settings panel
@@ -5,9 +5,10 @@
5
5
  *
6
6
  * Map parity is with extension-library's theater handler (FEATURES.md):
7
7
  * Space play/pause, ←/→ seek, ↑/↓ volume, M mute, −/= speed, Backspace reset
8
- * speed, </> frame step, [/] chapter jumps, B bookmark, Shift+T transcript
9
- * (t is theater mode, YouTube's key — jatui-ee2ty), ?
10
- * shortcuts, Home/End, and `/` transcript search (the pane has none yet).
8
+ * speed, </> frame step, [/] chapter jumps, B bookmark, Shift+T transcript layout
9
+ * cycle (side / below / hidden — jatui-kgsxf), t is theater mode (YouTube's key
10
+ * — jatui-ee2ty), ? shortcuts, Home/End, and `/` transcript search (the pane has
11
+ * none yet).
11
12
  * Deliberately NOT ported: F/M (favourite/file-under — library-app verbs,
12
13
  * not player verbs). N (night cycle) landed with the night module
13
14
  * (mos-5lzns.33) and `,` (settings) with the settings panel
@@ -33,7 +34,7 @@ export const PLAYER_KEYS = [
33
34
  { keys: 'B', description: 'Bookmark at the current time', when: 'bookmarks' },
34
35
  // t = theater, YouTube's key; the transcript moved to Shift+T (jatui-ee2ty).
35
36
  { keys: 't', description: 'Theater mode on / off', when: 'theater' },
36
- { keys: 'Shift+T', description: 'Show / hide the transcript', when: 'transcript' },
37
+ { keys: 'Shift+T', description: 'Cycle transcript layout: side / below / hidden', when: 'transcript' },
37
38
  { keys: '/', description: 'Search transcript', when: 'transcript' },
38
39
  // Shift+wheel over the transcript pane steps PLAYBACK line by line —
39
40
  // a plain wheel there scrolls the pane natively and is never claimed.
@@ -26,5 +26,14 @@ export interface RememberedSpec<T> {
26
26
  }
27
27
  /** The playback level, 0–1 on the player's 0.05 gesture grid. */
28
28
  export declare const REMEMBERED_VOLUME: RememberedSpec<number>;
29
+ /**
30
+ * The transcript layout (E24, jatui-kgsxf) — the extension's T cycles three
31
+ * layouts: beside the video, below it, hidden (library.js:6137, 9346). The
32
+ * host's `transcriptPosition` prop is the default; a remembered choice wins
33
+ * over it, and survives a reload. `default` here is only the fallback when a
34
+ * store holds nothing usable — the host's prop is the real default.
35
+ */
36
+ export type TranscriptLayout = 'side' | 'below' | 'hidden';
37
+ export declare const REMEMBERED_TRANSCRIPT: RememberedSpec<TranscriptLayout>;
29
38
  /** Whether playback starts muted. */
30
39
  export declare const REMEMBERED_MUTED: RememberedSpec<boolean>;
@@ -30,6 +30,15 @@ export const REMEMBERED_VOLUME = {
30
30
  }
31
31
  }
32
32
  };
33
+ export const REMEMBERED_TRANSCRIPT = {
34
+ id: 'player.transcript',
35
+ default: 'below',
36
+ coerce(raw) {
37
+ if (raw === 'side' || raw === 'below' || raw === 'hidden')
38
+ return raw;
39
+ return undefined;
40
+ }
41
+ };
33
42
  /** Whether playback starts muted. */
34
43
  export const REMEMBERED_MUTED = {
35
44
  id: 'player.muted',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joewinke/jatui",
3
- "version": "0.46.0",
3
+ "version": "0.48.0",
4
4
  "private": false,
5
5
  "description": "Shared Svelte 5 component library for JAT projects",
6
6
  "type": "module",
@@ -87,6 +87,11 @@
87
87
  "types": "./dist/artifact-blocks/index.d.ts",
88
88
  "svelte": "./dist/artifact-blocks/index.js",
89
89
  "default": "./dist/artifact-blocks/index.js"
90
+ },
91
+ "./ambience": {
92
+ "types": "./dist/ambience/index.d.ts",
93
+ "svelte": "./dist/ambience/index.js",
94
+ "default": "./dist/ambience/index.js"
90
95
  }
91
96
  },
92
97
  "files": [