@kwtv/components 0.2.0 → 0.3.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/README.md CHANGED
@@ -7,13 +7,13 @@ there is no separate library build or consumer alias configuration.
7
7
 
8
8
  ## What ships
9
9
 
10
- | Path | What it is |
11
- | --- | --- |
12
- | `src/astro/*.astro` | Static components. Scoped styles, no client JS. |
13
- | `src/react/*.tsx` | Interactive islands. Select, Modal, Toast, Drawer, QuantityStepper, NewsCarousel. |
14
- | `src/styles/base.css` | The shared primitives the React islands import: focus ring, touch targets, screen-reader helper. Astro components carry their own scoped styles. |
15
- | `src/styles/controls.css` | Button and icon-button styles for React islands that render the `ui-btn` / `ui-iconbtn` classes. |
16
- | `src/styles/motion.css` | The `pl-` motion layer: lift, underline, arrow and row nudges, rise, marquee, chip rail and swipe carousel. |
10
+ | Path | What it is |
11
+ | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
12
+ | `src/astro/*.astro` | Static components. Scoped styles, no client JS. |
13
+ | `src/react/*.tsx` | Interactive islands. Select, Modal, Toast, Drawer, QuantityStepper, NewsCarousel. |
14
+ | `src/styles/base.css` | The shared primitives the React islands import: focus ring, touch targets, screen-reader helper. Astro components carry their own scoped styles. |
15
+ | `src/styles/controls.css` | Button and icon-button styles for React islands that render the `ui-btn` / `ui-iconbtn` classes. |
16
+ | `src/styles/motion.css` | The `pl-` motion layer: lift, underline, arrow and row nudges, rise, marquee, chip rail and swipe carousel. |
17
17
 
18
18
  ## Install
19
19
 
@@ -31,7 +31,10 @@ in your app root, then use the components.
31
31
  import "@kwtv/tokens/tokens.css";
32
32
  import Button from "@kwtv/components/astro/Button.astro";
33
33
  ---
34
- <Button variant="primary" size="md">In de winkelwagen</Button>
34
+
35
+ <Button variant="primary" size="md">
36
+ In de winkelwagen
37
+ </Button>
35
38
  ```
36
39
 
37
40
  To use React islands, add Astro's React integration if the app does not
@@ -51,7 +54,12 @@ Then import an island:
51
54
  ---
52
55
  import Select from "@kwtv/components/react/Select.tsx";
53
56
  ---
54
- <Select client:load label="Groep" options={[{ value: "r1", label: "Recreatief 8–10 jaar" }]} />
57
+
58
+ <Select
59
+ client:load
60
+ label="Groep"
61
+ options={[{ value: "r1", label: "Recreatief 8–10 jaar" }]}
62
+ />
55
63
  ```
56
64
 
57
65
  The React islands need their shared stylesheets once in the app. Import
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kwtv/components",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "description": "KWTV UI components for Astro, with React islands. Built on the KWTV design tokens.",
@@ -34,6 +34,6 @@
34
34
  }
35
35
  },
36
36
  "dependencies": {
37
- "@kwtv/tokens": "^0.2.0"
37
+ "@kwtv/tokens": "^0.3.0"
38
38
  }
39
39
  }
@@ -14,14 +14,20 @@ interface Props {
14
14
  const { summary, open = false, class: className, ...rest } = Astro.props;
15
15
  ---
16
16
 
17
- <details class={["ui-accordion__item", className].filter(Boolean).join(" ")} open={open} {...rest}>
17
+ <details
18
+ class={["ui-accordion__item", className].filter(Boolean).join(" ")}
19
+ open={open}
20
+ {...rest}
21
+ >
18
22
  <summary class="ui-accordion__sum">
19
23
  {summary}
20
24
  <span class="ui-accordion__chev" aria-hidden="true">
21
25
  <Icon name="chevron-down" size={14} stroke={2.2} />
22
26
  </span>
23
27
  </summary>
24
- <div class="ui-accordion__body"><slot /></div>
28
+ <div class="ui-accordion__body">
29
+ <slot />
30
+ </div>
25
31
  </details>
26
32
 
27
33
  <style>
@@ -20,17 +20,26 @@ interface Props {
20
20
  class?: string;
21
21
  }
22
22
 
23
- const { tags, clearAllLabel = "Alles wissen", class: className, ...rest } = Astro.props;
23
+ const {
24
+ tags,
25
+ clearAllLabel = "Alles wissen",
26
+ class: className,
27
+ ...rest
28
+ } = Astro.props;
24
29
 
25
30
  const classes = ["ui-filtertags", className].filter(Boolean).join(" ");
26
31
  ---
27
32
 
28
33
  <div class={classes} {...rest}>
29
34
  {tags.map((tag) => (
30
- <Badge tone="outline" removable removeLabel={tag.removeLabel}>{tag.label}</Badge>
35
+ <Badge tone="outline" removable removeLabel={tag.removeLabel}>
36
+ {tag.label}
37
+ </Badge>
31
38
  ))}
32
39
  {clearAllLabel && tags.length > 0 && (
33
- <button class="ui-filtertags__clear" type="button">{clearAllLabel}</button>
40
+ <button class="ui-filtertags__clear" type="button">
41
+ {clearAllLabel}
42
+ </button>
34
43
  )}
35
44
  </div>
36
45
 
@@ -20,7 +20,11 @@ const { items, label = "Kruimelpad", class: className, ...rest } = Astro.props;
20
20
  const lastIndex = items.length - 1;
21
21
  ---
22
22
 
23
- <nav class={["ui-crumbs", className].filter(Boolean).join(" ")} aria-label={label} {...rest}>
23
+ <nav
24
+ class={["ui-crumbs", className].filter(Boolean).join(" ")}
25
+ aria-label={label}
26
+ {...rest}
27
+ >
24
28
  {items.map((item, i) => (
25
29
  <>
26
30
  {item.href && i !== lastIndex ? (
@@ -59,7 +59,9 @@ const tagProps = href
59
59
  {...tagProps}
60
60
  {...rest}
61
61
  >
62
- <span class="ui-btn__label"><slot /></span>
62
+ <span class="ui-btn__label">
63
+ <slot />
64
+ </span>
63
65
  {icon && (
64
66
  <span class="ui-btn__icon" aria-hidden="true">
65
67
  <Icon name={icon as any} size="1em" stroke={1.8} />
@@ -25,7 +25,11 @@ const {
25
25
 
26
26
  const Title = titleTag;
27
27
 
28
- const classes = ["ui-card", variant !== "default" && `ui-card--${variant}`, className]
28
+ const classes = [
29
+ "ui-card",
30
+ variant !== "default" && `ui-card--${variant}`,
31
+ className,
32
+ ]
29
33
  .filter(Boolean)
30
34
  .join(" ");
31
35
 
@@ -35,7 +39,9 @@ const hasFooter = Astro.slots.has("footer");
35
39
  <div class={classes} {...rest}>
36
40
  {eyebrow && <span class="ui-card__eyebrow">{eyebrow}</span>}
37
41
  {title && <Title class="ui-card__title">{title}</Title>}
38
- <span class="ui-card__body"><slot /></span>
42
+ <span class="ui-card__body">
43
+ <slot />
44
+ </span>
39
45
  {hasFooter && (
40
46
  <div class="ui-card__footer">
41
47
  <slot name="footer" />
@@ -35,13 +35,23 @@ const classes = ["ui-line", className].filter(Boolean).join(" ");
35
35
  ---
36
36
 
37
37
  <div class={classes} {...rest}>
38
- <img class="ui-line__thumb" loading="lazy" decoding="async" src={img} alt={alt} />
38
+ <img
39
+ class="ui-line__thumb"
40
+ loading="lazy"
41
+ decoding="async"
42
+ src={img}
43
+ alt={alt}
44
+ />
39
45
  <span class="ui-line__grow">
40
46
  <b class="ui-line__title">{title}</b>
41
47
  {note && <span class="ui-line__note">{note}</span>}
42
- <a class="ui-line__remove" href={removeHref}>{removeLabel}</a>
48
+ <a class="ui-line__remove" href={removeHref}>
49
+ {removeLabel}
50
+ </a>
51
+ </span>
52
+ <span class="ui-line__qty">
53
+ <slot name="qty" />
43
54
  </span>
44
- <span class="ui-line__qty"><slot name="qty" /></span>
45
55
  <b class="ui-line__price">{price}</b>
46
56
  </div>
47
57
 
@@ -17,7 +17,12 @@ const classes = ["pl-chips", className].filter(Boolean).join(" ");
17
17
 
18
18
  <nav class={classes} aria-label={label} {...rest}>
19
19
  {items.map((item) => (
20
- <a class="pl-chip" href={item.href} data-on={item.current ? "1" : undefined} aria-current={item.current ? "page" : undefined}>
20
+ <a
21
+ class="pl-chip"
22
+ href={item.href}
23
+ data-on={item.current ? "1" : undefined}
24
+ aria-current={item.current ? "page" : undefined}
25
+ >
21
26
  {item.label}
22
27
  </a>
23
28
  ))}
@@ -16,7 +16,9 @@ const classes = ["pl-dotgrid", compact && "pl-dotgrid--compact", className]
16
16
 
17
17
  <div class={classes} {...rest}>
18
18
  <span class="pl-dotgrid__dots" aria-hidden="true"></span>
19
- <span class="pl-dotgrid__content"><slot /></span>
19
+ <span class="pl-dotgrid__content">
20
+ <slot />
21
+ </span>
20
22
  </div>
21
23
 
22
24
  <style>
@@ -32,9 +34,12 @@ const classes = ["pl-dotgrid", compact && "pl-dotgrid--compact", className]
32
34
  .pl-dotgrid__dots {
33
35
  position: absolute;
34
36
  inset: 0;
35
- background-image: radial-gradient(var(--dotgrid-color, var(--sage-light)) 1.4px, transparent 1.6px);
37
+ background-image: radial-gradient(
38
+ var(--dotgrid-color, var(--sage-light)) 1.4px,
39
+ transparent 1.6px
40
+ );
36
41
  background-size: 28px 28px;
37
- opacity: var(--dotgrid-opacity, var(--dotgrid-theme-opacity, .7));
42
+ opacity: var(--dotgrid-opacity, var(--dotgrid-theme-opacity, 0.7));
38
43
  animation: pl-drift 6s linear infinite;
39
44
  }
40
45
 
@@ -58,9 +63,13 @@ const classes = ["pl-dotgrid", compact && "pl-dotgrid--compact", className]
58
63
  }
59
64
 
60
65
  /* Explicit themes win over OS preference, as in the token cascade. */
61
- :global([data-theme="dark"]) .pl-dotgrid { --dotgrid-theme-opacity: .2; }
66
+ :global([data-theme="dark"]) .pl-dotgrid {
67
+ --dotgrid-theme-opacity: 0.2;
68
+ }
62
69
  @media (prefers-color-scheme: dark) {
63
- :global(:root:not([data-theme])) .pl-dotgrid { --dotgrid-theme-opacity: .2; }
70
+ :global(:root:not([data-theme])) .pl-dotgrid {
71
+ --dotgrid-theme-opacity: 0.2;
72
+ }
64
73
  }
65
74
 
66
75
  @media (prefers-reduced-motion: reduce) {
@@ -15,7 +15,9 @@ const hasAction = Astro.slots.has("action");
15
15
 
16
16
  <div class={["ui-empty", className].filter(Boolean).join(" ")} {...rest}>
17
17
  <b class="ui-empty__title">{title}</b>
18
- <span class="ui-empty__body"><slot /></span>
18
+ <span class="ui-empty__body">
19
+ <slot />
20
+ </span>
19
21
  {hasAction && (
20
22
  <div class="ui-empty__action">
21
23
  <slot name="action" />
@@ -29,8 +29,7 @@ const classes = ["ui-filtergroup", className].filter(Boolean).join(" ");
29
29
  margin: 0;
30
30
  padding: var(--space-3) 0 0;
31
31
  border: 0;
32
- /* More space above the rule than below it, so the rule belongs to this
33
- group's top edge. */
32
+ /* More space above the rule than below it, so the rule belongs to this group's top edge. */
34
33
  margin-top: var(--space-4);
35
34
  border-top: 1px solid var(--border);
36
35
  }
@@ -23,7 +23,13 @@ const classes = ["ui-filters", className].filter(Boolean).join(" ");
23
23
  <aside class={classes} {...rest}>
24
24
  <div class="ui-filterbar">
25
25
  <span class="ui-eyebrow">Filters</span>
26
- {clearLabel && <slot name="clear"><button class="ui-filters__clear" type="button">{clearLabel}</button></slot>}
26
+ {clearLabel && (
27
+ <slot name="clear">
28
+ <button class="ui-filters__clear" type="button">
29
+ {clearLabel}
30
+ </button>
31
+ </slot>
32
+ )}
27
33
  </div>
28
34
  <slot />
29
35
  </aside>
@@ -33,16 +33,27 @@ const classes = ["ui-footer", `ui-footer--${variant}`, className]
33
33
 
34
34
  <footer class={classes} {...rest}>
35
35
  <div class="ui-footer__brandblock">
36
- <div class="ui-footer__brand">{brandName}<slot name="brand-suffix" /></div>
37
- <div class="ui-footer__addr"><slot name="address" /></div>
38
- <div class="ui-footer__social"><slot name="social" /></div>
36
+ <div class="ui-footer__brand">
37
+ {brandName}
38
+ <slot name="brand-suffix" />
39
+ </div>
40
+ <div class="ui-footer__addr">
41
+ <slot name="address" />
42
+ </div>
43
+ <div class="ui-footer__social">
44
+ <slot name="social" />
45
+ </div>
39
46
  </div>
40
47
 
41
48
  <slot />
42
49
 
43
50
  <div class="ui-footer__bar">
44
- <span class="ui-footer__legal"><slot name="legal" /></span>
45
- <span class="ui-footer__policies"><slot name="policies" /></span>
51
+ <span class="ui-footer__legal">
52
+ <slot name="legal" />
53
+ </span>
54
+ <span class="ui-footer__policies">
55
+ <slot name="policies" />
56
+ </span>
46
57
  </div>
47
58
  </footer>
48
59
 
@@ -166,7 +177,8 @@ const classes = ["ui-footer", `ui-footer--${variant}`, className]
166
177
  gap: var(--space-2);
167
178
  margin-top: calc(var(--space-6) * -1 + var(--space-2));
168
179
  }
169
- .ui-footer--mobile.ui-footer--site :global(.ui-footer__contact .ui-footer__title),
180
+ .ui-footer--mobile.ui-footer--site
181
+ :global(.ui-footer__contact .ui-footer__title),
170
182
  .ui-footer--mobile.ui-footer--site :global(.ui-footer__desktop-only) {
171
183
  display: none;
172
184
  }
@@ -214,7 +226,8 @@ const classes = ["ui-footer", `ui-footer--${variant}`, className]
214
226
  .ui-footer--mobile.ui-footer--shop :global(.ui-footer__shop-links) {
215
227
  gap: var(--space-4);
216
228
  }
217
- .ui-footer--mobile.ui-footer--shop :global(.ui-footer__shop-links .ui-footer__title) {
229
+ .ui-footer--mobile.ui-footer--shop
230
+ :global(.ui-footer__shop-links .ui-footer__title) {
218
231
  display: none;
219
232
  }
220
233
  .ui-footer--mobile.ui-footer--shop .ui-footer__social:empty {
@@ -13,16 +13,25 @@ interface Props {
13
13
  class?: string;
14
14
  }
15
15
 
16
- const { brandName = "KWTV", brandHref = "/", class: className, ...rest } =
17
- Astro.props;
16
+ const {
17
+ brandName = "KWTV",
18
+ brandHref = "/",
19
+ class: className,
20
+ ...rest
21
+ } = Astro.props;
18
22
 
19
23
  const classes = ["ui-header", className].filter(Boolean).join(" ");
20
24
  ---
21
25
 
22
26
  <header class={classes} {...rest}>
23
27
  <a class="ui-brand" href={brandHref}>
24
- <span class="ui-brand__mark"><slot name="logo" /></span>
25
- <span class="ui-brand__name">{brandName}<slot name="brand-suffix" /></span>
28
+ <span class="ui-brand__mark">
29
+ <slot name="logo" />
30
+ </span>
31
+ <span class="ui-brand__name">
32
+ {brandName}
33
+ <slot name="brand-suffix" />
34
+ </span>
26
35
  </a>
27
36
 
28
37
  <nav class="ui-nav">
@@ -42,14 +42,16 @@ const paths: Record<Props["name"], string> = {
42
42
  "chevron-right": '<path d="M9 6l6 6-6 6"/>',
43
43
  "chevron-left": '<path d="M15 6l-6 6 6 6"/>',
44
44
  info: '<circle cx="12" cy="12" r="9"/><path d="M12 11v5M12 8v.5"/>',
45
- "alert-circle": '<circle cx="12" cy="12" r="9"/><path d="M12 8v4.5M12 16v.5"/>',
45
+ "alert-circle":
46
+ '<circle cx="12" cy="12" r="9"/><path d="M12 8v4.5M12 16v.5"/>',
46
47
  "alert-triangle": '<path d="M12 4l8.5 15h-17L12 4zM12 10v4M12 16.5v.5"/>',
47
48
  "arrow-right": '<path d="M5 12h14M13 6l6 6-6 6"/>',
48
49
  "arrow-up-right": '<path d="M7 17 17 7M8 7h9v9"/>',
49
50
  "arrow-down": '<path d="M12 5v14M6 13l6 6 6-6"/>',
50
51
  instagram:
51
52
  '<rect x="4" y="4" width="16" height="16" rx="5"/><circle cx="12" cy="12" r="3.6"/><path d="M17 7.2v.01"/>',
52
- facebook: '<path d="M14.5 8.5h2M14.5 8.5c0-2 .8-3 2.5-3M14.5 8.5V19M11 12h6"/>',
53
+ facebook:
54
+ '<path d="M14.5 8.5h2M14.5 8.5c0-2 .8-3 2.5-3M14.5 8.5V19M11 12h6"/>',
53
55
  youtube:
54
56
  '<rect x="3.5" y="6.5" width="17" height="11" rx="3.2"/><path d="M11 10.2l3.4 1.8-3.4 1.8z"/>',
55
57
  };
@@ -41,7 +41,9 @@ const classes = ["ui-iconbtn", className].filter(Boolean).join(" ");
41
41
  >
42
42
  {icon ? <Icon name={icon as any} size={20} stroke={1.7} /> : <slot />}
43
43
  {count !== undefined && (
44
- <span class="ui-iconbtn__count" aria-hidden="true">{count}</span>
44
+ <span class="ui-iconbtn__count" aria-hidden="true">
45
+ {count}
46
+ </span>
45
47
  )}
46
48
  </button>
47
49
 
@@ -21,18 +21,16 @@ const { items, divider = "✦", class: className } = Astro.props;
21
21
 
22
22
  <div class:list={["ui-marquee", className]} aria-hidden="true">
23
23
  <div class="ui-marquee__track">
24
- {
25
- [0, 1].map(() => (
26
- <div class="ui-marquee__group">
27
- {items.map((item) => (
28
- <>
29
- <span>{item}</span>
30
- <span class="ui-marquee__div">{divider}</span>
31
- </>
32
- ))}
33
- </div>
34
- ))
35
- }
24
+ {[0, 1].map(() => (
25
+ <div class="ui-marquee__group">
26
+ {items.map((item) => (
27
+ <>
28
+ <span>{item}</span>
29
+ <span class="ui-marquee__div">{divider}</span>
30
+ </>
31
+ ))}
32
+ </div>
33
+ ))}
36
34
  </div>
37
35
  </div>
38
36
 
@@ -27,9 +27,15 @@ const classes = ["ui-navgroup", open && "is-open", className]
27
27
  ---
28
28
 
29
29
  <div class={classes} data-navdropdown {...rest}>
30
- <button class="ui-navlink" type="button" aria-expanded={open ? "true" : "false"}>
30
+ <button
31
+ class="ui-navlink"
32
+ type="button"
33
+ aria-expanded={open ? "true" : "false"}
34
+ >
31
35
  {label}
32
- <span aria-hidden="true"><Icon name="chevron-down" size={11} stroke={2.4} /></span>
36
+ <span aria-hidden="true">
37
+ <Icon name="chevron-down" size={11} stroke={2.4} />
38
+ </span>
33
39
  </button>
34
40
  <div class="ui-navpanel">
35
41
  <slot />
@@ -40,42 +46,52 @@ const classes = ["ui-navgroup", open && "is-open", className]
40
46
  // Click-to-toggle, on top of the CSS hover/focus-within path: the only way
41
47
  // a touchscreen tap, or a Safari click (which does not focus a <button>),
42
48
  // can open the panel at all.
43
- document.querySelectorAll<HTMLElement>("[data-navdropdown]").forEach((group) => {
44
- const trigger = group.querySelector<HTMLButtonElement>(".ui-navlink");
45
- if (!trigger) return;
46
-
47
- const close = () => {
48
- group.classList.remove("is-open");
49
- trigger.setAttribute("aria-expanded", "false");
50
- };
51
-
52
- trigger.addEventListener("click", (event) => {
53
- const willOpen = !group.classList.contains("is-open");
54
- document.querySelectorAll<HTMLElement>("[data-navdropdown].is-open").forEach((other) => {
55
- if (other === group) return;
56
- other.classList.remove("is-open");
57
- other.querySelector<HTMLButtonElement>(".ui-navlink")?.setAttribute("aria-expanded", "false");
49
+ document
50
+ .querySelectorAll<HTMLElement>("[data-navdropdown]")
51
+ .forEach((group) => {
52
+ const trigger = group.querySelector<HTMLButtonElement>(".ui-navlink");
53
+ if (!trigger) return;
54
+
55
+ const close = () => {
56
+ group.classList.remove("is-open");
57
+ trigger.setAttribute("aria-expanded", "false");
58
+ };
59
+
60
+ trigger.addEventListener("click", (event) => {
61
+ const willOpen = !group.classList.contains("is-open");
62
+ document
63
+ .querySelectorAll<HTMLElement>("[data-navdropdown].is-open")
64
+ .forEach((other) => {
65
+ if (other === group) return;
66
+ other.classList.remove("is-open");
67
+ other
68
+ .querySelector<HTMLButtonElement>(".ui-navlink")
69
+ ?.setAttribute("aria-expanded", "false");
70
+ });
71
+ group.classList.toggle("is-open", willOpen);
72
+ trigger.setAttribute("aria-expanded", String(willOpen));
73
+ event.stopPropagation();
58
74
  });
59
- group.classList.toggle("is-open", willOpen);
60
- trigger.setAttribute("aria-expanded", String(willOpen));
61
- event.stopPropagation();
62
- });
63
75
 
64
- group.addEventListener("keydown", (event) => {
65
- if (event.key === "Escape") {
66
- close();
67
- trigger.focus();
68
- }
76
+ group.addEventListener("keydown", (event) => {
77
+ if (event.key === "Escape") {
78
+ close();
79
+ trigger.focus();
80
+ }
81
+ });
69
82
  });
70
- });
71
83
 
72
84
  document.addEventListener("click", (event) => {
73
- document.querySelectorAll<HTMLElement>("[data-navdropdown].is-open").forEach((group) => {
74
- if (!group.contains(event.target as Node)) {
75
- group.classList.remove("is-open");
76
- group.querySelector<HTMLButtonElement>(".ui-navlink")?.setAttribute("aria-expanded", "false");
77
- }
78
- });
85
+ document
86
+ .querySelectorAll<HTMLElement>("[data-navdropdown].is-open")
87
+ .forEach((group) => {
88
+ if (!group.contains(event.target as Node)) {
89
+ group.classList.remove("is-open");
90
+ group
91
+ .querySelector<HTMLButtonElement>(".ui-navlink")
92
+ ?.setAttribute("aria-expanded", "false");
93
+ }
94
+ });
79
95
  });
80
96
  </script>
81
97
 
@@ -33,8 +33,7 @@ const classes = ["ui-navlink", current && "is-current", className]
33
33
  display: inline-flex;
34
34
  align-items: center;
35
35
  gap: var(--space-1-5);
36
- /* 7 px block / 10 px inline padding with an equal negative margin, so the
37
- focus ring has room but the header rhythm does not shift. */
36
+ /* 7 px block / 10 px inline padding with an equal negative margin, so the focus ring has room but the header rhythm does not shift. */
38
37
  padding: 7px 10px;
39
38
  margin: -7px -10px;
40
39
  border-radius: var(--radius-sm);
@@ -40,7 +40,9 @@ const classes = ["ui-news", stacked && "ui-news--stacked", className]
40
40
  ---
41
41
 
42
42
  <form class={classes} action={action} method={method} {...rest}>
43
- <label class="ui-sr-only" for={id}>{label}</label>
43
+ <label class="ui-sr-only" for={id}>
44
+ {label}
45
+ </label>
44
46
  <input
45
47
  class="ui-news__input"
46
48
  id={id}
@@ -50,7 +52,9 @@ const classes = ["ui-news", stacked && "ui-news--stacked", className]
50
52
  placeholder={placeholder}
51
53
  autocomplete="email"
52
54
  />
53
- <button class="ui-news__btn" type="submit">{buttonText}</button>
55
+ <button class="ui-news__btn" type="submit">
56
+ {buttonText}
57
+ </button>
54
58
  </form>
55
59
 
56
60
  <style>
@@ -68,17 +68,21 @@ const hasNext = current < total;
68
68
  <Icon name="chevron-left" size={17} stroke={2.3} />
69
69
  </a>
70
70
  ) : (
71
- <span class="is-disabled" aria-hidden="true"><Icon name="chevron-left" size={17} stroke={2.3} /></span>
71
+ <span class="is-disabled" aria-hidden="true">
72
+ <Icon name="chevron-left" size={17} stroke={2.3} />
73
+ </span>
72
74
  )}
73
75
 
74
76
  {list.map((p) =>
75
77
  p === "…" ? (
76
78
  <span class="ui-pager__gap">…</span>
77
79
  ) : p === current ? (
78
- <a class="is-current" href={href(p)} aria-current="page">{p}</a>
80
+ <a class="is-current" href={href(p)} aria-current="page">
81
+ {p}
82
+ </a>
79
83
  ) : (
80
84
  <a href={href(p)}>{p}</a>
81
- )
85
+ ),
82
86
  )}
83
87
 
84
88
  {hasNext ? (
@@ -86,7 +90,9 @@ const hasNext = current < total;
86
90
  <Icon name="chevron-right" size={17} stroke={2.3} />
87
91
  </a>
88
92
  ) : (
89
- <span class="is-disabled" aria-hidden="true"><Icon name="chevron-right" size={17} stroke={2.3} /></span>
93
+ <span class="is-disabled" aria-hidden="true">
94
+ <Icon name="chevron-right" size={17} stroke={2.3} />
95
+ </span>
90
96
  )}
91
97
  </nav>
92
98
 
@@ -119,13 +125,24 @@ const hasNext = current < total;
119
125
  const nav = a.closest("[data-ui-pager][data-ui-pager-preserve]");
120
126
  if (!nav) return;
121
127
  const me = e as MouseEvent;
122
- if (me.button !== 0 || me.metaKey || me.ctrlKey || me.shiftKey || me.altKey) return;
128
+ if (
129
+ me.button !== 0 ||
130
+ me.metaKey ||
131
+ me.ctrlKey ||
132
+ me.shiftKey ||
133
+ me.altKey
134
+ )
135
+ return;
123
136
  const { page, url } = pageFrom(nav, a);
124
137
  if (page == null) return;
125
138
  e.preventDefault();
126
139
  e.stopImmediatePropagation();
127
140
  history.pushState({ uiPager: true, page }, "", url.href);
128
- window.dispatchEvent(new CustomEvent("ui:pagerchange", { detail: { page, url: url.href } }));
141
+ window.dispatchEvent(
142
+ new CustomEvent("ui:pagerchange", {
143
+ detail: { page, url: url.href },
144
+ }),
145
+ );
129
146
  },
130
147
  true,
131
148
  );
@@ -136,7 +153,11 @@ const hasNext = current < total;
136
153
  const p = parseInt(u.searchParams.get("page") || "1", 10);
137
154
  window.dispatchEvent(
138
155
  new CustomEvent("ui:pagerchange", {
139
- detail: { page: Number.isFinite(p) ? p : 1, url: u.href, popped: true },
156
+ detail: {
157
+ page: Number.isFinite(p) ? p : 1,
158
+ url: u.href,
159
+ popped: true,
160
+ },
140
161
  }),
141
162
  );
142
163
  });
@@ -41,16 +41,27 @@ const ribbonLabel: Record<NonNullable<Props["ribbon"]>, string> = {
41
41
  fav: "Favoriet",
42
42
  };
43
43
 
44
- const classes = ["ui-product", soldOut && "is-disabled", className].filter(Boolean).join(" ");
44
+ const classes = ["ui-product", soldOut && "is-disabled", className]
45
+ .filter(Boolean)
46
+ .join(" ");
45
47
  ---
46
48
 
47
49
  <a class={classes} href={href} {...rest}>
48
50
  {(ribbon || soldOut) && (
49
- <span class={`ui-ribbon ui-ribbon--${soldOut ? "sold-out" : ribbon}`} aria-hidden="true">
51
+ <span
52
+ class={`ui-ribbon ui-ribbon--${soldOut ? "sold-out" : ribbon}`}
53
+ aria-hidden="true"
54
+ >
50
55
  <i>{soldOut ? "Uitverkocht" : ribbonLabel[ribbon!]}</i>
51
56
  </span>
52
57
  )}
53
- <img class="ui-product__img" loading="lazy" decoding="async" src={img} alt={alt} />
58
+ <img
59
+ class="ui-product__img"
60
+ loading="lazy"
61
+ decoding="async"
62
+ src={img}
63
+ alt={alt}
64
+ />
54
65
  <span class="ui-product__body">
55
66
  <span class="ui-product__title">{title}</span>
56
67
  {meta && <span class="ui-product__meta">{meta}</span>}
@@ -12,9 +12,18 @@ interface Props {
12
12
  class?: string;
13
13
  }
14
14
 
15
- const { variant = "block", width, height, radius, class: className, ...rest } = Astro.props;
15
+ const {
16
+ variant = "block",
17
+ width,
18
+ height,
19
+ radius,
20
+ class: className,
21
+ ...rest
22
+ } = Astro.props;
16
23
 
17
- const classes = ["ui-skel", `ui-skel--${variant}`, className].filter(Boolean).join(" ");
24
+ const classes = ["ui-skel", `ui-skel--${variant}`, className]
25
+ .filter(Boolean)
26
+ .join(" ");
18
27
 
19
28
  const style = [
20
29
  width && `width:${width}`,
@@ -25,7 +34,12 @@ const style = [
25
34
  .join(";");
26
35
  ---
27
36
 
28
- <span class={classes} style={style || undefined} aria-hidden="true" {...rest}></span>
37
+ <span
38
+ class={classes}
39
+ style={style || undefined}
40
+ aria-hidden="true"
41
+ {...rest}
42
+ ></span>
29
43
 
30
44
  <style>
31
45
  .ui-skel {
@@ -35,7 +35,8 @@ const classes = ["ui-social", className].filter(Boolean).join(" ");
35
35
  height: 40px;
36
36
  border-radius: var(--radius-md);
37
37
  color: var(--ivory-light);
38
- transition: color var(--duration-fast) var(--ease-standard),
38
+ transition:
39
+ color var(--duration-fast) var(--ease-standard),
39
40
  background-color var(--duration-fast) var(--ease-standard);
40
41
  }
41
42
  .ui-social:hover {
@@ -20,9 +20,18 @@ interface Props {
20
20
  class?: string;
21
21
  }
22
22
 
23
- const { steps, current, label, mobile = false, class: className, ...rest } = Astro.props;
23
+ const {
24
+ steps,
25
+ current,
26
+ label,
27
+ mobile = false,
28
+ class: className,
29
+ ...rest
30
+ } = Astro.props;
24
31
 
25
- const classes = ["ui-steps", mobile && "ui-steps--mobile", className].filter(Boolean).join(" ");
32
+ const classes = ["ui-steps", mobile && "ui-steps--mobile", className]
33
+ .filter(Boolean)
34
+ .join(" ");
26
35
  ---
27
36
 
28
37
  <nav class={classes} aria-label={label} {...rest}>
@@ -31,7 +40,10 @@ const classes = ["ui-steps", mobile && "ui-steps--mobile", className].filter(Boo
31
40
  return (
32
41
  <>
33
42
  {i > 0 && <span class="ui-steps__sep"></span>}
34
- <span class={["ui-step", state].filter(Boolean).join(" ")} aria-current={i === current ? "step" : undefined}>
43
+ <span
44
+ class={["ui-step", state].filter(Boolean).join(" ")}
45
+ aria-current={i === current ? "step" : undefined}
46
+ >
35
47
  <span class="ui-step__dot" aria-hidden="true">
36
48
  {i < current ? <Icon name="check" size={11} stroke={3} /> : i + 1}
37
49
  </span>
@@ -15,12 +15,33 @@ const classes = ["pl-sticker", `pl-sticker--${size}`, className]
15
15
  <span class={classes} aria-hidden="true" {...rest}>
16
16
  <svg class="pl-sticker__disc" viewBox="0 0 112 112">
17
17
  <circle cx="56" cy="56" r="52" fill="var(--surface-inverse)"></circle>
18
- <circle class="pl-sticker__ring" cx="56" cy="56" r="47" fill="none" stroke="var(--sage-light)" stroke-width="1.5" stroke-dasharray="5 7"></circle>
18
+ <circle
19
+ class="pl-sticker__ring"
20
+ cx="56"
21
+ cy="56"
22
+ r="47"
23
+ fill="none"
24
+ stroke="var(--sage-light)"
25
+ stroke-width="1.5"
26
+ stroke-dasharray="5 7"
27
+ ></circle>
19
28
  </svg>
20
29
  <span class="pl-sticker__content">
21
- <span><slot /></span>
30
+ <span>
31
+ <slot />
32
+ </span>
22
33
  {arrow && (
23
- <svg class="pl-sticker__arrow" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round">
34
+ <svg
35
+ class="pl-sticker__arrow"
36
+ width="18"
37
+ height="18"
38
+ viewBox="0 0 24 24"
39
+ fill="none"
40
+ stroke="currentColor"
41
+ stroke-width="2.4"
42
+ stroke-linecap="round"
43
+ stroke-linejoin="round"
44
+ >
24
45
  <path d="M12 5v14M6 13l6 6 6-6"></path>
25
46
  </svg>
26
47
  )}
@@ -5,7 +5,11 @@ interface Props {
5
5
  class?: string;
6
6
  }
7
7
 
8
- const { label = "Uitgelichte artikelen", class: className, ...rest } = Astro.props;
8
+ const {
9
+ label = "Uitgelichte artikelen",
10
+ class: className,
11
+ ...rest
12
+ } = Astro.props;
9
13
  const classes = ["pl-carousel", className].filter(Boolean).join(" ");
10
14
  ---
11
15
 
@@ -27,8 +27,14 @@ interface Props {
27
27
  class?: string;
28
28
  }
29
29
 
30
- const { columns, numeric = false, busy = false, caption, class: className, ...rest } =
31
- Astro.props;
30
+ const {
31
+ columns,
32
+ numeric = false,
33
+ busy = false,
34
+ caption,
35
+ class: className,
36
+ ...rest
37
+ } = Astro.props;
32
38
 
33
39
  const classes = ["ui-table", numeric && "ui-table--numeric", className]
34
40
  .filter(Boolean)
@@ -15,8 +15,12 @@ const classes = ["ui-tablecard", className].filter(Boolean).join(" ");
15
15
  ---
16
16
 
17
17
  <div class={classes} aria-hidden={busy ? "true" : undefined} {...rest}>
18
- <span class="ui-tablecard__main"><slot name="main" /></span>
19
- <span class="ui-tablecard__end"><slot name="end" /></span>
18
+ <span class="ui-tablecard__main">
19
+ <slot name="main" />
20
+ </span>
21
+ <span class="ui-tablecard__end">
22
+ <slot name="end" />
23
+ </span>
20
24
  </div>
21
25
 
22
26
  <style>
@@ -64,14 +64,19 @@ const inputClasses = [
64
64
  .filter(Boolean)
65
65
  .join(" ");
66
66
 
67
- const charCount = count && maxlength ? `${String(value).length} / ${maxlength}` : null;
67
+ const charCount =
68
+ count && maxlength ? `${String(value).length} / ${maxlength}` : null;
68
69
  ---
69
70
 
70
71
  <label class={["ui-field", className].filter(Boolean).join(" ")}>
71
72
  {label && (
72
73
  <span class="ui-field__label">
73
74
  {label}
74
- {required && <span class="ui-field__req" aria-hidden="true">*</span>}
75
+ {required && (
76
+ <span class="ui-field__req" aria-hidden="true">
77
+ *
78
+ </span>
79
+ )}
75
80
  </span>
76
81
  )}
77
82
 
@@ -115,7 +120,11 @@ const charCount = count && maxlength ? `${String(value).length} / ${maxlength}`
115
120
  {error}
116
121
  </span>
117
122
  ) : (
118
- hint && <span class="ui-field__hint" id={hintId}>{hint}</span>
123
+ hint && (
124
+ <span class="ui-field__hint" id={hintId}>
125
+ {hint}
126
+ </span>
127
+ )
119
128
  )}
120
129
 
121
130
  {charCount && <span class="ui-field__count">{charCount}</span>}
@@ -1,8 +1,4 @@
1
- import {
2
- useEffect,
3
- useRef,
4
- type ReactNode,
5
- } from "react";
1
+ import { useEffect, useRef, type ReactNode } from "react";
6
2
  import "./Drawer.css";
7
3
 
8
4
  /**
@@ -89,7 +89,9 @@ export default function Modal({
89
89
  if (!panel) return;
90
90
  const items = Array.from(
91
91
  panel.querySelectorAll<HTMLElement>(FOCUSABLE),
92
- ).filter((el) => el.offsetParent !== null || el === document.activeElement);
92
+ ).filter(
93
+ (el) => el.offsetParent !== null || el === document.activeElement,
94
+ );
93
95
  if (items.length === 0) {
94
96
  e.preventDefault();
95
97
  panel.focus();
@@ -124,11 +126,7 @@ export default function Modal({
124
126
  >
125
127
  <div
126
128
  ref={panelRef}
127
- className={[
128
- "ui-modal",
129
- isSheet && "ui-sheet",
130
- className,
131
- ]
129
+ className={["ui-modal", isSheet && "ui-sheet", className]
132
130
  .filter(Boolean)
133
131
  .join(" ")}
134
132
  role="dialog"
@@ -29,7 +29,13 @@ export interface NewsCarouselProps {
29
29
  }
30
30
 
31
31
  const ChevronIcon = ({ direction }: { direction: "left" | "right" }) => (
32
- <svg width={18} height={18} viewBox="0 0 24 24" fill="none" aria-hidden="true">
32
+ <svg
33
+ width={18}
34
+ height={18}
35
+ viewBox="0 0 24 24"
36
+ fill="none"
37
+ aria-hidden="true"
38
+ >
33
39
  <path
34
40
  d={direction === "left" ? "M15 6l-6 6 6 6" : "M9 6l6 6-6 6"}
35
41
  stroke="currentColor"
@@ -129,7 +129,9 @@ export default function Select({
129
129
  const openList = useCallback(() => {
130
130
  if (disabled) return;
131
131
  setOpen(true);
132
- const current = selectable.findIndex((o) => o.value === value && !o.disabled);
132
+ const current = selectable.findIndex(
133
+ (o) => o.value === value && !o.disabled,
134
+ );
133
135
  const firstEnabled = selectable.findIndex((o) => !o.disabled);
134
136
  setActiveIndex(current >= 0 ? current : firstEnabled);
135
137
  }, [disabled, selectable, value]);
@@ -236,7 +238,11 @@ export default function Select({
236
238
 
237
239
  const describedBy = [errorId, hintId].filter(Boolean).join(" ") || undefined;
238
240
 
239
- const fieldClasses = ["ui-select", invalid && "ui-select--invalid", open && "is-open"]
241
+ const fieldClasses = [
242
+ "ui-select",
243
+ invalid && "ui-select--invalid",
244
+ open && "is-open",
245
+ ]
240
246
  .filter(Boolean)
241
247
  .join(" ");
242
248
 
@@ -71,20 +71,54 @@ export function Toast({
71
71
  >
72
72
  <span className="ui-toast__icon" aria-hidden="true">
73
73
  {tone === "success" ? (
74
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
74
+ <svg
75
+ width="18"
76
+ height="18"
77
+ viewBox="0 0 24 24"
78
+ fill="none"
79
+ stroke="currentColor"
80
+ strokeWidth="2.4"
81
+ strokeLinecap="round"
82
+ strokeLinejoin="round"
83
+ >
75
84
  <path d="M5 13l4 4L19 7" />
76
85
  </svg>
77
86
  ) : tone === "warning" ? (
78
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
87
+ <svg
88
+ width="18"
89
+ height="18"
90
+ viewBox="0 0 24 24"
91
+ fill="none"
92
+ stroke="currentColor"
93
+ strokeWidth="2"
94
+ strokeLinecap="round"
95
+ strokeLinejoin="round"
96
+ >
79
97
  <path d="M12 4l8.5 15h-17L12 4zM12 10v4M12 16.5v.5" />
80
98
  </svg>
81
99
  ) : tone === "info" ? (
82
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
100
+ <svg
101
+ width="18"
102
+ height="18"
103
+ viewBox="0 0 24 24"
104
+ fill="none"
105
+ stroke="currentColor"
106
+ strokeWidth="2"
107
+ strokeLinecap="round"
108
+ >
83
109
  <circle cx="12" cy="12" r="9" />
84
110
  <path d="M12 11v5M12 8v.5" />
85
111
  </svg>
86
112
  ) : (
87
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
113
+ <svg
114
+ width="18"
115
+ height="18"
116
+ viewBox="0 0 24 24"
117
+ fill="none"
118
+ stroke="currentColor"
119
+ strokeWidth="2"
120
+ strokeLinecap="round"
121
+ >
88
122
  <circle cx="12" cy="12" r="9" />
89
123
  <path d="M12 8v4.5M12 16v.5" />
90
124
  </svg>