@joewinke/jatui 0.14.0 → 0.16.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.
- package/dist/components/VideoPlayer.svelte +843 -19
- package/dist/components/VideoPlayer.svelte.d.ts +103 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +10 -0
- package/dist/video/README.md +2 -2
- package/dist/video/keys.d.ts +5 -5
- package/dist/video/keys.js +5 -4
- package/dist/video/presence/flip.d.ts +145 -0
- package/dist/video/presence/flip.js +253 -0
- package/dist/video/presence/idle.d.ts +82 -0
- package/dist/video/presence/idle.js +145 -0
- package/dist/video/presence/resume.d.ts +76 -0
- package/dist/video/presence/resume.js +139 -0
- package/dist/video/presence/winddown.d.ts +106 -0
- package/dist/video/presence/winddown.js +104 -0
- package/dist/video/presence.d.ts +25 -21
- package/dist/video/presence.js +25 -21
- package/dist/video/settings/SettingsPanel.svelte +414 -0
- package/dist/video/settings/SettingsPanel.svelte.d.ts +16 -0
- package/dist/video/settings/panel.d.ts +24 -0
- package/dist/video/settings/panel.js +58 -0
- package/dist/video/settings/registry.d.ts +82 -0
- package/dist/video/settings/registry.js +331 -0
- package/dist/video/settings/store.d.ts +55 -0
- package/dist/video/settings/store.js +116 -0
- package/dist/video/settings.d.ts +15 -46
- package/dist/video/settings.js +15 -82
- package/package.json +1 -1
package/dist/video/presence.d.ts
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Presence
|
|
3
|
-
*
|
|
2
|
+
* Presence (mos-5lzns.34) — the barrel. Three behaviours about whether you
|
|
3
|
+
* are there and where you were, each an OPTIONAL VideoPlayer prop that is
|
|
4
|
+
* inert when absent (extension-library FEATURES.md §Wind-down, §Resume at a
|
|
5
|
+
* sentence, §Open like an object):
|
|
4
6
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
+
* · `presence/winddown.ts` — pure: the begin decision, fade levels, hint.
|
|
8
|
+
* The desktop says nobody is there → picture and sound fade over 20s and
|
|
9
|
+
* the video pauses. The fade IS the prompt; any input cancels it.
|
|
10
|
+
* · `presence/idle.ts` — the desktop signal: Chromium's Idle Detection API,
|
|
11
|
+
* feature-detected, never prompting unless the host opted in (and then
|
|
12
|
+
* only from a gesture). Elsewhere the player falls back to page presence,
|
|
13
|
+
* reported as `source: 'page'` so the host can label it.
|
|
14
|
+
* · `presence/resume.ts` — resume-at-sentence glue around .27's
|
|
15
|
+
* `advisors/resume-point` (the snapping is NOT duplicated here): the
|
|
16
|
+
* host-supplied store (localStorage default), the data mapping, and the
|
|
17
|
+
* two integration rules — snap when the boundaries land, stand down once
|
|
18
|
+
* the user has taken the wheel.
|
|
19
|
+
* · `presence/flip.ts` — FLIP open/close over WAAPI with the poster painted
|
|
20
|
+
* on every path; a no-op under prefers-reduced-motion except the poster.
|
|
7
21
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* · resume-at-sentence — snap a resume point back to the nearest sentence /
|
|
12
|
-
* speaker turn / chapter within 15s (CLOSEST wins, not most-specific);
|
|
13
|
-
* quote the line, never paraphrase; stand down if the user has taken the
|
|
14
|
-
* wheel (>2.5s from the stored point). Runs when the transcript lands, not
|
|
15
|
-
* at open.
|
|
16
|
-
* · FLIP open — the clicked card's rectangle → the player shell, WAAPI, two
|
|
17
|
-
* keyframes, transform-origin top-left, poster painted under the video on
|
|
18
|
-
* EVERY path (a black rectangle is a bug at any motion setting); no-op
|
|
19
|
-
* under prefers-reduced-motion except the poster.
|
|
20
|
-
*
|
|
21
|
-
* All three are advisors/mechanics: they read no settings, touch no
|
|
22
|
-
* <video> outside the callbacks the component passes, and fail toward doing
|
|
23
|
-
* nothing.
|
|
22
|
+
* Advisors and mechanics: they read no settings and touch no <video>; the
|
|
23
|
+
* component passes knobs in and applies what comes back, and every one of
|
|
24
|
+
* them fails toward doing nothing.
|
|
24
25
|
*/
|
|
25
|
-
export
|
|
26
|
+
export { WIND_DOWN_FADE_MS, WIND_DOWN_DEPTH, WIND_DOWN_AFTER_DEFAULT, WIND_DOWN_AFTER_MIN, WIND_DOWN_AFTER_MAX, WIND_DOWN_PROBE_MS, windDownAfterMs, shouldBeginWindDown, desktopPresent, windDownProgress, windDownLevels, windDownHint, type PresenceSource, type DesktopSignal, type WindDownPhase, type WindDownState, type WindDownInput } from './presence/winddown';
|
|
27
|
+
export { IDLE_THRESHOLD_MIN_MS, idleDetectionSupported, idleDetectionPermission, requestIdleDetection, startDesktopWatch, toDesktopSignal, type IdlePermission, type DesktopWatch } from './presence/idle';
|
|
28
|
+
export { RESUME_WHEEL_SEC, RESUME_MAX_BACK, RESUME_SAVE_MS, RESUME_FLOOR_SEC, RESUME_TAIL_SEC, localResumeStore, coerceResumeSec, resumeToSave, toResumeCtx, hasBoundaries, planResumeSnap, type VideoResumeStore, type ResumeSnapPlan } from './presence/resume';
|
|
29
|
+
export { FLIP_EASE, FLIP_OPEN_MS, FLIP_CLOSE_MS, FLIP_FADE_WITH_POSTER, FLIP_FADE_BARE, FLIP_POSTER_CLASS, flipOpen, flipClose, clearPoster, posterBackground, rectOf, onScreen, flipGeometry, awayFrame, homeFrame, type FlipSource, type FlipRect } from './presence/flip';
|
package/dist/video/presence.js
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Presence
|
|
3
|
-
*
|
|
2
|
+
* Presence (mos-5lzns.34) — the barrel. Three behaviours about whether you
|
|
3
|
+
* are there and where you were, each an OPTIONAL VideoPlayer prop that is
|
|
4
|
+
* inert when absent (extension-library FEATURES.md §Wind-down, §Resume at a
|
|
5
|
+
* sentence, §Open like an object):
|
|
4
6
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
+
* · `presence/winddown.ts` — pure: the begin decision, fade levels, hint.
|
|
8
|
+
* The desktop says nobody is there → picture and sound fade over 20s and
|
|
9
|
+
* the video pauses. The fade IS the prompt; any input cancels it.
|
|
10
|
+
* · `presence/idle.ts` — the desktop signal: Chromium's Idle Detection API,
|
|
11
|
+
* feature-detected, never prompting unless the host opted in (and then
|
|
12
|
+
* only from a gesture). Elsewhere the player falls back to page presence,
|
|
13
|
+
* reported as `source: 'page'` so the host can label it.
|
|
14
|
+
* · `presence/resume.ts` — resume-at-sentence glue around .27's
|
|
15
|
+
* `advisors/resume-point` (the snapping is NOT duplicated here): the
|
|
16
|
+
* host-supplied store (localStorage default), the data mapping, and the
|
|
17
|
+
* two integration rules — snap when the boundaries land, stand down once
|
|
18
|
+
* the user has taken the wheel.
|
|
19
|
+
* · `presence/flip.ts` — FLIP open/close over WAAPI with the poster painted
|
|
20
|
+
* on every path; a no-op under prefers-reduced-motion except the poster.
|
|
7
21
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* · resume-at-sentence — snap a resume point back to the nearest sentence /
|
|
12
|
-
* speaker turn / chapter within 15s (CLOSEST wins, not most-specific);
|
|
13
|
-
* quote the line, never paraphrase; stand down if the user has taken the
|
|
14
|
-
* wheel (>2.5s from the stored point). Runs when the transcript lands, not
|
|
15
|
-
* at open.
|
|
16
|
-
* · FLIP open — the clicked card's rectangle → the player shell, WAAPI, two
|
|
17
|
-
* keyframes, transform-origin top-left, poster painted under the video on
|
|
18
|
-
* EVERY path (a black rectangle is a bug at any motion setting); no-op
|
|
19
|
-
* under prefers-reduced-motion except the poster.
|
|
20
|
-
*
|
|
21
|
-
* All three are advisors/mechanics: they read no settings, touch no
|
|
22
|
-
* <video> outside the callbacks the component passes, and fail toward doing
|
|
23
|
-
* nothing.
|
|
22
|
+
* Advisors and mechanics: they read no settings and touch no <video>; the
|
|
23
|
+
* component passes knobs in and applies what comes back, and every one of
|
|
24
|
+
* them fails toward doing nothing.
|
|
24
25
|
*/
|
|
25
|
-
export
|
|
26
|
+
export { WIND_DOWN_FADE_MS, WIND_DOWN_DEPTH, WIND_DOWN_AFTER_DEFAULT, WIND_DOWN_AFTER_MIN, WIND_DOWN_AFTER_MAX, WIND_DOWN_PROBE_MS, windDownAfterMs, shouldBeginWindDown, desktopPresent, windDownProgress, windDownLevels, windDownHint } from './presence/winddown';
|
|
27
|
+
export { IDLE_THRESHOLD_MIN_MS, idleDetectionSupported, idleDetectionPermission, requestIdleDetection, startDesktopWatch, toDesktopSignal } from './presence/idle';
|
|
28
|
+
export { RESUME_WHEEL_SEC, RESUME_MAX_BACK, RESUME_SAVE_MS, RESUME_FLOOR_SEC, RESUME_TAIL_SEC, localResumeStore, coerceResumeSec, resumeToSave, toResumeCtx, hasBoundaries, planResumeSnap } from './presence/resume';
|
|
29
|
+
export { FLIP_EASE, FLIP_OPEN_MS, FLIP_CLOSE_MS, FLIP_FADE_WITH_POSTER, FLIP_FADE_BARE, FLIP_POSTER_CLASS, flipOpen, flipClose, clearPoster, posterBackground, rectOf, onScreen, flipGeometry, awayFrame, homeFrame } from './presence/flip';
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
let nextId = 0;
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<script lang="ts">
|
|
6
|
+
/**
|
|
7
|
+
* The VideoPlayer settings panel (mos-5lzns.35) — extension-library's
|
|
8
|
+
* `openSettings()` ported. Generated wholly from `VIDEO_SETTINGS` via
|
|
9
|
+
* `panelGroups`: every row is a label, one control, and the sentence saying
|
|
10
|
+
* what the thing does. The sentence is not decoration — it is the reason
|
|
11
|
+
* this screen exists rather than a list of switches.
|
|
12
|
+
*
|
|
13
|
+
* Keyboard: focus lands on the first control and Tab cycles inside the
|
|
14
|
+
* dialog; Esc (or `,`) closes and hands focus back. Controls follow the
|
|
15
|
+
* ARIA APG patterns — a switch, a radio group (arrows move the choice), a
|
|
16
|
+
* native range. It is a FORM, not a list: arrow keys belong to the
|
|
17
|
+
* controls (a range and a radio group both need them), so there is no
|
|
18
|
+
* row-level listNav to fight them.
|
|
19
|
+
*
|
|
20
|
+
* Values are live, not on close: these are things you judge by watching
|
|
21
|
+
* the result, so the result has to move while you drag.
|
|
22
|
+
*
|
|
23
|
+
* Token-only chrome; the only motion (a short fade) is dropped under
|
|
24
|
+
* prefers-reduced-motion.
|
|
25
|
+
*/
|
|
26
|
+
import { onMount } from 'svelte';
|
|
27
|
+
import type { SettingSpec, SettingValue } from './registry';
|
|
28
|
+
import { formatSettingValue, stepOption, type SettingsPanelGroup } from './panel';
|
|
29
|
+
|
|
30
|
+
let {
|
|
31
|
+
groups,
|
|
32
|
+
values,
|
|
33
|
+
onchange,
|
|
34
|
+
onclose,
|
|
35
|
+
helpHref = null,
|
|
36
|
+
persisted = false
|
|
37
|
+
}: {
|
|
38
|
+
groups: SettingsPanelGroup[];
|
|
39
|
+
/** Effective value per id — what the player is doing right now. */
|
|
40
|
+
values: Record<string, SettingValue>;
|
|
41
|
+
onchange: (id: string, value: SettingValue) => void;
|
|
42
|
+
onclose: () => void;
|
|
43
|
+
/** A host help page explaining all of this ("How all of this works →"). */
|
|
44
|
+
helpHref?: string | null;
|
|
45
|
+
/** The host supplied a store — the footer says choices are remembered. */
|
|
46
|
+
persisted?: boolean;
|
|
47
|
+
} = $props();
|
|
48
|
+
|
|
49
|
+
const uid = `vp-set-${++nextId}`;
|
|
50
|
+
let cardEl: HTMLDivElement | undefined = $state();
|
|
51
|
+
|
|
52
|
+
function rowId(spec: SettingSpec) {
|
|
53
|
+
return `${uid}-${spec.id.replace(/[^a-z0-9]/gi, '-')}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function focusables(): HTMLElement[] {
|
|
57
|
+
if (!cardEl) return [];
|
|
58
|
+
return Array.from(
|
|
59
|
+
cardEl.querySelectorAll<HTMLElement>('button:not([tabindex="-1"]), input, a[href]')
|
|
60
|
+
).filter((el) => !el.hasAttribute('disabled'));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
onMount(() => {
|
|
64
|
+
const first = cardEl?.querySelector<HTMLElement>('.vp-set-control button:not([tabindex="-1"]), .vp-set-control input');
|
|
65
|
+
(first ?? cardEl)?.focus();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
function onKeyDown(e: KeyboardEvent) {
|
|
69
|
+
// The player's own shortcuts must not fire from inside the dialog
|
|
70
|
+
// (Space on a switch would also play/pause).
|
|
71
|
+
e.stopPropagation();
|
|
72
|
+
if (e.key === 'Escape' || (e.key === ',' && (e.target as HTMLElement)?.tagName !== 'INPUT')) {
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
onclose();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (e.key === 'Tab') {
|
|
78
|
+
const els = focusables();
|
|
79
|
+
if (!els.length) return;
|
|
80
|
+
const i = els.indexOf(document.activeElement as HTMLElement);
|
|
81
|
+
const next = e.shiftKey ? (i <= 0 ? els.length - 1 : i - 1) : i === els.length - 1 ? 0 : i + 1;
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
els[next].focus();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function onRadioKey(e: KeyboardEvent, spec: SettingSpec) {
|
|
88
|
+
const dir = e.key === 'ArrowRight' || e.key === 'ArrowDown' ? 1 : e.key === 'ArrowLeft' || e.key === 'ArrowUp' ? -1 : 0;
|
|
89
|
+
if (!dir) return;
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
const next = stepOption(spec, values[spec.id], dir);
|
|
92
|
+
if (next === undefined) return;
|
|
93
|
+
onchange(spec.id, next);
|
|
94
|
+
const group = (e.currentTarget as HTMLElement).closest('[role="radiogroup"]');
|
|
95
|
+
queueMicrotask(() => group?.querySelector<HTMLElement>(`[data-value="${next}"]`)?.focus());
|
|
96
|
+
}
|
|
97
|
+
</script>
|
|
98
|
+
|
|
99
|
+
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
|
100
|
+
<div class="vp-set-backdrop" onclick={(e) => { if (e.target === e.currentTarget) onclose(); }}>
|
|
101
|
+
<div
|
|
102
|
+
class="vp-set"
|
|
103
|
+
role="dialog"
|
|
104
|
+
aria-modal="true"
|
|
105
|
+
aria-labelledby="{uid}-title"
|
|
106
|
+
tabindex="-1"
|
|
107
|
+
bind:this={cardEl}
|
|
108
|
+
onkeydown={onKeyDown}
|
|
109
|
+
>
|
|
110
|
+
<div class="vp-set-head">
|
|
111
|
+
<h2 class="vp-set-title" id="{uid}-title">Settings</h2>
|
|
112
|
+
<button type="button" class="vp-set-close" aria-label="Close settings" onclick={onclose}>
|
|
113
|
+
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
|
|
114
|
+
<path d="M6 6l12 12M18 6 6 18" />
|
|
115
|
+
</svg>
|
|
116
|
+
</button>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<div class="vp-set-body">
|
|
120
|
+
{#each groups as g (g.group)}
|
|
121
|
+
<section class="vp-set-group" aria-labelledby="{uid}-g-{g.group}">
|
|
122
|
+
<h3 class="vp-set-group-title" id="{uid}-g-{g.group}">{g.group}</h3>
|
|
123
|
+
{#each g.rows as spec (spec.id)}
|
|
124
|
+
{@const id = rowId(spec)}
|
|
125
|
+
{@const value = values[spec.id] ?? spec.default}
|
|
126
|
+
<div class="vp-set-row" data-setting={spec.id}>
|
|
127
|
+
<span class="vp-set-label" id="{id}-label">{spec.label}</span>
|
|
128
|
+
<div class="vp-set-control">
|
|
129
|
+
{#if spec.type === 'bool'}
|
|
130
|
+
<button
|
|
131
|
+
type="button"
|
|
132
|
+
class="vp-set-switch"
|
|
133
|
+
role="switch"
|
|
134
|
+
aria-checked={value === true}
|
|
135
|
+
aria-labelledby="{id}-label"
|
|
136
|
+
aria-describedby="{id}-hint"
|
|
137
|
+
onclick={() => onchange(spec.id, value !== true)}
|
|
138
|
+
>
|
|
139
|
+
<span class="vp-set-knob" aria-hidden="true"></span>
|
|
140
|
+
<span class="vp-set-switch-text">{value === true ? 'On' : 'Off'}</span>
|
|
141
|
+
</button>
|
|
142
|
+
{:else if spec.type === 'enum'}
|
|
143
|
+
<div
|
|
144
|
+
class="vp-set-seg"
|
|
145
|
+
role="radiogroup"
|
|
146
|
+
aria-labelledby="{id}-label"
|
|
147
|
+
aria-describedby="{id}-hint"
|
|
148
|
+
>
|
|
149
|
+
{#each spec.options ?? [] as opt (opt.value)}
|
|
150
|
+
<button
|
|
151
|
+
type="button"
|
|
152
|
+
role="radio"
|
|
153
|
+
data-value={opt.value}
|
|
154
|
+
aria-checked={value === opt.value}
|
|
155
|
+
tabindex={value === opt.value ? 0 : -1}
|
|
156
|
+
onclick={() => onchange(spec.id, opt.value)}
|
|
157
|
+
onkeydown={(e) => onRadioKey(e, spec)}
|
|
158
|
+
>
|
|
159
|
+
{opt.label}
|
|
160
|
+
</button>
|
|
161
|
+
{/each}
|
|
162
|
+
</div>
|
|
163
|
+
{:else}
|
|
164
|
+
<input
|
|
165
|
+
type="range"
|
|
166
|
+
class="vp-set-range"
|
|
167
|
+
min={spec.min}
|
|
168
|
+
max={spec.max}
|
|
169
|
+
step={spec.step}
|
|
170
|
+
value={value}
|
|
171
|
+
aria-labelledby="{id}-label"
|
|
172
|
+
aria-describedby="{id}-hint"
|
|
173
|
+
aria-valuetext={formatSettingValue(spec, value)}
|
|
174
|
+
oninput={(e) => onchange(spec.id, Number((e.currentTarget as HTMLInputElement).value))}
|
|
175
|
+
/>
|
|
176
|
+
<span class="vp-set-value" aria-hidden="true">{formatSettingValue(spec, value)}</span>
|
|
177
|
+
{/if}
|
|
178
|
+
</div>
|
|
179
|
+
<p class="vp-set-hint" id="{id}-hint">{spec.hint}</p>
|
|
180
|
+
</div>
|
|
181
|
+
{/each}
|
|
182
|
+
</section>
|
|
183
|
+
{:else}
|
|
184
|
+
<p class="vp-set-empty">Nothing on this video can be adjusted yet — settings appear as the features they control arrive.</p>
|
|
185
|
+
{/each}
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<div class="vp-set-foot">
|
|
189
|
+
<span>{persisted ? 'Your choices are remembered.' : 'Choices last while this player is open.'} Esc closes.</span>
|
|
190
|
+
{#if helpHref}
|
|
191
|
+
<a class="vp-set-help" href={helpHref}>How all of this works →</a>
|
|
192
|
+
{/if}
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<style>
|
|
198
|
+
.vp-set-backdrop {
|
|
199
|
+
position: absolute;
|
|
200
|
+
inset: 0;
|
|
201
|
+
display: flex;
|
|
202
|
+
align-items: center;
|
|
203
|
+
justify-content: center;
|
|
204
|
+
padding: var(--jatui-space-lg, 12px);
|
|
205
|
+
background: color-mix(in oklch, var(--color-base-100) 70%, transparent);
|
|
206
|
+
z-index: 11;
|
|
207
|
+
border-radius: inherit;
|
|
208
|
+
animation: vp-set-in var(--motion-quick, 150ms) var(--motion-glide, ease-out);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.vp-set {
|
|
212
|
+
width: min(28rem, 100%);
|
|
213
|
+
max-height: 100%;
|
|
214
|
+
display: flex;
|
|
215
|
+
flex-direction: column;
|
|
216
|
+
background: var(--color-base-100);
|
|
217
|
+
color: var(--color-base-content);
|
|
218
|
+
border: var(--jatui-border-w, 1px) solid color-mix(in oklch, var(--color-base-content) 18%, transparent);
|
|
219
|
+
border-radius: var(--jatui-radius-box, 0.5rem);
|
|
220
|
+
box-shadow: 0 8px 30px color-mix(in oklch, var(--color-base-content) 25%, transparent);
|
|
221
|
+
outline: none;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.vp-set-head {
|
|
225
|
+
display: flex;
|
|
226
|
+
align-items: center;
|
|
227
|
+
justify-content: space-between;
|
|
228
|
+
padding: var(--jatui-space-md, 6px) var(--jatui-space-lg, 12px);
|
|
229
|
+
border-bottom: var(--jatui-border-w, 1px) solid color-mix(in oklch, var(--color-base-content) 10%, transparent);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.vp-set-title {
|
|
233
|
+
margin: 0;
|
|
234
|
+
font-size: 0.85rem;
|
|
235
|
+
font-weight: 700;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.vp-set-close {
|
|
239
|
+
display: inline-flex;
|
|
240
|
+
padding: var(--jatui-space-xs, 2px);
|
|
241
|
+
background: none;
|
|
242
|
+
border: none;
|
|
243
|
+
color: inherit;
|
|
244
|
+
border-radius: var(--jatui-radius-field, 0.25rem);
|
|
245
|
+
cursor: pointer;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.vp-set-body {
|
|
249
|
+
overflow-y: auto;
|
|
250
|
+
padding: var(--jatui-space-sm, 4px) var(--jatui-space-lg, 12px) var(--jatui-space-md, 6px);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.vp-set-group-title {
|
|
254
|
+
margin: var(--jatui-space-md, 6px) 0 var(--jatui-space-xs, 2px);
|
|
255
|
+
font-size: 0.66rem;
|
|
256
|
+
font-weight: 700;
|
|
257
|
+
text-transform: uppercase;
|
|
258
|
+
letter-spacing: 0.04em;
|
|
259
|
+
color: color-mix(in oklch, var(--color-base-content) 55%, transparent);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.vp-set-row {
|
|
263
|
+
display: grid;
|
|
264
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
265
|
+
align-items: center;
|
|
266
|
+
gap: var(--jatui-space-xs, 2px) var(--jatui-space-md, 6px);
|
|
267
|
+
padding: var(--jatui-space-sm, 4px) 0;
|
|
268
|
+
border-top: var(--jatui-border-w, 1px) solid color-mix(in oklch, var(--color-base-content) 6%, transparent);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.vp-set-group-title + .vp-set-row {
|
|
272
|
+
border-top: none;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.vp-set-label {
|
|
276
|
+
font-size: 0.78rem;
|
|
277
|
+
font-weight: 600;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.vp-set-control {
|
|
281
|
+
display: flex;
|
|
282
|
+
align-items: center;
|
|
283
|
+
gap: var(--jatui-space-sm, 4px);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.vp-set-hint {
|
|
287
|
+
grid-column: 1 / -1;
|
|
288
|
+
margin: 0;
|
|
289
|
+
font-size: 0.7rem;
|
|
290
|
+
line-height: 1.4;
|
|
291
|
+
color: color-mix(in oklch, var(--color-base-content) 68%, transparent);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/* Switch: the state reads in text AND position, never colour alone. */
|
|
295
|
+
.vp-set-switch {
|
|
296
|
+
display: inline-flex;
|
|
297
|
+
align-items: center;
|
|
298
|
+
gap: var(--jatui-space-sm, 4px);
|
|
299
|
+
padding: 2px var(--jatui-space-sm, 4px) 2px 2px;
|
|
300
|
+
font: inherit;
|
|
301
|
+
font-size: 0.7rem;
|
|
302
|
+
color: inherit;
|
|
303
|
+
background: color-mix(in oklch, var(--color-base-content) 10%, transparent);
|
|
304
|
+
border: var(--jatui-border-w, 1px) solid color-mix(in oklch, var(--color-base-content) 20%, transparent);
|
|
305
|
+
border-radius: 999px;
|
|
306
|
+
cursor: pointer;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.vp-set-knob {
|
|
310
|
+
width: 0.8rem;
|
|
311
|
+
height: 0.8rem;
|
|
312
|
+
border-radius: 999px;
|
|
313
|
+
background: color-mix(in oklch, var(--color-base-content) 45%, transparent);
|
|
314
|
+
transition: transform var(--motion-quick, 150ms) var(--motion-glide, ease-out);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.vp-set-switch[aria-checked='true'] {
|
|
318
|
+
background: color-mix(in oklch, var(--color-primary) 22%, transparent);
|
|
319
|
+
border-color: color-mix(in oklch, var(--color-primary) 55%, transparent);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.vp-set-switch[aria-checked='true'] .vp-set-knob {
|
|
323
|
+
background: var(--color-primary);
|
|
324
|
+
transform: translateX(0.15rem);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.vp-set-switch-text {
|
|
328
|
+
min-width: 1.6em;
|
|
329
|
+
text-align: left;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.vp-set-seg {
|
|
333
|
+
display: inline-flex;
|
|
334
|
+
border: var(--jatui-border-w, 1px) solid color-mix(in oklch, var(--color-base-content) 20%, transparent);
|
|
335
|
+
border-radius: var(--jatui-radius-field, 0.25rem);
|
|
336
|
+
overflow: hidden;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.vp-set-seg button {
|
|
340
|
+
padding: 2px var(--jatui-space-md, 6px);
|
|
341
|
+
font: inherit;
|
|
342
|
+
font-size: 0.7rem;
|
|
343
|
+
color: inherit;
|
|
344
|
+
background: none;
|
|
345
|
+
border: none;
|
|
346
|
+
cursor: pointer;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.vp-set-seg button + button {
|
|
350
|
+
border-left: var(--jatui-border-w, 1px) solid color-mix(in oklch, var(--color-base-content) 20%, transparent);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.vp-set-seg button[aria-checked='true'] {
|
|
354
|
+
background: color-mix(in oklch, var(--color-primary) 22%, transparent);
|
|
355
|
+
font-weight: 700;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.vp-set-range {
|
|
359
|
+
width: 7rem;
|
|
360
|
+
accent-color: var(--color-primary);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.vp-set-value {
|
|
364
|
+
min-width: 3.2em;
|
|
365
|
+
font-size: 0.7rem;
|
|
366
|
+
font-variant-numeric: tabular-nums;
|
|
367
|
+
text-align: right;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.vp-set-switch:focus-visible,
|
|
371
|
+
.vp-set-seg button:focus-visible,
|
|
372
|
+
.vp-set-range:focus-visible,
|
|
373
|
+
.vp-set-close:focus-visible,
|
|
374
|
+
.vp-set-help:focus-visible {
|
|
375
|
+
outline: 2px solid var(--color-primary);
|
|
376
|
+
outline-offset: 1px;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.vp-set-empty {
|
|
380
|
+
margin: var(--jatui-space-md, 6px) 0;
|
|
381
|
+
font-size: 0.75rem;
|
|
382
|
+
color: color-mix(in oklch, var(--color-base-content) 68%, transparent);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.vp-set-foot {
|
|
386
|
+
display: flex;
|
|
387
|
+
flex-wrap: wrap;
|
|
388
|
+
justify-content: space-between;
|
|
389
|
+
gap: var(--jatui-space-sm, 4px);
|
|
390
|
+
padding: var(--jatui-space-md, 6px) var(--jatui-space-lg, 12px);
|
|
391
|
+
font-size: 0.66rem;
|
|
392
|
+
color: color-mix(in oklch, var(--color-base-content) 60%, transparent);
|
|
393
|
+
border-top: var(--jatui-border-w, 1px) solid color-mix(in oklch, var(--color-base-content) 10%, transparent);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.vp-set-help {
|
|
397
|
+
color: var(--color-primary);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
@keyframes vp-set-in {
|
|
401
|
+
from {
|
|
402
|
+
opacity: 0;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
@media (prefers-reduced-motion: reduce) {
|
|
407
|
+
.vp-set-backdrop {
|
|
408
|
+
animation: none;
|
|
409
|
+
}
|
|
410
|
+
.vp-set-knob {
|
|
411
|
+
transition: none;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { SettingValue } from './registry';
|
|
2
|
+
import { type SettingsPanelGroup } from './panel';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
groups: SettingsPanelGroup[];
|
|
5
|
+
/** Effective value per id — what the player is doing right now. */
|
|
6
|
+
values: Record<string, SettingValue>;
|
|
7
|
+
onchange: (id: string, value: SettingValue) => void;
|
|
8
|
+
onclose: () => void;
|
|
9
|
+
/** A host help page explaining all of this ("How all of this works →"). */
|
|
10
|
+
helpHref?: string | null;
|
|
11
|
+
/** The host supplied a store — the footer says choices are remembered. */
|
|
12
|
+
persisted?: boolean;
|
|
13
|
+
};
|
|
14
|
+
declare const SettingsPanel: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
15
|
+
type SettingsPanel = ReturnType<typeof SettingsPanel>;
|
|
16
|
+
export default SettingsPanel;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The settings panel's model (mos-5lzns.35) — pure, so what the panel shows
|
|
3
|
+
* is node-testable without a DOM. The component renders exactly what these
|
|
4
|
+
* return; nothing about which rows exist is decided in markup.
|
|
5
|
+
*/
|
|
6
|
+
import { type SettingAvailability, type SettingSpec, type SettingValue } from './registry';
|
|
7
|
+
export interface SettingsPanelGroup {
|
|
8
|
+
group: string;
|
|
9
|
+
rows: SettingSpec[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The rows this player can honestly offer, grouped in panel order. A row
|
|
13
|
+
* whose `when` is not true in `available` is omitted — never shown disabled:
|
|
14
|
+
* a switch for a feature this player doesn't have is noise, and a disabled
|
|
15
|
+
* one invites the question it can't answer.
|
|
16
|
+
*/
|
|
17
|
+
export declare function panelGroups(available: SettingAvailability, specs?: SettingSpec[]): SettingsPanelGroup[];
|
|
18
|
+
/** How a value reads in the panel (the range's readout, an enum's label, On/Off). */
|
|
19
|
+
export declare function formatSettingValue(spec: SettingSpec, value: SettingValue): string;
|
|
20
|
+
/**
|
|
21
|
+
* Radio-group arrow keys: the next option in `dir`, wrapping (APG radio
|
|
22
|
+
* pattern). An unknown current value starts from the first option.
|
|
23
|
+
*/
|
|
24
|
+
export declare function stepOption(spec: SettingSpec, current: SettingValue, dir: 1 | -1): string | undefined;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The settings panel's model (mos-5lzns.35) — pure, so what the panel shows
|
|
3
|
+
* is node-testable without a DOM. The component renders exactly what these
|
|
4
|
+
* return; nothing about which rows exist is decided in markup.
|
|
5
|
+
*/
|
|
6
|
+
import { SETTING_GROUPS, VIDEO_SETTINGS } from './registry';
|
|
7
|
+
/**
|
|
8
|
+
* The rows this player can honestly offer, grouped in panel order. A row
|
|
9
|
+
* whose `when` is not true in `available` is omitted — never shown disabled:
|
|
10
|
+
* a switch for a feature this player doesn't have is noise, and a disabled
|
|
11
|
+
* one invites the question it can't answer.
|
|
12
|
+
*/
|
|
13
|
+
export function panelGroups(available, specs = VIDEO_SETTINGS) {
|
|
14
|
+
const order = [...SETTING_GROUPS];
|
|
15
|
+
for (const s of specs)
|
|
16
|
+
if (!order.includes(s.group))
|
|
17
|
+
order.push(s.group);
|
|
18
|
+
const out = [];
|
|
19
|
+
for (const group of order) {
|
|
20
|
+
const rows = specs.filter((s) => s.group === group && s.wired !== false && available[s.when] === true);
|
|
21
|
+
if (rows.length)
|
|
22
|
+
out.push({ group, rows });
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
/** How a value reads in the panel (the range's readout, an enum's label, On/Off). */
|
|
27
|
+
export function formatSettingValue(spec, value) {
|
|
28
|
+
try {
|
|
29
|
+
if (spec.type === 'bool')
|
|
30
|
+
return value ? 'On' : 'Off';
|
|
31
|
+
if (spec.type === 'enum')
|
|
32
|
+
return spec.options?.find((o) => o.value === value)?.label ?? String(value);
|
|
33
|
+
const n = typeof value === 'number' ? value : Number(value);
|
|
34
|
+
if (!Number.isFinite(n))
|
|
35
|
+
return '';
|
|
36
|
+
// Round to the step so 0.30000000000000004 never reaches the screen.
|
|
37
|
+
const step = spec.step && spec.step > 0 ? spec.step : 0;
|
|
38
|
+
const decimals = step ? Math.max(0, Math.min(6, (String(step).split('.')[1] ?? '').length)) : 2;
|
|
39
|
+
const shown = Number(n.toFixed(decimals));
|
|
40
|
+
return spec.format ? spec.format(shown) : String(shown);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Radio-group arrow keys: the next option in `dir`, wrapping (APG radio
|
|
48
|
+
* pattern). An unknown current value starts from the first option.
|
|
49
|
+
*/
|
|
50
|
+
export function stepOption(spec, current, dir) {
|
|
51
|
+
const opts = spec.options ?? [];
|
|
52
|
+
if (!opts.length)
|
|
53
|
+
return undefined;
|
|
54
|
+
const i = opts.findIndex((o) => o.value === current);
|
|
55
|
+
if (i < 0)
|
|
56
|
+
return opts[0].value;
|
|
57
|
+
return opts[(i + dir + opts.length) % opts.length].value;
|
|
58
|
+
}
|