@kwtv/components 0.8.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.
@@ -0,0 +1,148 @@
1
+ /*
2
+ * Feedback styles as plain, importable CSS. This is the single copy of the
3
+ * .ui-alert, .ui-empty and .ui-skel rules. Alert.astro, EmptyState.astro and
4
+ * Skeleton.astro import it themselves, so a pure-Astro app needs nothing extra.
5
+ * Bundlers emit it once, however many components and pages pull it in.
6
+ *
7
+ * React and Preact islands cannot render Astro components. An island that
8
+ * shows an alert, an empty state or a loading skeleton writes the same markup
9
+ * as the Astro component and imports this sheet once:
10
+ *
11
+ * import "@kwtv/components/styles/feedback.css";
12
+ *
13
+ * Import `@kwtv/tokens/tokens.css` before this file.
14
+ */
15
+
16
+ @layer reset, tokens, base, components, motion, utilities;
17
+
18
+ @layer components {
19
+ /* ---- Alert ---------------------------------------------------------------- */
20
+ .ui-alert {
21
+ display: flex;
22
+ align-items: center;
23
+ gap: var(--space-2-5, 0.625rem);
24
+ padding: var(--space-3) var(--space-4);
25
+ border-left: 3px solid currentColor;
26
+ border-radius: var(--radius-md);
27
+ font-family: var(--font-sans);
28
+ font-size: var(--text-sm);
29
+ line-height: var(--leading-sm);
30
+ }
31
+
32
+ .ui-alert__icon {
33
+ display: inline-flex;
34
+ flex-shrink: 0;
35
+ align-self: flex-start;
36
+ margin-top: calc((var(--leading-sm, 1.375rem) - 16px) / 2);
37
+ }
38
+
39
+ /* Body keeps full strength; the left rule carries the tone. */
40
+ .ui-alert__body {
41
+ color: var(--text);
42
+ display: flex;
43
+ flex-direction: column;
44
+ gap: var(--space-1);
45
+ }
46
+
47
+ .ui-alert__title {
48
+ font-weight: var(--weight-semibold);
49
+ }
50
+
51
+ .ui-alert--info {
52
+ background: var(--surface-accent);
53
+ color: var(--surface-accent-fg, var(--primary));
54
+ }
55
+
56
+ .ui-alert--success {
57
+ background: var(--success-bg);
58
+ color: var(--success);
59
+ }
60
+
61
+ .ui-alert--warning {
62
+ background: var(--warning-bg);
63
+ color: var(--warning);
64
+ }
65
+
66
+ .ui-alert--danger {
67
+ background: var(--danger-bg);
68
+ color: var(--danger);
69
+ }
70
+
71
+ /* ---- Empty state ---------------------------------------------------------- */
72
+ .ui-empty {
73
+ display: flex;
74
+ flex-direction: column;
75
+ align-items: flex-start;
76
+ gap: var(--space-2);
77
+ padding: var(--space-8) var(--space-6);
78
+ border: 1px dashed var(--border-strong);
79
+ border-radius: var(--radius-lg);
80
+ background: var(--surface);
81
+ }
82
+
83
+ .ui-empty__title {
84
+ font-size: var(--text-lg);
85
+ font-weight: var(--weight-semibold);
86
+ color: var(--text);
87
+ }
88
+
89
+ .ui-empty__body {
90
+ font-size: var(--text-sm);
91
+ line-height: var(--leading-sm);
92
+ color: var(--text-muted);
93
+ }
94
+
95
+ /* The flex gap above spaces the action from the body. No extra margin. */
96
+
97
+ /* ---- Skeleton ------------------------------------------------------------- */
98
+ .ui-skel {
99
+ display: block;
100
+ background: var(--surface-hover);
101
+ background-image: linear-gradient(
102
+ 90deg,
103
+ var(--surface-hover) 0%,
104
+ var(--border) 50%,
105
+ var(--surface-hover) 100%
106
+ );
107
+ background-size: 200% 100%;
108
+ border-radius: var(--radius-md);
109
+ animation: ui-skel-shimmer 1400ms ease-in-out infinite;
110
+ }
111
+
112
+ .ui-skel--img {
113
+ width: 100%;
114
+ aspect-ratio: 4 / 3;
115
+ border-radius: 0;
116
+ }
117
+
118
+ .ui-skel--title {
119
+ height: 0.875rem;
120
+ border-radius: var(--radius-sm);
121
+ }
122
+
123
+ .ui-skel--line {
124
+ height: 0.625rem;
125
+ border-radius: var(--radius-sm);
126
+ }
127
+
128
+ .ui-skel--block {
129
+ height: 1rem;
130
+ }
131
+
132
+ @keyframes ui-skel-shimmer {
133
+ 0% {
134
+ background-position: 200% 0;
135
+ }
136
+ 100% {
137
+ background-position: -200% 0;
138
+ }
139
+ }
140
+
141
+ /* The shimmer stops for readers who ask for reduced motion. */
142
+ @media (prefers-reduced-motion: reduce) {
143
+ .ui-skel {
144
+ animation: none;
145
+ background-image: none;
146
+ }
147
+ }
148
+ }
@@ -0,0 +1,255 @@
1
+ /*
2
+ * Filter and selection styles as plain, importable CSS. This is the single
3
+ * copy of the .ui-chip, .ui-swatch, .ui-check and .ui-filtertags rules.
4
+ * Chip.astro, Swatch.astro, Checkbox.astro, Radio.astro and
5
+ * ActiveFilterTags.astro import it themselves, so a pure-Astro app needs
6
+ * nothing extra. Bundlers emit it once, however many components and pages
7
+ * pull it in.
8
+ *
9
+ * React and Preact islands cannot render Astro components. An island that
10
+ * shows chips, swatches, checkboxes, radios or the active filter row writes
11
+ * the same markup as the Astro component and imports this sheet once:
12
+ *
13
+ * import "@kwtv/components/styles/filters.css";
14
+ *
15
+ * The active filter tags are outline badges, so that island imports
16
+ * `styles/badge.css` too. Import `@kwtv/tokens/tokens.css` before this file.
17
+ */
18
+
19
+ @layer reset, tokens, base, components, motion, utilities;
20
+
21
+ @layer components {
22
+ /* ---- Chip ----------------------------------------------------------------- */
23
+ .ui-chip {
24
+ display: inline-flex;
25
+ align-items: center;
26
+ justify-content: center;
27
+ min-height: 36px;
28
+ padding: 0 var(--space-4);
29
+ background: var(--surface);
30
+ border: 1px solid var(--border);
31
+ border-radius: var(--radius-md);
32
+ font-family: var(--font-sans);
33
+ font-size: var(--text-sm);
34
+ font-weight: var(--weight-medium);
35
+ color: var(--text);
36
+ cursor: pointer;
37
+ transition:
38
+ border-color var(--duration-fast) var(--ease-standard),
39
+ background-color var(--duration-fast) var(--ease-standard);
40
+ }
41
+
42
+ .ui-chip:hover,
43
+ .ui-chip.is-hover {
44
+ border-color: var(--border-strong);
45
+ }
46
+
47
+ .ui-chip:focus-visible,
48
+ .ui-chip.is-focus {
49
+ outline: 2px solid var(--focus);
50
+ outline-offset: 2px;
51
+ box-shadow: 0 0 0 4px var(--focus-outer);
52
+ }
53
+
54
+ /* Selected: 2 px border plus the accent surface, readable without colour. */
55
+ .ui-chip.is-selected,
56
+ .ui-chip[aria-pressed="true"] {
57
+ border-width: 2px;
58
+ border-color: var(--border-strong);
59
+ background: var(--surface-accent);
60
+ color: var(--surface-accent-fg, var(--text));
61
+ font-weight: var(--weight-semibold);
62
+ }
63
+
64
+ .ui-chip.is-disabled,
65
+ .ui-chip:disabled {
66
+ opacity: 0.5;
67
+ cursor: not-allowed;
68
+ }
69
+
70
+ .ui-chip--struck {
71
+ text-decoration: line-through;
72
+ }
73
+
74
+ /* Filter chip: 32 px. */
75
+ .ui-chip--filter {
76
+ min-height: 32px;
77
+ border-radius: var(--radius-full);
78
+ padding: 0 var(--space-3);
79
+ }
80
+
81
+ .ui-chip--filter.is-selected,
82
+ .ui-chip--filter[aria-pressed="true"] {
83
+ background: var(--surface-accent);
84
+ }
85
+
86
+ @media (prefers-reduced-motion: reduce) {
87
+ .ui-chip {
88
+ transition: none;
89
+ }
90
+ }
91
+
92
+ @media (max-width: 700px), (pointer: coarse) {
93
+ .ui-chip {
94
+ min-height: 36px;
95
+ }
96
+ .ui-chip--filter {
97
+ min-height: 40px;
98
+ }
99
+ }
100
+
101
+ /* ---- Swatch --------------------------------------------------------------- */
102
+ .ui-swatch {
103
+ width: 30px;
104
+ height: 30px;
105
+ padding: 0;
106
+ background: var(--swatch);
107
+ border: 1px solid var(--border);
108
+ border-radius: var(--radius-full);
109
+ cursor: pointer;
110
+ transition: box-shadow var(--duration-fast) var(--ease-standard);
111
+ }
112
+
113
+ .ui-swatch:hover,
114
+ .ui-swatch.is-hover {
115
+ box-shadow: 0 0 0 2px var(--border-strong);
116
+ }
117
+
118
+ .ui-swatch:focus-visible,
119
+ .ui-swatch.is-focus {
120
+ outline: 2px solid var(--focus);
121
+ outline-offset: 2px;
122
+ box-shadow: 0 0 0 4px var(--focus-outer);
123
+ }
124
+
125
+ /* Selected: 2 px ring, readable without colour. */
126
+ .ui-swatch.is-selected,
127
+ .ui-swatch[aria-pressed="true"] {
128
+ box-shadow:
129
+ 0 0 0 2px var(--surface),
130
+ 0 0 0 4px var(--border-strong);
131
+ }
132
+
133
+ .ui-swatch.is-disabled,
134
+ .ui-swatch:disabled {
135
+ opacity: 0.5;
136
+ cursor: not-allowed;
137
+ }
138
+
139
+ @media (prefers-reduced-motion: reduce) {
140
+ .ui-swatch {
141
+ transition: none;
142
+ }
143
+ }
144
+
145
+ @media (max-width: 700px), (pointer: coarse) {
146
+ .ui-swatch {
147
+ width: 32px;
148
+ height: 32px;
149
+ }
150
+ }
151
+
152
+ /* ---- Checkbox and Radio --------------------------------------------------- */
153
+ /* Checkbox and Radio share the row. The input type picks the box. */
154
+ .ui-check {
155
+ display: flex;
156
+ align-items: flex-start;
157
+ gap: var(--space-2);
158
+ min-height: 32px;
159
+ font-family: var(--font-sans);
160
+ font-size: var(--text-sm);
161
+ color: var(--text);
162
+ cursor: pointer;
163
+ }
164
+
165
+ .ui-check:has(> input[type="radio"]) {
166
+ align-items: center;
167
+ }
168
+
169
+ .ui-check input[type="checkbox"] {
170
+ appearance: none;
171
+ -webkit-appearance: none;
172
+ width: 17px;
173
+ height: 17px;
174
+ flex-shrink: 0;
175
+ margin-top: calc((var(--leading-sm, 1.375rem) - 17px) / 2);
176
+ border: 1.5px solid var(--border-strong);
177
+ border-radius: var(--radius-xs, 3px);
178
+ background: var(--surface);
179
+ cursor: pointer;
180
+ transition:
181
+ background-color var(--duration-fast, 100ms) var(--ease-standard, ease),
182
+ border-color var(--duration-fast, 100ms) var(--ease-standard, ease);
183
+ }
184
+
185
+ .ui-check input[type="checkbox"]:checked {
186
+ background: var(--primary);
187
+ border-color: var(--primary);
188
+ 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");
189
+ background-repeat: no-repeat;
190
+ background-position: center;
191
+ background-size: 10px 8px;
192
+ }
193
+
194
+ .ui-check input[type="radio"] {
195
+ width: 17px;
196
+ height: 17px;
197
+ flex-shrink: 0;
198
+ accent-color: var(--primary);
199
+ cursor: pointer;
200
+ }
201
+
202
+ .ui-check input:focus-visible {
203
+ outline: 2px solid var(--focus);
204
+ outline-offset: 2px;
205
+ box-shadow: 0 0 0 4px var(--focus-outer);
206
+ }
207
+
208
+ .ui-check.is-disabled {
209
+ cursor: not-allowed;
210
+ color: var(--text-muted);
211
+ }
212
+
213
+ .ui-check.is-disabled input {
214
+ cursor: not-allowed;
215
+ }
216
+
217
+ .ui-check.is-disabled input[type="checkbox"] {
218
+ opacity: 0.45;
219
+ }
220
+
221
+ @media (max-width: 700px), (pointer: coarse) {
222
+ .ui-check {
223
+ min-height: 40px;
224
+ }
225
+ }
226
+
227
+ /* ---- Active filter tags --------------------------------------------------- */
228
+ .ui-filtertags {
229
+ display: flex;
230
+ flex-wrap: wrap;
231
+ align-items: center;
232
+ gap: var(--space-2);
233
+ }
234
+
235
+ .ui-filtertags__clear {
236
+ padding: var(--space-1-5) var(--space-2);
237
+ background: transparent;
238
+ border: 0;
239
+ border-radius: var(--radius-sm);
240
+ font-family: var(--font-sans);
241
+ font-size: var(--text-xs);
242
+ font-weight: var(--weight-semibold);
243
+ color: var(--link);
244
+ cursor: pointer;
245
+ }
246
+ .ui-filtertags__clear:hover {
247
+ background: var(--surface-hover);
248
+ color: var(--link-hover);
249
+ }
250
+ .ui-filtertags__clear:focus-visible {
251
+ outline: 2px solid var(--focus);
252
+ outline-offset: 2px;
253
+ box-shadow: 0 0 0 4px var(--focus-outer);
254
+ }
255
+ }
@@ -0,0 +1,63 @@
1
+ /*
2
+ * Pager styles as plain, importable CSS. This is the single copy of the
3
+ * .ui-pager rules. Pager.astro imports it itself, so a pure-Astro app needs
4
+ * nothing extra.
5
+ *
6
+ * React and Preact islands cannot render Astro components. An island that
7
+ * renders a pager writes the same markup as the Astro component and imports
8
+ * this sheet once:
9
+ *
10
+ * import "@kwtv/components/styles/pager.css";
11
+ *
12
+ * Import `@kwtv/tokens/tokens.css` before this file.
13
+ */
14
+
15
+ @layer reset, tokens, base, components, motion, utilities;
16
+
17
+ @layer components {
18
+ .ui-pager {
19
+ display: flex;
20
+ align-items: center;
21
+ gap: var(--space-1);
22
+ font-size: var(--text-sm);
23
+ }
24
+
25
+ .ui-pager a,
26
+ .ui-pager span:not(.ui-pager__gap) {
27
+ display: inline-flex;
28
+ align-items: center;
29
+ justify-content: center;
30
+ min-width: 36px;
31
+ height: 36px;
32
+ padding: 0 var(--space-1);
33
+ border-radius: var(--radius-md);
34
+ color: var(--link);
35
+ text-decoration: none;
36
+ }
37
+
38
+ .ui-pager a:hover {
39
+ background: var(--surface-hover);
40
+ color: var(--link-hover);
41
+ }
42
+ .ui-pager a:focus-visible {
43
+ outline: 2px solid var(--focus);
44
+ outline-offset: 2px;
45
+ }
46
+
47
+ .ui-pager a.is-current {
48
+ background: var(--primary);
49
+ color: var(--primary-fg);
50
+ font-weight: var(--weight-semibold);
51
+ }
52
+
53
+ .ui-pager .is-disabled {
54
+ color: var(--border);
55
+ cursor: not-allowed;
56
+ }
57
+
58
+ .ui-pager__gap {
59
+ min-width: 24px;
60
+ text-align: center;
61
+ color: var(--text-muted);
62
+ }
63
+ }
@@ -0,0 +1,148 @@
1
+ /*
2
+ * Product card styles as plain, importable CSS. This is the single copy of
3
+ * the .ui-product and .ui-ribbon rules. ProductCard.astro imports it itself,
4
+ * so a pure-Astro app needs nothing extra.
5
+ *
6
+ * React and Preact islands cannot render Astro components. An island that
7
+ * renders product cards writes the same markup as the Astro component and
8
+ * imports this sheet once:
9
+ *
10
+ * import "@kwtv/components/styles/product-card.css";
11
+ *
12
+ * The sold-out text uses .ui-sr-only from `styles/base.css`. Import
13
+ * `@kwtv/tokens/tokens.css` before this file.
14
+ */
15
+
16
+ @layer reset, tokens, base, components, motion, utilities;
17
+
18
+ @layer components {
19
+ .ui-product {
20
+ position: relative;
21
+ display: flex;
22
+ flex-direction: column;
23
+ border: 1px solid var(--border);
24
+ border-radius: var(--radius-lg);
25
+ overflow: hidden;
26
+ background: var(--surface);
27
+ color: var(--text);
28
+ text-decoration: none;
29
+ transition: border-color var(--duration-fast) var(--ease-standard);
30
+ }
31
+
32
+ .ui-product:hover,
33
+ .ui-product.is-hover {
34
+ border-color: var(--border-strong);
35
+ }
36
+
37
+ .ui-product:focus-visible {
38
+ outline: 2px solid var(--focus);
39
+ outline-offset: 2px;
40
+ box-shadow: 0 0 0 4px var(--focus-outer);
41
+ }
42
+
43
+ .ui-product__img {
44
+ display: block;
45
+ width: 100%;
46
+ aspect-ratio: 4 / 3;
47
+ object-fit: cover;
48
+ background: var(--surface-hover);
49
+ }
50
+
51
+ .ui-product__body {
52
+ display: flex;
53
+ flex-direction: column;
54
+ gap: var(--space-1);
55
+ padding: var(--space-3) var(--space-4) var(--space-4);
56
+ }
57
+
58
+ .ui-product__title {
59
+ font-size: var(--text-base);
60
+ font-weight: var(--weight-semibold);
61
+ color: var(--text);
62
+ }
63
+
64
+ .ui-product__meta {
65
+ font-size: var(--text-xs);
66
+ color: var(--text-muted);
67
+ }
68
+
69
+ .ui-product__price {
70
+ display: flex;
71
+ align-items: baseline;
72
+ gap: var(--space-2);
73
+ margin-top: var(--space-1);
74
+ font-size: var(--text-lg);
75
+ font-weight: var(--weight-semibold);
76
+ font-variant-numeric: tabular-nums;
77
+ color: var(--text);
78
+ }
79
+
80
+ .ui-product__price s {
81
+ font-size: var(--text-sm);
82
+ font-weight: var(--weight-regular);
83
+ color: var(--text-muted);
84
+ }
85
+
86
+ /* Sold out: card stays readable and clickable. */
87
+ .ui-product.is-disabled .ui-product__img {
88
+ opacity: 0.7;
89
+ }
90
+ .ui-product.is-disabled .ui-product__price {
91
+ color: var(--text-muted);
92
+ font-weight: var(--weight-regular);
93
+ }
94
+
95
+ /* Ribbon. One per card, decorative. 112 px box. */
96
+ .ui-ribbon {
97
+ position: absolute;
98
+ top: 0;
99
+ left: 0;
100
+ width: 112px;
101
+ height: 112px;
102
+ overflow: hidden;
103
+ pointer-events: none;
104
+ z-index: 1;
105
+ }
106
+ .ui-ribbon i {
107
+ position: absolute;
108
+ top: 22px;
109
+ left: -34px;
110
+ width: 140px;
111
+ transform: rotate(-45deg);
112
+ text-align: center;
113
+ font-size: var(--text-2xs);
114
+ font-weight: var(--weight-bold);
115
+ letter-spacing: var(--tracking-wider);
116
+ text-transform: uppercase;
117
+ font-style: normal;
118
+ padding: var(--space-1) 0;
119
+ color: var(--primary-fg);
120
+ background: var(--primary);
121
+ }
122
+ .ui-ribbon--new i {
123
+ background: var(--primary);
124
+ color: var(--primary-fg);
125
+ }
126
+ .ui-ribbon--promo i {
127
+ background: var(--accent);
128
+ color: var(--accent-fg);
129
+ }
130
+ .ui-ribbon--last i {
131
+ background: var(--warning);
132
+ color: var(--primary-fg);
133
+ }
134
+ .ui-ribbon--fav i {
135
+ background: var(--surface-accent);
136
+ color: var(--surface-accent-fg);
137
+ }
138
+ .ui-ribbon--sold-out i {
139
+ background: var(--danger-bg);
140
+ color: var(--danger);
141
+ }
142
+
143
+ @media (prefers-reduced-motion: reduce) {
144
+ .ui-product {
145
+ transition: none;
146
+ }
147
+ }
148
+ }