@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
@@ -1,67 +1,71 @@
1
1
  /* Mobile menu drawer. All values from the design-system tokens. */
2
2
 
3
- .ui-drawer {
4
- display: flex;
5
- flex-direction: column;
6
- gap: var(--space-6);
7
- padding: var(--space-4) var(--space-4) var(--space-6);
8
- background: var(--bg);
9
- }
3
+ @layer reset, tokens, base, components, motion, utilities;
10
4
 
11
- .ui-drawer__group {
12
- display: flex;
13
- flex-direction: column;
14
- }
5
+ @layer components {
6
+ .ui-drawer {
7
+ display: flex;
8
+ flex-direction: column;
9
+ gap: var(--space-6);
10
+ padding: var(--space-4) var(--space-4) var(--space-6);
11
+ background: var(--bg);
12
+ }
15
13
 
16
- .ui-drawer__grouptitle {
17
- padding: 0 var(--space-3);
18
- margin-bottom: var(--space-1);
19
- font-family: var(--font-mono);
20
- font-size: var(--text-2xs);
21
- font-weight: var(--weight-semibold);
22
- letter-spacing: var(--tracking-wider);
23
- text-transform: uppercase;
24
- color: var(--text-muted);
25
- }
14
+ .ui-drawer__group {
15
+ display: flex;
16
+ flex-direction: column;
17
+ }
26
18
 
27
- /* Rows. No separators. At least 44 px. Soft fill on hover, press, current. */
28
- .ui-drawerlink {
29
- display: flex;
30
- flex-direction: column;
31
- justify-content: center;
32
- gap: 2px;
33
- min-height: 44px;
34
- padding: var(--space-2) var(--space-3);
35
- border-radius: var(--radius-md);
36
- color: var(--text);
37
- font-size: var(--text-base);
38
- font-weight: var(--weight-medium);
39
- text-decoration: none;
40
- }
19
+ .ui-drawer__grouptitle {
20
+ padding: 0 var(--space-3);
21
+ margin-bottom: var(--space-1);
22
+ font-family: var(--font-mono);
23
+ font-size: var(--text-2xs);
24
+ font-weight: var(--weight-semibold);
25
+ letter-spacing: var(--tracking-wider);
26
+ text-transform: uppercase;
27
+ color: var(--text-muted);
28
+ }
41
29
 
42
- .ui-drawerlink small {
43
- font-size: var(--text-xs);
44
- font-weight: var(--weight-regular);
45
- color: var(--text-muted);
46
- }
30
+ /* Rows. No separators. At least 44 px. Soft fill on hover, press, current. */
31
+ .ui-drawerlink {
32
+ display: flex;
33
+ flex-direction: column;
34
+ justify-content: center;
35
+ gap: 2px;
36
+ min-height: 44px;
37
+ padding: var(--space-2) var(--space-3);
38
+ border-radius: var(--radius-md);
39
+ color: var(--text);
40
+ font-size: var(--text-base);
41
+ font-weight: var(--weight-medium);
42
+ text-decoration: none;
43
+ }
47
44
 
48
- .ui-drawerlink:hover,
49
- .ui-drawerlink.is-hover,
50
- .ui-drawerlink:active,
51
- .ui-drawerlink.is-active {
52
- background: var(--surface-hover);
53
- }
45
+ .ui-drawerlink small {
46
+ font-size: var(--text-xs);
47
+ font-weight: var(--weight-regular);
48
+ color: var(--text-muted);
49
+ }
54
50
 
55
- .ui-drawerlink.is-current {
56
- background: var(--surface-hover);
57
- font-weight: var(--weight-semibold);
58
- }
51
+ .ui-drawerlink:hover,
52
+ .ui-drawerlink.is-hover,
53
+ .ui-drawerlink:active,
54
+ .ui-drawerlink.is-active {
55
+ background: var(--surface-hover);
56
+ }
59
57
 
60
- .ui-drawerlink:focus-visible {
61
- outline: 2px solid var(--focus);
62
- outline-offset: -2px;
63
- }
58
+ .ui-drawerlink.is-current {
59
+ background: var(--surface-hover);
60
+ font-weight: var(--weight-semibold);
61
+ }
62
+
63
+ .ui-drawerlink:focus-visible {
64
+ outline: 2px solid var(--focus);
65
+ outline-offset: -2px;
66
+ }
64
67
 
65
- .ui-drawer__cta {
66
- margin-top: var(--space-2);
68
+ .ui-drawer__cta {
69
+ margin-top: var(--space-2);
70
+ }
67
71
  }
@@ -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
  /**
@@ -1,116 +1,120 @@
1
1
  /* Modal and sheet. All values from the design-system tokens. */
2
2
 
3
- .ui-scrim {
4
- position: fixed;
5
- inset: 0;
6
- z-index: var(--z-modal);
7
- display: flex;
8
- align-items: center;
9
- justify-content: center;
10
- padding: var(--space-4);
11
- /* A raw ramp step, not a role: the scrim must stay dark in both themes, and
12
- every role that is dark in light mode goes light in dark mode. */
13
- background: color-mix(in srgb, var(--charcoal-xdark) 52%, transparent);
14
- animation: ui-scrim-in var(--duration-base) var(--ease-standard);
15
- }
16
-
17
- .ui-scrim--bottom {
18
- align-items: flex-end;
19
- padding: 0;
20
- }
21
-
22
- .ui-modal {
23
- display: flex;
24
- flex-direction: column;
25
- gap: var(--space-3);
26
- width: 100%;
27
- max-width: 440px;
28
- padding: var(--space-6);
29
- background: var(--surface);
30
- border-radius: var(--radius-xl);
31
- box-shadow: var(--shadow-xl);
32
- animation: ui-modal-in var(--duration-base) var(--ease-enter);
33
- }
3
+ @layer reset, tokens, base, components, motion, utilities;
34
4
 
35
- .ui-modal:focus-visible {
36
- outline: 2px solid var(--focus);
37
- outline-offset: 2px;
38
- }
5
+ @layer components {
6
+ .ui-scrim {
7
+ position: fixed;
8
+ inset: 0;
9
+ z-index: var(--z-modal);
10
+ display: flex;
11
+ align-items: center;
12
+ justify-content: center;
13
+ padding: var(--space-4);
14
+ /* A raw ramp step, not a role: the scrim must stay dark in both themes, and
15
+ every role that is dark in light mode goes light in dark mode. */
16
+ background: color-mix(in srgb, var(--charcoal-xdark) 52%, transparent);
17
+ animation: ui-scrim-in var(--duration-base) var(--ease-standard);
18
+ }
39
19
 
40
- .ui-modal__title {
41
- font-size: var(--text-xl);
42
- font-weight: var(--weight-semibold);
43
- letter-spacing: var(--tracking-tight);
44
- color: var(--text);
45
- }
20
+ .ui-scrim--bottom {
21
+ align-items: flex-end;
22
+ padding: 0;
23
+ }
46
24
 
47
- .ui-modal__body {
48
- font-size: var(--text-sm);
49
- line-height: var(--leading-sm);
50
- color: var(--text-muted);
51
- }
25
+ .ui-modal {
26
+ display: flex;
27
+ flex-direction: column;
28
+ gap: var(--space-3);
29
+ width: 100%;
30
+ max-width: 440px;
31
+ padding: var(--space-6);
32
+ background: var(--surface);
33
+ border-radius: var(--radius-xl);
34
+ box-shadow: var(--shadow-xl);
35
+ animation: ui-modal-in var(--duration-base) var(--ease-enter);
36
+ }
52
37
 
53
- .ui-modal__actions {
54
- display: flex;
55
- justify-content: flex-end;
56
- gap: var(--space-2);
57
- margin-top: var(--space-2);
58
- }
38
+ .ui-modal:focus-visible {
39
+ outline: 2px solid var(--focus);
40
+ outline-offset: 2px;
41
+ }
59
42
 
60
- /* Bottom sheet. The safest choice sits at the bottom, under the thumb, so
61
- stack the actions reversed there when the consumer orders them safe-first. */
62
- .ui-sheet {
63
- max-width: none;
64
- border-radius: var(--radius-xl) var(--radius-xl) 0 0;
65
- padding-top: var(--space-3);
66
- animation: ui-sheet-in var(--duration-slow) var(--ease-enter);
67
- }
43
+ .ui-modal__title {
44
+ font-size: var(--text-xl);
45
+ font-weight: var(--weight-semibold);
46
+ letter-spacing: var(--tracking-tight);
47
+ color: var(--text);
48
+ }
68
49
 
69
- .ui-sheet__grip {
70
- align-self: center;
71
- width: 36px;
72
- height: 4px;
73
- margin-bottom: var(--space-2);
74
- border-radius: var(--radius-full);
75
- background: var(--border);
76
- }
50
+ .ui-modal__body {
51
+ font-size: var(--text-sm);
52
+ line-height: var(--leading-sm);
53
+ color: var(--text-muted);
54
+ }
77
55
 
78
- /* Force the sheet presentation on narrow screens for the plain dialog too. */
79
- @media (max-width: 700px) {
80
- .ui-scrim:not(.ui-scrim--bottom) {
81
- align-items: flex-end;
82
- padding: 0;
56
+ .ui-modal__actions {
57
+ display: flex;
58
+ justify-content: flex-end;
59
+ gap: var(--space-2);
60
+ margin-top: var(--space-2);
83
61
  }
84
- .ui-modal:not(.ui-sheet) {
62
+
63
+ /* Bottom sheet. The safest choice sits at the bottom, under the thumb, so
64
+ stack the actions reversed there when the consumer orders them safe-first. */
65
+ .ui-sheet {
85
66
  max-width: none;
86
67
  border-radius: var(--radius-xl) var(--radius-xl) 0 0;
68
+ padding-top: var(--space-3);
87
69
  animation: ui-sheet-in var(--duration-slow) var(--ease-enter);
88
70
  }
89
- }
90
71
 
91
- @keyframes ui-scrim-in {
92
- from {
93
- opacity: 0;
72
+ .ui-sheet__grip {
73
+ align-self: center;
74
+ width: 36px;
75
+ height: 4px;
76
+ margin-bottom: var(--space-2);
77
+ border-radius: var(--radius-full);
78
+ background: var(--border);
94
79
  }
95
- }
96
80
 
97
- @keyframes ui-modal-in {
98
- from {
99
- opacity: 0;
100
- transform: translateY(8px) scale(0.98);
81
+ /* Force the sheet presentation on narrow screens for the plain dialog too. */
82
+ @media (max-width: 700px) {
83
+ .ui-scrim:not(.ui-scrim--bottom) {
84
+ align-items: flex-end;
85
+ padding: 0;
86
+ }
87
+ .ui-modal:not(.ui-sheet) {
88
+ max-width: none;
89
+ border-radius: var(--radius-xl) var(--radius-xl) 0 0;
90
+ animation: ui-sheet-in var(--duration-slow) var(--ease-enter);
91
+ }
101
92
  }
102
- }
103
93
 
104
- @keyframes ui-sheet-in {
105
- from {
106
- transform: translateY(100%);
94
+ @keyframes ui-scrim-in {
95
+ from {
96
+ opacity: 0;
97
+ }
107
98
  }
108
- }
109
99
 
110
- @media (prefers-reduced-motion: reduce) {
111
- .ui-scrim,
112
- .ui-modal,
113
- .ui-sheet {
114
- animation: none;
100
+ @keyframes ui-modal-in {
101
+ from {
102
+ opacity: 0;
103
+ transform: translateY(8px) scale(0.98);
104
+ }
105
+ }
106
+
107
+ @keyframes ui-sheet-in {
108
+ from {
109
+ transform: translateY(100%);
110
+ }
111
+ }
112
+
113
+ @media (prefers-reduced-motion: reduce) {
114
+ .ui-scrim,
115
+ .ui-modal,
116
+ .ui-sheet {
117
+ animation: none;
118
+ }
115
119
  }
116
120
  }
@@ -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"
@@ -1,122 +1,126 @@
1
1
  /* News carousel. Playful layer, prefix pl-. All values from the tokens. */
2
2
 
3
- .pl-news {
4
- position: relative;
5
- display: grid;
6
- }
3
+ @layer reset, tokens, base, components, motion, utilities;
7
4
 
8
- /* Items stack; the active one fades in. Off-screen items cannot be reached. */
9
- .pl-news-item {
10
- grid-area: 1 / 1;
11
- display: flex;
12
- flex-direction: column;
13
- gap: var(--space-1);
14
- padding: var(--space-4);
15
- border-radius: var(--radius-lg);
16
- background: var(--surface);
17
- border: 1px solid var(--border);
18
- text-decoration: none;
19
- color: var(--text);
20
- opacity: 0;
21
- pointer-events: none;
22
- transition: opacity 420ms var(--ease-standard);
23
- }
5
+ @layer components {
6
+ .pl-news {
7
+ position: relative;
8
+ display: grid;
9
+ }
24
10
 
25
- .pl-news-item[data-on] {
26
- opacity: 1;
27
- pointer-events: auto;
28
- }
11
+ /* Items stack; the active one fades in. Off-screen items cannot be reached. */
12
+ .pl-news-item {
13
+ grid-area: 1 / 1;
14
+ display: flex;
15
+ flex-direction: column;
16
+ gap: var(--space-1);
17
+ padding: var(--space-4);
18
+ border-radius: var(--radius-lg);
19
+ background: var(--surface);
20
+ border: 1px solid var(--border);
21
+ text-decoration: none;
22
+ color: var(--text);
23
+ opacity: 0;
24
+ pointer-events: none;
25
+ transition: opacity 420ms var(--ease-standard);
26
+ }
29
27
 
30
- .pl-news-item:focus-visible {
31
- outline: 2px solid var(--focus);
32
- outline-offset: 2px;
33
- }
28
+ .pl-news-item[data-on] {
29
+ opacity: 1;
30
+ pointer-events: auto;
31
+ }
34
32
 
35
- /* Prev/next arrows. Sit in the controls row beside the dots, never over the
36
- item itself, so they never compete with the item's own link target. */
37
- .pl-news-controls {
38
- display: flex;
39
- align-items: center;
40
- justify-content: flex-start;
41
- gap: var(--space-3);
42
- margin-top: var(--space-2);
43
- }
33
+ .pl-news-item:focus-visible {
34
+ outline: 2px solid var(--focus);
35
+ outline-offset: 2px;
36
+ }
44
37
 
45
- .pl-news-nav {
46
- display: inline-flex;
47
- align-items: center;
48
- justify-content: center;
49
- width: 28px;
50
- height: 28px;
51
- padding: 0;
52
- border: 1px solid var(--border);
53
- border-radius: var(--radius-full);
54
- background: var(--surface);
55
- color: var(--text);
56
- cursor: pointer;
57
- transition:
58
- background-color var(--duration-fast) var(--ease-standard),
59
- border-color var(--duration-fast) var(--ease-standard);
60
- }
38
+ /* Prev/next arrows. Sit in the controls row beside the dots, never over the
39
+ item itself, so they never compete with the item's own link target. */
40
+ .pl-news-controls {
41
+ display: flex;
42
+ align-items: center;
43
+ justify-content: flex-start;
44
+ gap: var(--space-3);
45
+ margin-top: var(--space-2);
46
+ }
61
47
 
62
- .pl-news-nav:hover {
63
- background: var(--surface-hover);
64
- border-color: var(--border-strong);
65
- }
48
+ .pl-news-nav {
49
+ display: inline-flex;
50
+ align-items: center;
51
+ justify-content: center;
52
+ width: 28px;
53
+ height: 28px;
54
+ padding: 0;
55
+ border: 1px solid var(--border);
56
+ border-radius: var(--radius-full);
57
+ background: var(--surface);
58
+ color: var(--text);
59
+ cursor: pointer;
60
+ transition:
61
+ background-color var(--duration-fast) var(--ease-standard),
62
+ border-color var(--duration-fast) var(--ease-standard);
63
+ }
66
64
 
67
- .pl-news-nav:focus-visible {
68
- outline: 2px solid var(--focus);
69
- outline-offset: 2px;
70
- }
65
+ .pl-news-nav:hover {
66
+ background: var(--surface-hover);
67
+ border-color: var(--border-strong);
68
+ }
71
69
 
72
- .ui-eyebrow {
73
- font-family: var(--font-mono);
74
- font-size: var(--text-2xs);
75
- font-weight: var(--weight-semibold);
76
- letter-spacing: var(--tracking-wider);
77
- text-transform: uppercase;
78
- color: var(--text-muted);
79
- }
70
+ .pl-news-nav:focus-visible {
71
+ outline: 2px solid var(--focus);
72
+ outline-offset: 2px;
73
+ }
80
74
 
81
- .ui-newsrow__title {
82
- font-size: var(--text-lg);
83
- font-weight: var(--weight-semibold);
84
- letter-spacing: var(--tracking-tight);
85
- color: var(--text);
86
- }
75
+ .ui-eyebrow {
76
+ font-family: var(--font-mono);
77
+ font-size: var(--text-2xs);
78
+ font-weight: var(--weight-semibold);
79
+ letter-spacing: var(--tracking-wider);
80
+ text-transform: uppercase;
81
+ color: var(--text-muted);
82
+ }
87
83
 
88
- .pl-news-dots {
89
- display: flex;
90
- align-items: center;
91
- gap: var(--space-1-5);
92
- }
84
+ .ui-newsrow__title {
85
+ font-size: var(--text-lg);
86
+ font-weight: var(--weight-semibold);
87
+ letter-spacing: var(--tracking-tight);
88
+ color: var(--text);
89
+ }
93
90
 
94
- .pl-news-dot {
95
- width: 7px;
96
- height: 7px;
97
- padding: 0;
98
- border: 0;
99
- border-radius: var(--radius-full);
100
- background: var(--border-strong);
101
- cursor: pointer;
102
- transition:
103
- width var(--duration-base) var(--ease-enter),
104
- background-color var(--duration-base) var(--ease-standard);
105
- outline-offset: 4px;
106
- }
91
+ .pl-news-dots {
92
+ display: flex;
93
+ align-items: center;
94
+ gap: var(--space-1-5);
95
+ }
107
96
 
108
- .pl-news-dot[data-on] {
109
- width: 22px;
110
- background: var(--primary);
111
- }
97
+ .pl-news-dot {
98
+ width: 7px;
99
+ height: 7px;
100
+ padding: 0;
101
+ border: 0;
102
+ border-radius: var(--radius-full);
103
+ background: var(--border-strong);
104
+ cursor: pointer;
105
+ transition:
106
+ width var(--duration-base) var(--ease-enter),
107
+ background-color var(--duration-base) var(--ease-standard);
108
+ outline-offset: 4px;
109
+ }
112
110
 
113
- .pl-news-dot:focus-visible {
114
- outline: 2px solid var(--focus);
115
- }
111
+ .pl-news-dot[data-on] {
112
+ width: 22px;
113
+ background: var(--primary);
114
+ }
116
115
 
117
- @media (prefers-reduced-motion: reduce) {
118
- .pl-news-item,
119
- .pl-news-dot {
120
- transition: none;
116
+ .pl-news-dot:focus-visible {
117
+ outline: 2px solid var(--focus);
118
+ }
119
+
120
+ @media (prefers-reduced-motion: reduce) {
121
+ .pl-news-item,
122
+ .pl-news-dot {
123
+ transition: none;
124
+ }
121
125
  }
122
126
  }
@@ -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"