@kwtv/components 0.2.0 → 0.4.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.
Files changed (56) hide show
  1. package/README.md +52 -9
  2. package/package.json +2 -2
  3. package/src/astro/Accordion.astro +10 -6
  4. package/src/astro/AccordionItem.astro +55 -45
  5. package/src/astro/ActiveFilterTags.astro +41 -28
  6. package/src/astro/Alert.astro +45 -41
  7. package/src/astro/Badge.astro +80 -76
  8. package/src/astro/Band.astro +104 -58
  9. package/src/astro/Breadcrumb.astro +38 -30
  10. package/src/astro/Button.astro +135 -129
  11. package/src/astro/Card.astro +72 -62
  12. package/src/astro/CartLine.astro +72 -58
  13. package/src/astro/Checkbox.astro +36 -32
  14. package/src/astro/Chip.astro +68 -64
  15. package/src/astro/ChipRail.astro +6 -1
  16. package/src/astro/DotGrid.astro +55 -42
  17. package/src/astro/EmptyState.astro +29 -23
  18. package/src/astro/FilterGroup.astro +29 -26
  19. package/src/astro/Filters.astro +57 -47
  20. package/src/astro/Footer.astro +195 -178
  21. package/src/astro/FooterColumn.astro +39 -35
  22. package/src/astro/Header.astro +80 -67
  23. package/src/astro/Icon.astro +4 -2
  24. package/src/astro/IconButton.astro +67 -61
  25. package/src/astro/Marquee.astro +43 -41
  26. package/src/astro/NavDropdown.astro +169 -149
  27. package/src/astro/NavLink.astro +59 -56
  28. package/src/astro/Newsletter.astro +64 -56
  29. package/src/astro/Pager.astro +71 -46
  30. package/src/astro/PickCard.astro +72 -68
  31. package/src/astro/ProductCard.astro +134 -119
  32. package/src/astro/Radio.astro +36 -32
  33. package/src/astro/Skeleton.astro +60 -42
  34. package/src/astro/Social.astro +26 -21
  35. package/src/astro/StepIndicator.astro +70 -54
  36. package/src/astro/Sticker.astro +74 -49
  37. package/src/astro/Swatch.astro +44 -40
  38. package/src/astro/SwipeCarousel.astro +5 -1
  39. package/src/astro/Table.astro +66 -56
  40. package/src/astro/TableCard.astro +45 -37
  41. package/src/astro/TextField.astro +104 -91
  42. package/src/astro/Totals.astro +39 -35
  43. package/src/react/Drawer.css +60 -56
  44. package/src/react/Drawer.tsx +1 -5
  45. package/src/react/Modal.css +97 -93
  46. package/src/react/Modal.tsx +4 -6
  47. package/src/react/NewsCarousel.css +108 -104
  48. package/src/react/NewsCarousel.tsx +7 -1
  49. package/src/react/QuantityStepper.css +84 -80
  50. package/src/react/Select.css +159 -155
  51. package/src/react/Select.tsx +8 -2
  52. package/src/react/Toast.css +128 -124
  53. package/src/react/Toast.tsx +38 -4
  54. package/src/styles/base.css +30 -26
  55. package/src/styles/controls.css +183 -179
  56. package/src/styles/motion.css +203 -199
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
@@ -63,6 +71,41 @@ import "@kwtv/components/styles/base.css";
63
71
  import "@kwtv/components/styles/controls.css";
64
72
  ```
65
73
 
74
+ ## Cascade layers
75
+
76
+ Every stylesheet in this package and in `@kwtv/tokens` sits in a cascade
77
+ layer, in one fixed order:
78
+
79
+ ```css
80
+ @layer reset, tokens, base, components, motion, utilities;
81
+ ```
82
+
83
+ | Layer | Holds |
84
+ | ------------ | --------------------------------------------------------------- |
85
+ | `reset` | Empty in the packages. The app's element defaults go here. |
86
+ | `tokens` | The token custom properties, in `palette`, `theme` and `scale`. |
87
+ | `base` | `base.css`: focus ring, touch targets, screen-reader helper. |
88
+ | `components` | Astro component styles, React island CSS and `controls.css`. |
89
+ | `motion` | `motion.css`. Above components, because it adjusts their parts. |
90
+ | `utilities` | `@kwtv/tokens/utilities.css`. |
91
+
92
+ Every package stylesheet opens with that order statement. The first one to
93
+ load fixes the order, so it holds whatever order the bundler emits files in.
94
+
95
+ In the app:
96
+
97
+ - Put element defaults (`*`, `html`, `body`, `a`, `button`) in
98
+ `@layer reset { }`. Left unlayered, a bare `a { color }` beats the footer
99
+ and button link colours.
100
+ - Leave everything else unlayered. Unlayered CSS beats every layer, so a
101
+ single class overrides any component rule. Do not raise specificity with
102
+ ids or repeated classes.
103
+ - `!important` flips inside layers: an important declaration in a lower layer
104
+ wins. The package uses it only to stop animation under reduced motion.
105
+
106
+ `npm run layers:check` fails when a package stylesheet is missing the order
107
+ statement or has a rule outside its layer.
108
+
66
109
  ## Conventions
67
110
 
68
111
  - Use the `.astro`, `.tsx` and `.css` extensions shown in these examples.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kwtv/components",
3
- "version": "0.2.0",
3
+ "version": "0.4.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.4.0"
38
38
  }
39
39
  }
@@ -15,11 +15,15 @@ const { class: className, ...rest } = Astro.props;
15
15
  </div>
16
16
 
17
17
  <style>
18
- .ui-accordion {
19
- display: flex;
20
- flex-direction: column;
21
- border: 1px solid var(--border);
22
- border-radius: var(--radius-lg);
23
- overflow: hidden;
18
+ @layer reset, tokens, base, components, motion, utilities;
19
+
20
+ @layer components {
21
+ .ui-accordion {
22
+ display: flex;
23
+ flex-direction: column;
24
+ border: 1px solid var(--border);
25
+ border-radius: var(--radius-lg);
26
+ overflow: hidden;
27
+ }
24
28
  }
25
29
  </style>
@@ -14,64 +14,74 @@ 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>
28
- .ui-accordion__item + .ui-accordion__item {
29
- border-top: 1px solid var(--border);
30
- }
31
-
32
- .ui-accordion__sum {
33
- display: flex;
34
- align-items: center;
35
- justify-content: space-between;
36
- gap: var(--space-3);
37
- min-height: 44px;
38
- padding: var(--space-2) var(--space-4);
39
- font-size: var(--text-sm);
40
- font-weight: var(--weight-medium);
41
- color: var(--text);
42
- cursor: pointer;
43
- list-style: none;
44
- }
45
- .ui-accordion__sum::-webkit-details-marker {
46
- display: none;
47
- }
48
- .ui-accordion__sum:hover {
49
- background: var(--surface-hover);
50
- }
51
- .ui-accordion__sum:focus-visible {
52
- outline: 2px solid var(--focus);
53
- outline-offset: -2px;
54
- }
34
+ @layer reset, tokens, base, components, motion, utilities;
55
35
 
56
- .ui-accordion__chev {
57
- display: inline-flex;
58
- color: var(--text-muted);
59
- transition: transform var(--duration-fast) var(--ease-standard);
60
- }
61
- .ui-accordion__item[open] .ui-accordion__chev {
62
- transform: rotate(180deg);
63
- }
36
+ @layer components {
37
+ .ui-accordion__item + .ui-accordion__item {
38
+ border-top: 1px solid var(--border);
39
+ }
64
40
 
65
- .ui-accordion__body {
66
- padding: 0 var(--space-4) var(--space-4);
67
- font-size: var(--text-sm);
68
- line-height: var(--leading-sm);
69
- color: var(--text-muted);
70
- }
41
+ .ui-accordion__sum {
42
+ display: flex;
43
+ align-items: center;
44
+ justify-content: space-between;
45
+ gap: var(--space-3);
46
+ min-height: 44px;
47
+ padding: var(--space-2) var(--space-4);
48
+ font-size: var(--text-sm);
49
+ font-weight: var(--weight-medium);
50
+ color: var(--text);
51
+ cursor: pointer;
52
+ list-style: none;
53
+ }
54
+ .ui-accordion__sum::-webkit-details-marker {
55
+ display: none;
56
+ }
57
+ .ui-accordion__sum:hover {
58
+ background: var(--surface-hover);
59
+ }
60
+ .ui-accordion__sum:focus-visible {
61
+ outline: 2px solid var(--focus);
62
+ outline-offset: -2px;
63
+ }
71
64
 
72
- @media (prefers-reduced-motion: reduce) {
73
65
  .ui-accordion__chev {
74
- transition: none;
66
+ display: inline-flex;
67
+ color: var(--text-muted);
68
+ transition: transform var(--duration-fast) var(--ease-standard);
69
+ }
70
+ .ui-accordion__item[open] .ui-accordion__chev {
71
+ transform: rotate(180deg);
72
+ }
73
+
74
+ .ui-accordion__body {
75
+ padding: 0 var(--space-4) var(--space-4);
76
+ font-size: var(--text-sm);
77
+ line-height: var(--leading-sm);
78
+ color: var(--text-muted);
79
+ }
80
+
81
+ @media (prefers-reduced-motion: reduce) {
82
+ .ui-accordion__chev {
83
+ transition: none;
84
+ }
75
85
  }
76
86
  }
77
87
  </style>
@@ -20,46 +20,59 @@ 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
 
37
46
  <style>
38
- .ui-filtertags {
39
- display: flex;
40
- flex-wrap: wrap;
41
- align-items: center;
42
- gap: var(--space-2);
43
- }
47
+ @layer reset, tokens, base, components, motion, utilities;
44
48
 
45
- .ui-filtertags__clear {
46
- padding: var(--space-1-5) var(--space-2);
47
- background: transparent;
48
- border: 0;
49
- border-radius: var(--radius-sm);
50
- font-family: var(--font-sans);
51
- font-size: var(--text-xs);
52
- font-weight: var(--weight-semibold);
53
- color: var(--link);
54
- cursor: pointer;
55
- }
56
- .ui-filtertags__clear:hover {
57
- background: var(--surface-hover);
58
- color: var(--link-hover);
59
- }
60
- .ui-filtertags__clear:focus-visible {
61
- outline: 2px solid var(--focus);
62
- outline-offset: 2px;
63
- box-shadow: 0 0 0 4px var(--focus-outer);
49
+ @layer components {
50
+ .ui-filtertags {
51
+ display: flex;
52
+ flex-wrap: wrap;
53
+ align-items: center;
54
+ gap: var(--space-2);
55
+ }
56
+
57
+ .ui-filtertags__clear {
58
+ padding: var(--space-1-5) var(--space-2);
59
+ background: transparent;
60
+ border: 0;
61
+ border-radius: var(--radius-sm);
62
+ font-family: var(--font-sans);
63
+ font-size: var(--text-xs);
64
+ font-weight: var(--weight-semibold);
65
+ color: var(--link);
66
+ cursor: pointer;
67
+ }
68
+ .ui-filtertags__clear:hover {
69
+ background: var(--surface-hover);
70
+ color: var(--link-hover);
71
+ }
72
+ .ui-filtertags__clear:focus-visible {
73
+ outline: 2px solid var(--focus);
74
+ outline-offset: 2px;
75
+ box-shadow: 0 0 0 4px var(--focus-outer);
76
+ }
64
77
  }
65
78
  </style>
@@ -41,53 +41,57 @@ const classes = ["ui-alert", `ui-alert--${tone}`, className]
41
41
  </div>
42
42
 
43
43
  <style>
44
- .ui-alert {
45
- display: flex;
46
- align-items: flex-start;
47
- gap: var(--space-2-5, 0.625rem);
48
- padding: var(--space-3) var(--space-4);
49
- border-left: 3px solid currentColor;
50
- border-radius: var(--radius-md);
51
- font-family: var(--font-sans);
52
- font-size: var(--text-sm);
53
- line-height: var(--leading-sm);
54
- }
44
+ @layer reset, tokens, base, components, motion, utilities;
55
45
 
56
- .ui-alert__icon {
57
- display: inline-flex;
58
- flex-shrink: 0;
59
- margin-top: 1px;
60
- }
46
+ @layer components {
47
+ .ui-alert {
48
+ display: flex;
49
+ align-items: flex-start;
50
+ gap: var(--space-2-5, 0.625rem);
51
+ padding: var(--space-3) var(--space-4);
52
+ border-left: 3px solid currentColor;
53
+ border-radius: var(--radius-md);
54
+ font-family: var(--font-sans);
55
+ font-size: var(--text-sm);
56
+ line-height: var(--leading-sm);
57
+ }
61
58
 
62
- /* Body keeps full strength; the left rule carries the tone. */
63
- .ui-alert__body {
64
- color: var(--text);
65
- display: flex;
66
- flex-direction: column;
67
- gap: var(--space-1);
68
- }
59
+ .ui-alert__icon {
60
+ display: inline-flex;
61
+ flex-shrink: 0;
62
+ margin-top: 1px;
63
+ }
69
64
 
70
- .ui-alert__title {
71
- font-weight: var(--weight-semibold);
72
- }
65
+ /* Body keeps full strength; the left rule carries the tone. */
66
+ .ui-alert__body {
67
+ color: var(--text);
68
+ display: flex;
69
+ flex-direction: column;
70
+ gap: var(--space-1);
71
+ }
73
72
 
74
- .ui-alert--info {
75
- background: var(--surface-accent);
76
- color: var(--surface-accent-fg, var(--primary));
77
- }
73
+ .ui-alert__title {
74
+ font-weight: var(--weight-semibold);
75
+ }
78
76
 
79
- .ui-alert--success {
80
- background: var(--success-bg);
81
- color: var(--success);
82
- }
77
+ .ui-alert--info {
78
+ background: var(--surface-accent);
79
+ color: var(--surface-accent-fg, var(--primary));
80
+ }
83
81
 
84
- .ui-alert--warning {
85
- background: var(--warning-bg);
86
- color: var(--warning);
87
- }
82
+ .ui-alert--success {
83
+ background: var(--success-bg);
84
+ color: var(--success);
85
+ }
86
+
87
+ .ui-alert--warning {
88
+ background: var(--warning-bg);
89
+ color: var(--warning);
90
+ }
88
91
 
89
- .ui-alert--danger {
90
- background: var(--danger-bg);
91
- color: var(--danger);
92
+ .ui-alert--danger {
93
+ background: var(--danger-bg);
94
+ color: var(--danger);
95
+ }
92
96
  }
93
97
  </style>
@@ -58,81 +58,85 @@ const classes = [
58
58
  </span>
59
59
 
60
60
  <style>
61
- .ui-badge {
62
- display: inline-flex;
63
- align-items: center;
64
- gap: var(--space-1-5);
65
- min-height: 24px;
66
- padding: 0 var(--space-2);
67
- border: 1px solid transparent;
68
- border-radius: var(--radius-full);
69
- font-family: var(--font-sans);
70
- font-size: var(--text-xs);
71
- font-weight: var(--weight-medium);
72
- line-height: 1;
73
- white-space: nowrap;
74
- }
75
-
76
- .ui-badge__icon {
77
- display: inline-flex;
78
- flex-shrink: 0;
79
- }
80
-
81
- .ui-badge--neutral {
82
- background: var(--surface-accent);
83
- color: var(--surface-accent-fg, var(--text));
84
- }
85
-
86
- .ui-badge--success {
87
- background: var(--success-bg);
88
- color: var(--success);
89
- }
90
-
91
- .ui-badge--warning {
92
- background: var(--warning-bg);
93
- color: var(--warning);
94
- }
95
-
96
- .ui-badge--danger {
97
- background: var(--danger-bg);
98
- color: var(--danger);
99
- }
100
-
101
- /* Outline keeps the strong border and full-strength text, so it reads
102
- without relying on a fill. */
103
- .ui-badge--outline {
104
- background: transparent;
105
- border: 1px solid var(--border-strong);
106
- color: var(--text);
107
- }
108
-
109
- .ui-badge--removable {
110
- padding-right: var(--space-1);
111
- }
112
-
113
- /* Small × target grows with padding plus an equal negative margin, so the
114
- hit area grows without moving the layout. */
115
- .ui-badge__x {
116
- display: inline-flex;
117
- align-items: center;
118
- justify-content: center;
119
- padding: var(--space-2);
120
- margin: calc(-1 * var(--space-2));
121
- margin-left: 0;
122
- background: transparent;
123
- border: 0;
124
- border-radius: var(--radius-sm);
125
- color: inherit;
126
- cursor: pointer;
127
- }
128
-
129
- .ui-badge__x:hover {
130
- color: var(--danger);
131
- }
132
-
133
- .ui-badge__x:focus-visible {
134
- outline: 2px solid var(--focus);
135
- outline-offset: 2px;
136
- box-shadow: 0 0 0 4px var(--focus-outer);
61
+ @layer reset, tokens, base, components, motion, utilities;
62
+
63
+ @layer components {
64
+ .ui-badge {
65
+ display: inline-flex;
66
+ align-items: center;
67
+ gap: var(--space-1-5);
68
+ min-height: 24px;
69
+ padding: 0 var(--space-2);
70
+ border: 1px solid transparent;
71
+ border-radius: var(--radius-full);
72
+ font-family: var(--font-sans);
73
+ font-size: var(--text-xs);
74
+ font-weight: var(--weight-medium);
75
+ line-height: 1;
76
+ white-space: nowrap;
77
+ }
78
+
79
+ .ui-badge__icon {
80
+ display: inline-flex;
81
+ flex-shrink: 0;
82
+ }
83
+
84
+ .ui-badge--neutral {
85
+ background: var(--surface-accent);
86
+ color: var(--surface-accent-fg, var(--text));
87
+ }
88
+
89
+ .ui-badge--success {
90
+ background: var(--success-bg);
91
+ color: var(--success);
92
+ }
93
+
94
+ .ui-badge--warning {
95
+ background: var(--warning-bg);
96
+ color: var(--warning);
97
+ }
98
+
99
+ .ui-badge--danger {
100
+ background: var(--danger-bg);
101
+ color: var(--danger);
102
+ }
103
+
104
+ /* Outline keeps the strong border and full-strength text, so it reads */
105
+ /* without relying on a fill. */
106
+ .ui-badge--outline {
107
+ background: transparent;
108
+ border: 1px solid var(--border-strong);
109
+ color: var(--text);
110
+ }
111
+
112
+ .ui-badge--removable {
113
+ padding-right: var(--space-1);
114
+ }
115
+
116
+ /* Small × target grows with padding plus an equal negative margin, so the */
117
+ /* hit area grows without moving the layout. */
118
+ .ui-badge__x {
119
+ display: inline-flex;
120
+ align-items: center;
121
+ justify-content: center;
122
+ padding: var(--space-2);
123
+ margin: calc(-1 * var(--space-2));
124
+ margin-left: 0;
125
+ background: transparent;
126
+ border: 0;
127
+ border-radius: var(--radius-sm);
128
+ color: inherit;
129
+ cursor: pointer;
130
+ }
131
+
132
+ .ui-badge__x:hover {
133
+ color: var(--danger);
134
+ }
135
+
136
+ .ui-badge__x:focus-visible {
137
+ outline: 2px solid var(--focus);
138
+ outline-offset: 2px;
139
+ box-shadow: 0 0 0 4px var(--focus-outer);
140
+ }
137
141
  }
138
142
  </style>