@joewinke/jatui 0.27.0 → 0.28.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.
@@ -14,27 +14,106 @@
14
14
  * Policy: shared/craft/animation-discipline.md in the jat repo
15
15
  * ("Curve vs spring" + "Surfaces" sections).
16
16
  */
17
+ /*
18
+ * Base tokens: the STANDARD-level values. Hosts and components should read
19
+ * the derived tokens further down (--motion-instant, --motion-travel, ...),
20
+ * never these — the derived ones carry the motion level.
21
+ */
17
22
  :root {
18
23
  /* Easing */
19
24
  --motion-glide: cubic-bezier(0.16, 1, 0.3, 1); /* ease-out-expo family; default */
20
- --motion-spring: cubic-bezier(0.34, 1.56, 0.64, 1); /* single overshoot; confirmation only */
25
+ --motion-spring-base: cubic-bezier(0.34, 1.56, 0.64, 1); /* single overshoot; confirmation only */
21
26
  --motion-standard: cubic-bezier(0.2, 0, 0, 1); /* M3 standard; symmetric moves */
22
27
 
23
28
  /* Duration */
24
- --motion-instant: 80ms; /* press/hover micro-feedback (50–100ms band) */
25
- --motion-fast: 150ms; /* default state-confirmation */
26
- --motion-enter: 200ms; /* entering UI: modals, sheets, dropdowns, toasts */
27
- --motion-move: 300ms; /* container morphs, cross-panel moves */
29
+ --motion-instant-base: 80ms; /* press/hover micro-feedback (50–100ms band) */
30
+ --motion-fast-base: 150ms; /* default state-confirmation */
31
+ --motion-enter-base: 200ms; /* entering UI: modals, sheets, dropdowns, toasts */
32
+ --motion-move-base: 300ms; /* container morphs, cross-panel moves */
33
+
34
+ /* Level inputs (standard) */
35
+ --motion-scale: 1; /* duration multiplier */
36
+ --motion-travel: 16px; /* entrance travel distance */
37
+ --motion-ambient: 1; /* 1 = decorative/ambient motion may run, 0 = off */
38
+ }
39
+
40
+ /*
41
+ * Motion level (jatui-j6833) — data-motion="calm|standard|lively" on <html>
42
+ * or any subtree. Scheme + semantics: docs/motion.md.
43
+ *
44
+ * The derived tokens are RE-DECLARED on every [data-motion] element (not only
45
+ * on :root) because a custom property's var()/calc() resolves where it is
46
+ * declared — a subtree that only changed --motion-scale would otherwise
47
+ * inherit :root's already-computed durations.
48
+ *
49
+ * Values MUST match src/lib/motion/level.ts (tests/unit/motionLevel.test.ts).
50
+ */
51
+ :root,
52
+ [data-motion] {
53
+ --motion-instant: calc(var(--motion-instant-base) * var(--motion-scale));
54
+ --motion-fast: calc(var(--motion-fast-base) * var(--motion-scale));
55
+ --motion-enter: calc(var(--motion-enter-base) * var(--motion-scale));
56
+ --motion-move: calc(var(--motion-move-base) * var(--motion-scale));
57
+ --motion-spring: var(--motion-spring-base);
58
+ }
59
+
60
+ [data-motion='standard'] {
61
+ --motion-scale: 1;
62
+ --motion-travel: 16px;
63
+ --motion-ambient: 1;
64
+ }
65
+
66
+ /* calm: quicker, shorter travel, no springs, no ambient/decorative motion */
67
+ [data-motion='calm'] {
68
+ --motion-scale: 0.75;
69
+ --motion-travel: 8px;
70
+ --motion-ambient: 0;
71
+ --motion-spring: var(--motion-glide);
72
+ }
73
+
74
+ /* lively: a touch slower and further — surfaces whose job is to feel alive */
75
+ [data-motion='lively'] {
76
+ --motion-scale: 1.25;
77
+ --motion-travel: 24px;
78
+ --motion-ambient: 1;
79
+ }
80
+
81
+ /*
82
+ * Ambient/decorative motion hook: mark looping or purely decorative
83
+ * animation (shimmer, pulse, floating blobs, marquee) with
84
+ * .jatui-motion-ambient and it stops on calm surfaces and under reduced
85
+ * motion. Never put this class on motion that carries meaning (a spinner).
86
+ */
87
+ [data-motion='calm'] .jatui-motion-ambient,
88
+ .jatui-motion-ambient[data-motion='calm'] {
89
+ animation: none !important;
28
90
  }
29
91
 
30
92
  /*
31
93
  * Global reduced-motion floor: strip motion, keep end states visible.
94
+ * Every motion level yields to it — durations and travel collapse to 0,
95
+ * springs flatten, ambient motion stops.
32
96
  * Components should still pair every animation with a static affordance
33
97
  * (color, position, label) so the state change survives without motion.
34
98
  * Component-local guards remain good practice — not every consumer loads
35
99
  * this file — but this is the backstop.
36
100
  */
37
101
  @media (prefers-reduced-motion: reduce) {
102
+ :root,
103
+ [data-motion],
104
+ [data-motion='calm'],
105
+ [data-motion='standard'],
106
+ [data-motion='lively'] {
107
+ --motion-scale: 0;
108
+ --motion-travel: 0px;
109
+ --motion-ambient: 0;
110
+ --motion-spring: var(--motion-glide);
111
+ }
112
+
113
+ .jatui-motion-ambient {
114
+ animation: none !important;
115
+ }
116
+
38
117
  *,
39
118
  *::before,
40
119
  *::after {
@@ -92,7 +171,7 @@
92
171
 
93
172
  @keyframes jatui-reveal-fade-in-bottom {
94
173
  from {
95
- transform: translateY(24px);
174
+ transform: translateY(calc(var(--motion-travel) * 1.5));
96
175
  opacity: 0;
97
176
  }
98
177
  to {
@@ -103,7 +182,7 @@
103
182
 
104
183
  @keyframes jatui-reveal-fade-in-top {
105
184
  from {
106
- transform: translateY(-24px);
185
+ transform: translateY(calc(var(--motion-travel) * -1.5));
107
186
  opacity: 0;
108
187
  }
109
188
  to {
@@ -114,7 +193,7 @@
114
193
 
115
194
  @keyframes jatui-reveal-fade-in-left {
116
195
  from {
117
- transform: translateX(-24px);
196
+ transform: translateX(calc(var(--motion-travel) * -1.5));
118
197
  opacity: 0;
119
198
  }
120
199
  to {
@@ -125,7 +204,7 @@
125
204
 
126
205
  @keyframes jatui-reveal-fade-in-right {
127
206
  from {
128
- transform: translateX(24px);
207
+ transform: translateX(calc(var(--motion-travel) * 1.5));
129
208
  opacity: 0;
130
209
  }
131
210
  to {
@@ -1,17 +1,27 @@
1
+ import type { EasingFn } from '../motion/easing';
2
+ import { type MotionDurationToken, type MotionEasingToken, type MotionLevel } from '../motion/level';
1
3
  export type GlideOrigin = 'center' | 'top' | 'tr' | 'right' | 'br' | 'bottom' | 'bl' | 'left' | 'tl';
2
4
  export interface GlideParams {
3
5
  /** Direction the element slides in from. Default 'bottom'. */
4
6
  from?: GlideOrigin;
5
- /** Travel distance in px. Default 16. */
7
+ /**
8
+ * Travel distance in px at the STANDARD level. Default: the level's
9
+ * `--motion-travel` (8 calm / 16 standard / 24 lively). An explicit value
10
+ * is scaled by the same ratio, so calm surfaces always travel half as far.
11
+ */
6
12
  distance?: number;
7
13
  /** Scale factor at the start of the enter (e.g. 0.95). Omit = no scale. */
8
14
  scale?: number;
9
15
  /** Add motion-blur (blur filter) during travel. Default false. */
10
16
  blur?: boolean;
11
- /** Duration in ms. Default 200. */
12
- duration?: number;
13
- /** Delay in ms. Default 0. */
17
+ /** Duration: a token or standard-level ms, scaled by the level. Default 'enter' (200ms). */
18
+ duration?: MotionDurationToken | number;
19
+ /** Delay in standard-level ms, scaled by the level. Default 0. */
14
20
  delay?: number;
21
+ /** Easing token or function. Default 'glide'. */
22
+ easing?: MotionEasingToken | EasingFn;
23
+ /** Pin the motion level instead of reading the nearest [data-motion]. */
24
+ level?: MotionLevel;
15
25
  }
16
26
  /**
17
27
  * Parameterized directional enter/exit transition.
@@ -21,11 +31,13 @@ export interface GlideParams {
21
31
  *
22
32
  * <div in:glide={{ from: 'bottom' }} out:glide={{ from: 'bottom' }}>…</div>
23
33
  *
24
- * Reduced-motion: opacity fade only (no translate, scale, or blur).
34
+ * Level-aware (jatui-j6833): duration, delay and travel scale with the
35
+ * surface's `data-motion` level and ease on the `--motion-glide` curve.
36
+ * Reduced-motion: duration 0 — the element lands in its end state.
25
37
  */
26
38
  export declare function glide(node: HTMLElement, params?: GlideParams): {
27
39
  delay: number;
28
40
  duration: number;
29
- easing: (t: number) => number;
41
+ easing: EasingFn;
30
42
  css(t: number): string;
31
43
  };
@@ -1,4 +1,5 @@
1
- // direction taxonomy after animista.net (Ana Travas, BSD-2-Clause site license); keyframes re-authored on jatui motion tokens
1
+ import { MOTION_LEVEL_TRAVEL } from '../motion/level';
2
+ import { resolveMotionTiming } from '../motion/transitions';
2
3
  /** Map from direction → [dx, dy] unit vector for the entrance travel. */
3
4
  const ORIGIN_VECTOR = {
4
5
  center: [0, 0],
@@ -19,44 +20,38 @@ const ORIGIN_VECTOR = {
19
20
  *
20
21
  * <div in:glide={{ from: 'bottom' }} out:glide={{ from: 'bottom' }}>…</div>
21
22
  *
22
- * Reduced-motion: opacity fade only (no translate, scale, or blur).
23
+ * Level-aware (jatui-j6833): duration, delay and travel scale with the
24
+ * surface's `data-motion` level and ease on the `--motion-glide` curve.
25
+ * Reduced-motion: duration 0 — the element lands in its end state.
23
26
  */
24
27
  export function glide(node, params = {}) {
25
- const { from = 'bottom', distance = 16, scale, blur = false, duration = 200, delay = 0, } = params;
26
- const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
28
+ const { from = 'bottom', distance, scale, blur = false } = params;
29
+ const { ctx, delay, duration, easing } = resolveMotionTiming(node, params, {
30
+ duration: 'enter',
31
+ easing: 'glide'
32
+ });
27
33
  const [ux, uy] = ORIGIN_VECTOR[from];
34
+ const travel = distance === undefined
35
+ ? ctx.travel
36
+ : distance * (ctx.travel / MOTION_LEVEL_TRAVEL.standard);
28
37
  // Entrance start state — element travels FROM this position TO its natural position
29
- const tx = prefersReduced ? 0 : ux * distance;
30
- const ty = prefersReduced ? 0 : uy * distance;
31
- const s = (!prefersReduced && scale !== undefined) ? scale : 1;
32
- const blurPx = (!prefersReduced && blur) ? 4 : 0;
33
- function buildTransform(progress) {
34
- // progress goes 0 → 1 on enter; Svelte reverses for exit
35
- const invP = 1 - progress;
36
- const x = tx * invP;
37
- const y = ty * invP;
38
- const sc = 1 - (1 - s) * invP;
39
- const b = blurPx * invP;
40
- let t = `translate(${x}px, ${y}px)`;
41
- if (s !== 1)
42
- t += ` scale(${sc})`;
43
- const filter = b > 0 ? `blur(${b.toFixed(1)}px)` : '';
44
- return { transform: t, filter };
45
- }
38
+ const tx = ux * travel;
39
+ const ty = uy * travel;
40
+ const s = !ctx.reduced && scale !== undefined ? scale : 1;
41
+ const blurPx = !ctx.reduced && blur ? 4 : 0;
46
42
  return {
47
43
  delay,
48
44
  duration,
49
- easing: (t) => t, // easing applied via CSS var below
45
+ easing,
50
46
  css(t) {
51
- // t: 0 = start, 1 = end (for `in:`; Svelte inverts for `out:`)
52
- const { transform, filter } = buildTransform(t);
53
- // Apply the glide easing via CSS transition so the CSS var is honored
54
- return `
55
- opacity: ${t};
56
- transform: ${transform};
57
- ${filter ? `filter: ${filter};` : ''}
58
- transition-timing-function: var(--motion-glide, cubic-bezier(0.16, 1, 0.3, 1));
59
- `;
47
+ // t: 0 = start, 1 = end (for `in:`; Svelte inverts for `out:`), already eased
48
+ const invP = 1 - t;
49
+ let transform = `translate(${tx * invP}px, ${ty * invP}px)`;
50
+ if (s !== 1)
51
+ transform += ` scale(${1 - (1 - s) * invP})`;
52
+ const b = blurPx * invP;
53
+ return (`opacity: ${Math.min(1, Math.max(0, t))}; transform: ${transform};` +
54
+ (b > 0 ? ` filter: blur(${b.toFixed(1)}px);` : ''));
60
55
  },
61
56
  };
62
57
  }
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Legacy sound API (pre-semantic). Kept source-compatible, but every call now
3
+ * routes through the shared preferences store and player in `../sound`, so
4
+ * old call sites obey volume, categories, quiet hours and the hidden-tab rule
5
+ * too. New code: `import { playSound, getSoundPreferences } from '../../../dist'`.
6
+ */
1
7
  export declare function isSoundEnabled(): boolean;
2
8
  export declare function setSoundEnabled(enabled: boolean): void;
3
9
  export declare function registerSound(name: string, dataUri: string): Promise<void>;
@@ -1,62 +1,20 @@
1
- let ctx = null;
2
- const buffers = new Map();
3
- const STORAGE_KEY = 'jatui-sound-enabled';
4
- function getContext() {
5
- if (ctx)
6
- return ctx;
7
- try {
8
- ctx = new AudioContext();
9
- return ctx;
10
- }
11
- catch {
12
- return null;
13
- }
14
- }
1
+ /**
2
+ * Legacy sound API (pre-semantic). Kept source-compatible, but every call now
3
+ * routes through the shared preferences store and player in `../sound`, so
4
+ * old call sites obey volume, categories, quiet hours and the hidden-tab rule
5
+ * too. New code: `import { playSound, getSoundPreferences } from '../../../dist'`.
6
+ */
7
+ import { getSoundPlayer } from '../sound/player';
8
+ import { getSoundPreferences } from '../sound/preferences';
15
9
  export function isSoundEnabled() {
16
- if (typeof localStorage === 'undefined')
17
- return false;
18
- return localStorage.getItem(STORAGE_KEY) === 'true';
10
+ return getSoundPreferences().get().enabled;
19
11
  }
20
12
  export function setSoundEnabled(enabled) {
21
- if (typeof localStorage === 'undefined')
22
- return;
23
- localStorage.setItem(STORAGE_KEY, String(enabled));
13
+ getSoundPreferences().set({ enabled });
24
14
  }
25
15
  export async function registerSound(name, dataUri) {
26
- const audioCtx = getContext();
27
- if (!audioCtx)
28
- return;
29
- try {
30
- const base64 = dataUri.split(',')[1];
31
- const binary = atob(base64);
32
- const bytes = new Uint8Array(binary.length);
33
- for (let i = 0; i < binary.length; i++)
34
- bytes[i] = binary.charCodeAt(i);
35
- const buffer = await audioCtx.decodeAudioData(bytes.buffer);
36
- buffers.set(name, buffer);
37
- }
38
- catch {
39
- // Silently ignore decode failures — clip may not be supported in this browser
40
- }
16
+ await getSoundPlayer().register(name, dataUri);
41
17
  }
42
18
  export function play(name, options = {}) {
43
- if (!isSoundEnabled())
44
- return;
45
- const audioCtx = getContext();
46
- if (!audioCtx)
47
- return;
48
- const buffer = buffers.get(name);
49
- if (!buffer)
50
- return;
51
- // Resume suspended context (browser autoplay policy requires user gesture first)
52
- if (audioCtx.state === 'suspended') {
53
- audioCtx.resume().catch(() => { });
54
- }
55
- const source = audioCtx.createBufferSource();
56
- const gain = audioCtx.createGain();
57
- gain.gain.value = options.volume ?? 0.15;
58
- source.buffer = buffer;
59
- source.connect(gain);
60
- gain.connect(audioCtx.destination);
61
- source.start();
19
+ getSoundPlayer().play(name, options);
62
20
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joewinke/jatui",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "private": false,
5
5
  "description": "Shared Svelte 5 component library for JAT projects",
6
6
  "type": "module",
@@ -81,7 +81,7 @@
81
81
  "build": "vite build",
82
82
  "build:site": "npm run package && vite build",
83
83
  "preview": "vite preview",
84
- "package": "svelte-package --input src/lib --output dist && node scripts/compile-rune-modules.mjs",
84
+ "package": "svelte-package --input src/lib --output dist && node scripts/compile-rune-modules.mjs && node scripts/verify-dist.mjs",
85
85
  "check:demos": "node scripts/check-demo-coverage.mjs",
86
86
  "guard": "node scripts/check-demo-coverage.mjs",
87
87
  "test:dist": "svelte-kit sync && vitest run --config vitest.dist.config.ts",