@streamscloud/kit 0.65.0 → 0.66.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.
@@ -1,5 +1,9 @@
1
1
  import type { Action } from 'svelte/action';
2
- export declare const horizontalWheelScroll: Action<HTMLElement, {
2
+ type HorizontalWheelScrollParams = {
3
3
  isolateScroll: boolean;
4
+ /** `false` lets the wheel through to the page once the element can't scroll further that way. @default true */
5
+ isolateAtEdges?: boolean;
4
6
  speed?: number;
5
- } | undefined>;
7
+ };
8
+ export declare const horizontalWheelScroll: Action<HTMLElement, HorizontalWheelScrollParams | undefined>;
9
+ export {};
@@ -5,8 +5,10 @@ export const horizontalWheelScroll = (node, param = { isolateScroll: true, speed
5
5
  return;
6
6
  }
7
7
  const delta = Math.abs(event.deltaX) > Math.abs(event.deltaY) ? event.deltaX : event.deltaY;
8
+ const maxScrollLeft = node.scrollWidth - node.clientWidth;
9
+ const atEdge = (delta < 0 && node.scrollLeft <= 0) || (delta > 0 && node.scrollLeft >= maxScrollLeft - 1);
8
10
  node.scrollBy({ left: delta * (param.speed ?? 1) });
9
- if (param.isolateScroll) {
11
+ if (param.isolateScroll && (param.isolateAtEdges !== false || !atEdge)) {
10
12
  event.preventDefault();
11
13
  event.stopPropagation();
12
14
  }
@@ -1,3 +1,4 @@
1
1
  export declare class BreadcrumbLocalization {
2
2
  get navLabel(): string;
3
+ get hiddenLevels(): string;
3
4
  }
@@ -3,10 +3,17 @@ const loc = {
3
3
  navLabel: {
4
4
  en: 'Breadcrumb',
5
5
  no: 'Brødsmuler'
6
+ },
7
+ hiddenLevels: {
8
+ en: 'Show hidden levels',
9
+ no: 'Vis skjulte nivåer'
6
10
  }
7
11
  };
8
12
  export class BreadcrumbLocalization {
9
13
  get navLabel() {
10
14
  return loc.navLabel[AppLocale.current];
11
15
  }
16
+ get hiddenLevels() {
17
+ return loc.hiddenLevels[AppLocale.current];
18
+ }
12
19
  }
@@ -1,19 +1,95 @@
1
- <script lang="ts">import { BreadcrumbLocalization } from './breadcrumb-localization';
2
- let { items, separator = '/', underline = 'hover' } = $props();
1
+ <script lang="ts">import { horizontalWheelScroll } from '../../core/actions';
2
+ import { Popover } from '../popover';
3
+ import { BreadcrumbLocalization } from './breadcrumb-localization';
4
+ import { tick } from 'svelte';
5
+ let { items, separator = '/', underline = 'hover', overflow = 'wrap', maxItems } = $props();
3
6
  const localization = new BreadcrumbLocalization();
7
+ let canScrollBack = $state(false);
8
+ let canScrollForward = $state(false);
9
+ let menuOpened = $state(false);
10
+ const tailLength = $derived(maxItems === undefined ? 0 : Math.max(2, maxItems) - 1);
11
+ const collapsed = $derived(maxItems !== undefined && items.length > tailLength + 1);
12
+ const hiddenItems = $derived(collapsed ? items.slice(1, items.length - tailLength) : []);
13
+ const shownItems = $derived(collapsed ? [items[0], ...items.slice(items.length - tailLength)] : items);
14
+ const scrolls = $derived(overflow === 'scroll');
15
+ // The "…" Popover renders inside the track; a mask would hide it.
16
+ const masked = $derived(scrolls && !menuOpened);
17
+ const updateScrollState = (node) => {
18
+ canScrollBack = node.scrollLeft > 0;
19
+ canScrollForward = node.scrollLeft < node.scrollWidth - node.clientWidth - 1;
20
+ };
21
+ const scrollTrack = (node, _items) => {
22
+ const scrollToCurrent = () => {
23
+ if (scrolls) {
24
+ node.scrollLeft = node.scrollWidth;
25
+ }
26
+ updateScrollState(node);
27
+ };
28
+ // Children too: the track's own box is pinned at max-width, so a web-font swap only resizes its content.
29
+ const observer = new ResizeObserver(scrollToCurrent);
30
+ const observeTrackAndCrumbs = () => {
31
+ observer.disconnect();
32
+ observer.observe(node);
33
+ for (const child of node.children) {
34
+ observer.observe(child);
35
+ }
36
+ };
37
+ observeTrackAndCrumbs();
38
+ return {
39
+ update: async () => {
40
+ await tick();
41
+ observeTrackAndCrumbs();
42
+ scrollToCurrent();
43
+ },
44
+ destroy: () => observer.disconnect()
45
+ };
46
+ };
4
47
  </script>
5
48
 
6
- <div class="breadcrumb breadcrumb--underline-{underline}" role="navigation" aria-label={localization.navLabel}>
7
- {#each items as item, i (item.label + i)}
49
+ {#snippet crumb(item: BreadcrumbItem, current: boolean, menuItem: boolean)}
50
+ {#if current}
51
+ <span class="breadcrumb__current" aria-current="page">{item.label}</span>
52
+ {:else if item.href}
53
+ <a class="breadcrumb__link" class:breadcrumb__link--menu={menuItem} href={item.href} onclick={(e) => item.on?.click?.(e)}>{item.label}</a>
54
+ {:else if item.on?.click}
55
+ <button type="button" class="breadcrumb__link" class:breadcrumb__link--menu={menuItem} onclick={(e) => item.on?.click?.(e)}>{item.label}</button>
56
+ {:else}
57
+ <span class="breadcrumb__current" class:breadcrumb__current--menu={menuItem}>{item.label}</span>
58
+ {/if}
59
+ {/snippet}
60
+
61
+ <div
62
+ class="breadcrumb breadcrumb--underline-{underline}"
63
+ class:breadcrumb--scroll={scrolls}
64
+ class:breadcrumb--mask-back={masked && canScrollBack && !canScrollForward}
65
+ class:breadcrumb--mask-forward={masked && canScrollForward && !canScrollBack}
66
+ class:breadcrumb--mask-both={masked && canScrollBack && canScrollForward}
67
+ role="navigation"
68
+ aria-label={localization.navLabel}
69
+ use:scrollTrack={shownItems}
70
+ use:horizontalWheelScroll={{ isolateScroll: scrolls, isolateAtEdges: false }}
71
+ onscroll={(e) => updateScrollState(e.currentTarget)}>
72
+ {#each shownItems as item, i (`${i}:${item.label}`)}
8
73
  {#if i > 0}<span class="breadcrumb__separator" aria-hidden="true">{separator}</span>{/if}
9
- {#if i === items.length - 1}
10
- <span class="breadcrumb__current" aria-current="page">{item.label}</span>
11
- {:else if item.href}
12
- <a class="breadcrumb__link" href={item.href} onclick={(e) => item.on?.click?.(e)}>{item.label}</a>
13
- {:else if item.on?.click}
14
- <button type="button" class="breadcrumb__link" onclick={(e) => item.on?.click?.(e)}>{item.label}</button>
15
- {:else}
16
- <span class="breadcrumb__current">{item.label}</span>
74
+ {@render crumb(item, i === shownItems.length - 1, false)}
75
+ {#if i === 0 && collapsed}
76
+ <span class="breadcrumb__separator" aria-hidden="true">{separator}</span>
77
+ <span class="breadcrumb__more" onwheel={(e) => e.stopPropagation()}>
78
+ <Popover
79
+ position="bottom-start"
80
+ fixedPosition
81
+ aria-label={localization.hiddenLevels}
82
+ on={{ opened: () => (menuOpened = true), closed: () => (menuOpened = false) }}>
83
+ {#snippet trigger()}
84
+ <span class="breadcrumb__more-trigger">…</span>
85
+ {/snippet}
86
+ <div class="breadcrumb__menu">
87
+ {#each hiddenItems as hidden, j (`${j}:${hidden.label}`)}
88
+ {@render crumb(hidden, false, true)}
89
+ {/each}
90
+ </div>
91
+ </Popover>
92
+ </span>
17
93
  {/if}
18
94
  {/each}
19
95
  </div>
@@ -27,7 +103,9 @@ A horizontal trail of crumbs ending at the current page. Each non-last item rend
27
103
  - plain text `<span>` when neither is set.
28
104
 
29
105
  The last item is always plain text with `aria-current="page"`. `underline` decides whether the crumb
30
- links show their underline on hover only (default), always, or never.
106
+ links show their underline on hover only (default), always, or never. `overflow="scroll"` keeps the
107
+ trail on one line that scrolls sideways; `maxItems` folds the middle into a "…" crumb that lists the
108
+ hidden levels.
31
109
 
32
110
  ### CSS Custom Properties
33
111
  | Property | Description | Default |
@@ -63,7 +141,34 @@ links show their underline on hover only (default), always, or never.
63
141
  .breadcrumb--underline-never {
64
142
  --_breadcrumb--link-text-decoration-hover: none;
65
143
  }
144
+ .breadcrumb--scroll {
145
+ --_breadcrumb--mask-size: 1.5rem;
146
+ flex-wrap: nowrap;
147
+ max-width: 100%;
148
+ padding-block: 0.25rem;
149
+ margin-block: -0.25rem;
150
+ white-space: nowrap;
151
+ overflow-x: auto;
152
+ scrollbar-width: none;
153
+ }
154
+ .breadcrumb--scroll::-webkit-scrollbar {
155
+ display: none;
156
+ }
157
+ .breadcrumb--mask-back {
158
+ mask-image: linear-gradient(to right, transparent 0%, black var(--_breadcrumb--mask-size), black 100%);
159
+ }
160
+ .breadcrumb--mask-forward {
161
+ mask-image: linear-gradient(to right, black 0%, black calc(100% - var(--_breadcrumb--mask-size)), transparent 100%);
162
+ }
163
+ .breadcrumb--mask-both {
164
+ mask-image: linear-gradient(to right, transparent 0%, black var(--_breadcrumb--mask-size), black calc(100% - var(--_breadcrumb--mask-size)), transparent 100%);
165
+ }
66
166
  .breadcrumb__link {
167
+ padding: 0;
168
+ font: inherit;
169
+ background: none;
170
+ border: none;
171
+ cursor: pointer;
67
172
  color: var(--_breadcrumb--link-color);
68
173
  text-decoration: var(--_breadcrumb--link-text-decoration);
69
174
  transition: color var(--sc-kit--duration--base) var(--sc-kit--ease--default);
@@ -77,11 +182,44 @@ links show their underline on hover only (default), always, or never.
77
182
  outline-offset: 2px;
78
183
  border-radius: var(--sc-kit--radius--sm);
79
184
  }
185
+ .breadcrumb__link--menu {
186
+ display: block;
187
+ width: 100%;
188
+ padding: var(--sc-kit--space--1) var(--sc-kit--space--3);
189
+ text-align: start;
190
+ border-radius: var(--sc-kit--radius--sm);
191
+ white-space: nowrap;
192
+ }
193
+ .breadcrumb__link--menu:hover {
194
+ background: var(--sc-kit--color--bg--hover);
195
+ }
80
196
  .breadcrumb__current {
81
197
  color: var(--_breadcrumb--current-color);
82
198
  font-weight: var(--sc-kit--font-weight--medium);
83
199
  }
200
+ .breadcrumb__current--menu {
201
+ display: block;
202
+ padding: var(--sc-kit--space--1) var(--sc-kit--space--3);
203
+ font-weight: var(--sc-kit--font-weight--regular);
204
+ white-space: nowrap;
205
+ }
84
206
  .breadcrumb__separator {
85
207
  color: var(--_breadcrumb--color);
86
208
  user-select: none;
209
+ }
210
+ .breadcrumb__more {
211
+ display: inline-flex;
212
+ }
213
+ .breadcrumb__more-trigger {
214
+ color: var(--_breadcrumb--link-color);
215
+ transition: color var(--sc-kit--duration--base) var(--sc-kit--ease--default);
216
+ }
217
+ .breadcrumb__more-trigger:hover {
218
+ color: var(--_breadcrumb--link-color-hover);
219
+ }
220
+ .breadcrumb__menu {
221
+ display: flex;
222
+ flex-direction: column;
223
+ padding: var(--sc-kit--space--1);
224
+ font-size: var(--_breadcrumb--font-size);
87
225
  }</style>
@@ -5,6 +5,10 @@ type Props = {
5
5
  separator?: string;
6
6
  /** When the crumb links show their underline: only on hover, permanently, or not at all. @default 'hover' */
7
7
  underline?: 'hover' | 'always' | 'never';
8
+ /** `'wrap'` breaks the trail onto new lines; `'scroll'` keeps one line that scrolls sideways (wheel included) and starts scrolled to the current crumb. @default 'wrap' */
9
+ overflow?: 'wrap' | 'scroll';
10
+ /** Collapse the middle of a longer trail: keep the first crumb and the last `maxItems - 1`, the hidden ones open from a "…" crumb. Values below 2 count as 2. */
11
+ maxItems?: number;
8
12
  };
9
13
  /**
10
14
  * A horizontal trail of crumbs ending at the current page. Each non-last item renders as:
@@ -14,7 +18,9 @@ type Props = {
14
18
  * - plain text `<span>` when neither is set.
15
19
  *
16
20
  * The last item is always plain text with `aria-current="page"`. `underline` decides whether the crumb
17
- * links show their underline on hover only (default), always, or never.
21
+ * links show their underline on hover only (default), always, or never. `overflow="scroll"` keeps the
22
+ * trail on one line that scrolls sideways; `maxItems` folds the middle into a "…" crumb that lists the
23
+ * hidden levels.
18
24
  *
19
25
  * ### CSS Custom Properties
20
26
  * | Property | Description | Default |
@@ -11,8 +11,9 @@ const DialogView = $derived(dialog.view);
11
11
  // doesn't always prevent dialog close. Using keydown to block ESC earlier.
12
12
  // Remove when browser bug is fixed.
13
13
  // See: https://issues.chromium.org/issues/41487622
14
+ // On window, not <dialog>: after a stacked dialog unmounts focus is on <body> and Esc natively closes this one.
14
15
  const handleKeydown = (event) => {
15
- if (event.key !== /*@wc-ignore*/ 'Escape') {
16
+ if (event.key !== 'Escape' || !isActive) {
16
17
  return;
17
18
  }
18
19
  if (!canDismiss) {
@@ -62,6 +63,8 @@ const customWidth = $derived(isCustomWidth ? size : null);
62
63
  const isStretchContent = $derived(position === 'full-screen' || position === 'full-height' || position === 'side-left' || position === 'side-right');
63
64
  </script>
64
65
 
66
+ <svelte:window onkeydown={handleKeydown} />
67
+
65
68
  <dialog
66
69
  bind:this={dialogElement}
67
70
  class="dialog-container"
@@ -79,7 +82,6 @@ const isStretchContent = $derived(position === 'full-screen' || position === 'fu
79
82
  class:dialog-container--position-full-height={position === 'full-height'}
80
83
  class:dialog-container--position-side-left={position === 'side-left'}
81
84
  class:dialog-container--position-side-right={position === 'side-right'}
82
- onkeydown={handleKeydown}
83
85
  oncancel={handleCancel}
84
86
  onclick={handleBackdropClick}
85
87
  style:--dialog-container--custom-width={customWidth}>
@@ -52,16 +52,16 @@ const onMouseDown = (e) => {
52
52
  const handleScrubberKeyDown = (event) => {
53
53
  const step = 0.05;
54
54
  let newValue;
55
- if (event.key === /*@wc-ignore*/ 'ArrowLeft') {
55
+ if (event.key === 'ArrowLeft') {
56
56
  newValue = Math.max(0, value - step);
57
57
  }
58
- else if (event.key === /*@wc-ignore*/ 'ArrowRight') {
58
+ else if (event.key === 'ArrowRight') {
59
59
  newValue = Math.min(1, value + step);
60
60
  }
61
- else if (event.key === /*@wc-ignore*/ 'Home') {
61
+ else if (event.key === 'Home') {
62
62
  newValue = 0;
63
63
  }
64
- else if (event.key === /*@wc-ignore*/ 'End') {
64
+ else if (event.key === 'End') {
65
65
  newValue = 1;
66
66
  }
67
67
  if (newValue !== undefined) {