@netlify/spark-ui 1.31.0-alpha.8 → 1.32.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@netlify/spark-ui",
3
3
  "description": "Assets, design tokens, components, and utilities",
4
- "version": "1.31.0-alpha.8",
4
+ "version": "1.32.0",
5
5
  "type": "module",
6
6
  "main": "dist/components/preact/index.js",
7
7
  "types": "dist/components/preact/index.d.ts",
@@ -65,7 +65,6 @@
65
65
  "fake-indexeddb": "^6.2.5",
66
66
  "glob": "^11.0.1",
67
67
  "happy-dom": "^20.11.6",
68
- "jsdom": "^30.0.0",
69
68
  "preact": "^10.26.4",
70
69
  "prettier": "^3.5.3",
71
70
  "prettier-plugin-astro": "^0.14.1",
@@ -159,6 +159,26 @@
159
159
  --neutral-700: var(--ntl-neutral-700);
160
160
  --neutral-800: var(--ntl-neutral-800);
161
161
  --neutral-900: var(--ntl-neutral-900);
162
+ --neutral-dark-000: var(--ntl-neutral-dark-000);
163
+ --neutral-dark-100: var(--ntl-neutral-dark-100);
164
+ --neutral-dark-200: var(--ntl-neutral-dark-200);
165
+ --neutral-dark-300: var(--ntl-neutral-dark-300);
166
+ --neutral-dark-400: var(--ntl-neutral-dark-400);
167
+ --neutral-dark-500: var(--ntl-neutral-dark-500);
168
+ --neutral-dark-600: var(--ntl-neutral-dark-600);
169
+ --neutral-dark-700: var(--ntl-neutral-dark-700);
170
+ --neutral-dark-800: var(--ntl-neutral-dark-800);
171
+ --neutral-dark-900: var(--ntl-neutral-dark-900);
172
+ --neutral-light-000: var(--ntl-neutral-light-000);
173
+ --neutral-light-100: var(--ntl-neutral-light-100);
174
+ --neutral-light-200: var(--ntl-neutral-light-200);
175
+ --neutral-light-300: var(--ntl-neutral-light-300);
176
+ --neutral-light-400: var(--ntl-neutral-light-400);
177
+ --neutral-light-500: var(--ntl-neutral-light-500);
178
+ --neutral-light-600: var(--ntl-neutral-light-600);
179
+ --neutral-light-700: var(--ntl-neutral-light-700);
180
+ --neutral-light-800: var(--ntl-neutral-light-800);
181
+ --neutral-light-900: var(--ntl-neutral-light-900);
162
182
  --page-content-max-width: var(--ntl-page-content-max-width);
163
183
  --page-gutter: var(--ntl-page-gutter);
164
184
  --pink-000: var(--ntl-pink-000);
@@ -142,16 +142,36 @@
142
142
  --ntl-motion-easing-in: ease-in;
143
143
  --ntl-motion-easing-out: ease-out;
144
144
  --ntl-motion-easing-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
145
- --ntl-neutral-000: color(srgb 1 1 1);
146
- --ntl-neutral-100: color(srgb 0.9647058823529412 0.9647058823529412 0.9686274509803922);
147
- --ntl-neutral-200: color(srgb 0.9137254901960784 0.9215686274509803 0.9294117647058824);
148
- --ntl-neutral-300: color(srgb 0.8196078431372549 0.8352941176470589 0.8549019607843137);
149
- --ntl-neutral-400: color(srgb 0.615686274509804 0.6549019607843137 0.6980392156862745);
150
- --ntl-neutral-500: color(srgb 0.4666666666666667 0.5019607843137255 0.5372549019607843);
151
- --ntl-neutral-600: color(srgb 0.32941176470588235 0.35294117647058826 0.3803921568627451);
152
- --ntl-neutral-700: color(srgb 0.20784313725490197 0.22745098039215686 0.24313725490196078);
153
- --ntl-neutral-800: color(srgb 0.09411764705882353 0.10196078431372549 0.10980392156862745);
154
- --ntl-neutral-900: color(srgb 0.023529411764705882 0.043137254901960784 0.06274509803921569);
145
+ --ntl-neutral-000: var(--ntl-neutral-light-000);
146
+ --ntl-neutral-100: var(--ntl-neutral-light-100);
147
+ --ntl-neutral-200: var(--ntl-neutral-light-200);
148
+ --ntl-neutral-300: var(--ntl-neutral-light-300);
149
+ --ntl-neutral-400: var(--ntl-neutral-light-400);
150
+ --ntl-neutral-500: var(--ntl-neutral-light-500);
151
+ --ntl-neutral-600: var(--ntl-neutral-light-600);
152
+ --ntl-neutral-700: var(--ntl-neutral-light-700);
153
+ --ntl-neutral-800: var(--ntl-neutral-light-800);
154
+ --ntl-neutral-900: var(--ntl-neutral-light-900);
155
+ --ntl-neutral-dark-000: color(srgb 1 1 1);
156
+ --ntl-neutral-dark-100: color(srgb 0.9019607843137255 0.9254901960784314 0.9490196078431372);
157
+ --ntl-neutral-dark-200: color(srgb 0.6705882352941176 0.7098039215686275 0.7490196078431373);
158
+ --ntl-neutral-dark-300: color(srgb 0.49411764705882355 0.5294117647058824 0.5725490196078431);
159
+ --ntl-neutral-dark-400: color(srgb 0.30196078431372547 0.33725490196078434 0.37254901960784315);
160
+ --ntl-neutral-dark-500: color(srgb 0.23137254901960785 0.2627450980392157 0.2980392156862745);
161
+ --ntl-neutral-dark-600: color(srgb 0.15294117647058825 0.1843137254901961 0.2196078431372549);
162
+ --ntl-neutral-dark-700: color(srgb 0.11764705882352941 0.1411764705882353 0.17254901960784313);
163
+ --ntl-neutral-dark-800: color(srgb 0.07058823529411765 0.09411764705882353 0.12156862745098039);
164
+ --ntl-neutral-dark-900: color(srgb 0.023529411764705882 0.043137254901960784 0.06274509803921569);
165
+ --ntl-neutral-light-000: color(srgb 1 1 1);
166
+ --ntl-neutral-light-100: color(srgb 0.9647058823529412 0.9647058823529412 0.9686274509803922);
167
+ --ntl-neutral-light-200: color(srgb 0.9137254901960784 0.9215686274509803 0.9294117647058824);
168
+ --ntl-neutral-light-300: color(srgb 0.8196078431372549 0.8352941176470589 0.8549019607843137);
169
+ --ntl-neutral-light-400: color(srgb 0.615686274509804 0.6549019607843137 0.6980392156862745);
170
+ --ntl-neutral-light-500: color(srgb 0.4666666666666667 0.5019607843137255 0.5372549019607843);
171
+ --ntl-neutral-light-600: color(srgb 0.32941176470588235 0.35294117647058826 0.3803921568627451);
172
+ --ntl-neutral-light-700: color(srgb 0.20784313725490197 0.22745098039215686 0.24313725490196078);
173
+ --ntl-neutral-light-800: color(srgb 0.09411764705882353 0.10196078431372549 0.10980392156862745);
174
+ --ntl-neutral-light-900: color(srgb 0.023529411764705882 0.043137254901960784 0.06274509803921569);
155
175
  --ntl-page-content-max-width: 80rem;
156
176
  --ntl-page-gutter: var(--ntl-space-l);
157
177
  --ntl-pink-000: color(srgb 0.9882352941176471 0.9411764705882353 0.984313725490196);
@@ -314,16 +334,16 @@
314
334
  --ntl-eyebrow-text-color: var(--ntl-teal-200);
315
335
  --ntl-filter-invert: invert(1);
316
336
  --ntl-heading-text-color: var(--ntl-neutral-100);
317
- --ntl-neutral-000: color(srgb 1 1 1);
318
- --ntl-neutral-100: color(srgb 0.9019607843137255 0.9254901960784314 0.9490196078431372);
319
- --ntl-neutral-200: color(srgb 0.6705882352941176 0.7098039215686275 0.7490196078431373);
320
- --ntl-neutral-300: color(srgb 0.49411764705882355 0.5294117647058824 0.5725490196078431);
321
- --ntl-neutral-400: color(srgb 0.30196078431372547 0.33725490196078434 0.37254901960784315);
322
- --ntl-neutral-500: color(srgb 0.23137254901960785 0.2627450980392157 0.2980392156862745);
323
- --ntl-neutral-600: color(srgb 0.15294117647058825 0.1843137254901961 0.2196078431372549);
324
- --ntl-neutral-700: color(srgb 0.11764705882352941 0.1411764705882353 0.17254901960784313);
325
- --ntl-neutral-800: color(srgb 0.07058823529411765 0.09411764705882353 0.12156862745098039);
326
- --ntl-neutral-900: color(srgb 0.023529411764705882 0.043137254901960784 0.06274509803921569);
337
+ --ntl-neutral-000: var(--ntl-neutral-dark-000);
338
+ --ntl-neutral-100: var(--ntl-neutral-dark-100);
339
+ --ntl-neutral-200: var(--ntl-neutral-dark-200);
340
+ --ntl-neutral-300: var(--ntl-neutral-dark-300);
341
+ --ntl-neutral-400: var(--ntl-neutral-dark-400);
342
+ --ntl-neutral-500: var(--ntl-neutral-dark-500);
343
+ --ntl-neutral-600: var(--ntl-neutral-dark-600);
344
+ --ntl-neutral-700: var(--ntl-neutral-dark-700);
345
+ --ntl-neutral-800: var(--ntl-neutral-dark-800);
346
+ --ntl-neutral-900: var(--ntl-neutral-dark-900);
327
347
  --ntl-search-answer-message-color: var(--ntl-neutral-600);
328
348
  --ntl-search-back-btn-bg-color: var(--ntl-neutral-500);
329
349
  --ntl-search-bg-color: var(--ntl-neutral-700);
@@ -413,16 +433,16 @@
413
433
  --ntl-eyebrow-text-color: var(--ntl-teal-200);
414
434
  --ntl-filter-invert: invert(1);
415
435
  --ntl-heading-text-color: var(--ntl-neutral-100);
416
- --ntl-neutral-000: color(srgb 1 1 1);
417
- --ntl-neutral-100: color(srgb 0.9019607843137255 0.9254901960784314 0.9490196078431372);
418
- --ntl-neutral-200: color(srgb 0.6705882352941176 0.7098039215686275 0.7490196078431373);
419
- --ntl-neutral-300: color(srgb 0.49411764705882355 0.5294117647058824 0.5725490196078431);
420
- --ntl-neutral-400: color(srgb 0.30196078431372547 0.33725490196078434 0.37254901960784315);
421
- --ntl-neutral-500: color(srgb 0.23137254901960785 0.2627450980392157 0.2980392156862745);
422
- --ntl-neutral-600: color(srgb 0.15294117647058825 0.1843137254901961 0.2196078431372549);
423
- --ntl-neutral-700: color(srgb 0.11764705882352941 0.1411764705882353 0.17254901960784313);
424
- --ntl-neutral-800: color(srgb 0.07058823529411765 0.09411764705882353 0.12156862745098039);
425
- --ntl-neutral-900: color(srgb 0.023529411764705882 0.043137254901960784 0.06274509803921569);
436
+ --ntl-neutral-000: var(--ntl-neutral-dark-000);
437
+ --ntl-neutral-100: var(--ntl-neutral-dark-100);
438
+ --ntl-neutral-200: var(--ntl-neutral-dark-200);
439
+ --ntl-neutral-300: var(--ntl-neutral-dark-300);
440
+ --ntl-neutral-400: var(--ntl-neutral-dark-400);
441
+ --ntl-neutral-500: var(--ntl-neutral-dark-500);
442
+ --ntl-neutral-600: var(--ntl-neutral-dark-600);
443
+ --ntl-neutral-700: var(--ntl-neutral-dark-700);
444
+ --ntl-neutral-800: var(--ntl-neutral-dark-800);
445
+ --ntl-neutral-900: var(--ntl-neutral-dark-900);
426
446
  --ntl-search-answer-message-color: var(--ntl-neutral-600);
427
447
  --ntl-search-back-btn-bg-color: var(--ntl-neutral-500);
428
448
  --ntl-search-bg-color: var(--ntl-neutral-700);
@@ -511,15 +531,15 @@
511
531
  --ntl-eyebrow-text-color: var(--ntl-blue-700);
512
532
  --ntl-filter-invert: none;
513
533
  --ntl-heading-text-color: var(--ntl-neutral-800);
514
- --ntl-neutral-000: color(srgb 1 1 1);
515
- --ntl-neutral-100: color(srgb 0.9647058823529412 0.9647058823529412 0.9686274509803922);
516
- --ntl-neutral-200: color(srgb 0.9137254901960784 0.9215686274509803 0.9294117647058824);
517
- --ntl-neutral-300: color(srgb 0.8196078431372549 0.8352941176470589 0.8549019607843137);
518
- --ntl-neutral-400: color(srgb 0.615686274509804 0.6549019607843137 0.6980392156862745);
519
- --ntl-neutral-500: color(srgb 0.4666666666666667 0.5019607843137255 0.5372549019607843);
520
- --ntl-neutral-600: color(srgb 0.32941176470588235 0.35294117647058826 0.3803921568627451);
521
- --ntl-neutral-700: color(srgb 0.20784313725490197 0.22745098039215686 0.24313725490196078);
522
- --ntl-neutral-800: color(srgb 0.09411764705882353 0.10196078431372549 0.10980392156862745);
534
+ --ntl-neutral-000: var(--ntl-neutral-light-000);
535
+ --ntl-neutral-100: var(--ntl-neutral-light-100);
536
+ --ntl-neutral-200: var(--ntl-neutral-light-200);
537
+ --ntl-neutral-300: var(--ntl-neutral-light-300);
538
+ --ntl-neutral-400: var(--ntl-neutral-light-400);
539
+ --ntl-neutral-500: var(--ntl-neutral-light-500);
540
+ --ntl-neutral-600: var(--ntl-neutral-light-600);
541
+ --ntl-neutral-700: var(--ntl-neutral-light-700);
542
+ --ntl-neutral-800: var(--ntl-neutral-light-800);
523
543
  --ntl-search-answer-message-color: var(--ntl-neutral-200);
524
544
  --ntl-search-back-btn-bg-color: var(--ntl-neutral-100);
525
545
  --ntl-search-bg-color: var(--ntl-neutral-000);
@@ -1,9 +0,0 @@
1
- /**
2
- * Wire up the "Copy URL" / "Copy page" buttons rendered by ArticleCopyActions.astro.
3
- *
4
- * This runs on every `astro:page-load` (initial load *and* every client-side
5
- * navigation via `<ClientRouter />`), so the buttons keep working after routing
6
- * to another article/post. Each wrapper is guarded so re-initialising the same
7
- * DOM node never attaches duplicate listeners.
8
- */
9
- export declare function initArticleCopyActions(root?: ParentNode): void;
@@ -1 +0,0 @@
1
- export {};
@@ -1,122 +0,0 @@
1
- ---
2
- interface Props {
3
- url: string;
4
- content: string;
5
- title?: string;
6
- class?: string;
7
- copyUrlLabel?: string;
8
- copyContentLabel?: string;
9
- copiedLabel?: string;
10
- trackingProps?: Record<string, string>;
11
- trackingSelectorClass?: string;
12
- }
13
-
14
- const {
15
- url,
16
- content,
17
- title,
18
- class: className,
19
- copyUrlLabel = 'Copy URL',
20
- copyContentLabel = 'Copy page',
21
- copiedLabel = 'Copied!',
22
- trackingProps = {},
23
- trackingSelectorClass,
24
- } = Astro.props;
25
-
26
- const copyContent = title ? `# ${title}\n\n${content}` : content;
27
- ---
28
-
29
- <div class:list={['copy-actions', className]} data-copied-label={copiedLabel}>
30
- <button
31
- type="button"
32
- class:list={['copy-actions-action', 'copy-actions-copy-url', trackingSelectorClass]}
33
- data-copy="url"
34
- data-value={url}
35
- data-track-action="copy_url"
36
- {...trackingProps}
37
- >
38
- <svg
39
- class="copy-actions-icon"
40
- viewBox="0 0 16 16"
41
- fill="none"
42
- stroke="currentColor"
43
- stroke-width="1.3"
44
- stroke-linecap="round"
45
- stroke-linejoin="round"
46
- aria-hidden="true"
47
- role="presentation"
48
- >
49
- <path d="M6.5 9.5a2.5 2.5 0 0 0 3.6.1l2-2a2.5 2.5 0 0 0-3.6-3.6l-1.1 1.1"></path>
50
- <path d="M9.5 6.5a2.5 2.5 0 0 0-3.6-.1l-2 2a2.5 2.5 0 0 0 3.6 3.6l1.1-1.1"></path>
51
- </svg>
52
- <span class="copy-actions-label">{copyUrlLabel}</span>
53
- </button>
54
- <button
55
- type="button"
56
- class:list={['copy-actions-action', 'copy-actions-copy-content', trackingSelectorClass]}
57
- data-copy="page"
58
- data-track-action="copy_page"
59
- {...trackingProps}
60
- >
61
- <svg
62
- class="copy-actions-icon"
63
- viewBox="0 0 16 16"
64
- fill="currentColor"
65
- aria-hidden="true"
66
- role="presentation"
67
- >
68
- <path
69
- d="M4.83333 1C3.82214 1 3 1.78477 3 2.75V13.25C3 14.2152 3.82214 15 4.83333 15H12.1667C13.1779 15 14 14.2152 14 13.25V5.375H10.3333C9.8263 5.375 9.41667 4.98398 9.41667 4.5V1H4.83333ZM10.3333 1V4.5H14L10.3333 1ZM6.20833 8H10.7917C11.0438 8 11.25 8.19687 11.25 8.4375C11.25 8.67813 11.0438 8.875 10.7917 8.875H6.20833C5.95625 8.875 5.75 8.67813 5.75 8.4375C5.75 8.19687 5.95625 8 6.20833 8ZM6.20833 9.75H10.7917C11.0438 9.75 11.25 9.94687 11.25 10.1875C11.25 10.4281 11.0438 10.625 10.7917 10.625H6.20833C5.95625 10.625 5.75 10.4281 5.75 10.1875C5.75 9.94687 5.95625 9.75 6.20833 9.75ZM6.20833 11.5H10.7917C11.0438 11.5 11.25 11.6969 11.25 11.9375C11.25 12.1781 11.0438 12.375 10.7917 12.375H6.20833C5.95625 12.375 5.75 12.1781 5.75 11.9375C5.75 11.6969 5.95625 11.5 6.20833 11.5Z"
70
- ></path>
71
- </svg>
72
- <span class="copy-actions-label">{copyContentLabel}</span>
73
- </button>
74
- </div>
75
-
76
- <script is:inline type="text/plain" data-copy-actions-source set:text={copyContent} />
77
-
78
- <style>
79
- .copy-actions {
80
- display: flex;
81
- align-items: center;
82
- gap: var(--ntl-space-2xs);
83
- }
84
-
85
- .copy-actions-action {
86
- display: flex;
87
- align-items: center;
88
- gap: var(--ntl-space-3xs);
89
- background: none;
90
- border: none;
91
- padding: 0;
92
- font: inherit;
93
- color: var(--ntl-color-text-weak);
94
- cursor: pointer;
95
- }
96
-
97
- .copy-actions-label {
98
- font-size: var(--ntl-font-size-text-sm);
99
- }
100
-
101
- .copy-actions-icon {
102
- width: var(--ntl-space-s);
103
- height: var(--ntl-space-s);
104
- }
105
-
106
- .copy-actions-copy-url::after {
107
- content: '•';
108
- margin-inline-start: var(--ntl-space-2xs);
109
- }
110
-
111
- .copy-actions-action:hover {
112
- color: var(--ntl-color-text);
113
- }
114
- </style>
115
-
116
- <script>
117
- import { initArticleCopyActions } from './articleCopyActions';
118
-
119
- // `astro:page-load` fires on the initial load *and* after every client-side
120
- // navigation (`<ClientRouter />`).
121
- document.addEventListener('astro:page-load', () => initArticleCopyActions());
122
- </script>
@@ -1,143 +0,0 @@
1
- // @vitest-environment jsdom
2
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
3
- import { initArticleCopyActions } from './articleCopyActions';
4
-
5
- const ARTICLE_URL = 'https://www.netlify.com/blog/some-post/';
6
- const MARKDOWN = '# Some post\n\nBody content.';
7
-
8
- function renderCopyActions({ url = ARTICLE_URL, markdown = MARKDOWN, copiedLabel = 'Copied!' } = {}) {
9
- document.body.innerHTML = `
10
- <div class="copy-actions" data-copied-label="${copiedLabel}">
11
- <button
12
- type="button"
13
- class="copy-actions-action copy-actions-copy-url"
14
- data-copy="url"
15
- data-value="${url}"
16
- >
17
- <span>Copy URL</span>
18
- </button>
19
- <button
20
- type="button"
21
- class="copy-actions-action copy-actions-copy-content"
22
- data-copy="page"
23
- >
24
- <span>Copy page</span>
25
- </button>
26
- </div>
27
- <script type="text/plain" data-copy-actions-source>${markdown}</script>
28
- `;
29
-
30
- return {
31
- wrapper: document.querySelector<HTMLElement>('.copy-actions')!,
32
- copyUrlBtn: document.querySelector<HTMLButtonElement>('.copy-actions-copy-url')!,
33
- copyPageBtn: document.querySelector<HTMLButtonElement>('.copy-actions-copy-content')!,
34
- urlLabel: document.querySelector<HTMLElement>('.copy-actions-copy-url span')!,
35
- pageLabel: document.querySelector<HTMLElement>('.copy-actions-copy-content span')!,
36
- };
37
- }
38
-
39
- describe('initArticleCopyActions', () => {
40
- let writeText: ReturnType<typeof vi.fn>;
41
-
42
- beforeEach(() => {
43
- vi.useFakeTimers();
44
- writeText = vi.fn().mockResolvedValue(undefined);
45
- Object.defineProperty(navigator, 'clipboard', {
46
- value: { writeText },
47
- configurable: true,
48
- });
49
- });
50
-
51
- afterEach(() => {
52
- vi.runOnlyPendingTimers();
53
- vi.useRealTimers();
54
- document.body.innerHTML = '';
55
- });
56
-
57
- it('copies the article URL and shows a temporary confirmation', async () => {
58
- const { copyUrlBtn, urlLabel } = renderCopyActions();
59
- initArticleCopyActions();
60
-
61
- copyUrlBtn.click();
62
- await vi.waitFor(() => expect(writeText).toHaveBeenCalledWith(ARTICLE_URL));
63
- expect(urlLabel.textContent).toBe('Copied!');
64
- });
65
-
66
- it('copies the markdown source for the "Copy page" button', async () => {
67
- const { copyPageBtn, pageLabel } = renderCopyActions();
68
- initArticleCopyActions();
69
-
70
- copyPageBtn.click();
71
- await vi.waitFor(() => expect(writeText).toHaveBeenCalledWith(MARKDOWN));
72
- expect(pageLabel.textContent).toBe('Copied!');
73
- });
74
-
75
- it('resets the label back to its original text after the delay', async () => {
76
- const { copyUrlBtn, urlLabel } = renderCopyActions();
77
- initArticleCopyActions();
78
-
79
- copyUrlBtn.click();
80
- await vi.waitFor(() => expect(urlLabel.textContent).toBe('Copied!'));
81
-
82
- vi.advanceTimersByTime(1500);
83
- expect(urlLabel.textContent).toBe('Copy URL');
84
- });
85
-
86
- it('uses a custom copied label when data-copied-label is set', async () => {
87
- const { copyUrlBtn, urlLabel } = renderCopyActions({ copiedLabel: 'Done!' });
88
- initArticleCopyActions();
89
-
90
- copyUrlBtn.click();
91
- await vi.waitFor(() => expect(urlLabel.textContent).toBe('Done!'));
92
- });
93
-
94
- it('does not attach duplicate listeners when initialised twice on the same DOM', async () => {
95
- const { copyUrlBtn } = renderCopyActions();
96
- initArticleCopyActions();
97
- initArticleCopyActions(); // e.g. a spurious re-fire
98
-
99
- copyUrlBtn.click();
100
- await vi.waitFor(() => expect(writeText).toHaveBeenCalledTimes(1));
101
- });
102
-
103
- it('re-binds after a client-side navigation swaps in fresh buttons', async () => {
104
- // Simulate the ClientRouter bug: first page loads and works...
105
- const first = renderCopyActions({ url: 'https://example.com/first/' });
106
- initArticleCopyActions();
107
- first.copyUrlBtn.click();
108
- await vi.waitFor(() => expect(writeText).toHaveBeenCalledWith('https://example.com/first/'));
109
-
110
- // ...then a navigation replaces the DOM with a new post's buttons.
111
- const second = renderCopyActions({ url: 'https://example.com/second/' });
112
- // Re-running on `astro:page-load` is what makes the new buttons work.
113
- initArticleCopyActions();
114
-
115
- second.copyUrlBtn.click();
116
- await vi.waitFor(() => expect(writeText).toHaveBeenCalledWith('https://example.com/second/'));
117
- });
118
-
119
- it('scopes the markdown source lookup to each wrapper, not a global selector', async () => {
120
- document.body.innerHTML = `
121
- <div class="copy-actions" data-copied-label="Copied!">
122
- <button type="button" class="copy-actions-action copy-actions-copy-content" data-copy="page">
123
- <span>Copy page</span>
124
- </button>
125
- </div>
126
- <script type="text/plain" data-copy-actions-source>First post markdown</script>
127
- <div class="copy-actions" data-copied-label="Copied!">
128
- <button type="button" class="copy-actions-action copy-actions-copy-content" data-copy="page">
129
- <span>Copy page</span>
130
- </button>
131
- </div>
132
- <script type="text/plain" data-copy-actions-source>Second post markdown</script>
133
- `;
134
- initArticleCopyActions();
135
-
136
- const buttons = document.querySelectorAll<HTMLButtonElement>('.copy-actions-copy-content');
137
- buttons[1].click();
138
- await vi.waitFor(() => expect(writeText).toHaveBeenCalledWith('Second post markdown'));
139
-
140
- buttons[0].click();
141
- await vi.waitFor(() => expect(writeText).toHaveBeenCalledWith('First post markdown'));
142
- });
143
- });
@@ -1,41 +0,0 @@
1
- const COPIED_LABEL_FALLBACK = 'Copied!';
2
- const RESET_DELAY_MS = 1500;
3
-
4
- /**
5
- * Wire up the "Copy URL" / "Copy page" buttons rendered by ArticleCopyActions.astro.
6
- *
7
- * This runs on every `astro:page-load` (initial load *and* every client-side
8
- * navigation via `<ClientRouter />`), so the buttons keep working after routing
9
- * to another article/post. Each wrapper is guarded so re-initialising the same
10
- * DOM node never attaches duplicate listeners.
11
- */
12
- export function initArticleCopyActions(root: ParentNode = document): void {
13
- root.querySelectorAll<HTMLElement>('.copy-actions').forEach(wrapper => {
14
- if (wrapper.dataset.copyBound === 'true') return;
15
- wrapper.dataset.copyBound = 'true';
16
-
17
- const source = wrapper.nextElementSibling as HTMLElement | null;
18
- const copiedLabel = wrapper.dataset.copiedLabel ?? COPIED_LABEL_FALLBACK;
19
-
20
- wrapper.querySelectorAll<HTMLButtonElement>('.copy-actions-action').forEach(btn => {
21
- const label = btn.querySelector<HTMLElement>('span');
22
- const originalText = label?.textContent;
23
-
24
- btn.addEventListener('click', async () => {
25
- const value =
26
- btn.getAttribute('data-copy') === 'url'
27
- ? btn.getAttribute('data-value')
28
- : source?.textContent;
29
-
30
- if (!value || !label) return;
31
-
32
- await navigator.clipboard.writeText(value);
33
-
34
- label.textContent = copiedLabel;
35
- setTimeout(() => {
36
- label.textContent = originalText ?? '';
37
- }, RESET_DELAY_MS);
38
- });
39
- });
40
- });
41
- }