@kwtv/components 0.9.0 → 0.10.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,15 +7,19 @@ 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, OrderStatus, OrderTimeline. |
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` | The one copy of the button and icon-button styles. `Button` and `IconButton` import it. React islands import it for the `ui-btn` / `ui-iconbtn` classes. |
16
- | `src/styles/navlink.css` | The one copy of the header nav link styles. `NavLink` and `NavDropdown` import it. |
17
- | `src/styles/feedback.css` | The one copy of the alert, empty state and skeleton styles. `Alert`, `EmptyState` and `Skeleton` import it. React islands import it for the `ui-alert`, `ui-empty` and `ui-skel` classes. |
18
- | `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, OrderStatus, OrderTimeline. |
14
+ | `src/styles/base.css` | The shared primitives the React islands import: focus ring, touch targets, screen-reader helper. Most Astro components carry their own scoped styles. |
15
+ | `src/styles/controls.css` | The one copy of the button and icon-button styles. `Button` and `IconButton` import it. React islands import it for the `ui-btn` / `ui-iconbtn` classes. |
16
+ | `src/styles/navlink.css` | The one copy of the header nav link styles. `NavLink` and `NavDropdown` import it. |
17
+ | `src/styles/feedback.css` | The one copy of the alert, empty state and skeleton styles. `Alert`, `EmptyState` and `Skeleton` import it. React islands import it for the `ui-alert`, `ui-empty` and `ui-skel` classes. |
18
+ | `src/styles/filters.css` | The one copy of the chip, swatch, checkbox, radio and active filter tag styles. `Chip`, `Swatch`, `Checkbox`, `Radio` and `ActiveFilterTags` import it. React islands import it for the `ui-chip`, `ui-swatch`, `ui-check` and `ui-filtertags` classes. |
19
+ | `src/styles/badge.css` | The one copy of the badge styles. `Badge` imports it. React islands import it for the `ui-badge` classes, also for the active filter tags. |
20
+ | `src/styles/pager.css` | The one copy of the pager styles. `Pager` imports it. React islands import it for the `ui-pager` classes. |
21
+ | `src/styles/product-card.css` | The one copy of the product card and ribbon styles. `ProductCard` imports it. React islands import it for the `ui-product` and `ui-ribbon` classes. |
22
+ | `src/styles/motion.css` | The `pl-` motion layer: lift, underline, arrow and row nudges, rise, marquee, chip rail and swipe carousel. |
19
23
 
20
24
  ## Install
21
25
 
@@ -64,16 +68,86 @@ import Select from "@kwtv/components/react/Select.tsx";
64
68
  />
65
69
  ```
66
70
 
67
- The React islands need their shared stylesheets once in the app. Import
68
- `controls.css` too if any React code renders the `ui-btn` / `ui-iconbtn`
69
- classes (Astro cannot pass its scoped styles into a React island):
71
+ The React islands need their shared stylesheets once in the app. Astro
72
+ cannot pass its scoped styles into a React island, so an island that writes
73
+ the markup of an Astro component imports that component's sheet:
70
74
 
71
75
  ```ts
72
76
  import "@kwtv/components/styles/base.css";
73
- import "@kwtv/components/styles/controls.css";
74
- import "@kwtv/components/styles/feedback.css";
77
+ import "@kwtv/components/styles/controls.css"; // ui-btn, ui-iconbtn
78
+ import "@kwtv/components/styles/feedback.css"; // ui-alert, ui-empty, ui-skel
79
+ import "@kwtv/components/styles/filters.css"; // ui-chip, ui-swatch, ui-check, ui-filtertags
80
+ import "@kwtv/components/styles/badge.css"; // ui-badge
81
+ import "@kwtv/components/styles/pager.css"; // ui-pager
82
+ import "@kwtv/components/styles/product-card.css"; // ui-product, ui-ribbon
75
83
  ```
76
84
 
85
+ The island writes the same markup as the Astro component, class for class.
86
+ The rules in these sheets are not scoped, so a consumer rule of equal
87
+ specificity in the `components` layer wins on source order.
88
+
89
+ ## Labels and other languages
90
+
91
+ The visible and screen-reader text inside the components defaults to Dutch.
92
+ Every such string has its own optional prop named `<thing>Label`. Leave it
93
+ out and the Dutch text renders exactly as before. Pass it to render another
94
+ language.
95
+
96
+ | Component | Prop | Default |
97
+ | ------------------ | ------------------------------------------------------------------------------- | ----------------------- |
98
+ | `ProductCard` | `newLabel` | `Nieuw` |
99
+ | | `promoLabel` | `Promo` |
100
+ | | `lastLabel` | `Laatste` |
101
+ | | `favLabel` | `Favoriet` |
102
+ | | `soldOutLabel` | `Uitverkocht` |
103
+ | `Pager` | `label` | `Paginering` |
104
+ | | `previousLabel` | `Vorige pagina` |
105
+ | | `nextLabel` | `Volgende pagina` |
106
+ | `Swatch` | `selectedLabel` | `geselecteerd` |
107
+ | `Filters` | `titleLabel` | `Filters` |
108
+ | | `clearLabel` | `Wissen` |
109
+ | `ActiveFilterTags` | `clearAllLabel` | `Alles wissen` |
110
+ | `Badge` | `removeLabel` | `Verwijderen` |
111
+ | `Breadcrumb` | `label` | `Kruimelpad` |
112
+ | `ChipRail` | `label` | `Categorieën` |
113
+ | `SwipeCarousel` | `label` | `Uitgelichte artikelen` |
114
+ | `CartLine` | `removeLabel` | `Verwijderen` |
115
+ | `CopyChip` | `label` | `Copy` |
116
+ | | `copiedLabel` | `Copied` |
117
+ | `Newsletter` | `label`, `placeholder`, `buttonText` | Dutch |
118
+ | `Social` | `label` | The platform name |
119
+ | `Drawer` | `label` | `Menu` |
120
+ | `Toast` | `closeLabel` | `Melding sluiten` |
121
+ | `Select` | `placeholder` | `Kies een optie` |
122
+ | `QuantityStepper` | `decrementLabel`, `incrementLabel`, `removeLabel`, `announce`, `announceRemove` | Dutch |
123
+ | `NewsCarousel` | `prevLabel`, `nextLabel`, `dotLabel` | Dutch |
124
+ | `OrderStatus` | `title`, `orderLabel` | Dutch |
125
+ | `OrderTimeline` | `stateWords` | Dutch |
126
+
127
+ `Swatch` adds `selectedLabel` to the colour name after a comma, so
128
+ `label="Sage" selectedLabel="selected"` is read as "Sage, selected". The
129
+ `ProductCard` screen-reader text is `soldOutLabel` followed by a full stop.
130
+ `AccountNav`, `AuthCard`, `StepIndicator`, `TextField`, `Totals` and `Modal`
131
+ take all their text from props or slots.
132
+
133
+ Older props that do not follow the `<thing>Label` name, such as `label`,
134
+ `buttonText` and `stateWords`, keep their names.
135
+
136
+ ## Toggle chips and plain buttons
137
+
138
+ `Chip` is a toggle by default and always carries `aria-pressed`, `"true"` or
139
+ `"false"` from `selected`. Pass `toggle={false}` for a chip that only opens or
140
+ runs something, such as the button that opens the filter sheet. It then has
141
+ no `aria-pressed`.
142
+
143
+ ## Drawer title and width
144
+
145
+ `title` shows an `h2` in the top row of the `Drawer`, left of the close button,
146
+ and names the drawer through `aria-labelledby` in place of `label`.
147
+ `size="full"` lets a `start` or `end` drawer fill the viewport width on
148
+ screens up to 700 px wide. Bottom drawers and the panel under the header are
149
+ full width already. Without these props the drawer renders as before.
150
+
77
151
  ## Cascade layers
78
152
 
79
153
  Every stylesheet in this package and in `@kwtv/tokens` sits in a cascade
@@ -88,7 +162,7 @@ layer, in one fixed order:
88
162
  | `reset` | Empty in the packages. The app's element defaults go here. |
89
163
  | `tokens` | The token custom properties, in `palette`, `theme` and `scale`. |
90
164
  | `base` | `base.css`: focus ring, touch targets, screen-reader helper. |
91
- | `components` | Astro component styles, React island CSS and `controls.css`. |
165
+ | `components` | Astro component styles, React island CSS and the shared sheets. |
92
166
  | `motion` | `motion.css`. Above components, because it adjusts their parts. |
93
167
  | `utilities` | `@kwtv/tokens/utilities.css`. |
94
168
 
@@ -114,8 +188,9 @@ statement or has a rule outside its layer.
114
188
  - Use the `.astro`, `.tsx` and `.css` extensions shown in these examples.
115
189
  - Import `@kwtv/components/styles/motion.css` when using the motion utilities.
116
190
  - Class names keep the `ui-` prefix from the component library so markup reads
117
- the same across both sites. Astro scopes them; the prefix is for readability
118
- and for consumer overrides.
191
+ the same across both sites. Astro scopes most of them; the shared sheets in
192
+ `src/styles/` are not scoped. The prefix is for readability and for consumer
193
+ overrides.
119
194
  - Colour, space, type, radius, shadow and motion all come from the token CSS
120
195
  variables. Components never hardcode a hex value.
121
196
  - Status is never colour alone. Every status pairs an icon with a word.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kwtv/components",
3
- "version": "0.9.0",
3
+ "version": "0.10.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.",
@@ -39,6 +39,6 @@
39
39
  }
40
40
  },
41
41
  "dependencies": {
42
- "@kwtv/tokens": "^0.9.0"
42
+ "@kwtv/tokens": "^0.10.0"
43
43
  }
44
44
  }
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  import Badge from "./Badge.astro";
3
+ import "../styles/filters.css";
3
4
 
4
5
  /**
5
6
  * The active filters repeated as removable tags above the grid, so the reader
@@ -7,6 +8,9 @@ import Badge from "./Badge.astro";
7
8
  * names the filter it clears. A trailing clear-all action removes them at once.
8
9
  *
9
10
  * The row wraps. The consumer wires each removeLabel to the matching filter.
11
+ *
12
+ * React and Preact islands write the same markup and import
13
+ * `@kwtv/components/styles/badge.css` and `@kwtv/components/styles/filters.css`.
10
14
  */
11
15
  interface Tag {
12
16
  label: string;
@@ -42,37 +46,3 @@ const classes = ["ui-filtertags", className].filter(Boolean).join(" ");
42
46
  </button>
43
47
  )}
44
48
  </div>
45
-
46
- <style>
47
- @layer reset, tokens, base, components, motion, utilities;
48
-
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
- }
77
- }
78
- </style>
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  import Icon from "./Icon.astro";
3
+ import "../styles/badge.css";
3
4
 
4
5
  /**
5
6
  * Badge. Status and stock. Each status tone carries an icon so the meaning
@@ -9,6 +10,9 @@ import Icon from "./Icon.astro";
9
10
  *
10
11
  * Stock badges should repeat the exact number in text rather than only shading
11
12
  * the pill.
13
+ *
14
+ * React and Preact islands write the same markup and import
15
+ * `@kwtv/components/styles/badge.css`.
12
16
  */
13
17
  interface Props {
14
18
  tone?: "neutral" | "success" | "warning" | "danger" | "outline";
@@ -60,87 +64,3 @@ const classes = [
60
64
  </button>
61
65
  )}
62
66
  </span>
63
-
64
- <style>
65
- @layer reset, tokens, base, components, motion, utilities;
66
-
67
- @layer components {
68
- .ui-badge {
69
- display: inline-flex;
70
- align-items: center;
71
- gap: var(--space-1-5);
72
- min-height: 24px;
73
- padding: 0 var(--space-2);
74
- border: 1px solid transparent;
75
- border-radius: var(--radius-full);
76
- font-family: var(--font-sans);
77
- font-size: var(--text-xs);
78
- font-weight: var(--weight-medium);
79
- line-height: 1;
80
- white-space: nowrap;
81
- }
82
-
83
- .ui-badge__icon {
84
- display: inline-flex;
85
- flex-shrink: 0;
86
- }
87
-
88
- .ui-badge--neutral {
89
- background: var(--surface-accent);
90
- color: var(--surface-accent-fg, var(--text));
91
- }
92
-
93
- .ui-badge--success {
94
- background: var(--success-bg);
95
- color: var(--success);
96
- }
97
-
98
- .ui-badge--warning {
99
- background: var(--warning-bg);
100
- color: var(--warning);
101
- }
102
-
103
- .ui-badge--danger {
104
- background: var(--danger-bg);
105
- color: var(--danger);
106
- }
107
-
108
- /* Outline keeps the strong border and full-strength text, so it reads */
109
- /* without relying on a fill. */
110
- .ui-badge--outline {
111
- background: transparent;
112
- border: 1px solid var(--border-strong);
113
- color: var(--text);
114
- }
115
-
116
- .ui-badge--removable {
117
- padding-right: var(--space-1);
118
- }
119
-
120
- /* Small × target grows with padding plus an equal negative margin, so the */
121
- /* hit area grows without moving the layout. */
122
- .ui-badge__x {
123
- display: inline-flex;
124
- align-items: center;
125
- justify-content: center;
126
- padding: var(--space-2);
127
- margin: calc(-1 * var(--space-2));
128
- margin-left: 0;
129
- background: transparent;
130
- border: 0;
131
- border-radius: var(--radius-sm);
132
- color: inherit;
133
- cursor: pointer;
134
- }
135
-
136
- .ui-badge__x:hover {
137
- color: var(--danger);
138
- }
139
-
140
- .ui-badge__x:focus-visible {
141
- outline: 2px solid var(--focus);
142
- outline-offset: 2px;
143
- box-shadow: 0 0 0 4px var(--focus-outer);
144
- }
145
- }
146
- </style>
@@ -1,4 +1,6 @@
1
1
  ---
2
+ import "../styles/filters.css";
3
+
2
4
  /**
3
5
  * Checkbox. Custom-styled native control: appearance:none box with a CSS
4
6
  * checkmark SVG, so the checked state is consistent across surfaces and themes.
@@ -7,6 +9,9 @@
7
9
  *
8
10
  * When several checkboxes form a question, put them in a <fieldset> with a
9
11
  * <legend> in the consuming page so the question is announced first.
12
+ *
13
+ * React and Preact islands write the same markup and import
14
+ * `@kwtv/components/styles/filters.css`.
10
15
  */
11
16
  interface Props {
12
17
  label?: string;
@@ -46,67 +51,3 @@ const classes = ["ui-check", disabled && "is-disabled", className]
46
51
  />
47
52
  <slot>{label}</slot>
48
53
  </label>
49
-
50
- <style>
51
- @layer reset, tokens, base, components, motion, utilities;
52
-
53
- @layer components {
54
- .ui-check {
55
- display: flex;
56
- align-items: flex-start;
57
- gap: var(--space-2);
58
- min-height: 32px;
59
- font-family: var(--font-sans);
60
- font-size: var(--text-sm);
61
- color: var(--text);
62
- cursor: pointer;
63
- }
64
-
65
- .ui-check input {
66
- appearance: none;
67
- -webkit-appearance: none;
68
- width: 17px;
69
- height: 17px;
70
- flex-shrink: 0;
71
- margin-top: calc((var(--leading-sm, 1.375rem) - 17px) / 2);
72
- border: 1.5px solid var(--border-strong);
73
- border-radius: var(--radius-xs, 3px);
74
- background: var(--surface);
75
- cursor: pointer;
76
- transition:
77
- background-color var(--duration-fast, 100ms) var(--ease-standard, ease),
78
- border-color var(--duration-fast, 100ms) var(--ease-standard, ease);
79
- }
80
-
81
- .ui-check input:checked {
82
- background: var(--primary);
83
- border-color: var(--primary);
84
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 10' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='1 5 4.5 8.5 11 1'/%3E%3C/svg%3E");
85
- background-repeat: no-repeat;
86
- background-position: center;
87
- background-size: 10px 8px;
88
- }
89
-
90
- .ui-check input:focus-visible {
91
- outline: 2px solid var(--focus);
92
- outline-offset: 2px;
93
- box-shadow: 0 0 0 4px var(--focus-outer);
94
- }
95
-
96
- .ui-check.is-disabled {
97
- cursor: not-allowed;
98
- color: var(--text-muted);
99
- }
100
-
101
- .ui-check.is-disabled input {
102
- cursor: not-allowed;
103
- opacity: 0.45;
104
- }
105
-
106
- @media (max-width: 700px), (pointer: coarse) {
107
- .ui-check {
108
- min-height: 40px;
109
- }
110
- }
111
- }
112
- </style>
@@ -1,4 +1,6 @@
1
1
  ---
2
+ import "../styles/filters.css";
3
+
2
4
  /**
3
5
  * Chip. Size selection and filter shortcuts. A <button> with aria-pressed, not
4
6
  * a link. Selection adds a 2 px border so it reads without colour. Sold-out
@@ -7,12 +9,22 @@
7
9
  *
8
10
  * The `--filter` variant is the 32 px shortcut that opens the filter sheet on
9
11
  * mobile.
12
+ *
13
+ * A chip is a toggle by default and always carries aria-pressed. Pass
14
+ * `toggle={false}` for a chip that is a plain button, such as the one that
15
+ * opens the filter sheet. It then has no aria-pressed, and `selected` only
16
+ * sets the look.
17
+ *
18
+ * React and Preact islands write the same markup and import
19
+ * `@kwtv/components/styles/filters.css`.
10
20
  */
11
21
  interface Props {
12
22
  variant?: "default" | "filter";
13
23
  selected?: boolean;
14
24
  disabled?: boolean;
15
25
  strikethrough?: boolean;
26
+ /** Set to false for a plain button. Drops aria-pressed. Defaults to true. */
27
+ toggle?: boolean;
16
28
  type?: "button" | "submit";
17
29
  class?: string;
18
30
  }
@@ -22,6 +34,7 @@ const {
22
34
  selected = false,
23
35
  disabled = false,
24
36
  strikethrough = false,
37
+ toggle = true,
25
38
  type = "button",
26
39
  class: className,
27
40
  ...rest
@@ -42,93 +55,9 @@ const classes = [
42
55
  <button
43
56
  class={classes}
44
57
  type={type}
45
- aria-pressed={String(selected)}
58
+ aria-pressed={toggle ? String(selected) : undefined}
46
59
  disabled={disabled}
47
60
  {...rest}
48
61
  >
49
62
  <slot />
50
63
  </button>
51
-
52
- <style>
53
- @layer reset, tokens, base, components, motion, utilities;
54
-
55
- @layer components {
56
- .ui-chip {
57
- display: inline-flex;
58
- align-items: center;
59
- justify-content: center;
60
- min-height: 36px;
61
- padding: 0 var(--space-4);
62
- background: var(--surface);
63
- border: 1px solid var(--border);
64
- border-radius: var(--radius-md);
65
- font-family: var(--font-sans);
66
- font-size: var(--text-sm);
67
- font-weight: var(--weight-medium);
68
- color: var(--text);
69
- cursor: pointer;
70
- transition:
71
- border-color var(--duration-fast) var(--ease-standard),
72
- background-color var(--duration-fast) var(--ease-standard);
73
- }
74
-
75
- .ui-chip:hover,
76
- .ui-chip.is-hover {
77
- border-color: var(--border-strong);
78
- }
79
-
80
- .ui-chip:focus-visible,
81
- .ui-chip.is-focus {
82
- outline: 2px solid var(--focus);
83
- outline-offset: 2px;
84
- box-shadow: 0 0 0 4px var(--focus-outer);
85
- }
86
-
87
- /* Selected: 2 px border plus the accent surface, readable without colour. */
88
- .ui-chip.is-selected,
89
- .ui-chip[aria-pressed="true"] {
90
- border-width: 2px;
91
- border-color: var(--border-strong);
92
- background: var(--surface-accent);
93
- color: var(--surface-accent-fg, var(--text));
94
- font-weight: var(--weight-semibold);
95
- }
96
-
97
- .ui-chip.is-disabled,
98
- .ui-chip:disabled {
99
- opacity: 0.5;
100
- cursor: not-allowed;
101
- }
102
-
103
- .ui-chip--struck {
104
- text-decoration: line-through;
105
- }
106
-
107
- /* Filter chip: 32 px. */
108
- .ui-chip--filter {
109
- min-height: 32px;
110
- border-radius: var(--radius-full);
111
- padding: 0 var(--space-3);
112
- }
113
-
114
- .ui-chip--filter.is-selected,
115
- .ui-chip--filter[aria-pressed="true"] {
116
- background: var(--surface-accent);
117
- }
118
-
119
- @media (prefers-reduced-motion: reduce) {
120
- .ui-chip {
121
- transition: none;
122
- }
123
- }
124
-
125
- @media (max-width: 700px), (pointer: coarse) {
126
- .ui-chip {
127
- min-height: 36px;
128
- }
129
- .ui-chip--filter {
130
- min-height: 40px;
131
- }
132
- }
133
- }
134
- </style>
@@ -7,19 +7,37 @@
7
7
  *
8
8
  * @prop {string} text The value to display and copy.
9
9
  * @prop {string} [label] Accessible label for the copy button (default: "Copy").
10
+ * @prop {string} [copiedLabel] Accessible label once copied (default: "Copied").
10
11
  * @prop {string} [class]
12
+ *
13
+ * The script reads both labels from `data-label` and `data-copied-label`.
14
+ * They are written only when they differ from the default, so markup with the
15
+ * defaults stays the same.
11
16
  */
12
17
  interface Props {
13
18
  text: string;
14
19
  label?: string;
20
+ copiedLabel?: string;
15
21
  class?: string;
16
22
  }
17
23
 
18
- const { text, label = "Copy", class: className, ...rest } = Astro.props;
24
+ const {
25
+ text,
26
+ label = "Copy",
27
+ copiedLabel = "Copied",
28
+ class: className,
29
+ ...rest
30
+ } = Astro.props;
19
31
  const classes = ["ui-copy-chip", className].filter(Boolean).join(" ");
20
32
  ---
21
33
 
22
- <span class={classes} data-copy={text} {...rest}>
34
+ <span
35
+ class={classes}
36
+ data-copy={text}
37
+ data-label={label !== "Copy" ? label : undefined}
38
+ data-copied-label={copiedLabel !== "Copied" ? copiedLabel : undefined}
39
+ {...rest}
40
+ >
23
41
  <span class="ui-copy-chip__text">{text}</span>
24
42
  </span>
25
43
 
@@ -29,6 +47,7 @@ const classes = ["ui-copy-chip", className].filter(Boolean).join(" ");
29
47
 
30
48
  const text = el.getAttribute("data-copy") ?? "";
31
49
  const label = el.getAttribute("data-label") ?? "Copy";
50
+ const copiedLabel = el.getAttribute("data-copied-label") ?? "Copied";
32
51
 
33
52
  const COPY_ICON = `<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="5" y="5" width="9" height="9" rx="1.5"/><path d="M3 11V3a1 1 0 0 1 1-1h8"/></svg>`;
34
53
  const CHECK_ICON = `<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="2 8 6 12 14 4"/></svg>`;
@@ -43,7 +62,7 @@ const classes = ["ui-copy-chip", className].filter(Boolean).join(" ");
43
62
  try {
44
63
  await navigator.clipboard.writeText(text);
45
64
  btn.innerHTML = CHECK_ICON;
46
- btn.setAttribute("aria-label", "Copied");
65
+ btn.setAttribute("aria-label", copiedLabel);
47
66
  setTimeout(() => {
48
67
  btn.innerHTML = COPY_ICON;
49
68
  btn.setAttribute("aria-label", label);
@@ -8,21 +8,30 @@
8
8
  * with variant="sheet" and place the FilterGroups inside it. The result count
9
9
  * should be announced in a polite live region by the consumer when filters
10
10
  * change.
11
+ *
12
+ * `titleLabel` is the bar's eyebrow. It defaults to "Filters".
11
13
  */
12
14
  interface Props {
13
15
  /** Text for the clear-all action. Omit to hide it. */
14
16
  clearLabel?: string;
17
+ /** The eyebrow in the bar. Defaults to "Filters". */
18
+ titleLabel?: string;
15
19
  class?: string;
16
20
  }
17
21
 
18
- const { clearLabel = "Wissen", class: className, ...rest } = Astro.props;
22
+ const {
23
+ clearLabel = "Wissen",
24
+ titleLabel = "Filters",
25
+ class: className,
26
+ ...rest
27
+ } = Astro.props;
19
28
 
20
29
  const classes = ["ui-filters", className].filter(Boolean).join(" ");
21
30
  ---
22
31
 
23
32
  <aside class={classes} {...rest}>
24
33
  <div class="ui-filterbar">
25
- <span class="ui-eyebrow">Filters</span>
34
+ <span class="ui-eyebrow">{titleLabel}</span>
26
35
  {clearLabel && (
27
36
  <slot name="clear">
28
37
  <button class="ui-filters__clear" type="button">