@heyramzi/ui 2.21.1 → 2.22.1
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/list-page/toolbar-density.js +26 -0
- package/package.json +1 -1
- package/src/canvas/ui/canvas-grid.svelte +72 -12
- package/src/components/list-page/list-page-shell.svelte +57 -2
- package/src/components/list-page/toolbar-density.ts +38 -0
- package/src/components/segmented/segmented-filter.svelte +84 -48
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { getContext, setContext } from "svelte";
|
|
2
|
+
// WHY context: the shell owns the measurement and 26 pages render their tabs
|
|
3
|
+
// through a snippet, so a prop would be an edit on every page.
|
|
4
|
+
const KEY = Symbol("list-page.toolbar-density");
|
|
5
|
+
export function setToolbarDensity(density) {
|
|
6
|
+
setContext(KEY, density);
|
|
7
|
+
}
|
|
8
|
+
/** Undefined outside a list-page toolbar, where nothing ever folds. */
|
|
9
|
+
export function getToolbarDensity() {
|
|
10
|
+
return getContext(KEY);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The width a row needs to sit on one line with every child at its natural
|
|
14
|
+
* size. Read in one synchronous pass, so the browser never paints the probe.
|
|
15
|
+
*/
|
|
16
|
+
// WHY `max-content` on a probe, not the row's own width: the row is sized by
|
|
17
|
+
// its container, so reading it back always says "it fits" (see collapse.ts).
|
|
18
|
+
export function naturalRowWidth(row) {
|
|
19
|
+
const { width, flexWrap } = row.style;
|
|
20
|
+
row.style.width = "max-content";
|
|
21
|
+
row.style.flexWrap = "nowrap";
|
|
22
|
+
const needed = row.getBoundingClientRect().width;
|
|
23
|
+
row.style.width = width;
|
|
24
|
+
row.style.flexWrap = flexWrap;
|
|
25
|
+
return needed;
|
|
26
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heyramzi/ui",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.22.1",
|
|
4
4
|
"description": "Svelte 5 UI for apps: primitives, effects and animations, plus work (tasks and docs), charts, slides, an editor, onboarding and a canvas as subpaths.",
|
|
5
5
|
"homepage": "https://ui.heyramzi.com",
|
|
6
6
|
"license": "MIT",
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
color?: string;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/** x, y, width, height in device pixels. */
|
|
20
|
+
type Rect = [number, number, number, number];
|
|
21
|
+
|
|
19
22
|
// =========================
|
|
20
23
|
// CONSTANTS
|
|
21
24
|
// =========================
|
|
@@ -58,6 +61,16 @@
|
|
|
58
61
|
// Pan redraws every frame, so a tile is built once per size and colour.
|
|
59
62
|
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- render cache written inside the draw effect, never rendered
|
|
60
63
|
const tiles = new Map<string, HTMLCanvasElement>();
|
|
64
|
+
// What the canvas holds now, its origin in device pixels. Plain: only the draw effect reads it.
|
|
65
|
+
let painted: {
|
|
66
|
+
zoom: number;
|
|
67
|
+
ratio: number;
|
|
68
|
+
fill: string;
|
|
69
|
+
w: number;
|
|
70
|
+
h: number;
|
|
71
|
+
x: number;
|
|
72
|
+
y: number;
|
|
73
|
+
} | null = null;
|
|
61
74
|
|
|
62
75
|
function dotTile(step: number, radius: number, fill: string) {
|
|
63
76
|
const key = `${step}:${radius.toFixed(2)}:${fill}`;
|
|
@@ -77,21 +90,20 @@
|
|
|
77
90
|
return tile;
|
|
78
91
|
}
|
|
79
92
|
|
|
93
|
+
/** Fills the given device-pixel rects with the dots, the grid's origin at `originX`, `originY`. */
|
|
80
94
|
// fallow-ignore-next-line complexity
|
|
81
|
-
function
|
|
82
|
-
|
|
83
|
-
|
|
95
|
+
function paint(
|
|
96
|
+
context: CanvasRenderingContext2D,
|
|
97
|
+
zoom: number,
|
|
84
98
|
fill: string,
|
|
99
|
+
originX: number,
|
|
100
|
+
originY: number,
|
|
101
|
+
rects: Rect[],
|
|
85
102
|
) {
|
|
86
|
-
const context = element.getContext("2d");
|
|
87
|
-
if (!context) return;
|
|
88
|
-
context.setTransform(1, 0, 0, 1, 0, 0);
|
|
89
|
-
context.clearRect(0, 0, element.width, element.height);
|
|
90
|
-
|
|
91
103
|
for (let level = 0; level < LEVELS; level++) {
|
|
92
|
-
const spacing = gap * 2 ** level *
|
|
104
|
+
const spacing = gap * 2 ** level * zoom;
|
|
93
105
|
// A coarser level draws bigger dots, so zooming out reads as moving away.
|
|
94
|
-
const screenRadius = (size / 2) * 2 ** level *
|
|
106
|
+
const screenRadius = (size / 2) * 2 ** level * zoom;
|
|
95
107
|
const radius = Math.min(MAX_RADIUS, Math.max(MIN_RADIUS, screenRadius)) * ratio;
|
|
96
108
|
const last = level === LEVELS - 1;
|
|
97
109
|
const alpha = last ? 1 : smoothstep(FADE_START, FADE_END, spacing);
|
|
@@ -102,16 +114,64 @@
|
|
|
102
114
|
const pattern = context.createPattern(dotTile(tileSize, radius / scale, fill), "repeat");
|
|
103
115
|
if (!pattern) return;
|
|
104
116
|
pattern.setTransform(
|
|
105
|
-
new DOMMatrix([scale, 0, 0, scale,
|
|
117
|
+
new DOMMatrix([scale, 0, 0, scale, originX - step / 2, originY - step / 2]),
|
|
106
118
|
);
|
|
107
119
|
context.globalAlpha = alpha;
|
|
108
120
|
context.fillStyle = pattern;
|
|
109
|
-
context.fillRect(
|
|
121
|
+
for (const [x, y, w, h] of rects) context.fillRect(x, y, w, h);
|
|
110
122
|
if (alpha >= 0.999) break;
|
|
111
123
|
}
|
|
112
124
|
context.globalAlpha = 1;
|
|
113
125
|
}
|
|
114
126
|
|
|
127
|
+
/** The strips a shift of `dx`, `dy` uncovers: a column on one side and a row on the other. */
|
|
128
|
+
function uncovered(dx: number, dy: number, w: number, h: number): Rect[] {
|
|
129
|
+
const rects: Rect[] = [];
|
|
130
|
+
if (dx > 0) rects.push([0, 0, dx, h]);
|
|
131
|
+
if (dx < 0) rects.push([w + dx, 0, -dx, h]);
|
|
132
|
+
if (dy > 0) rects.push([0, 0, w, dy]);
|
|
133
|
+
if (dy < 0) rects.push([0, h + dy, w, -dy]);
|
|
134
|
+
return rects;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// A pan moves the pixels already drawn and paints only the strip it uncovered; anything else repaints it all.
|
|
138
|
+
// fallow-ignore-next-line complexity
|
|
139
|
+
function draw(
|
|
140
|
+
element: HTMLCanvasElement,
|
|
141
|
+
view: { x: number; y: number; zoom: number },
|
|
142
|
+
fill: string,
|
|
143
|
+
) {
|
|
144
|
+
const context = element.getContext("2d");
|
|
145
|
+
if (!context) return;
|
|
146
|
+
const { width: w, height: h } = element;
|
|
147
|
+
const x = view.x * ratio;
|
|
148
|
+
const y = view.y * ratio;
|
|
149
|
+
const last = painted;
|
|
150
|
+
const same =
|
|
151
|
+
last &&
|
|
152
|
+
last.zoom === view.zoom &&
|
|
153
|
+
last.ratio === ratio &&
|
|
154
|
+
last.fill === fill &&
|
|
155
|
+
last.w === w &&
|
|
156
|
+
last.h === h;
|
|
157
|
+
// Whole pixels only, so the shifted dots stay sharp; the rest waits for the next pan or zoom, never past half a pixel.
|
|
158
|
+
const dx = same ? Math.round(x - last.x) : 0;
|
|
159
|
+
const dy = same ? Math.round(y - last.y) : 0;
|
|
160
|
+
if (same && Math.abs(dx) < w && Math.abs(dy) < h) {
|
|
161
|
+
if (!dx && !dy) return;
|
|
162
|
+
context.globalCompositeOperation = "copy";
|
|
163
|
+
context.drawImage(element, dx, dy);
|
|
164
|
+
context.globalCompositeOperation = "source-over";
|
|
165
|
+
painted = { ...last, x: last.x + dx, y: last.y + dy };
|
|
166
|
+
paint(context, view.zoom, fill, painted.x, painted.y, uncovered(dx, dy, w, h));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
context.setTransform(1, 0, 0, 1, 0, 0);
|
|
170
|
+
context.clearRect(0, 0, w, h);
|
|
171
|
+
painted = { zoom: view.zoom, ratio, fill, w, h, x, y };
|
|
172
|
+
paint(context, view.zoom, fill, x, y, [[0, 0, w, h]]);
|
|
173
|
+
}
|
|
174
|
+
|
|
115
175
|
// =========================
|
|
116
176
|
// EFFECTS
|
|
117
177
|
// =========================
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { pageGutterClass } from "../../lib/layout.js";
|
|
9
9
|
import { getSectionHost } from "./section-host.js";
|
|
10
10
|
import SectionRuns from "../segmented/section-runs.svelte";
|
|
11
|
+
import { naturalRowWidth, setToolbarDensity } from "./toolbar-density.js";
|
|
11
12
|
import type { PhosphorIcon, IconWeight } from "../../lib/icon-types.js";
|
|
12
13
|
|
|
13
14
|
// =========================
|
|
@@ -67,6 +68,16 @@
|
|
|
67
68
|
// =========================
|
|
68
69
|
// Null off an app shell, where the toolbar just has no section strip to draw.
|
|
69
70
|
const section = getSectionHost();
|
|
71
|
+
let row = $state<HTMLElement>();
|
|
72
|
+
let tabsBox = $state<HTMLElement>();
|
|
73
|
+
// True when the row at full size would wrap. The tabs then draw as one
|
|
74
|
+
// dropdown, the first control with a smaller self that still does its job.
|
|
75
|
+
let compact = $state(false);
|
|
76
|
+
setToolbarDensity({
|
|
77
|
+
get compact() {
|
|
78
|
+
return compact;
|
|
79
|
+
},
|
|
80
|
+
});
|
|
70
81
|
|
|
71
82
|
// =========================
|
|
72
83
|
// DERIVED
|
|
@@ -90,6 +101,37 @@
|
|
|
90
101
|
if (!hasToolbar) return;
|
|
91
102
|
return section?.claim();
|
|
92
103
|
});
|
|
104
|
+
// One row, never two (Ramzi, 9 Nov 2026): /tasks at 1500px put its status
|
|
105
|
+
// tabs and actions on a second line under search and filters.
|
|
106
|
+
// WHY the tabs' full width is remembered: once folded, the row only holds the
|
|
107
|
+
// dropdown, and a probe of it would say the full strip fits when it doesn't.
|
|
108
|
+
$effect(() => {
|
|
109
|
+
if (!row) return;
|
|
110
|
+
const el = row;
|
|
111
|
+
let fullTabs = 0;
|
|
112
|
+
let frame = 0;
|
|
113
|
+
const measure = () => {
|
|
114
|
+
frame = 0;
|
|
115
|
+
const needed = naturalRowWidth(el);
|
|
116
|
+
const tabs = tabsBox?.getBoundingClientRect().width ?? 0;
|
|
117
|
+
if (!compact) fullTabs = tabs;
|
|
118
|
+
const full = compact ? needed - tabs + fullTabs : needed;
|
|
119
|
+
compact = full > el.clientWidth + 0.5;
|
|
120
|
+
};
|
|
121
|
+
// A frame's delay folds a burst of resizes into one probe and keeps the
|
|
122
|
+
// observer from looping on its own state change.
|
|
123
|
+
const schedule = () => (frame ||= requestAnimationFrame(measure));
|
|
124
|
+
const observer = new ResizeObserver(schedule);
|
|
125
|
+
observer.observe(el);
|
|
126
|
+
for (const child of el.children) observer.observe(child);
|
|
127
|
+
// Scheduled, not called: a direct call would make this effect track
|
|
128
|
+
// `compact` and rerun on every fold, dropping the remembered width.
|
|
129
|
+
schedule();
|
|
130
|
+
return () => {
|
|
131
|
+
observer.disconnect();
|
|
132
|
+
cancelAnimationFrame(frame);
|
|
133
|
+
};
|
|
134
|
+
});
|
|
93
135
|
// The shared gutter from `lib/layout.ts`. A typed one put the search field
|
|
94
136
|
// 8px left of the section tabs above it.
|
|
95
137
|
let outerClass = $derived(
|
|
@@ -127,7 +169,16 @@
|
|
|
127
169
|
<div
|
|
128
170
|
class="@container/toolbar sticky top-0 z-20 -mx-4 -mt-4 bg-card/95 px-4 py-4 backdrop-blur supports-[backdrop-filter]:bg-card/70"
|
|
129
171
|
>
|
|
130
|
-
|
|
172
|
+
<!-- Folded and still short of room, the row stops wrapping and the
|
|
173
|
+
filters strip scrolls, since it's the one part that already can. -->
|
|
174
|
+
<div
|
|
175
|
+
bind:this={row}
|
|
176
|
+
class={cn(
|
|
177
|
+
"flex flex-wrap items-center gap-2",
|
|
178
|
+
compact && "@3xl/toolbar:flex-nowrap",
|
|
179
|
+
toolbarClass,
|
|
180
|
+
)}
|
|
181
|
+
>
|
|
131
182
|
<!-- The section tabs lead: which page comes before which slice of it.
|
|
132
183
|
One row replaces two rows that held four controls. -->
|
|
133
184
|
{#if section?.strip}
|
|
@@ -148,10 +199,14 @@
|
|
|
148
199
|
class={cn(
|
|
149
200
|
"order-2 flex min-w-0 flex-wrap items-center gap-2 @3xl/toolbar:order-3",
|
|
150
201
|
hasLeading ? "@3xl/toolbar:ms-auto" : "flex-1",
|
|
202
|
+
compact && "@3xl/toolbar:shrink-0",
|
|
151
203
|
)}
|
|
152
204
|
>
|
|
153
205
|
{#if tabs}
|
|
154
|
-
<div
|
|
206
|
+
<div
|
|
207
|
+
bind:this={tabsBox}
|
|
208
|
+
class={cn("flex min-w-0 items-center", !hasLeading && "me-auto")}
|
|
209
|
+
>
|
|
155
210
|
{@render tabs()}
|
|
156
211
|
</div>
|
|
157
212
|
{/if}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { getContext, setContext } from "svelte";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Whether the list-page toolbar has run out of room. `ListPageShell` measures
|
|
5
|
+
* its row and sets this; a control inside it reads it and draws a smaller self.
|
|
6
|
+
*/
|
|
7
|
+
export interface ToolbarDensity {
|
|
8
|
+
readonly compact: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// WHY context: the shell owns the measurement and 26 pages render their tabs
|
|
12
|
+
// through a snippet, so a prop would be an edit on every page.
|
|
13
|
+
const KEY = Symbol("list-page.toolbar-density");
|
|
14
|
+
|
|
15
|
+
export function setToolbarDensity(density: ToolbarDensity): void {
|
|
16
|
+
setContext(KEY, density);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Undefined outside a list-page toolbar, where nothing ever folds. */
|
|
20
|
+
export function getToolbarDensity(): ToolbarDensity | undefined {
|
|
21
|
+
return getContext<ToolbarDensity | undefined>(KEY);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The width a row needs to sit on one line with every child at its natural
|
|
26
|
+
* size. Read in one synchronous pass, so the browser never paints the probe.
|
|
27
|
+
*/
|
|
28
|
+
// WHY `max-content` on a probe, not the row's own width: the row is sized by
|
|
29
|
+
// its container, so reading it back always says "it fits" (see collapse.ts).
|
|
30
|
+
export function naturalRowWidth(row: HTMLElement): number {
|
|
31
|
+
const { width, flexWrap } = row.style;
|
|
32
|
+
row.style.width = "max-content";
|
|
33
|
+
row.style.flexWrap = "nowrap";
|
|
34
|
+
const needed = row.getBoundingClientRect().width;
|
|
35
|
+
row.style.width = width;
|
|
36
|
+
row.style.flexWrap = flexWrap;
|
|
37
|
+
return needed;
|
|
38
|
+
}
|
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
import { segmentedTriggerClass } from "./segmented.js";
|
|
16
16
|
import { Icon } from "../icon/index.js";
|
|
17
17
|
import { Mark } from "../mark/index.js";
|
|
18
|
+
import { CaretDown } from "phosphor-svelte";
|
|
19
|
+
import { Picker } from "../picker/index.js";
|
|
20
|
+
import { toolbarTriggerClass } from "../toolbar/toolbar-trigger.js";
|
|
21
|
+
import { getToolbarDensity } from "../list-page/toolbar-density.js";
|
|
18
22
|
|
|
19
23
|
// =========================
|
|
20
24
|
// TYPES
|
|
@@ -87,6 +91,20 @@
|
|
|
87
91
|
),
|
|
88
92
|
);
|
|
89
93
|
|
|
94
|
+
// Inside a list-page toolbar that ran out of room, the strip folds into one
|
|
95
|
+
// dropdown that names the current option. See `ListPageShell`.
|
|
96
|
+
const density = getToolbarDensity();
|
|
97
|
+
let folded = $derived(density?.compact === true);
|
|
98
|
+
let current = $derived(merged.find((option) => option.value === value) ?? merged[0]);
|
|
99
|
+
let pickerOptions = $derived(
|
|
100
|
+
merged.map((option) => ({
|
|
101
|
+
value: option.value,
|
|
102
|
+
label: option.label,
|
|
103
|
+
hint: option.count != null ? String(option.count) : undefined,
|
|
104
|
+
icon: option.icon,
|
|
105
|
+
})),
|
|
106
|
+
);
|
|
107
|
+
|
|
90
108
|
function pick(next: T) {
|
|
91
109
|
value = next;
|
|
92
110
|
onValueChange?.(next);
|
|
@@ -98,53 +116,71 @@
|
|
|
98
116
|
<!-- =========================== -->
|
|
99
117
|
<!-- WHY scroll, not wrap: it's one object. Past `max` the tail collapses into
|
|
100
118
|
a menu first, so scrolling is the last resort. -->
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
{
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
{#if folded && current}
|
|
120
|
+
<Picker
|
|
121
|
+
options={pickerOptions}
|
|
122
|
+
{value}
|
|
123
|
+
onSelect={pick}
|
|
124
|
+
triggerClass={toolbarTriggerClass({ className: "group/segment text-foreground" })}
|
|
125
|
+
triggerLabel={`${ariaLabel}: ${current.label}`}
|
|
126
|
+
align="end"
|
|
127
|
+
>
|
|
128
|
+
{#snippet trigger()}
|
|
129
|
+
<Segment label={current.label} count={current.count} icon={current.icon} active />
|
|
130
|
+
<CaretDown
|
|
131
|
+
class="size-3 shrink-0 opacity-60 transition-transform duration-150 group-data-[state=open]/segment:rotate-180"
|
|
132
|
+
/>
|
|
133
|
+
{/snippet}
|
|
134
|
+
</Picker>
|
|
135
|
+
{:else}
|
|
136
|
+
<div
|
|
137
|
+
class={cn("no-scrollbar flex max-w-full min-w-0 overflow-x-auto", className)}
|
|
138
|
+
role="group"
|
|
139
|
+
aria-label={ariaLabel}
|
|
140
|
+
>
|
|
141
|
+
<SegmentedTrack fill={block}>
|
|
142
|
+
{#each split.visible as option (option.value)}
|
|
143
|
+
{@const active = value === option.value}
|
|
144
|
+
{@const markOnly = iconOnly && Boolean(option.image || option.icon)}
|
|
145
|
+
<button
|
|
146
|
+
type="button"
|
|
147
|
+
class={segmentedTriggerClass({ fill: block, active })}
|
|
148
|
+
onclick={() => pick(option.value)}
|
|
149
|
+
aria-pressed={active}
|
|
150
|
+
aria-label={markOnly ? option.label : undefined}
|
|
151
|
+
title={markOnly ? option.label : undefined}
|
|
152
|
+
>
|
|
153
|
+
<!-- WHY the mark is drawn here, not by `Segment`: `Segment` always
|
|
119
154
|
writes the label, and a count or dot beside a bare mark reads as noise. -->
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
155
|
+
{#if markOnly && option.image}
|
|
156
|
+
<Mark
|
|
157
|
+
src={option.image}
|
|
158
|
+
label={option.label}
|
|
159
|
+
class={cn(
|
|
160
|
+
"opacity-75 transition-opacity duration-[var(--duration-fast)] ease-[var(--ease-smooth-out)] group-hover/segment:opacity-100",
|
|
161
|
+
active && "opacity-100",
|
|
162
|
+
)}
|
|
163
|
+
/>
|
|
164
|
+
{:else if markOnly && option.icon}
|
|
165
|
+
<Icon icon={option.icon} size={16} class="shrink-0" />
|
|
166
|
+
{:else}
|
|
167
|
+
<Segment
|
|
168
|
+
label={option.label}
|
|
169
|
+
count={option.count}
|
|
170
|
+
icon={option.icon}
|
|
171
|
+
image={option.image}
|
|
172
|
+
done={option.done}
|
|
173
|
+
badge={option.badge}
|
|
174
|
+
{active}
|
|
175
|
+
/>
|
|
176
|
+
{/if}
|
|
177
|
+
</button>
|
|
178
|
+
{/each}
|
|
179
|
+
{#snippet after()}
|
|
180
|
+
{#if split.hidden.length > 0}
|
|
181
|
+
<SegmentedOverflow items={split.hidden} onSelect={pick} label={moreLabel} fill={block} />
|
|
141
182
|
{/if}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
<SegmentedOverflow items={split.hidden} onSelect={pick} label={moreLabel} fill={block} />
|
|
147
|
-
{/if}
|
|
148
|
-
{/snippet}
|
|
149
|
-
</SegmentedTrack>
|
|
150
|
-
</div>
|
|
183
|
+
{/snippet}
|
|
184
|
+
</SegmentedTrack>
|
|
185
|
+
</div>
|
|
186
|
+
{/if}
|