@kwtv/components 0.3.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 (49) hide show
  1. package/README.md +35 -0
  2. package/package.json +2 -2
  3. package/src/astro/Accordion.astro +10 -6
  4. package/src/astro/AccordionItem.astro +47 -43
  5. package/src/astro/ActiveFilterTags.astro +29 -25
  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 +33 -29
  10. package/src/astro/Button.astro +132 -128
  11. package/src/astro/Card.astro +64 -60
  12. package/src/astro/CartLine.astro +59 -55
  13. package/src/astro/Checkbox.astro +36 -32
  14. package/src/astro/Chip.astro +68 -64
  15. package/src/astro/DotGrid.astro +50 -46
  16. package/src/astro/EmptyState.astro +26 -22
  17. package/src/astro/FilterGroup.astro +29 -25
  18. package/src/astro/Filters.astro +50 -46
  19. package/src/astro/Footer.astro +179 -175
  20. package/src/astro/FooterColumn.astro +39 -35
  21. package/src/astro/Header.astro +67 -63
  22. package/src/astro/Icon.astro +1 -1
  23. package/src/astro/IconButton.astro +64 -60
  24. package/src/astro/Marquee.astro +33 -29
  25. package/src/astro/NavDropdown.astro +120 -116
  26. package/src/astro/NavLink.astro +59 -55
  27. package/src/astro/Newsletter.astro +58 -54
  28. package/src/astro/Pager.astro +43 -39
  29. package/src/astro/PickCard.astro +72 -68
  30. package/src/astro/ProductCard.astro +120 -116
  31. package/src/astro/Radio.astro +36 -32
  32. package/src/astro/Skeleton.astro +43 -39
  33. package/src/astro/Social.astro +26 -22
  34. package/src/astro/StepIndicator.astro +55 -51
  35. package/src/astro/Sticker.astro +50 -46
  36. package/src/astro/Swatch.astro +44 -40
  37. package/src/astro/Table.astro +58 -54
  38. package/src/astro/TableCard.astro +39 -35
  39. package/src/astro/TextField.astro +92 -88
  40. package/src/astro/Totals.astro +39 -35
  41. package/src/react/Drawer.css +60 -56
  42. package/src/react/Modal.css +97 -93
  43. package/src/react/NewsCarousel.css +108 -104
  44. package/src/react/QuantityStepper.css +84 -80
  45. package/src/react/Select.css +159 -155
  46. package/src/react/Toast.css +128 -124
  47. package/src/styles/base.css +30 -26
  48. package/src/styles/controls.css +183 -179
  49. package/src/styles/motion.css +203 -199
@@ -10,20 +10,35 @@
10
10
  * Cards placed on the band move to a lighter tint of the band's own surface,
11
11
  * so they read as part of the band rather than a return to plain paper.
12
12
  *
13
+ * Product cards keep their own paper surface and text roles. Their new and
14
+ * favourite ribbons are sage by default, so on the band they move to
15
+ * charcoal and ivory. Promo stays burgundy, so all three stay distinct.
16
+ *
13
17
  * Corner radius matches every other surface in the system (Card, PickCard,
14
18
  * NavDropdown's panel): --radius-lg. A band that sits inset in a page, next
15
19
  * to a rounded card, needs the same rounding or the two read as unrelated.
20
+ * A band that runs edge to edge sets `bleed`. It drops the radius and the
21
+ * side borders, which would only show as clipped corners at the viewport.
16
22
  */
17
23
  interface Props {
18
24
  /** Renders as <section> by default. Use "div" when the band is not a
19
25
  * standalone region of the page. */
20
26
  as?: "section" | "div" | "aside";
27
+ /** Runs edge to edge: no corner radius and no side borders. */
28
+ bleed?: boolean;
21
29
  class?: string;
22
30
  }
23
31
 
24
- const { as: Tag = "section", class: className, ...rest } = Astro.props;
32
+ const {
33
+ as: Tag = "section",
34
+ bleed = false,
35
+ class: className,
36
+ ...rest
37
+ } = Astro.props;
25
38
 
26
- const classes = ["ui-band", className].filter(Boolean).join(" ");
39
+ const classes = ["ui-band", bleed && "ui-band--bleed", className]
40
+ .filter(Boolean)
41
+ .join(" ");
27
42
  ---
28
43
 
29
44
  <Tag class={classes} {...rest}>
@@ -31,67 +46,98 @@ const classes = ["ui-band", className].filter(Boolean).join(" ");
31
46
  </Tag>
32
47
 
33
48
  <style>
34
- .ui-band {
35
- background: var(--surface-accent);
36
- border: 1px solid var(--border-strong);
37
- border-radius: var(--radius-lg);
38
- color: var(--surface-accent-fg);
39
- }
49
+ @layer reset, tokens, base, components, motion, utilities;
40
50
 
41
- /* Every text role inside the band moves to the accent foreground. Sage is a
42
- mid tone: --text clears it, but --text-muted and --link do not. */
43
- .ui-band :global(p),
44
- .ui-band :global(span),
45
- .ui-band :global(a),
46
- .ui-band :global(td),
47
- .ui-band :global(th),
48
- .ui-band :global(strong),
49
- .ui-band :global(b),
50
- .ui-band :global(label),
51
- .ui-band :global(.ui-eyebrow),
52
- .ui-band :global(.ui-card__eyebrow),
53
- .ui-band :global(.ui-card__body) {
54
- color: var(--surface-accent-fg);
55
- }
51
+ @layer components {
52
+ .ui-band {
53
+ background: var(--surface-accent);
54
+ border: 1px solid var(--border-strong);
55
+ border-radius: var(--radius-lg);
56
+ color: var(--surface-accent-fg);
57
+ }
56
58
 
57
- /* Secondary copy gets the band's own muted role, which clears 4.5 on sage.
58
- --text-muted only reaches 3.6 here. */
59
- .ui-band :global(small),
60
- .ui-band :global(.ui-band__note) {
61
- color: var(--surface-accent-muted);
62
- }
59
+ .ui-band--bleed {
60
+ border-inline: 0;
61
+ border-radius: 0;
62
+ }
63
63
 
64
- /* A card on the band moves to a lighter tint of the band's own surface,
65
- rather than to plain paper, so it still reads as part of the band. It
66
- carries its own foreground role: --surface-accent-fg is tuned for the
67
- band's own, darker/mid tint and is not guaranteed to clear AA on this
68
- lighter one. */
69
- .ui-band :global(.ui-card),
70
- .ui-band :global(.ui-card--raised) {
71
- background: var(--surface-accent-subtle);
72
- border-color: var(--border-strong);
73
- }
74
- .ui-band :global(.ui-card__eyebrow),
75
- .ui-band :global(.ui-card__title),
76
- .ui-band :global(.ui-card__body) {
77
- color: var(--surface-accent-subtle-fg);
78
- }
64
+ /* Every text role inside the band moves to the accent foreground. Sage is a */
65
+ /* mid tone: --text clears it, but --text-muted and --link do not. */
66
+ .ui-band :global(p),
67
+ .ui-band :global(span),
68
+ .ui-band :global(a),
69
+ .ui-band :global(td),
70
+ .ui-band :global(th),
71
+ .ui-band :global(strong),
72
+ .ui-band :global(b),
73
+ .ui-band :global(label),
74
+ .ui-band :global(.ui-eyebrow),
75
+ .ui-band :global(.ui-card__eyebrow),
76
+ .ui-band :global(.ui-card__body) {
77
+ color: var(--surface-accent-fg);
78
+ }
79
79
 
80
- /* Buttons keep their own label colours: they carry their own fill, so the
81
- band's foreground flip must not reach inside them. */
82
- .ui-band :global(.ui-btn--primary),
83
- .ui-band :global(.ui-btn--primary span) {
84
- color: var(--primary-fg);
85
- }
80
+ /* Secondary copy gets the band's own muted role, which clears 4.5 on sage. */
81
+ /* --text-muted only reaches 3.6 here. */
82
+ .ui-band :global(small),
83
+ .ui-band :global(.ui-band__note) {
84
+ color: var(--surface-accent-muted);
85
+ }
86
86
 
87
- .ui-band :global(.ui-btn--accent),
88
- .ui-band :global(.ui-btn--accent span) {
89
- color: var(--accent-fg);
90
- }
87
+ /* A card on the band moves to a lighter tint of the band's own surface, */
88
+ /* rather than to plain paper, so it still reads as part of the band. It */
89
+ /* carries its own foreground role: --surface-accent-fg is tuned for the */
90
+ /* band's own, darker/mid tint and is not guaranteed to clear AA on this */
91
+ /* lighter one. */
92
+ .ui-band :global(.ui-card),
93
+ .ui-band :global(.ui-card--raised) {
94
+ background: var(--surface-accent-subtle);
95
+ border-color: var(--border-strong);
96
+ }
97
+ .ui-band :global(.ui-card__eyebrow),
98
+ .ui-band :global(.ui-card__title),
99
+ .ui-band :global(.ui-card__body) {
100
+ color: var(--surface-accent-subtle-fg);
101
+ }
102
+
103
+ /* A product card keeps its own paper surface, so its text keeps the plain */
104
+ /* roles. The span flip above would otherwise reach its title, meta and */
105
+ /* price. Sold-out and old-price styles in ProductCard still outrank these. */
106
+ .ui-band :global(.ui-product__title),
107
+ .ui-band :global(.ui-product__price) {
108
+ color: var(--text);
109
+ }
110
+ .ui-band :global(.ui-product__meta) {
111
+ color: var(--text-muted);
112
+ }
113
+
114
+ /* The new and favourite ribbons are sage by default and vanish on the */
115
+ /* band. The extra .ui-product outranks ProductCard's own ribbon rules. */
116
+ .ui-band :global(.ui-product .ui-ribbon--new i) {
117
+ background: var(--text);
118
+ color: var(--text-inverse);
119
+ }
120
+ .ui-band :global(.ui-product .ui-ribbon--fav i) {
121
+ background: var(--surface);
122
+ color: var(--text);
123
+ }
124
+
125
+ /* Buttons keep their own label colours: they carry their own fill, so the */
126
+ /* band's foreground flip must not reach inside them. */
127
+ .ui-band :global(.ui-btn--primary),
128
+ .ui-band :global(.ui-btn--primary span) {
129
+ color: var(--primary-fg);
130
+ }
131
+
132
+ .ui-band :global(.ui-btn--accent),
133
+ .ui-band :global(.ui-btn--accent span) {
134
+ color: var(--accent-fg);
135
+ }
91
136
 
92
- .ui-band :global(.ui-btn--secondary),
93
- .ui-band :global(.ui-btn--secondary span) {
94
- color: var(--surface-accent-fg);
95
- border-color: var(--border-strong);
137
+ .ui-band :global(.ui-btn--secondary),
138
+ .ui-band :global(.ui-btn--secondary span) {
139
+ color: var(--surface-accent-fg);
140
+ border-color: var(--border-strong);
141
+ }
96
142
  }
97
143
  </style>
@@ -42,37 +42,41 @@ const lastIndex = items.length - 1;
42
42
  </nav>
43
43
 
44
44
  <style>
45
- .ui-crumbs {
46
- display: flex;
47
- flex-wrap: wrap;
48
- align-items: center;
49
- gap: var(--space-1);
50
- font-size: var(--text-xs);
51
- line-height: var(--leading-xs);
52
- color: var(--text-muted);
53
- }
45
+ @layer reset, tokens, base, components, motion, utilities;
54
46
 
55
- .ui-crumbs a {
56
- color: var(--link);
57
- text-decoration: none;
58
- }
59
- .ui-crumbs a:hover {
60
- color: var(--link-hover);
61
- text-decoration: underline;
62
- }
63
- .ui-crumbs a:focus-visible {
64
- outline: 2px solid var(--focus);
65
- outline-offset: 2px;
66
- border-radius: var(--radius-sm);
67
- }
47
+ @layer components {
48
+ .ui-crumbs {
49
+ display: flex;
50
+ flex-wrap: wrap;
51
+ align-items: center;
52
+ gap: var(--space-1);
53
+ font-size: var(--text-xs);
54
+ line-height: var(--leading-xs);
55
+ color: var(--text-muted);
56
+ }
68
57
 
69
- .ui-crumbs [aria-current="page"] {
70
- color: var(--text);
71
- font-weight: var(--weight-medium);
72
- }
58
+ .ui-crumbs a {
59
+ color: var(--link);
60
+ text-decoration: none;
61
+ }
62
+ .ui-crumbs a:hover {
63
+ color: var(--link-hover);
64
+ text-decoration: underline;
65
+ }
66
+ .ui-crumbs a:focus-visible {
67
+ outline: 2px solid var(--focus);
68
+ outline-offset: 2px;
69
+ border-radius: var(--radius-sm);
70
+ }
71
+
72
+ .ui-crumbs [aria-current="page"] {
73
+ color: var(--text);
74
+ font-weight: var(--weight-medium);
75
+ }
73
76
 
74
- .ui-crumbs__sep {
75
- display: inline-flex;
76
- color: var(--border-strong);
77
+ .ui-crumbs__sep {
78
+ display: inline-flex;
79
+ color: var(--border-strong);
80
+ }
77
81
  }
78
82
  </style>
@@ -71,149 +71,153 @@ const tagProps = href
71
71
  </Tag>
72
72
 
73
73
  <style>
74
- .ui-btn {
75
- --_pad-x: var(--space-5);
76
- display: inline-flex;
77
- align-items: center;
78
- justify-content: center;
79
- gap: var(--space-2);
80
- position: relative;
81
- min-height: 40px;
82
- padding: var(--space-2) var(--_pad-x);
83
- border: 1px solid transparent;
84
- border-radius: var(--radius-md);
85
- font-family: var(--font-sans);
86
- font-size: var(--text-sm);
87
- font-weight: var(--weight-semibold);
88
- line-height: 1;
89
- text-decoration: none;
90
- cursor: pointer;
91
- transition:
92
- background-color var(--duration-fast) var(--ease-standard),
93
- border-color var(--duration-fast) var(--ease-standard),
94
- transform var(--duration-fast) var(--ease-standard);
95
- }
74
+ @layer reset, tokens, base, components, motion, utilities;
96
75
 
97
- /* Sizes: 32 / 40 / 48 px. */
98
- .ui-btn--sm {
99
- min-height: 32px;
100
- --_pad-x: var(--space-4);
101
- font-size: var(--text-xs);
102
- }
103
- .ui-btn--md {
104
- min-height: 40px;
105
- }
106
- .ui-btn--lg {
107
- min-height: 48px;
108
- --_pad-x: var(--space-6);
109
- font-size: var(--text-base);
110
- }
76
+ @layer components {
77
+ .ui-btn {
78
+ --_pad-x: var(--space-5);
79
+ display: inline-flex;
80
+ align-items: center;
81
+ justify-content: center;
82
+ gap: var(--space-2);
83
+ position: relative;
84
+ min-height: 40px;
85
+ padding: var(--space-2) var(--_pad-x);
86
+ border: 1px solid transparent;
87
+ border-radius: var(--radius-md);
88
+ font-family: var(--font-sans);
89
+ font-size: var(--text-sm);
90
+ font-weight: var(--weight-semibold);
91
+ line-height: 1;
92
+ text-decoration: none;
93
+ cursor: pointer;
94
+ transition:
95
+ background-color var(--duration-fast) var(--ease-standard),
96
+ border-color var(--duration-fast) var(--ease-standard),
97
+ transform var(--duration-fast) var(--ease-standard);
98
+ }
111
99
 
112
- .ui-btn--block {
113
- display: flex;
114
- width: 100%;
115
- }
100
+ /* Sizes: 32 / 40 / 48 px. */
101
+ .ui-btn--sm {
102
+ min-height: 32px;
103
+ --_pad-x: var(--space-4);
104
+ font-size: var(--text-xs);
105
+ }
106
+ .ui-btn--md {
107
+ min-height: 40px;
108
+ }
109
+ .ui-btn--lg {
110
+ min-height: 48px;
111
+ --_pad-x: var(--space-6);
112
+ font-size: var(--text-base);
113
+ }
116
114
 
117
- /* Primary. */
118
- .ui-btn--primary {
119
- background: var(--primary);
120
- color: var(--primary-fg);
121
- }
122
- .ui-btn--primary:hover,
123
- .ui-btn--primary.is-hover {
124
- background: var(--primary-hover);
125
- }
115
+ .ui-btn--block {
116
+ display: flex;
117
+ width: 100%;
118
+ }
126
119
 
127
- /* Secondary. */
128
- .ui-btn--secondary {
129
- background: transparent;
130
- border-color: var(--border-strong);
131
- color: var(--text);
132
- }
133
- .ui-btn--secondary:hover,
134
- .ui-btn--secondary.is-hover {
135
- background: var(--surface-hover);
136
- }
120
+ /* Primary. */
121
+ .ui-btn--primary {
122
+ background: var(--primary);
123
+ color: var(--primary-fg);
124
+ }
125
+ .ui-btn--primary:hover,
126
+ .ui-btn--primary.is-hover {
127
+ background: var(--primary-hover);
128
+ }
137
129
 
138
- /* Accent. One call to action per page. */
139
- .ui-btn--accent {
140
- background: var(--accent);
141
- color: var(--accent-fg);
142
- }
143
- .ui-btn--accent:hover,
144
- .ui-btn--accent.is-hover {
145
- background: var(--accent-hover);
146
- }
130
+ /* Secondary. */
131
+ .ui-btn--secondary {
132
+ background: transparent;
133
+ border-color: var(--border-strong);
134
+ color: var(--text);
135
+ }
136
+ .ui-btn--secondary:hover,
137
+ .ui-btn--secondary.is-hover {
138
+ background: var(--surface-hover);
139
+ }
147
140
 
148
- /* Ghost. Low-weight actions. */
149
- .ui-btn--ghost {
150
- background: transparent;
151
- color: var(--link);
152
- padding-inline: var(--space-3);
153
- }
154
- .ui-btn--ghost:hover,
155
- .ui-btn--ghost.is-hover {
156
- background: var(--surface-hover);
157
- color: var(--link-hover);
158
- }
141
+ /* Accent. One call to action per page. */
142
+ .ui-btn--accent {
143
+ background: var(--accent);
144
+ color: var(--accent-fg);
145
+ }
146
+ .ui-btn--accent:hover,
147
+ .ui-btn--accent.is-hover {
148
+ background: var(--accent-hover);
149
+ }
159
150
 
160
- /* Focus: 2 px ring plus 4 px halo. */
161
- .ui-btn:focus-visible,
162
- .ui-btn.is-focus {
163
- outline: 2px solid var(--focus);
164
- outline-offset: 2px;
165
- box-shadow: 0 0 0 4px var(--focus-outer);
166
- }
151
+ /* Ghost. Low-weight actions. */
152
+ .ui-btn--ghost {
153
+ background: transparent;
154
+ color: var(--link);
155
+ padding-inline: var(--space-3);
156
+ }
157
+ .ui-btn--ghost:hover,
158
+ .ui-btn--ghost.is-hover {
159
+ background: var(--surface-hover);
160
+ color: var(--link-hover);
161
+ }
167
162
 
168
- /* Pressed: 1 px downward shift. */
169
- .ui-btn:active,
170
- .ui-btn.is-active {
171
- transform: translateY(1px);
172
- }
163
+ /* Focus: 2 px ring plus 4 px halo. */
164
+ .ui-btn:focus-visible,
165
+ .ui-btn.is-focus {
166
+ outline: 2px solid var(--focus);
167
+ outline-offset: 2px;
168
+ box-shadow: 0 0 0 4px var(--focus-outer);
169
+ }
173
170
 
174
- /* Disabled. */
175
- .ui-btn:disabled,
176
- .ui-btn[aria-disabled="true"] {
177
- opacity: 0.5;
178
- cursor: not-allowed;
179
- pointer-events: none;
180
- }
171
+ /* Pressed: 1 px downward shift. */
172
+ .ui-btn:active,
173
+ .ui-btn.is-active {
174
+ transform: translateY(1px);
175
+ }
181
176
 
182
- /* Loading: label hidden, spinner shown. */
183
- .ui-btn.is-loading {
184
- cursor: progress;
185
- }
186
- .ui-btn.is-loading .ui-btn__label,
187
- .ui-btn.is-loading .ui-btn__icon {
188
- visibility: hidden;
189
- }
190
- .ui-btn__spinner {
191
- position: absolute;
192
- width: 16px;
193
- height: 16px;
194
- border: 2px solid currentColor;
195
- border-top-color: transparent;
196
- border-radius: var(--radius-full);
197
- animation: ui-btn-spin var(--duration-slower) linear infinite;
198
- }
177
+ /* Disabled. */
178
+ .ui-btn:disabled,
179
+ .ui-btn[aria-disabled="true"] {
180
+ opacity: 0.5;
181
+ cursor: not-allowed;
182
+ pointer-events: none;
183
+ }
199
184
 
200
- .ui-btn__icon {
201
- display: inline-flex;
202
- font-size: 1em;
203
- }
185
+ /* Loading: label hidden, spinner shown. */
186
+ .ui-btn.is-loading {
187
+ cursor: progress;
188
+ }
189
+ .ui-btn.is-loading .ui-btn__label,
190
+ .ui-btn.is-loading .ui-btn__icon {
191
+ visibility: hidden;
192
+ }
193
+ .ui-btn__spinner {
194
+ position: absolute;
195
+ width: 16px;
196
+ height: 16px;
197
+ border: 2px solid currentColor;
198
+ border-top-color: transparent;
199
+ border-radius: var(--radius-full);
200
+ animation: ui-btn-spin var(--duration-slower) linear infinite;
201
+ }
204
202
 
205
- @keyframes ui-btn-spin {
206
- to {
207
- transform: rotate(360deg);
203
+ .ui-btn__icon {
204
+ display: inline-flex;
205
+ font-size: 1em;
208
206
  }
209
- }
210
207
 
211
- @media (prefers-reduced-motion: reduce) {
212
- .ui-btn {
213
- transition: none;
208
+ @keyframes ui-btn-spin {
209
+ to {
210
+ transform: rotate(360deg);
211
+ }
214
212
  }
215
- .ui-btn__spinner {
216
- animation-duration: 1200ms;
213
+
214
+ @media (prefers-reduced-motion: reduce) {
215
+ .ui-btn {
216
+ transition: none;
217
+ }
218
+ .ui-btn__spinner {
219
+ animation-duration: 1200ms;
220
+ }
217
221
  }
218
222
  }
219
223
  </style>