@joewinke/jatui 0.54.0 → 0.55.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.
@@ -5,16 +5,25 @@
5
5
  *
6
6
  * Interaction contract:
7
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
8
+ * engine call, made synchronously inside the click handler. With no
9
+ * playable scape registered (links only) the click opens the menu.
10
+ * - The menu (menu button pattern) opens with ArrowDown/ArrowUp, a
11
+ * press-and-hold (~450 ms) or a right-click (Android/Chrome fire
11
12
  * 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.
13
+ * the keyboard path always works). Top level: a volume slider wired to
14
+ * `engine.setVolume`, then one row per section — Radio / Focus /
15
+ * Noise / Meditation … in the host's order (`AmbienceOptions.groups`,
16
+ * jatui-mdq2p) — each opening a submenu of its items. Right / Enter /
17
+ * Space open a submenu and move focus into it; Left / Escape close it
18
+ * and return focus to its row (the ContextMenu submenu pattern); a
19
+ * mouse hover opens a row's submenu without moving focus.
20
+ * - Scapes are `menuitemradio` (current one checked; its section row
21
+ * carries a dot and the scape's name). Link items (radio stations) are
22
+ * real `<a target="_blank">` menu items with an external-link glyph:
23
+ * the station's own site or player opens in a new tab. Nothing is
24
+ * relayed — we never proxy a third-party stream.
25
+ * - A playing playlist shows its current track's attribution (title,
26
+ * composer, license) under the playlist item and its section row.
18
27
  *
19
28
  * A11y: the trigger is `aria-pressed` while playing and carries the full
20
29
  * menu-button wiring (`aria-expanded` / `aria-haspopup` / `aria-controls`).
@@ -27,24 +36,24 @@
27
36
  * Token-only theming (DaisyUI + `--jatui-*` + `--motion-*`). SSR-safe: the
28
37
  * engine is lazy and nothing here touches `window` at import.
29
38
  */
30
- import type { Ambience, Scape, ScapeGroup } from '../ambience/types';
31
- /** Canonical menu group order (the Scape contract's groups). */
39
+ import type { AmbienceGroupDef, AmbienceItem, ScapeGroup } from '../ambience/types';
40
+ import { type AmbienceSection } from '../ambience/groups';
41
+ import { type PlaylistTrack } from '../ambience/scapes/playlist';
42
+ /** Default menu section order (when the host configures no `groups`). */
32
43
  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
- }
44
+ /** Human labels for the library's own groups. */
45
+ export declare const GROUP_LABELS: Readonly<Record<string, string>>;
46
+ /** One section of the menu: a top-level row and its submenu. */
47
+ export type AmbienceScapeGroup = AmbienceSection;
41
48
  /**
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.
49
+ * Group items for the menu: the host's section order (or the default),
50
+ * registration order within a section, empty sections omitted. A group
51
+ * missing from the order still renders, after the ordered ones, in
52
+ * first-seen order — a menu never silently drops a registered item.
46
53
  */
47
- export declare function groupScapes(scapes: Scape[]): AmbienceScapeGroup[];
54
+ export declare function groupScapes(items: AmbienceItem[], groups?: ReadonlyArray<ScapeGroup | AmbienceGroupDef>): AmbienceScapeGroup[];
55
+ /** "Gymnopédie No. 1 — Erik Satie · Public domain": a playing track's line. */
56
+ export declare function nowPlayingLine(track: PlaylistTrack | null | undefined): string;
48
57
  /**
49
58
  * The live label — "Ambience: Hibiscus, playing" while sounding,
50
59
  * "Ambience: Hibiscus, stopped" once a scape has been selected but is not
@@ -61,8 +70,30 @@ export declare const PLAY_ERROR_LABEL = "Ambience: couldn't start \u2014 tap aga
61
70
  * and settles on the exhale — 6% total, clamped against out-of-range input.
62
71
  */
63
72
  export declare function breathScale(value: number): number;
73
+ /** Submenu width (px) — matches the `w-64` class on the submenu panel. */
74
+ export declare const SUBMENU_WIDTH = 256;
75
+ /**
76
+ * Where a submenu goes, in viewport px. Beside the menu panel on the
77
+ * preferred side (the side the menu already extends to), else the other
78
+ * side, else — a phone, no room either side — overlapping the panel just
79
+ * below its row, clamped inside the viewport.
80
+ */
81
+ export declare function placeSubmenu(row: {
82
+ top: number;
83
+ bottom: number;
84
+ }, panel: {
85
+ left: number;
86
+ right: number;
87
+ }, viewport: {
88
+ width: number;
89
+ }, prefer: 'left' | 'right', width?: number, gap?: number, margin?: number): {
90
+ x: number;
91
+ y: number;
92
+ side: 'left' | 'right' | 'over';
93
+ };
94
+ import type { Ambience } from '../ambience/types';
64
95
  type $$ComponentProps = {
65
- /** The engine instance (from `createAmbience()`). The component never creates one, and never kills it — the host owns the engine's lifecycle. */
96
+ /** The engine instance (from `createAmbience()`). The component never creates one, and never kills it — the host owns the engine's lifecycle. Section order/labels come from the engine's `groups` option. */
66
97
  engine: Ambience;
67
98
  /** Smaller app-shell footprint: 40px trigger with an 18px glyph (default 44px / 20px). Never below the 40px touch floor. */
68
99
  compact?: boolean;
@@ -322,6 +322,7 @@
322
322
  max={maxDay}
323
323
  fixedWeeks
324
324
  size="sm"
325
+ fill={false}
325
326
  onSelect={choose}
326
327
  onFocusDate={cal.focus}
327
328
  />
@@ -398,6 +398,7 @@
398
398
  showOutsideDays={false}
399
399
  fixedWeeks
400
400
  size="sm"
401
+ fill={false}
401
402
  onSelect={pickDate}
402
403
  onFocusDate={handleFocusDate}
403
404
  onHover={(d) => (hover = d)}
@@ -790,6 +790,7 @@
790
790
  {locale}
791
791
  selected={date}
792
792
  size="sm"
793
+ fill={false}
793
794
  fixedWeeks
794
795
  onSelect={pickFromSwitcher}
795
796
  onFocusDate={(d) => {
@@ -38,6 +38,14 @@
38
38
  /** Tint every enabled in-month day (booking calendars: "these days are open"). */
39
39
  highlightEnabled?: boolean;
40
40
  size?: 'sm' | 'md' | 'lg';
41
+ /**
42
+ * Stretch to the container's width (default). Pass `false` inside a
43
+ * shrink-to-fit host — a popover with `width: max-content`, a flex item
44
+ * sized by its content: a `width: 100%` table's max-content is unbounded
45
+ * in Chrome, so the host grows to ~670,000px and the days land off-screen
46
+ * (jatui-ewfh3). `false` sizes the grid to seven cells and centres it.
47
+ */
48
+ fill?: boolean;
41
49
  /** Accessible name; defaults to "March 2026". */
42
50
  ariaLabel?: string;
43
51
  rtl?: boolean;
@@ -83,6 +91,7 @@
83
91
  hideWeekdays = false,
84
92
  highlightEnabled = false,
85
93
  size = 'md',
94
+ fill = true,
86
95
  ariaLabel,
87
96
  rtl = false,
88
97
  onSelect,
@@ -143,6 +152,7 @@
143
152
  <table
144
153
  bind:this={gridEl}
145
154
  class="jatui-month-grid jatui-month-grid--{size} {className}"
155
+ class:jatui-month-grid--intrinsic={!fill}
146
156
  role="grid"
147
157
  aria-label={label}
148
158
  dir={rtl ? 'rtl' : undefined}
@@ -230,6 +240,10 @@
230
240
  width: 100%;
231
241
  min-width: calc(var(--_cell) * 7);
232
242
  }
243
+ .jatui-month-grid--intrinsic {
244
+ width: calc(var(--_cell) * 7);
245
+ margin-inline: auto;
246
+ }
233
247
  .jatui-month-grid--sm {
234
248
  --_cell: 2rem;
235
249
  }
@@ -35,6 +35,14 @@ export interface MonthGridProps {
35
35
  /** Tint every enabled in-month day (booking calendars: "these days are open"). */
36
36
  highlightEnabled?: boolean;
37
37
  size?: 'sm' | 'md' | 'lg';
38
+ /**
39
+ * Stretch to the container's width (default). Pass `false` inside a
40
+ * shrink-to-fit host — a popover with `width: max-content`, a flex item
41
+ * sized by its content: a `width: 100%` table's max-content is unbounded
42
+ * in Chrome, so the host grows to ~670,000px and the days land off-screen
43
+ * (jatui-ewfh3). `false` sizes the grid to seven cells and centres it.
44
+ */
45
+ fill?: boolean;
38
46
  /** Accessible name; defaults to "March 2026". */
39
47
  ariaLabel?: string;
40
48
  rtl?: boolean;
package/dist/index.d.ts CHANGED
@@ -408,8 +408,9 @@ export { default as ImageReveal } from './components/ImageReveal.svelte';
408
408
  export { imageRevealPhase, imageRevealBusy, imageRevealInitials, imageRevealSize, imageRevealTone, IMAGE_REVEAL_SIZES, IMAGE_REVEAL_RADIUS } from './components/imageReveal';
409
409
  export type { ImageRevealPhase, ImageRevealShape, ImageRevealSizeName, ImageRevealToneName, ImageRevealState } from './components/imageReveal';
410
410
  export { default as AmbienceToggle, AMBIENCE_GROUP_ORDER } from './components/AmbienceToggle.svelte';
411
- export { createAmbience, createDefaultAmbience, DEFAULT_SCAPES, BreathClock, BREATHS_PER_MINUTE, MASTER, ScapeRegistry, hibiscusScape, focusScape, noiseScapes, brownNoiseScape, pinkNoiseScape, whiteNoiseScape, rain, waves } from './ambience';
412
- export type { DefaultAmbienceOptions } from './ambience';
413
- export type { Ambience, AmbienceOptions, AmbienceState, BreathState, Scape, ScapeContext, ScapeGroup, ScapeInstance } from './ambience';
411
+ export { createAmbience, createDefaultAmbience, DEFAULT_SCAPES, BreathClock, BREATHS_PER_MINUTE, MASTER, ScapeRegistry, hibiscusScape, focusScape, noiseScapes, brownNoiseScape, pinkNoiseScape, whiteNoiseScape, rain, waves, AMBIENCE_DEFAULT_GROUP_ORDER, AMBIENCE_DEFAULT_GROUP_LABELS, groupAmbienceItems, ambienceGroupLabel, isAmbienceLink, normalizeAmbienceGroups, radioStation, safeAmbienceHref } from './ambience';
412
+ export type { DefaultAmbienceOptions, AmbienceSection } from './ambience';
413
+ export type { Ambience, AmbienceGroupDef, AmbienceItem, AmbienceLink, AmbienceOptions, AmbienceState, BreathState, Scape, ScapeContext, ScapeGroup, ScapeInstance } from './ambience';
414
414
  export { createPlaylistScape } from './ambience';
415
+ export { isPlaylistScape, trackAttribution } from './ambience';
415
416
  export type { PlaylistScape, PlaylistScapeOptions, PlaylistManifest, PlaylistState, PlaylistStatus, PlaylistTrack } from './ambience';
package/dist/index.js CHANGED
@@ -457,7 +457,8 @@ export { imageRevealPhase, imageRevealBusy, imageRevealInitials, imageRevealSize
457
457
  // navigation; createDefaultAmbience() pre-registers every shipped scape
458
458
  // (hibiscus, focus, brown/pink/white noise, rain, waves).
459
459
  export { default as AmbienceToggle, AMBIENCE_GROUP_ORDER } from './components/AmbienceToggle.svelte';
460
- export { createAmbience, createDefaultAmbience, DEFAULT_SCAPES, BreathClock, BREATHS_PER_MINUTE, MASTER, ScapeRegistry, hibiscusScape, focusScape, noiseScapes, brownNoiseScape, pinkNoiseScape, whiteNoiseScape, rain, waves } from './ambience';
460
+ export { createAmbience, createDefaultAmbience, DEFAULT_SCAPES, BreathClock, BREATHS_PER_MINUTE, MASTER, ScapeRegistry, hibiscusScape, focusScape, noiseScapes, brownNoiseScape, pinkNoiseScape, whiteNoiseScape, rain, waves, AMBIENCE_DEFAULT_GROUP_ORDER, AMBIENCE_DEFAULT_GROUP_LABELS, groupAmbienceItems, ambienceGroupLabel, isAmbienceLink, normalizeAmbienceGroups, radioStation, safeAmbienceHref } from './ambience';
461
461
  // Playlist scape (phase 2) — per-host, never in DEFAULT_SCAPES: build with
462
462
  // createPlaylistScape() and register on the engine yourself.
463
463
  export { createPlaylistScape } from './ambience';
464
+ export { isPlaylistScape, trackAttribution } from './ambience';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joewinke/jatui",
3
- "version": "0.54.0",
3
+ "version": "0.55.0",
4
4
  "private": false,
5
5
  "description": "Shared Svelte 5 component library for JAT projects",
6
6
  "type": "module",