@sveltia/ui 0.67.2 → 0.68.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.
@@ -7,7 +7,7 @@ export const SHIKI_VERSION: "4.4.3";
7
7
  /**
8
8
  * Version of this package, used to resolve the prebuilt Shiki engine chunk from a CDN.
9
9
  */
10
- export const UI_VERSION: "0.67.2";
10
+ export const UI_VERSION: "0.68.1";
11
11
  /**
12
12
  * Available syntax highlighting languages, sorted by display name.
13
13
  * @type {{ id: string, name: string, aliases?: string[] }[]}
@@ -10,7 +10,7 @@ export const SHIKI_VERSION = "4.4.3";
10
10
  /**
11
11
  * Version of this package, used to resolve the prebuilt Shiki engine chunk from a CDN.
12
12
  */
13
- export const UI_VERSION = "0.67.2";
13
+ export const UI_VERSION = "0.68.1";
14
14
 
15
15
  /**
16
16
  * Available syntax highlighting languages, sorted by display name.
package/dist/index.d.ts CHANGED
@@ -80,6 +80,7 @@ export { default as EmptyState } from "./components/util/empty-state.svelte";
80
80
  export { default as Group } from "./components/util/group.svelte";
81
81
  export { default as Modal } from "./components/util/modal.svelte";
82
82
  export { default as Placeholder } from "./components/util/placeholder.svelte";
83
+ export { default as Popup } from "./components/util/popup.svelte";
83
84
  export { default as VisibilityObserver } from "./components/util/visibility-observer.svelte";
84
85
  export { loadCodeHighlighter } from "./components/text-editor/core.js";
85
86
  export { setCodeHighlighterCacheEnabled } from "./components/text-editor/shiki/cache.js";
package/dist/index.js CHANGED
@@ -80,6 +80,7 @@ export { default as EmptyState } from './components/util/empty-state.svelte';
80
80
  export { default as Group } from './components/util/group.svelte';
81
81
  export { default as Modal } from './components/util/modal.svelte';
82
82
  export { default as Placeholder } from './components/util/placeholder.svelte';
83
+ export { default as Popup } from './components/util/popup.svelte';
83
84
  export { default as VisibilityObserver } from './components/util/visibility-observer.svelte';
84
85
  export { loadCodeHighlighter } from './components/text-editor/core.js';
85
86
  export { setCodeHighlighterCacheEnabled } from './components/text-editor/shiki/cache.js';
@@ -0,0 +1,116 @@
1
+ # Dialog action button labels
2
+ ok: OK
3
+ cancel: Abbrechen
4
+ close: Schließen
5
+
6
+ # Button to clear content, e.g. search input or calendar date
7
+ clear: Leeren
8
+
9
+ # Text editor dialog buttons for link/image insertion
10
+ insert: Einfügen
11
+ update: Aktualisieren
12
+ remove: Entfernen
13
+
14
+ # Combobox dropdown toggle button aria-labels
15
+ collapse: Zuklappen
16
+ expand: Aufklappen
17
+
18
+ # Infobar/toast dismiss button label
19
+ dismiss: Ausblenden
20
+
21
+ # Emoji suggestion listbox aria-label, shared by the text fields and the text editor
22
+ emoji_suggestions: Emoji-Vorschläge
23
+
24
+ # Calendar component
25
+ calendar:
26
+ # View switcher group aria-labels and navigation button aria-labels
27
+ year: Jahr
28
+ previous_decade: Vorheriges Jahrzehnt
29
+ next_decade: Nächstes Jahrzehnt
30
+ month: Monat
31
+ previous_month: Vorheriger Monat
32
+ next_month: Nächster Monat
33
+ # “Today” button label in footer
34
+ today: Heute
35
+
36
+ # Split button component
37
+ split_button:
38
+ # Wrapper aria-label, e.g. “Save Options” when primary button is “Save”
39
+ x_options: 'Optionen für {$name}'
40
+ # Dropdown toggle aria-label
41
+ more_options: Weitere Optionen
42
+
43
+ # Combobox component
44
+ combobox:
45
+ # Placeholder when no option is selected
46
+ select_an_option: Option auswählen…
47
+ # Filter input aria-label
48
+ filter_options: Optionen filtern
49
+ # Empty state message when filter has no matches
50
+ no_matching_options: Keine passenden Optionen gefunden
51
+
52
+ # Number input component
53
+ number_input:
54
+ # Spin button aria-labels for incrementing/decrementing the number value
55
+ increase: Erhöhen
56
+ decrease: Verringern
57
+
58
+ # Password input component
59
+ password_input:
60
+ # Visibility toggle button aria-labels
61
+ show_password: Passwort anzeigen
62
+ hide_password: Passwort ausblenden
63
+
64
+ # Secret input component (API keys, tokens, etc.)
65
+ secret_input:
66
+ # Visibility toggle button aria-labels
67
+ show_secret: Geheimnis anzeigen
68
+ hide_secret: Geheimnis ausblenden
69
+
70
+ # Select tags component (multi-select)
71
+ select_tags:
72
+ # Listbox aria-label
73
+ selected_options: Ausgewählte Optionen
74
+ # Remove button aria-label, e.g. “Remove Option 1”
75
+ remove_x: '{$name} entfernen'
76
+
77
+ # Text editor component
78
+ text_editor:
79
+ # Toolbar aria-labels
80
+ text_editor: Texteditor
81
+ code_editor: Code-Editor
82
+ # Block style menu button and menu aria-labels
83
+ text_style_options: Textstil-Optionen
84
+ show_text_style_options: Textstil-Optionen anzeigen
85
+ # Block style menu items
86
+ paragraph: Absatz
87
+ heading_1: Überschrift 1
88
+ heading_2: Überschrift 2
89
+ heading_3: Überschrift 3
90
+ heading_4: Überschrift 4
91
+ heading_5: Überschrift 5
92
+ heading_6: Überschrift 6
93
+ bulleted_list: Aufzählungsliste
94
+ numbered_list: Nummerierte Liste
95
+ blockquote: Zitat
96
+ code_block: Code-Block
97
+ # Inline style button aria-labels
98
+ bold: Fett
99
+ italic: Kursiv
100
+ strikethrough: Durchgestrichen
101
+ code: Code
102
+ link: Link
103
+ # Link dialog title
104
+ insert_link: Link einfügen
105
+ update_link: Link aktualisieren
106
+ # Link dialog input field labels
107
+ text: Text
108
+ url: URL
109
+ # Markdown mode toggle button aria-label
110
+ edit_in_markdown: In Markdown bearbeiten
111
+ # Error message when rich text conversion fails
112
+ converter_error: Der Rich-Text-Modus konnte nicht aktiviert werden. Bitte verwenden Sie stattdessen den Klartext-Editor.
113
+ # Language selector aria-label
114
+ language: Sprache
115
+ # Plain text mode option in language selector
116
+ plain_text: Klartext
@@ -18,6 +18,9 @@ expand: Fäll ut
18
18
  # Infobar/toast dismiss button label
19
19
  dismiss: Avfärda
20
20
 
21
+ # Emoji suggestion listbox aria-label, shared by the text fields and the text editor
22
+ emoji_suggestions: Emojiförslag
23
+
21
24
  # Calendar component
22
25
  calendar:
23
26
  # View switcher group aria-labels and navigation button aria-labels
@@ -35,7 +35,6 @@ declare class Popup {
35
35
  maxWidth: string | undefined;
36
36
  height: string | undefined;
37
37
  };
38
- observer: IntersectionObserver;
39
38
  /**
40
39
  * A reference to the `<dialog>` element used for the popup, which also serves as the backdrop.
41
40
  * This is `undefined` while the element is not in the DOM tree, which is the case for a closed
@@ -57,6 +56,7 @@ declare class Popup {
57
56
  intersectionObserver: IntersectionObserver;
58
57
  resizeObserver: ResizeObserver;
59
58
  _rafId: number;
59
+ viewportResizeObserver: ResizeObserver;
60
60
  /**
61
61
  * Attach the `<dialog>` element used for the popup. This is called every time the element is
62
62
  * mounted, which happens on each open.
@@ -83,8 +83,14 @@ declare class Popup {
83
83
  */
84
84
  get isReadOnly(): boolean;
85
85
  /**
86
- * Check the position of the anchor element. This is a no-op while the popup element is not in the
87
- * DOM tree; the caller is expected to call this again once the element is attached.
86
+ * Check the position of the anchor element and, if the popup is mounted, recalculate and apply
87
+ * its inset and size. This is a no-op while the popup element is not in the DOM tree; the caller
88
+ * is expected to call this again once the element is attached.
89
+ *
90
+ * This measures the anchor synchronously with `getBoundingClientRect()` rather than reacting to
91
+ * an `IntersectionObserver`, whose delivery isn’t guaranteed to be timely or frame-aligned the
92
+ * way `ResizeObserver`’s is: a stale computation could arrive after a fresher one and silently
93
+ * overwrite it, occasionally leaving the popup mispositioned after a resize.
88
94
  */
89
95
  checkPosition(): void;
90
96
  /**
@@ -63,118 +63,6 @@ class Popup {
63
63
  }),
64
64
  );
65
65
 
66
- observer = new IntersectionObserver((entries) => {
67
- entries.forEach(({ intersectionRect, rootBounds }) => {
68
- if (!intersectionRect || !rootBounds) {
69
- return;
70
- }
71
-
72
- // Use the tracked content element rather than searching the popup element, which for a nested
73
- // popup is the shared parent `<dialog>` and would yield the parent’s content
74
- const content = /** @type {HTMLElement | null} */ (
75
- this.contentElement ?? this.popupElement?.querySelector('.content') ?? null
76
- );
77
-
78
- // The content is not in the DOM tree yet; `checkPosition()` will be called again once the
79
- // popup element is attached
80
- if (!content) {
81
- return;
82
- }
83
-
84
- const { scrollHeight: contentHeight, scrollWidth: contentWidth } = content;
85
- const topMargin = intersectionRect.top - 8;
86
- const bottomMargin = rootBounds.height - intersectionRect.bottom - 8;
87
- let { position } = this;
88
- let height;
89
-
90
- // Normalize RTL-friendly positions to LTR for LTR documents
91
- // @todo Rename `PopupPosition` enums to be direction-agnostic
92
- if (document.dir === 'rtl') {
93
- if (position.endsWith('-left')) {
94
- position = /** @type {PopupPosition} */ (position.replace('-left', '-right'));
95
- } else if (position.endsWith('-right')) {
96
- position = /** @type {PopupPosition} */ (position.replace('-right', '-left'));
97
- }
98
-
99
- if (position.startsWith('left-')) {
100
- position = /** @type {PopupPosition} */ (position.replace('left-', 'right-'));
101
- } else if (position.startsWith('right-')) {
102
- position = /** @type {PopupPosition} */ (position.replace('right-', 'left-'));
103
- }
104
- }
105
-
106
- // Alter the position if the space is limited
107
- // @todo Handle more overflow cases
108
- if (position.startsWith('bottom-')) {
109
- if (contentHeight > bottomMargin) {
110
- if (topMargin > bottomMargin) {
111
- position = /** @type {PopupPosition} */ (position.replace('bottom-', 'top-'));
112
- height = topMargin;
113
- } else {
114
- height = bottomMargin;
115
- }
116
- }
117
- }
118
-
119
- // If the popup overflows the viewport, change the position
120
- if (position.endsWith('-left')) {
121
- if (intersectionRect.left + contentWidth > rootBounds.width - 8) {
122
- position = /** @type {PopupPosition} */ (position.replace('-left', '-right'));
123
- }
124
- }
125
-
126
- if (position.endsWith('-right')) {
127
- if (intersectionRect.right - contentWidth < 8) {
128
- position = /** @type {PopupPosition} */ (position.replace('-right', '-left'));
129
- }
130
- }
131
-
132
- const top = position.startsWith('bottom-')
133
- ? `${Math.round(intersectionRect.bottom)}px`
134
- : position.endsWith('-top')
135
- ? `${Math.round(intersectionRect.top)}px`
136
- : 'auto';
137
-
138
- const right = position.startsWith('left-')
139
- ? `${Math.round(rootBounds.width - intersectionRect.left)}px`
140
- : position.endsWith('-right')
141
- ? `${Math.round(rootBounds.width - intersectionRect.right)}px`
142
- : 'auto';
143
-
144
- const bottom = position.startsWith('top-')
145
- ? `${Math.round(rootBounds.height - intersectionRect.top)}px`
146
- : position.endsWith('-bottom')
147
- ? `${Math.round(rootBounds.height - intersectionRect.bottom)}px`
148
- : 'auto';
149
-
150
- const left = position.startsWith('right-')
151
- ? `${Math.round(intersectionRect.right)}px`
152
- : position.endsWith('-left')
153
- ? `${Math.round(intersectionRect.left)}px`
154
- : 'auto';
155
-
156
- const style = {
157
- inset: [top, right, bottom, left].join(' '),
158
- zIndex: 1000,
159
- minWidth: `${Math.round(intersectionRect.width)}px`,
160
- maxWidth: position.endsWith('-left')
161
- ? `${Math.round(rootBounds.width - intersectionRect.left - 8)}px`
162
- : `${Math.round(intersectionRect.right - 8)}px`,
163
- height: height ? `${Math.round(height)}px` : 'auto',
164
- };
165
-
166
- if (
167
- style.inset !== this.style.inset ||
168
- style.zIndex !== this.style.zIndex ||
169
- style.minWidth !== this.style.minWidth ||
170
- style.maxWidth !== this.style.maxWidth ||
171
- style.height !== this.style.height
172
- ) {
173
- this.style = style;
174
- }
175
- });
176
- });
177
-
178
66
  /**
179
67
  * A reference to the `<dialog>` element used for the popup, which also serves as the backdrop.
180
68
  * This is `undefined` while the element is not in the DOM tree, which is the case for a closed
@@ -253,6 +141,21 @@ class Popup {
253
141
  });
254
142
  this.resizeObserver.observe(this.positionBaseElement);
255
143
 
144
+ // Recalculate the position when the viewport is resized. This is needed in addition to the
145
+ // `ResizeObserver` above, which only reacts to the anchor’s own size changing, not to it
146
+ // simply moving — which happens, for example, when a dialog containing the anchor is
147
+ // re-centered as the viewport is resized. This deliberately avoids the `resize` event on
148
+ // `window`, which isn’t guaranteed to fire for every layout-affecting viewport change across
149
+ // browsers; observing this popup’s own `<dialog>` element instead is reliable, because that
150
+ // element is always sized to exactly `100dvw` × `100dvh` (see `Modal.svelte`), so its box
151
+ // changes precisely when the viewport does — independent of the host app’s own CSS, unlike
152
+ // watching `document.body` or `document.documentElement` would be.
153
+ this.viewportResizeObserver = new ResizeObserver(() => {
154
+ if (this.open) {
155
+ this.checkPosition();
156
+ }
157
+ });
158
+
256
159
  if (popupElement) {
257
160
  this.attachPopupElement(popupElement);
258
161
  }
@@ -274,6 +177,7 @@ class Popup {
274
177
  this.detachPopupElement();
275
178
  this.popupElement = popupElement;
276
179
  this.contentElement = contentElement;
180
+ this.viewportResizeObserver.observe(popupElement);
277
181
 
278
182
  // Identify the popup by the element that actually holds its content. Labelling the `<dialog>`
279
183
  // would be wrong for a nested popup, which shares its parent’s: it would overwrite the parent’s
@@ -327,6 +231,10 @@ class Popup {
327
231
  this.#removeEventListeners?.();
328
232
  this.#removeEventListeners = undefined;
329
233
 
234
+ if (this.popupElement) {
235
+ this.viewportResizeObserver.unobserve(this.popupElement);
236
+ }
237
+
330
238
  // The content is leaving the DOM tree, so the anchor must stop referencing it. Only clear a
331
239
  // reference this popup owns; the consumer may have pointed the anchor somewhere else.
332
240
  if (this.anchorElement.getAttribute('aria-controls') === this.id) {
@@ -354,16 +262,140 @@ class Popup {
354
262
  }
355
263
 
356
264
  /**
357
- * Check the position of the anchor element. This is a no-op while the popup element is not in the
358
- * DOM tree; the caller is expected to call this again once the element is attached.
265
+ * Check the position of the anchor element and, if the popup is mounted, recalculate and apply
266
+ * its inset and size. This is a no-op while the popup element is not in the DOM tree; the caller
267
+ * is expected to call this again once the element is attached.
268
+ *
269
+ * This measures the anchor synchronously with `getBoundingClientRect()` rather than reacting to
270
+ * an `IntersectionObserver`, whose delivery isn’t guaranteed to be timely or frame-aligned the
271
+ * way `ResizeObserver`’s is: a stale computation could arrive after a fresher one and silently
272
+ * overwrite it, occasionally leaving the popup mispositioned after a resize.
359
273
  */
360
274
  checkPosition() {
361
275
  if (!this.popupElement) {
362
276
  return;
363
277
  }
364
278
 
365
- this.observer.unobserve(this.positionBaseElement);
366
- this.observer.observe(this.positionBaseElement);
279
+ // Use the tracked content element rather than searching the popup element, which for a nested
280
+ // popup is the shared parent `<dialog>` and would yield the parent’s content
281
+ const content = /** @type {HTMLElement | null} */ (
282
+ this.contentElement ?? this.popupElement?.querySelector('.content') ?? null
283
+ );
284
+
285
+ // The content is not in the DOM tree yet; `checkPosition()` will be called again once the
286
+ // popup element is attached
287
+ if (!content) {
288
+ return;
289
+ }
290
+
291
+ const anchorRect = this.positionBaseElement.getBoundingClientRect();
292
+ const rootBounds = { width: window.innerWidth, height: window.innerHeight };
293
+
294
+ // Clip the anchor’s rect to the viewport, mirroring what `IntersectionObserver` used to report
295
+ // as `intersectionRect`
296
+ const intersectionRect = {
297
+ top: Math.max(anchorRect.top, 0),
298
+ left: Math.max(anchorRect.left, 0),
299
+ right: Math.min(anchorRect.right, rootBounds.width),
300
+ bottom: Math.min(anchorRect.bottom, rootBounds.height),
301
+ width: 0,
302
+ height: 0,
303
+ };
304
+
305
+ intersectionRect.width = Math.max(0, intersectionRect.right - intersectionRect.left);
306
+ intersectionRect.height = Math.max(0, intersectionRect.bottom - intersectionRect.top);
307
+
308
+ const { scrollHeight: contentHeight, scrollWidth: contentWidth } = content;
309
+ const topMargin = intersectionRect.top - 8;
310
+ const bottomMargin = rootBounds.height - intersectionRect.bottom - 8;
311
+ let { position } = this;
312
+ let height;
313
+
314
+ // Normalize RTL-friendly positions to LTR for LTR documents
315
+ // @todo Rename `PopupPosition` enums to be direction-agnostic
316
+ if (document.dir === 'rtl') {
317
+ if (position.endsWith('-left')) {
318
+ position = /** @type {PopupPosition} */ (position.replace('-left', '-right'));
319
+ } else if (position.endsWith('-right')) {
320
+ position = /** @type {PopupPosition} */ (position.replace('-right', '-left'));
321
+ }
322
+
323
+ if (position.startsWith('left-')) {
324
+ position = /** @type {PopupPosition} */ (position.replace('left-', 'right-'));
325
+ } else if (position.startsWith('right-')) {
326
+ position = /** @type {PopupPosition} */ (position.replace('right-', 'left-'));
327
+ }
328
+ }
329
+
330
+ // Alter the position if the space is limited
331
+ // @todo Handle more overflow cases
332
+ if (position.startsWith('bottom-')) {
333
+ if (contentHeight > bottomMargin) {
334
+ if (topMargin > bottomMargin) {
335
+ position = /** @type {PopupPosition} */ (position.replace('bottom-', 'top-'));
336
+ height = topMargin;
337
+ } else {
338
+ height = bottomMargin;
339
+ }
340
+ }
341
+ }
342
+
343
+ // If the popup overflows the viewport, change the position
344
+ if (position.endsWith('-left')) {
345
+ if (intersectionRect.left + contentWidth > rootBounds.width - 8) {
346
+ position = /** @type {PopupPosition} */ (position.replace('-left', '-right'));
347
+ }
348
+ }
349
+
350
+ if (position.endsWith('-right')) {
351
+ if (intersectionRect.right - contentWidth < 8) {
352
+ position = /** @type {PopupPosition} */ (position.replace('-right', '-left'));
353
+ }
354
+ }
355
+
356
+ const top = position.startsWith('bottom-')
357
+ ? `${Math.round(intersectionRect.bottom)}px`
358
+ : position.endsWith('-top')
359
+ ? `${Math.round(intersectionRect.top)}px`
360
+ : 'auto';
361
+
362
+ const right = position.startsWith('left-')
363
+ ? `${Math.round(rootBounds.width - intersectionRect.left)}px`
364
+ : position.endsWith('-right')
365
+ ? `${Math.round(rootBounds.width - intersectionRect.right)}px`
366
+ : 'auto';
367
+
368
+ const bottom = position.startsWith('top-')
369
+ ? `${Math.round(rootBounds.height - intersectionRect.top)}px`
370
+ : position.endsWith('-bottom')
371
+ ? `${Math.round(rootBounds.height - intersectionRect.bottom)}px`
372
+ : 'auto';
373
+
374
+ const left = position.startsWith('right-')
375
+ ? `${Math.round(intersectionRect.right)}px`
376
+ : position.endsWith('-left')
377
+ ? `${Math.round(intersectionRect.left)}px`
378
+ : 'auto';
379
+
380
+ const style = {
381
+ inset: [top, right, bottom, left].join(' '),
382
+ zIndex: 1000,
383
+ minWidth: `${Math.round(intersectionRect.width)}px`,
384
+ maxWidth: position.endsWith('-left')
385
+ ? `${Math.round(rootBounds.width - intersectionRect.left - 8)}px`
386
+ : `${Math.round(intersectionRect.right - 8)}px`,
387
+ height: height ? `${Math.round(height)}px` : 'auto',
388
+ };
389
+
390
+ if (
391
+ style.inset !== this.style.inset ||
392
+ style.zIndex !== this.style.zIndex ||
393
+ style.minWidth !== this.style.minWidth ||
394
+ style.maxWidth !== this.style.maxWidth ||
395
+ style.height !== this.style.height
396
+ ) {
397
+ this.style = style;
398
+ }
367
399
  }
368
400
 
369
401
  /**
@@ -390,7 +422,7 @@ class Popup {
390
422
  this.detachPopupElement();
391
423
  this.intersectionObserver?.disconnect();
392
424
  this.resizeObserver?.disconnect();
393
- this.observer?.disconnect();
425
+ this.viewportResizeObserver?.disconnect();
394
426
 
395
427
  if (this._rafId) {
396
428
  cancelAnimationFrame(this._rafId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sveltia/ui",
3
- "version": "0.67.2",
3
+ "version": "0.68.1",
4
4
  "description": "A collection of Svelte components and utilities for building user interfaces.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -46,7 +46,7 @@
46
46
  "@lexical/selection": "^0.49.0",
47
47
  "@lexical/table": "^0.49.0",
48
48
  "@lexical/utils": "^0.49.0",
49
- "@sveltia/i18n": "^1.2.3",
49
+ "@sveltia/i18n": "^1.3.0",
50
50
  "@sveltia/utils": "^0.12.1",
51
51
  "lexical": "^0.49.0"
52
52
  },
@@ -57,7 +57,7 @@
57
57
  "@sveltejs/package": "^2.5.8",
58
58
  "@sveltejs/vite-plugin-svelte": "^7.3.0",
59
59
  "@vitest/coverage-v8": "^4.1.11",
60
- "cspell": "^10.0.1",
60
+ "cspell": "^10.1.1",
61
61
  "eslint": "^9.39.5",
62
62
  "eslint-config-airbnb-extended": "^3.2.0",
63
63
  "eslint-config-prettier": "^10.1.8",
@@ -66,13 +66,13 @@
66
66
  "eslint-plugin-package-json": "^1.7.1",
67
67
  "eslint-plugin-svelte": "^3.23.0",
68
68
  "globals": "^17.11.0",
69
- "happy-dom": "^20.11.6",
70
- "oxlint": "^1.79.0",
69
+ "happy-dom": "^20.11.8",
70
+ "oxlint": "^1.80.0",
71
71
  "postcss": "^8.5.26",
72
72
  "postcss-html": "^2.0.0",
73
73
  "prettier": "^3.9.6",
74
74
  "prettier-plugin-svelte": "^4.1.1",
75
- "rolldown": "^1.2.5",
75
+ "rolldown": "^1.2.6",
76
76
  "sass": "^1.103.1",
77
77
  "shiki": "^4.4.3",
78
78
  "stylelint": "^17.14.1",