@dsn-starter-kit/components-web 1.0.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/LICENSE +21 -0
- package/README.md +120 -0
- package/dist/button/Button.d.ts +50 -0
- package/dist/button/Button.d.ts.map +1 -0
- package/dist/button/Button.js +247 -0
- package/dist/button/Button.js.map +1 -0
- package/dist/button/Button.test.d.ts +2 -0
- package/dist/button/Button.test.d.ts.map +1 -0
- package/dist/button/Button.test.js +299 -0
- package/dist/button/Button.test.js.map +1 -0
- package/dist/button/button-styles.generated.d.ts +2 -0
- package/dist/button/button-styles.generated.d.ts.map +1 -0
- package/dist/button/button-styles.generated.js +4 -0
- package/dist/button/button-styles.generated.js.map +1 -0
- package/dist/button/index.d.ts +3 -0
- package/dist/button/index.d.ts.map +1 -0
- package/dist/button/index.js +2 -0
- package/dist/button/index.js.map +1 -0
- package/dist/heading/Heading.d.ts +30 -0
- package/dist/heading/Heading.d.ts.map +1 -0
- package/dist/heading/Heading.js +79 -0
- package/dist/heading/Heading.js.map +1 -0
- package/dist/heading/Heading.test.d.ts +2 -0
- package/dist/heading/Heading.test.d.ts.map +1 -0
- package/dist/heading/Heading.test.js +157 -0
- package/dist/heading/Heading.test.js.map +1 -0
- package/dist/heading/heading-styles.generated.d.ts +2 -0
- package/dist/heading/heading-styles.generated.d.ts.map +1 -0
- package/dist/heading/heading-styles.generated.js +4 -0
- package/dist/heading/heading-styles.generated.js.map +1 -0
- package/dist/heading/index.d.ts +3 -0
- package/dist/heading/index.d.ts.map +1 -0
- package/dist/heading/index.js +2 -0
- package/dist/heading/index.js.map +1 -0
- package/dist/icon/Icon.d.ts +31 -0
- package/dist/icon/Icon.d.ts.map +1 -0
- package/dist/icon/Icon.js +110 -0
- package/dist/icon/Icon.js.map +1 -0
- package/dist/icon/Icon.test.d.ts +2 -0
- package/dist/icon/Icon.test.d.ts.map +1 -0
- package/dist/icon/Icon.test.js +193 -0
- package/dist/icon/Icon.test.js.map +1 -0
- package/dist/icon/icon-paths.generated.d.ts +2 -0
- package/dist/icon/icon-paths.generated.d.ts.map +1 -0
- package/dist/icon/icon-paths.generated.js +56 -0
- package/dist/icon/icon-paths.generated.js.map +1 -0
- package/dist/icon/icon-styles.generated.d.ts +2 -0
- package/dist/icon/icon-styles.generated.d.ts.map +1 -0
- package/dist/icon/icon-styles.generated.js +4 -0
- package/dist/icon/icon-styles.generated.js.map +1 -0
- package/dist/icon/index.d.ts +3 -0
- package/dist/icon/index.d.ts.map +1 -0
- package/dist/icon/index.js +2 -0
- package/dist/icon/index.js.map +1 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +71 -0
- package/dist/index.js.map +1 -0
- package/dist/link/Link.d.ts +52 -0
- package/dist/link/Link.d.ts.map +1 -0
- package/dist/link/Link.js +260 -0
- package/dist/link/Link.js.map +1 -0
- package/dist/link/Link.test.d.ts +2 -0
- package/dist/link/Link.test.d.ts.map +1 -0
- package/dist/link/Link.test.js +256 -0
- package/dist/link/Link.test.js.map +1 -0
- package/dist/link/index.d.ts +3 -0
- package/dist/link/index.d.ts.map +1 -0
- package/dist/link/index.js +2 -0
- package/dist/link/index.js.map +1 -0
- package/dist/link/link-styles.generated.d.ts +2 -0
- package/dist/link/link-styles.generated.d.ts.map +1 -0
- package/dist/link/link-styles.generated.js +4 -0
- package/dist/link/link-styles.generated.js.map +1 -0
- package/dist/ordered-list/OrderedList.d.ts +32 -0
- package/dist/ordered-list/OrderedList.d.ts.map +1 -0
- package/dist/ordered-list/OrderedList.js +72 -0
- package/dist/ordered-list/OrderedList.js.map +1 -0
- package/dist/ordered-list/OrderedList.test.d.ts +2 -0
- package/dist/ordered-list/OrderedList.test.d.ts.map +1 -0
- package/dist/ordered-list/OrderedList.test.js +82 -0
- package/dist/ordered-list/OrderedList.test.js.map +1 -0
- package/dist/ordered-list/index.d.ts +2 -0
- package/dist/ordered-list/index.d.ts.map +1 -0
- package/dist/ordered-list/index.js +2 -0
- package/dist/ordered-list/index.js.map +1 -0
- package/dist/ordered-list/ordered-list-styles.generated.d.ts +2 -0
- package/dist/ordered-list/ordered-list-styles.generated.d.ts.map +1 -0
- package/dist/ordered-list/ordered-list-styles.generated.js +4 -0
- package/dist/ordered-list/ordered-list-styles.generated.js.map +1 -0
- package/dist/paragraph/Paragraph.d.ts +26 -0
- package/dist/paragraph/Paragraph.d.ts.map +1 -0
- package/dist/paragraph/Paragraph.js +60 -0
- package/dist/paragraph/Paragraph.js.map +1 -0
- package/dist/paragraph/Paragraph.test.d.ts +2 -0
- package/dist/paragraph/Paragraph.test.d.ts.map +1 -0
- package/dist/paragraph/Paragraph.test.js +121 -0
- package/dist/paragraph/Paragraph.test.js.map +1 -0
- package/dist/paragraph/index.d.ts +3 -0
- package/dist/paragraph/index.d.ts.map +1 -0
- package/dist/paragraph/index.js +2 -0
- package/dist/paragraph/index.js.map +1 -0
- package/dist/paragraph/paragraph-styles.generated.d.ts +2 -0
- package/dist/paragraph/paragraph-styles.generated.d.ts.map +1 -0
- package/dist/paragraph/paragraph-styles.generated.js +4 -0
- package/dist/paragraph/paragraph-styles.generated.js.map +1 -0
- package/dist/unordered-list/UnorderedList.d.ts +22 -0
- package/dist/unordered-list/UnorderedList.d.ts.map +1 -0
- package/dist/unordered-list/UnorderedList.js +42 -0
- package/dist/unordered-list/UnorderedList.js.map +1 -0
- package/dist/unordered-list/UnorderedList.test.d.ts +2 -0
- package/dist/unordered-list/UnorderedList.test.d.ts.map +1 -0
- package/dist/unordered-list/UnorderedList.test.js +56 -0
- package/dist/unordered-list/UnorderedList.test.js.map +1 -0
- package/dist/unordered-list/index.d.ts +2 -0
- package/dist/unordered-list/index.d.ts.map +1 -0
- package/dist/unordered-list/index.js +2 -0
- package/dist/unordered-list/index.js.map +1 -0
- package/dist/unordered-list/unordered-list-styles.generated.d.ts +2 -0
- package/dist/unordered-list/unordered-list-styles.generated.d.ts.map +1 -0
- package/dist/unordered-list/unordered-list-styles.generated.js +4 -0
- package/dist/unordered-list/unordered-list-styles.generated.js.map +1 -0
- package/package.json +42 -0
- package/src/button/Button.test.ts +360 -0
- package/src/button/Button.ts +299 -0
- package/src/button/button-demo.html +477 -0
- package/src/button/button-styles.generated.ts +3 -0
- package/src/button/button.shadow.css +20 -0
- package/src/button/index.ts +2 -0
- package/src/heading/Heading.test.ts +186 -0
- package/src/heading/Heading.ts +104 -0
- package/src/heading/heading-styles.generated.ts +3 -0
- package/src/heading/heading.shadow.css +11 -0
- package/src/heading/index.ts +2 -0
- package/src/icon/Icon.test.ts +228 -0
- package/src/icon/Icon.ts +136 -0
- package/src/icon/icon-paths.generated.ts +55 -0
- package/src/icon/icon-styles.generated.ts +3 -0
- package/src/icon/icon.shadow.css +13 -0
- package/src/icon/index.ts +2 -0
- package/src/index.ts +83 -0
- package/src/link/Link.test.ts +309 -0
- package/src/link/Link.ts +284 -0
- package/src/link/index.ts +2 -0
- package/src/link/link-styles.generated.ts +3 -0
- package/src/link/link.shadow.css +15 -0
- package/src/ordered-list/OrderedList.test.ts +97 -0
- package/src/ordered-list/OrderedList.ts +84 -0
- package/src/ordered-list/index.ts +1 -0
- package/src/ordered-list/ordered-list-styles.generated.ts +3 -0
- package/src/ordered-list/ordered-list.shadow.css +11 -0
- package/src/paragraph/Paragraph.test.ts +143 -0
- package/src/paragraph/Paragraph.ts +77 -0
- package/src/paragraph/index.ts +2 -0
- package/src/paragraph/paragraph-styles.generated.ts +3 -0
- package/src/paragraph/paragraph.shadow.css +11 -0
- package/src/unordered-list/UnorderedList.test.ts +66 -0
- package/src/unordered-list/UnorderedList.ts +50 -0
- package/src/unordered-list/index.ts +1 -0
- package/src/unordered-list/unordered-list-styles.generated.ts +3 -0
- package/src/unordered-list/unordered-list.shadow.css +11 -0
package/src/link/Link.ts
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSN Link Web Component
|
|
3
|
+
*
|
|
4
|
+
* A custom element that provides a styled anchor link with icon support.
|
|
5
|
+
* Uses the same CSS classes and design tokens as the HTML and React link components.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```html
|
|
9
|
+
* <dsn-link href="/about">About us</dsn-link>
|
|
10
|
+
* <dsn-link href="https://example.com" external>External (opens in new tab)</dsn-link>
|
|
11
|
+
* <dsn-link href="/dashboard" current>Dashboard</dsn-link>
|
|
12
|
+
* <dsn-link disabled>Unavailable</dsn-link>
|
|
13
|
+
* <dsn-link size="small" href="/small">Small link</dsn-link>
|
|
14
|
+
* <dsn-link size="large" href="/large">Large link</dsn-link>
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export type LinkSize = 'small' | 'default' | 'large';
|
|
19
|
+
|
|
20
|
+
// Auto-generated CSS from shared source — run `node scripts/build-css.js` to regenerate
|
|
21
|
+
import { linkStyles } from './link-styles.generated';
|
|
22
|
+
|
|
23
|
+
export class DsnLink extends HTMLElement {
|
|
24
|
+
static get observedAttributes(): string[] {
|
|
25
|
+
return ['href', 'target', 'rel', 'disabled', 'current', 'size', 'external'];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private _anchor: HTMLAnchorElement;
|
|
29
|
+
private _hintNode: Text | null = null;
|
|
30
|
+
private _hrefBackup: string | null = null;
|
|
31
|
+
|
|
32
|
+
constructor() {
|
|
33
|
+
super();
|
|
34
|
+
this.attachShadow({ mode: 'open' });
|
|
35
|
+
|
|
36
|
+
// Create the internal anchor element
|
|
37
|
+
this._anchor = document.createElement('a');
|
|
38
|
+
this._anchor.classList.add('dsn-link');
|
|
39
|
+
this._anchor.innerHTML =
|
|
40
|
+
'<slot name="icon-start"></slot><slot></slot><slot name="icon-end"></slot>';
|
|
41
|
+
|
|
42
|
+
// Create style element with embedded CSS
|
|
43
|
+
const style = document.createElement('style');
|
|
44
|
+
style.textContent = linkStyles;
|
|
45
|
+
|
|
46
|
+
this.shadowRoot!.appendChild(style);
|
|
47
|
+
this.shadowRoot!.appendChild(this._anchor);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
connectedCallback(): void {
|
|
51
|
+
this._updateClasses();
|
|
52
|
+
this._updateHref();
|
|
53
|
+
this._updateTarget();
|
|
54
|
+
this._updateRel();
|
|
55
|
+
this._updateDisabled();
|
|
56
|
+
this._updateCurrent();
|
|
57
|
+
this._updateExternal();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
attributeChangedCallback(name: string): void {
|
|
61
|
+
switch (name) {
|
|
62
|
+
case 'href':
|
|
63
|
+
this._updateHref();
|
|
64
|
+
break;
|
|
65
|
+
case 'target':
|
|
66
|
+
this._updateTarget();
|
|
67
|
+
break;
|
|
68
|
+
case 'rel':
|
|
69
|
+
this._updateRel();
|
|
70
|
+
break;
|
|
71
|
+
case 'disabled':
|
|
72
|
+
this._updateDisabled();
|
|
73
|
+
break;
|
|
74
|
+
case 'current':
|
|
75
|
+
this._updateCurrent();
|
|
76
|
+
break;
|
|
77
|
+
case 'size':
|
|
78
|
+
this._updateClasses();
|
|
79
|
+
break;
|
|
80
|
+
case 'external':
|
|
81
|
+
this._updateExternal();
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private _updateClasses(): void {
|
|
87
|
+
const classes = ['dsn-link'];
|
|
88
|
+
|
|
89
|
+
// Only add size class when explicitly set via attribute.
|
|
90
|
+
// Without a size attribute, the link inherits font from its parent,
|
|
91
|
+
// which is the expected behavior for inline usage in paragraphs.
|
|
92
|
+
if (this.hasAttribute('size')) {
|
|
93
|
+
classes.push(`dsn-link--size-${this.size}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
this._anchor.className = classes.join(' ');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private _updateHref(): void {
|
|
100
|
+
if (this.disabled) return;
|
|
101
|
+
const href = this.getAttribute('href');
|
|
102
|
+
if (href) {
|
|
103
|
+
this._anchor.setAttribute('href', href);
|
|
104
|
+
} else {
|
|
105
|
+
this._anchor.removeAttribute('href');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private _updateTarget(): void {
|
|
110
|
+
const target = this.getAttribute('target');
|
|
111
|
+
if (target) {
|
|
112
|
+
this._anchor.setAttribute('target', target);
|
|
113
|
+
} else {
|
|
114
|
+
this._anchor.removeAttribute('target');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private _updateRel(): void {
|
|
119
|
+
const rel = this.getAttribute('rel');
|
|
120
|
+
if (rel) {
|
|
121
|
+
this._anchor.setAttribute('rel', rel);
|
|
122
|
+
} else {
|
|
123
|
+
this._anchor.removeAttribute('rel');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private _updateDisabled(): void {
|
|
128
|
+
if (this.disabled) {
|
|
129
|
+
this._hrefBackup = this._anchor.getAttribute('href');
|
|
130
|
+
this._anchor.removeAttribute('href');
|
|
131
|
+
this._anchor.setAttribute('aria-disabled', 'true');
|
|
132
|
+
this._anchor.setAttribute('tabindex', '-1');
|
|
133
|
+
} else {
|
|
134
|
+
this._anchor.removeAttribute('aria-disabled');
|
|
135
|
+
this._anchor.removeAttribute('tabindex');
|
|
136
|
+
// Restore href
|
|
137
|
+
if (this._hrefBackup) {
|
|
138
|
+
this._anchor.setAttribute('href', this._hrefBackup);
|
|
139
|
+
this._hrefBackup = null;
|
|
140
|
+
} else {
|
|
141
|
+
this._updateHref();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private _updateCurrent(): void {
|
|
147
|
+
if (this.current) {
|
|
148
|
+
this._anchor.setAttribute('aria-current', 'page');
|
|
149
|
+
} else {
|
|
150
|
+
this._anchor.removeAttribute('aria-current');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private _updateExternal(): void {
|
|
155
|
+
if (this.external) {
|
|
156
|
+
// Set target and rel if not explicitly provided
|
|
157
|
+
if (!this.getAttribute('target')) {
|
|
158
|
+
this._anchor.setAttribute('target', '_blank');
|
|
159
|
+
}
|
|
160
|
+
if (!this.getAttribute('rel')) {
|
|
161
|
+
this._anchor.setAttribute('rel', 'noopener noreferrer');
|
|
162
|
+
}
|
|
163
|
+
// Add visible hint text
|
|
164
|
+
if (!this._hintNode) {
|
|
165
|
+
this._hintNode = document.createTextNode(' (opens in new tab)');
|
|
166
|
+
}
|
|
167
|
+
this._anchor.appendChild(this._hintNode);
|
|
168
|
+
} else {
|
|
169
|
+
if (this._hintNode && this._hintNode.parentNode) {
|
|
170
|
+
this._hintNode.remove();
|
|
171
|
+
}
|
|
172
|
+
// Restore target/rel to attribute values
|
|
173
|
+
this._updateTarget();
|
|
174
|
+
this._updateRel();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Property: href
|
|
179
|
+
get href(): string | null {
|
|
180
|
+
return this.getAttribute('href');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
set href(value: string | null) {
|
|
184
|
+
if (value) {
|
|
185
|
+
this.setAttribute('href', value);
|
|
186
|
+
} else {
|
|
187
|
+
this.removeAttribute('href');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Property: target
|
|
192
|
+
get target(): string | null {
|
|
193
|
+
return this.getAttribute('target');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
set target(value: string | null) {
|
|
197
|
+
if (value) {
|
|
198
|
+
this.setAttribute('target', value);
|
|
199
|
+
} else {
|
|
200
|
+
this.removeAttribute('target');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Property: rel
|
|
205
|
+
get rel(): string | null {
|
|
206
|
+
return this.getAttribute('rel');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
set rel(value: string | null) {
|
|
210
|
+
if (value) {
|
|
211
|
+
this.setAttribute('rel', value);
|
|
212
|
+
} else {
|
|
213
|
+
this.removeAttribute('rel');
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Property: disabled
|
|
218
|
+
get disabled(): boolean {
|
|
219
|
+
return this.hasAttribute('disabled');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
set disabled(value: boolean) {
|
|
223
|
+
if (value) {
|
|
224
|
+
this.setAttribute('disabled', '');
|
|
225
|
+
} else {
|
|
226
|
+
this.removeAttribute('disabled');
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Property: current
|
|
231
|
+
get current(): boolean {
|
|
232
|
+
return this.hasAttribute('current');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
set current(value: boolean) {
|
|
236
|
+
if (value) {
|
|
237
|
+
this.setAttribute('current', '');
|
|
238
|
+
} else {
|
|
239
|
+
this.removeAttribute('current');
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Property: external
|
|
244
|
+
get external(): boolean {
|
|
245
|
+
return this.hasAttribute('external');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
set external(value: boolean) {
|
|
249
|
+
if (value) {
|
|
250
|
+
this.setAttribute('external', '');
|
|
251
|
+
} else {
|
|
252
|
+
this.removeAttribute('external');
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Property: size
|
|
257
|
+
get size(): LinkSize {
|
|
258
|
+
return (this.getAttribute('size') as LinkSize) || 'default';
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
set size(value: LinkSize) {
|
|
262
|
+
this.setAttribute('size', value);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Focus delegation
|
|
266
|
+
focus(options?: FocusOptions): void {
|
|
267
|
+
this._anchor.focus(options);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
blur(): void {
|
|
271
|
+
this._anchor.blur();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
click(): void {
|
|
275
|
+
this._anchor.click();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Register the custom element
|
|
280
|
+
export function defineLink(tagName = 'dsn-link'): void {
|
|
281
|
+
if (!customElements.get(tagName)) {
|
|
282
|
+
customElements.define(tagName, DsnLink);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// Auto-generated — do not edit manually.
|
|
2
|
+
// Source: link.shadow.css + components-html source
|
|
3
|
+
export const linkStyles = "/**\n * Shadow DOM wrapper styles for the Link Web Component.\n * The core link styles are imported from the shared HTML/CSS source.\n */\n\n:host {\n display: inline;\n}\n\n::slotted(*) {\n pointer-events: none;\n}\n\n/* Shared link styles - single source of truth */\n/**\n * Link Component\n * Uses component tokens for consistent styling\n */\n\n.dsn-link {\n /* Layout */\n display: inline-flex;\n align-self: flex-start;\n align-items: center;\n gap: var(--dsn-link-gap);\n position: relative; /* Ankerpunt voor DotBadge */\n\n /* Typography */\n color: var(--dsn-link-color);\n text-decoration-line: var(--dsn-link-text-decoration-line);\n text-decoration-color: var(--dsn-link-text-decoration-color);\n text-decoration-thickness: var(--dsn-link-text-decoration-thickness);\n text-underline-offset: var(--dsn-link-text-underline-offset);\n font: inherit;\n\n /* Interaction */\n cursor: pointer;\n transition:\n color var(--dsn-transition-duration-normal)\n var(--dsn-transition-easing-default),\n text-decoration-color var(--dsn-transition-duration-normal)\n var(--dsn-transition-easing-default);\n}\n\n/* Icon sizing */\n.dsn-link > .dsn-icon {\n width: var(--dsn-link-icon-size);\n height: var(--dsn-link-icon-size);\n flex-shrink: 0;\n}\n\n/* Size Variants */\n.dsn-link--size-small {\n font-size: var(--dsn-link-size-small-font-size);\n gap: var(--dsn-link-size-small-gap);\n}\n\n.dsn-link--size-default {\n font-size: var(--dsn-link-size-default-font-size);\n gap: var(--dsn-link-size-default-gap);\n}\n\n.dsn-link--size-large {\n font-size: var(--dsn-link-size-large-font-size);\n gap: var(--dsn-link-size-large-gap);\n}\n\n/* Icon sizing per link size */\n.dsn-link--size-small > .dsn-icon {\n width: var(--dsn-link-size-small-icon-size);\n height: var(--dsn-link-size-small-icon-size);\n}\n\n.dsn-link--size-default > .dsn-icon {\n width: var(--dsn-link-size-default-icon-size);\n height: var(--dsn-link-size-default-icon-size);\n}\n\n.dsn-link--size-large > .dsn-icon {\n width: var(--dsn-link-size-large-icon-size);\n height: var(--dsn-link-size-large-icon-size);\n}\n\n/* Hover */\n.dsn-link:hover:not([aria-disabled='true']) {\n color: var(--dsn-link-hover-color);\n text-decoration-line: var(--dsn-link-hover-text-decoration-line);\n}\n\n/* Active */\n.dsn-link:active:not([aria-disabled='true']) {\n color: var(--dsn-link-active-color);\n}\n\n/* Focus */\n.dsn-link:focus-visible {\n background-color: var(--dsn-focus-background-color);\n color: var(--dsn-focus-color);\n text-decoration: none;\n outline: var(--dsn-focus-outline-width) var(--dsn-focus-outline-style)\n var(--dsn-focus-outline-color);\n outline-offset: var(--dsn-focus-outline-offset);\n box-shadow: 0 0 0\n calc(var(--dsn-focus-outline-offset) + var(--dsn-focus-outline-width))\n var(--dsn-focus-inverse-outline-color);\n box-decoration-break: clone;\n}\n\n/* Current page */\n.dsn-link[aria-current='page'] {\n cursor: default;\n text-decoration-line: none;\n}\n\n/* Disabled */\n.dsn-link[aria-disabled='true'] {\n color: var(--dsn-link-disabled-color);\n text-decoration-line: none;\n cursor: not-allowed;\n pointer-events: none;\n}\n\n";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shadow DOM wrapper styles for the Link Web Component.
|
|
3
|
+
* The core link styles are imported from the shared HTML/CSS source.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
:host {
|
|
7
|
+
display: inline;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
::slotted(*) {
|
|
11
|
+
pointer-events: none;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* Shared link styles - single source of truth */
|
|
15
|
+
@import '../../../components-html/src/link/link.css';
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
|
2
|
+
import { DsnOrderedList, defineOrderedList } from './OrderedList';
|
|
3
|
+
|
|
4
|
+
// Register the custom element once for all tests
|
|
5
|
+
beforeAll(() => {
|
|
6
|
+
defineOrderedList();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
function createElement(attrs: Record<string, string> = {}): DsnOrderedList {
|
|
10
|
+
const el = document.createElement('dsn-ordered-list') as DsnOrderedList;
|
|
11
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
12
|
+
el.setAttribute(key, value);
|
|
13
|
+
}
|
|
14
|
+
document.body.appendChild(el);
|
|
15
|
+
return el;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getInternalList(el: DsnOrderedList): HTMLOListElement {
|
|
19
|
+
return el.shadowRoot!.querySelector('ol')!;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('DsnOrderedList', () => {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
document.body.innerHTML = '';
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('rendering', () => {
|
|
28
|
+
it('creates a shadow root', () => {
|
|
29
|
+
const el = createElement();
|
|
30
|
+
expect(el.shadowRoot).not.toBeNull();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('renders an internal ol element', () => {
|
|
34
|
+
const el = createElement();
|
|
35
|
+
const list = getInternalList(el);
|
|
36
|
+
expect(list).toBeInstanceOf(HTMLOListElement);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('renders a style element in shadow DOM', () => {
|
|
40
|
+
const el = createElement();
|
|
41
|
+
const style = el.shadowRoot!.querySelector('style');
|
|
42
|
+
expect(style).not.toBeNull();
|
|
43
|
+
expect(style!.textContent).toContain('.dsn-ordered-list');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('renders a default slot for content projection', () => {
|
|
47
|
+
const el = createElement();
|
|
48
|
+
const slot = el.shadowRoot!.querySelector('slot');
|
|
49
|
+
expect(slot).not.toBeNull();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('applies dsn-ordered-list class to internal list', () => {
|
|
53
|
+
const el = createElement();
|
|
54
|
+
const list = getInternalList(el);
|
|
55
|
+
expect(list.classList.contains('dsn-ordered-list')).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('start attribute', () => {
|
|
60
|
+
it('sets start on internal ol', () => {
|
|
61
|
+
const el = createElement({ start: '5' });
|
|
62
|
+
const list = getInternalList(el);
|
|
63
|
+
expect(list.getAttribute('start')).toBe('5');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('defaults to no start attribute', () => {
|
|
67
|
+
const el = createElement();
|
|
68
|
+
const list = getInternalList(el);
|
|
69
|
+
expect(list.hasAttribute('start')).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('updates internal ol when start changes', () => {
|
|
73
|
+
const el = createElement({ start: '3' });
|
|
74
|
+
const list = getInternalList(el);
|
|
75
|
+
expect(list.getAttribute('start')).toBe('3');
|
|
76
|
+
|
|
77
|
+
el.start = 10;
|
|
78
|
+
expect(list.getAttribute('start')).toBe('10');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('returns 1 from start property when no attribute set', () => {
|
|
82
|
+
const el = createElement();
|
|
83
|
+
expect(el.start).toBe(1);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('defineOrderedList', () => {
|
|
89
|
+
it('does not throw when called with an already-registered tag', () => {
|
|
90
|
+
expect(() => defineOrderedList('dsn-ordered-list')).not.toThrow();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('registers a custom element with the default tag name', () => {
|
|
94
|
+
const el = document.createElement('dsn-ordered-list');
|
|
95
|
+
expect(el).toBeInstanceOf(DsnOrderedList);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSN Ordered List Web Component
|
|
3
|
+
*
|
|
4
|
+
* A custom element that provides a styled ordered list with numbered markers.
|
|
5
|
+
* Uses the same CSS classes and design tokens as the HTML and React components.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```html
|
|
9
|
+
* <dsn-ordered-list>
|
|
10
|
+
* <li>First step</li>
|
|
11
|
+
* <li>Second step</li>
|
|
12
|
+
* <li>Third step</li>
|
|
13
|
+
* </dsn-ordered-list>
|
|
14
|
+
*
|
|
15
|
+
* <dsn-ordered-list start="5">
|
|
16
|
+
* <li>Fifth step</li>
|
|
17
|
+
* <li>Sixth step</li>
|
|
18
|
+
* </dsn-ordered-list>
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// Auto-generated CSS from shared source — run `node scripts/build-css.js` to regenerate
|
|
23
|
+
import { orderedListStyles } from './ordered-list-styles.generated';
|
|
24
|
+
|
|
25
|
+
export class DsnOrderedList extends HTMLElement {
|
|
26
|
+
static get observedAttributes(): string[] {
|
|
27
|
+
return ['start'];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private _list: HTMLOListElement;
|
|
31
|
+
|
|
32
|
+
constructor() {
|
|
33
|
+
super();
|
|
34
|
+
this.attachShadow({ mode: 'open' });
|
|
35
|
+
|
|
36
|
+
// Create the internal list element
|
|
37
|
+
this._list = document.createElement('ol');
|
|
38
|
+
this._list.classList.add('dsn-ordered-list');
|
|
39
|
+
this._list.innerHTML = '<slot></slot>';
|
|
40
|
+
|
|
41
|
+
// Create style element with embedded CSS
|
|
42
|
+
const style = document.createElement('style');
|
|
43
|
+
style.textContent = orderedListStyles;
|
|
44
|
+
|
|
45
|
+
this.shadowRoot!.appendChild(style);
|
|
46
|
+
this.shadowRoot!.appendChild(this._list);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
connectedCallback(): void {
|
|
50
|
+
this._list.className = 'dsn-ordered-list';
|
|
51
|
+
this._updateStart();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
attributeChangedCallback(name: string): void {
|
|
55
|
+
if (name === 'start') {
|
|
56
|
+
this._updateStart();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private _updateStart(): void {
|
|
61
|
+
const start = this.getAttribute('start');
|
|
62
|
+
if (start) {
|
|
63
|
+
this._list.setAttribute('start', start);
|
|
64
|
+
} else {
|
|
65
|
+
this._list.removeAttribute('start');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Property: start
|
|
70
|
+
get start(): number {
|
|
71
|
+
return parseInt(this.getAttribute('start') || '1', 10);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
set start(value: number) {
|
|
75
|
+
this.setAttribute('start', String(value));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Register the custom element
|
|
80
|
+
export function defineOrderedList(tagName = 'dsn-ordered-list'): void {
|
|
81
|
+
if (!customElements.get(tagName)) {
|
|
82
|
+
customElements.define(tagName, DsnOrderedList);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DsnOrderedList, defineOrderedList } from './OrderedList';
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// Auto-generated — do not edit manually.
|
|
2
|
+
// Source: ordered-list.shadow.css + components-html source
|
|
3
|
+
export const orderedListStyles = "/**\n * Shadow DOM wrapper styles for the OrderedList Web Component.\n * The core styles are imported from the shared HTML/CSS source.\n */\n\n:host {\n display: block;\n}\n\n/* Shared ordered list styles - single source of truth */\n/**\n * Ordered List Component\n * Uses component tokens for consistent styling.\n */\n\n.dsn-ordered-list {\n font-family: var(--dsn-ordered-list-font-family);\n font-weight: var(--dsn-ordered-list-font-weight);\n color: var(--dsn-ordered-list-color);\n font-size: var(--dsn-ordered-list-font-size);\n line-height: var(--dsn-ordered-list-line-height);\n max-inline-size: var(--dsn-ordered-list-max-inline-size);\n padding-inline-start: var(--dsn-ordered-list-padding-inline-start);\n margin-block-start: 0;\n margin-block-end: var(--dsn-ordered-list-margin-block-end);\n list-style-type: decimal;\n}\n\n/* Number markers */\n.dsn-ordered-list ::marker {\n color: var(--dsn-ordered-list-marker-color);\n}\n\n/* Spacing between list items */\n.dsn-ordered-list > li {\n margin-block-end: var(--dsn-ordered-list-gap);\n}\n\n.dsn-ordered-list > li:last-child {\n margin-block-end: 0;\n}\n\n";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shadow DOM wrapper styles for the OrderedList Web Component.
|
|
3
|
+
* The core styles are imported from the shared HTML/CSS source.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
:host {
|
|
7
|
+
display: block;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/* Shared ordered list styles - single source of truth */
|
|
11
|
+
@import '../../../components-html/src/ordered-list/ordered-list.css';
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
|
2
|
+
import { DsnParagraph, defineParagraph } from './Paragraph';
|
|
3
|
+
|
|
4
|
+
// Register the custom element once for all tests
|
|
5
|
+
beforeAll(() => {
|
|
6
|
+
defineParagraph();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
function createElement(attrs: Record<string, string> = {}): DsnParagraph {
|
|
10
|
+
const el = document.createElement('dsn-paragraph') as DsnParagraph;
|
|
11
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
12
|
+
el.setAttribute(key, value);
|
|
13
|
+
}
|
|
14
|
+
document.body.appendChild(el);
|
|
15
|
+
return el;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getInternalParagraph(el: DsnParagraph): HTMLParagraphElement {
|
|
19
|
+
return el.shadowRoot!.querySelector('p')!;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('DsnParagraph', () => {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
document.body.innerHTML = '';
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('rendering', () => {
|
|
28
|
+
it('creates a shadow root', () => {
|
|
29
|
+
const el = createElement();
|
|
30
|
+
expect(el.shadowRoot).not.toBeNull();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('renders an internal paragraph element', () => {
|
|
34
|
+
const el = createElement();
|
|
35
|
+
const p = getInternalParagraph(el);
|
|
36
|
+
expect(p).toBeInstanceOf(HTMLParagraphElement);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('renders a style element in shadow DOM', () => {
|
|
40
|
+
const el = createElement();
|
|
41
|
+
const style = el.shadowRoot!.querySelector('style');
|
|
42
|
+
expect(style).not.toBeNull();
|
|
43
|
+
expect(style!.textContent).toContain('.dsn-paragraph');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('renders a slot for content projection', () => {
|
|
47
|
+
const el = createElement();
|
|
48
|
+
const slot = el.shadowRoot!.querySelector('slot');
|
|
49
|
+
expect(slot).not.toBeNull();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('variant attribute', () => {
|
|
54
|
+
it('defaults to "default" variant — no modifier class added', () => {
|
|
55
|
+
const el = createElement();
|
|
56
|
+
const p = getInternalParagraph(el);
|
|
57
|
+
expect(p.className).toContain('dsn-paragraph');
|
|
58
|
+
expect(p.className).not.toContain('dsn-paragraph--default');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('applies "lead" variant', () => {
|
|
62
|
+
const el = createElement({ variant: 'lead' });
|
|
63
|
+
const p = getInternalParagraph(el);
|
|
64
|
+
expect(p.className).toContain('dsn-paragraph--lead');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('applies "small-print" variant', () => {
|
|
68
|
+
const el = createElement({ variant: 'small-print' });
|
|
69
|
+
const p = getInternalParagraph(el);
|
|
70
|
+
expect(p.className).toContain('dsn-paragraph--small-print');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('updates classes when variant changes', () => {
|
|
74
|
+
const el = createElement({ variant: 'default' });
|
|
75
|
+
const p = getInternalParagraph(el);
|
|
76
|
+
expect(p.className).not.toContain('dsn-paragraph--default');
|
|
77
|
+
|
|
78
|
+
el.setAttribute('variant', 'lead');
|
|
79
|
+
expect(p.className).toContain('dsn-paragraph--lead');
|
|
80
|
+
expect(p.className).not.toContain('dsn-paragraph--default');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('variant property', () => {
|
|
85
|
+
it('returns "default" when no attribute set', () => {
|
|
86
|
+
const el = createElement();
|
|
87
|
+
expect(el.variant).toBe('default');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('reflects the attribute value', () => {
|
|
91
|
+
const el = createElement({ variant: 'lead' });
|
|
92
|
+
expect(el.variant).toBe('lead');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('sets the attribute when property is set', () => {
|
|
96
|
+
const el = createElement();
|
|
97
|
+
el.variant = 'small-print';
|
|
98
|
+
expect(el.getAttribute('variant')).toBe('small-print');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('updates classes when property is set', () => {
|
|
102
|
+
const el = createElement();
|
|
103
|
+
const p = getInternalParagraph(el);
|
|
104
|
+
|
|
105
|
+
el.variant = 'lead';
|
|
106
|
+
expect(p.className).toContain('dsn-paragraph--lead');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('base class', () => {
|
|
111
|
+
it('always includes dsn-paragraph base class', () => {
|
|
112
|
+
const el = createElement();
|
|
113
|
+
const p = getInternalParagraph(el);
|
|
114
|
+
expect(p.className).toContain('dsn-paragraph');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('includes base class with lead variant', () => {
|
|
118
|
+
const el = createElement({ variant: 'lead' });
|
|
119
|
+
const p = getInternalParagraph(el);
|
|
120
|
+
expect(p.className).toContain('dsn-paragraph');
|
|
121
|
+
expect(p.className).toContain('dsn-paragraph--lead');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('includes base class with small-print variant', () => {
|
|
125
|
+
const el = createElement({ variant: 'small-print' });
|
|
126
|
+
const p = getInternalParagraph(el);
|
|
127
|
+
expect(p.className).toContain('dsn-paragraph');
|
|
128
|
+
expect(p.className).toContain('dsn-paragraph--small-print');
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('custom element registration', () => {
|
|
133
|
+
it('is defined as dsn-paragraph', () => {
|
|
134
|
+
expect(customElements.get('dsn-paragraph')).toBe(DsnParagraph);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('does not re-register if already defined', () => {
|
|
138
|
+
// Should not throw
|
|
139
|
+
defineParagraph();
|
|
140
|
+
expect(customElements.get('dsn-paragraph')).toBe(DsnParagraph);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
});
|