@sveltia/ui 0.36.1 → 0.37.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.
@@ -5,13 +5,11 @@
5
5
  <script>
6
6
  import { sleep } from '@sveltia/utils/misc';
7
7
  import { onMount } from 'svelte';
8
- import { writable } from 'svelte/store';
9
8
  import { activatePopup } from '../../services/popup.svelte.js';
10
9
  import Modal from './modal.svelte';
11
10
 
12
11
  /**
13
12
  * @import { Snippet } from 'svelte';
14
- * @import { Writable } from 'svelte/store';
15
13
  * @import { ModalProps, PopupPosition } from '../../typedefs';
16
14
  */
17
15
 
@@ -78,23 +76,11 @@
78
76
  */
79
77
  let contentType = $state();
80
78
  /**
81
- * Style to be applied to the content.
82
- * @type {Writable<Record<string, any>>}
79
+ * @type {{ style: { inset: string | undefined, zIndex: number | undefined, minWidth: string |
80
+ * undefined, maxWidth: string | undefined, height: string | undefined }, open: boolean,
81
+ * checkPosition: () => void } | undefined}
83
82
  */
84
- let style = $state(
85
- writable({
86
- inset: undefined,
87
- zIndex: undefined,
88
- width: undefined,
89
- height: undefined,
90
- }),
91
- );
92
-
93
- /**
94
- * @type {{ style: Writable<Record<string, any>>, open: Writable<boolean>, checkPosition:
95
- * () => void } | undefined}
96
- */
97
- let popupInstance = undefined;
83
+ let popupInstance = $state();
98
84
  let hoveredTimeout = 0;
99
85
 
100
86
  /**
@@ -103,15 +89,16 @@
103
89
  const init = () => {
104
90
  popupInstance = activatePopup(anchor, dialogElement, position, positionBaseElement);
105
91
 
106
- style = popupInstance.style;
107
- popupInstance.open.subscribe((_open) => {
108
- open = _open;
109
- });
110
-
111
92
  contentType = anchor?.getAttribute('aria-haspopup') ?? undefined;
112
93
  initialized = true;
113
94
  };
114
95
 
96
+ $effect(() => {
97
+ if (popupInstance) {
98
+ open = popupInstance.open;
99
+ }
100
+ });
101
+
115
102
  $effect(() => {
116
103
  if (parentDialogElement && !dialogElement && content) {
117
104
  dialogElement = parentDialogElement;
@@ -145,12 +132,12 @@
145
132
  role="none"
146
133
  class="content {className} {contentType}"
147
134
  class:touch
148
- style:inset={$style.inset}
149
- style:z-index={$style.zIndex}
150
- style:min-width={$style.minWidth}
151
- style:max-width={$style.maxWidth}
152
- style:max-height={$style.height}
153
- style:visibility={$style.inset ? undefined : 'hidden'}
135
+ style:inset={popupInstance?.style.inset}
136
+ style:z-index={popupInstance?.style.zIndex}
137
+ style:min-width={popupInstance?.style.minWidth}
138
+ style:max-width={popupInstance?.style.maxWidth}
139
+ style:max-height={popupInstance?.style.height}
140
+ style:visibility={popupInstance?.style.inset ? undefined : 'hidden'}
154
141
  onmouseenter={() => {
155
142
  hovered = true;
156
143
 
@@ -1,6 +1,5 @@
1
1
  export function activatePopup(...args: any[]): Popup;
2
2
  /**
3
- * @import { Writable } from 'svelte/store';
4
3
  * @import { PopupPosition } from '../typedefs';
5
4
  */
6
5
  /**
@@ -16,23 +15,23 @@ declare class Popup {
16
15
  * will be the `anchorElement`.
17
16
  */
18
17
  constructor(anchorElement: HTMLButtonElement, popupElement: HTMLDialogElement, position: PopupPosition, positionBaseElement?: HTMLElement | undefined);
19
- open: Writable<boolean>;
20
18
  /**
21
- * @type {Writable<{
22
- * inset: string | undefined,
23
- * zIndex: number | undefined,
24
- * minWidth: string | undefined,
25
- * maxWidth: string | undefined,
26
- * height: string | undefined,
27
- * }>}
19
+ * Open or close the popup, running side effects synchronously.
20
+ * @param {boolean} value `true` to open, `false` to close.
28
21
  */
29
- style: Writable<{
22
+ set open(value: boolean);
23
+ /**
24
+ * Whether the popup is open.
25
+ * @returns {boolean} `true` if the popup is open.
26
+ */
27
+ get open(): boolean;
28
+ style: {
30
29
  inset: string | undefined;
31
30
  zIndex: number | undefined;
32
31
  minWidth: string | undefined;
33
32
  maxWidth: string | undefined;
34
33
  height: string | undefined;
35
- }>;
34
+ };
36
35
  observer: IntersectionObserver;
37
36
  anchorElement: HTMLButtonElement;
38
37
  popupElement: HTMLDialogElement;
@@ -58,7 +57,7 @@ declare class Popup {
58
57
  * Hide the popup immediately (when the anchor is being hidden).
59
58
  */
60
59
  hideImmediately(): Promise<void>;
60
+ #private;
61
61
  }
62
- import type { Writable } from 'svelte/store';
63
62
  import type { PopupPosition } from '../typedefs';
64
63
  export {};
@@ -1,10 +1,7 @@
1
1
  import { generateElementId } from '@sveltia/utils/element';
2
2
  import { sleep } from '@sveltia/utils/misc';
3
3
  import { on } from 'svelte/events';
4
- import { get, writable } from 'svelte/store';
5
-
6
4
  /**
7
- * @import { Writable } from 'svelte/store';
8
5
  * @import { PopupPosition } from '../typedefs';
9
6
  */
10
7
 
@@ -12,24 +9,46 @@ import { get, writable } from 'svelte/store';
12
9
  * Implement the popup handler.
13
10
  */
14
11
  class Popup {
15
- open = writable(false);
12
+ #open = $state(false);
16
13
 
17
14
  /**
18
- * @type {Writable<{
19
- * inset: string | undefined,
20
- * zIndex: number | undefined,
21
- * minWidth: string | undefined,
22
- * maxWidth: string | undefined,
23
- * height: string | undefined,
24
- * }>}
15
+ * Whether the popup is open.
16
+ * @returns {boolean} `true` if the popup is open.
25
17
  */
26
- style = writable({
27
- inset: undefined,
28
- zIndex: undefined,
29
- minWidth: undefined,
30
- maxWidth: undefined,
31
- height: undefined,
32
- });
18
+ get open() {
19
+ return this.#open;
20
+ }
21
+
22
+ /**
23
+ * Open or close the popup, running side effects synchronously.
24
+ * @param {boolean} value `true` to open, `false` to close.
25
+ */
26
+ set open(value) {
27
+ this.#open = value;
28
+
29
+ if (value) {
30
+ this.checkPosition();
31
+ } else if (this.anchorElement.getAttribute('aria-expanded') === 'true') {
32
+ this.anchorElement.focus();
33
+ this.anchorElement.removeAttribute('aria-controls');
34
+ }
35
+
36
+ this.anchorElement.setAttribute('aria-expanded', String(value));
37
+ }
38
+
39
+ style = $state(
40
+ /**
41
+ * @type {{ inset: string | undefined, zIndex: number | undefined, minWidth: string | undefined,
42
+ * maxWidth: string | undefined, height: string | undefined }}
43
+ */
44
+ ({
45
+ inset: undefined,
46
+ zIndex: undefined,
47
+ minWidth: undefined,
48
+ maxWidth: undefined,
49
+ height: undefined,
50
+ }),
51
+ );
33
52
 
34
53
  observer = new IntersectionObserver((entries) => {
35
54
  entries.forEach(({ intersectionRect, rootBounds }) => {
@@ -120,16 +139,14 @@ class Popup {
120
139
  height: height ? `${Math.round(height)}px` : 'auto',
121
140
  };
122
141
 
123
- const current = get(this.style);
124
-
125
142
  if (
126
- style.inset !== current.inset ||
127
- style.zIndex !== current.zIndex ||
128
- style.minWidth !== current.minWidth ||
129
- style.maxWidth !== current.maxWidth ||
130
- style.height !== current.height
143
+ style.inset !== this.style.inset ||
144
+ style.zIndex !== this.style.zIndex ||
145
+ style.minWidth !== this.style.minWidth ||
146
+ style.maxWidth !== this.style.maxWidth ||
147
+ style.height !== this.style.height
131
148
  ) {
132
- this.style.set(style);
149
+ this.style = style;
133
150
  }
134
151
  });
135
152
  });
@@ -151,10 +168,11 @@ class Popup {
151
168
 
152
169
  this.anchorElement.setAttribute('aria-controls', this.id);
153
170
  this.popupElement.setAttribute('id', this.id);
171
+ this.anchorElement.setAttribute('aria-expanded', 'false');
154
172
 
155
173
  on(anchorElement, 'click', () => {
156
174
  if (!this.isDisabled && !this.isReadOnly) {
157
- this.open.set(!get(this.open));
175
+ this.open = !this.open;
158
176
  }
159
177
  });
160
178
 
@@ -165,7 +183,7 @@ class Popup {
165
183
  if (!this.isDisabled && !this.isReadOnly && ['Enter', ' '].includes(key) && !hasModifier) {
166
184
  event.preventDefault();
167
185
  event.stopPropagation();
168
- this.open.set(!get(this.open));
186
+ this.open = !this.open;
169
187
  }
170
188
  });
171
189
 
@@ -176,7 +194,7 @@ class Popup {
176
194
  });
177
195
 
178
196
  new IntersectionObserver(([entry]) => {
179
- if (!entry.isIntersecting && get(this.open)) {
197
+ if (!entry.isIntersecting && this.open) {
180
198
  this.hideImmediately();
181
199
  }
182
200
  }).observe(this.anchorElement);
@@ -189,10 +207,10 @@ class Popup {
189
207
  const target = /** @type {HTMLElement} */ (event.target);
190
208
 
191
209
  if (
192
- get(this.open) &&
210
+ this.open &&
193
211
  (target === this.popupElement || target.matches('[role^="menuitem"], [role="option"]'))
194
212
  ) {
195
- this.open.set(false);
213
+ this.open = false;
196
214
  }
197
215
  });
198
216
 
@@ -203,21 +221,10 @@ class Popup {
203
221
  if (key === 'Escape' && !hasModifier) {
204
222
  event.preventDefault();
205
223
  event.stopPropagation();
206
- this.open.set(false);
224
+ this.open = false;
207
225
  }
208
226
  });
209
227
 
210
- this.open.subscribe((open) => {
211
- if (open) {
212
- this.checkPosition();
213
- } else if (this.anchorElement.getAttribute('aria-expanded') === 'true') {
214
- this.anchorElement.focus();
215
- this.anchorElement.removeAttribute('aria-controls');
216
- }
217
-
218
- this.anchorElement.setAttribute('aria-expanded', String(open));
219
- });
220
-
221
228
  // Update the popup width when the base element is resized
222
229
  new ResizeObserver(() => {
223
230
  cancelAnimationFrame(this._rafId);
@@ -254,7 +261,7 @@ class Popup {
254
261
  */
255
262
  async hideImmediately() {
256
263
  this.popupElement.hidden = true;
257
- this.open.set(false);
264
+ this.open = false;
258
265
  await sleep(50);
259
266
  this.popupElement.hidden = false;
260
267
  }
package/package.json CHANGED
@@ -1,12 +1,37 @@
1
1
  {
2
2
  "name": "@sveltia/ui",
3
- "version": "0.36.1",
4
- "license": "MIT",
5
- "type": "module",
3
+ "version": "0.37.0",
4
+ "description": "A collection of Svelte components and utilities for building user interfaces.",
6
5
  "repository": {
7
6
  "type": "git",
8
- "url": "github:sveltia/sveltia-ui"
7
+ "url": "git+https://github.com/sveltia/sveltia-ui.git"
8
+ },
9
+ "license": "MIT",
10
+ "author": {
11
+ "name": "Kohei Yoshino",
12
+ "url": "https://github.com/kyoshino"
13
+ },
14
+ "sideEffects": false,
15
+ "type": "module",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "svelte": "./dist/index.js",
20
+ "default": "./dist/index.js"
21
+ }
9
22
  },
23
+ "svelte": "./dist/index.js",
24
+ "typesVersions": {
25
+ ">4.0": {
26
+ "index": [
27
+ "./dist/index.d.ts"
28
+ ]
29
+ }
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "!dist/**/*.test.*"
34
+ ],
10
35
  "dependencies": {
11
36
  "@lexical/code": "^0.42.0",
12
37
  "@lexical/code-prism": "^0.42.0",
@@ -20,15 +45,12 @@
20
45
  "@lexical/selection": "^0.42.0",
21
46
  "@lexical/table": "^0.42.0",
22
47
  "@lexical/utils": "^0.42.0",
23
- "@sveltia/i18n": "^0.1.3",
48
+ "@sveltia/i18n": "^1.0.1",
24
49
  "@sveltia/utils": "^0.10.0",
25
50
  "lexical": "^0.42.0",
26
51
  "prismjs": "^1.30.0",
27
52
  "yaml": "^2.8.3"
28
53
  },
29
- "peerDependencies": {
30
- "svelte": "^5.0.0"
31
- },
32
54
  "devDependencies": {
33
55
  "@sveltejs/adapter-auto": "^7.0.1",
34
56
  "@sveltejs/kit": "^2.56.1",
@@ -37,10 +59,11 @@
37
59
  "@vitest/coverage-v8": "^4.1.2",
38
60
  "cspell": "^9.8.0",
39
61
  "eslint": "^9.39.4",
40
- "eslint-config-airbnb-extended": "^3.0.1",
62
+ "eslint-config-airbnb-extended": "^3.1.0",
41
63
  "eslint-config-prettier": "^10.1.8",
42
64
  "eslint-plugin-import": "^2.32.0",
43
65
  "eslint-plugin-jsdoc": "^62.9.0",
66
+ "eslint-plugin-package-json": "^0.91.1",
44
67
  "eslint-plugin-svelte": "^3.17.0",
45
68
  "globals": "^17.4.0",
46
69
  "happy-dom": "^20.8.9",
@@ -57,45 +80,30 @@
57
80
  "svelte-check": "^4.4.6",
58
81
  "svelte-preprocess": "^6.0.3",
59
82
  "tslib": "^2.8.1",
60
- "vite": "^8.0.3",
83
+ "vite": "^8.0.5",
61
84
  "vitest": "^4.1.2"
62
85
  },
63
- "exports": {
64
- ".": {
65
- "types": "./dist/index.d.ts",
66
- "svelte": "./dist/index.js",
67
- "default": "./dist/index.js"
68
- }
69
- },
70
- "files": [
71
- "dist"
72
- ],
73
- "svelte": "./dist/index.js",
74
- "typesVersions": {
75
- ">4.0": {
76
- "index": [
77
- "./dist/index.d.ts"
78
- ]
79
- }
86
+ "peerDependencies": {
87
+ "svelte": "^5.0.0"
80
88
  },
81
89
  "publishConfig": {
82
90
  "access": "public",
83
91
  "provenance": true
84
92
  },
85
93
  "scripts": {
86
- "dev": "vite dev",
87
94
  "build": "svelte-kit sync && svelte-package",
88
95
  "build:watch": "svelte-kit sync && svelte-package --watch",
89
- "preview": "vite preview",
90
- "format": "prettier --write .",
91
96
  "check": "pnpm run '/^check:.*/'",
92
97
  "check:audit": "pnpm audit",
93
98
  "check:cspell": "cspell --no-progress",
94
- "check:svelte": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
95
- "check:prettier": "prettier --check .",
96
99
  "check:eslint": "eslint .",
97
100
  "check:oxlint": "oxlint .",
101
+ "check:prettier": "prettier --check .",
98
102
  "check:stylelint": "stylelint '**/*.{css,scss,svelte}'",
103
+ "check:svelte": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
104
+ "dev": "vite dev",
105
+ "format": "prettier --write .",
106
+ "preview": "vite preview",
99
107
  "test": "vitest run",
100
108
  "test:coverage": "vitest run --coverage",
101
109
  "test:watch": "vitest"
@@ -1 +0,0 @@
1
- export {};
@@ -1,98 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import {
3
- AVAILABLE_BUTTONS,
4
- BLOCK_BUTTON_TYPES,
5
- IMAGE_COMPONENT_IDS,
6
- INLINE_BUTTON_TYPES,
7
- TEXT_FORMAT_BUTTON_TYPES,
8
- } from './constants.js';
9
-
10
- describe('AVAILABLE_BUTTONS', () => {
11
- it('should contain all expected button keys', () => {
12
- const expectedKeys = [
13
- 'bold',
14
- 'italic',
15
- 'strikethrough',
16
- 'code',
17
- 'link',
18
- 'paragraph',
19
- 'heading-1',
20
- 'heading-2',
21
- 'heading-3',
22
- 'heading-4',
23
- 'heading-5',
24
- 'heading-6',
25
- 'bulleted-list',
26
- 'numbered-list',
27
- 'blockquote',
28
- 'code-block',
29
- ];
30
-
31
- expectedKeys.forEach((key) => {
32
- expect(AVAILABLE_BUTTONS).toHaveProperty(key);
33
- });
34
- });
35
-
36
- it('should mark inline buttons correctly', () => {
37
- expect(AVAILABLE_BUTTONS.bold.inline).toBe(true);
38
- expect(AVAILABLE_BUTTONS.italic.inline).toBe(true);
39
- expect(AVAILABLE_BUTTONS.link.inline).toBe(true);
40
- expect(AVAILABLE_BUTTONS.paragraph.inline).toBe(false);
41
- expect(AVAILABLE_BUTTONS['heading-1'].inline).toBe(false);
42
- });
43
-
44
- it('should have a labelKey and icon for each button', () => {
45
- Object.values(AVAILABLE_BUTTONS).forEach(({ labelKey, icon }) => {
46
- expect(typeof labelKey).toBe('string');
47
- expect(labelKey.length).toBeGreaterThan(0);
48
- expect(typeof icon).toBe('string');
49
- expect(icon.length).toBeGreaterThan(0);
50
- });
51
- });
52
- });
53
-
54
- describe('TEXT_FORMAT_BUTTON_TYPES', () => {
55
- it('should include bold, italic, strikethrough, code', () => {
56
- expect(TEXT_FORMAT_BUTTON_TYPES).toEqual(['bold', 'italic', 'strikethrough', 'code']);
57
- });
58
- });
59
-
60
- describe('INLINE_BUTTON_TYPES', () => {
61
- it('should include all text format types plus link', () => {
62
- expect(INLINE_BUTTON_TYPES).toContain('bold');
63
- expect(INLINE_BUTTON_TYPES).toContain('italic');
64
- expect(INLINE_BUTTON_TYPES).toContain('strikethrough');
65
- expect(INLINE_BUTTON_TYPES).toContain('code');
66
- expect(INLINE_BUTTON_TYPES).toContain('link');
67
- });
68
-
69
- it('should be a superset of TEXT_FORMAT_BUTTON_TYPES', () => {
70
- TEXT_FORMAT_BUTTON_TYPES.forEach((type) => {
71
- expect(INLINE_BUTTON_TYPES).toContain(type);
72
- });
73
- });
74
- });
75
-
76
- describe('BLOCK_BUTTON_TYPES', () => {
77
- it('should include paragraph and all heading levels', () => {
78
- expect(BLOCK_BUTTON_TYPES).toContain('paragraph');
79
-
80
- for (let i = 1; i <= 6; i += 1) {
81
- expect(BLOCK_BUTTON_TYPES).toContain(`heading-${i}`);
82
- }
83
- });
84
-
85
- it('should include list, blockquote, and code-block types', () => {
86
- expect(BLOCK_BUTTON_TYPES).toContain('bulleted-list');
87
- expect(BLOCK_BUTTON_TYPES).toContain('numbered-list');
88
- expect(BLOCK_BUTTON_TYPES).toContain('blockquote');
89
- expect(BLOCK_BUTTON_TYPES).toContain('code-block');
90
- });
91
- });
92
-
93
- describe('IMAGE_COMPONENT_IDS', () => {
94
- it('should include image and linked-image', () => {
95
- expect(IMAGE_COMPONENT_IDS).toContain('image');
96
- expect(IMAGE_COMPONENT_IDS).toContain('linked-image');
97
- });
98
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,84 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { increaseListIndentation, splitMultilineFormatting } from './markdown.js';
3
-
4
- describe('splitMultilineFormatting', () => {
5
- it('should split italic formatting across lines', () => {
6
- expect(splitMultilineFormatting(' _foo\nbar_ ')).toBe(' _foo_\n_bar_ ');
7
- });
8
-
9
- it('should split bold formatting across lines', () => {
10
- expect(splitMultilineFormatting(' **foo\nbar** ')).toBe(' **foo**\n**bar** ');
11
- });
12
-
13
- it('should split strikethrough formatting across lines', () => {
14
- expect(splitMultilineFormatting(' ~~foo\nbar~~ ')).toBe(' ~~foo~~\n~~bar~~ ');
15
- });
16
-
17
- it('should split code formatting across lines', () => {
18
- expect(splitMultilineFormatting(' `foo\nbar` ')).toBe(' `foo`\n`bar` ');
19
- });
20
-
21
- it('should handle multiple formatting types', () => {
22
- expect(
23
- splitMultilineFormatting(' _italic\ntext_ **bold\ntext** ~~strike\nthrough~~ '),
24
- ).toBe(' _italic_\n_text_ **bold**\n**text** ~~strike~~\n~~through~~ ');
25
- });
26
-
27
- it('should not affect properly formatted single-line text', () => {
28
- expect(splitMultilineFormatting('_italic_ **bold** ~~strike~~ `code`')).toBe(
29
- '_italic_ **bold** ~~strike~~ `code`',
30
- );
31
- });
32
-
33
- it('should only split when surrounded by whitespace', () => {
34
- expect(splitMultilineFormatting('_foo\nbar_')).toBe('_foo\nbar_');
35
- });
36
-
37
- it('should handle formatting at different indentation levels', () => {
38
- expect(splitMultilineFormatting(' _foo\nbar_ ')).toBe(' _foo_\n_bar_ ');
39
- });
40
- });
41
-
42
- describe('increaseListIndentation', () => {
43
- it('should double indentation for bullet lists', () => {
44
- expect(increaseListIndentation(' - item')).toBe(' - item');
45
- });
46
-
47
- it('should double indentation for numbered lists', () => {
48
- expect(increaseListIndentation(' 1. item')).toBe(' 1. item');
49
- });
50
-
51
- it('should handle different list markers', () => {
52
- expect(increaseListIndentation(' - a\n + b\n * c')).toBe(' - a\n + b\n * c');
53
- });
54
-
55
- it('should double different indentation levels', () => {
56
- expect(increaseListIndentation(' - level 1\n - level 2')).toBe(
57
- ' - level 1\n - level 2',
58
- );
59
- });
60
-
61
- it('should not affect non-list content', () => {
62
- expect(increaseListIndentation('regular text')).toBe('regular text');
63
- });
64
-
65
- it('should not affect lists without preceding spaces', () => {
66
- expect(increaseListIndentation('- item')).toBe('- item');
67
- });
68
-
69
- it('should handle mixed content', () => {
70
- expect(increaseListIndentation('paragraph\n - item\nmore text')).toBe(
71
- 'paragraph\n - item\nmore text',
72
- );
73
- });
74
-
75
- it('should handle lists with various indentation levels', () => {
76
- expect(increaseListIndentation(' - a\n - b\n - c')).toBe(
77
- ' - a\n - b\n - c',
78
- );
79
- });
80
-
81
- it('should return unchanged string if no matching lists', () => {
82
- expect(increaseListIndentation('paragraph with - no list')).toBe('paragraph with - no list');
83
- });
84
- });
@@ -1 +0,0 @@
1
- export {};