@kouji-ui/components 0.9.2 → 0.9.4

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 (73) hide show
  1. package/fesm2022/kouji-ui-components.mjs +49 -3
  2. package/fesm2022/kouji-ui-components.mjs.map +1 -1
  3. package/package.json +1 -1
  4. package/src/accordion/accordion.css +68 -0
  5. package/src/action-sheet/action-sheet.css +79 -0
  6. package/src/alert/alert.css +133 -0
  7. package/src/avatar/avatar.css +93 -0
  8. package/src/badge/badge.css +59 -0
  9. package/src/breadcrumb/breadcrumb.css +145 -0
  10. package/src/button/button.css +295 -0
  11. package/src/button-group/button-group.css +114 -0
  12. package/src/calendar/calendar.css +129 -0
  13. package/src/card/card.css +120 -0
  14. package/src/carousel/carousel.css +181 -0
  15. package/src/cascade-select/cascade-select.css +120 -0
  16. package/src/chart/chart.css +26 -0
  17. package/src/chat/chat-ai.css +352 -0
  18. package/src/chat/chat.css +231 -0
  19. package/src/checkbox/checkbox.css +74 -0
  20. package/src/color-picker/color-picker.css +183 -0
  21. package/src/combobox/combobox.css +101 -0
  22. package/src/command-palette/command-palette.css +234 -0
  23. package/src/confirm-popup/confirm-popup.css +47 -0
  24. package/src/date-picker/date-picker.css +61 -0
  25. package/src/date-range-presets/date-range-presets.css +49 -0
  26. package/src/datetime-picker/datetime-picker.css +84 -0
  27. package/src/dialog/dialog.css +56 -0
  28. package/src/direction-toggle/direction-toggle.css +55 -0
  29. package/src/divider/divider.css +93 -0
  30. package/src/drawer/drawer.css +130 -0
  31. package/src/dropdown-menu/dropdown-menu.css +108 -0
  32. package/src/editor/editor.css +140 -0
  33. package/src/empty-state/empty-state.css +114 -0
  34. package/src/field/field.css +148 -0
  35. package/src/file-upload/file-upload.css +207 -0
  36. package/src/form/form.css +63 -0
  37. package/src/input/input.css +70 -0
  38. package/src/input-group/input-group.css +125 -0
  39. package/src/input-mask/input-mask.css +39 -0
  40. package/src/input-otp/input-otp.css +87 -0
  41. package/src/kbd/kbd.css +67 -0
  42. package/src/link/link.css +104 -0
  43. package/src/list/list.css +166 -0
  44. package/src/menubar/menubar.css +62 -0
  45. package/src/number-input/number-input.css +115 -0
  46. package/src/overflow/overflow.css +43 -0
  47. package/src/overlay/overlay.css +45 -0
  48. package/src/overlay-badge/overlay-badge.css +118 -0
  49. package/src/pagination/pagination.css +146 -0
  50. package/src/password-input/password-input.css +155 -0
  51. package/src/popover/popover.css +71 -0
  52. package/src/progress-bar/progress-bar.css +113 -0
  53. package/src/radio/radio.css +58 -0
  54. package/src/rich-text/rich-text-editor.css +351 -0
  55. package/src/select/select.css +91 -0
  56. package/src/sheet/sheet.css +121 -0
  57. package/src/skeleton/skeleton.css +111 -0
  58. package/src/skip-link/skip-link.css +58 -0
  59. package/src/slider/slider.css +172 -0
  60. package/src/speed-dial/speed-dial.css +137 -0
  61. package/src/spinner/spinner.css +161 -0
  62. package/src/stepper/stepper.css +147 -0
  63. package/src/table/table-filters/filters.css +53 -0
  64. package/src/table/table.css +509 -0
  65. package/src/tabs/tabs.css +162 -0
  66. package/src/tag/tag.css +183 -0
  67. package/src/textarea/textarea.css +117 -0
  68. package/src/time-picker/time-picker.css +126 -0
  69. package/src/toast/toast.css +115 -0
  70. package/src/toggle/toggle.css +123 -0
  71. package/src/tooltip/tooltip.css +61 -0
  72. package/src/tree-select/tree-select.css +220 -0
  73. package/types/kouji-ui-components.d.ts +19 -0
@@ -0,0 +1,146 @@
1
+ /* ─────────────────────────────────────────────────────────────
2
+ .kj-pagination — component-layer tokens + structural CSS
3
+ The directive family reflects state via `data-current` /
4
+ `data-disabled` / `aria-current` / `aria-disabled` /
5
+ `data-pagination-action` — every selector here keys off those.
6
+ ──────────────────────────────────────────────────────────── */
7
+
8
+ @layer kj.component {
9
+ .kj-pagination {
10
+ /* component knobs — themes/users can override any of these */
11
+ --kj-pagination-gap: var(--kj-space-xs);
12
+ --kj-pagination-item-size: 2.75rem; /* 44px — WCAG 2.5.5 */
13
+ --kj-pagination-item-radius: var(--kj-radius-field);
14
+ --kj-pagination-item-bg: transparent;
15
+ --kj-pagination-item-bg-hover: var(--kj-bg-surface);
16
+ --kj-pagination-item-bg-current: var(--kj-bg-primary);
17
+ --kj-pagination-item-fg: var(--kj-fg-default);
18
+ --kj-pagination-item-fg-current: var(--kj-fg-on-primary);
19
+ --kj-pagination-item-border-color: var(--kj-border-default);
20
+ --kj-pagination-item-border-width: var(--kj-border);
21
+ --kj-pagination-item-font: var(--kj-font-sans);
22
+ --kj-pagination-item-font-size: var(--kj-text-sm);
23
+ --kj-pagination-ellipsis-color: var(--kj-fg-default);
24
+ --kj-pagination-info-color: var(--kj-fg-default);
25
+
26
+ display: inline-flex;
27
+ align-items: center;
28
+ flex-wrap: wrap;
29
+ gap: var(--kj-pagination-gap);
30
+ color: var(--kj-fg-default);
31
+ }
32
+
33
+ /* Numeric items + boundary controls share the same visual chrome —
34
+ the `.kj-pagination-action` class names map to Previous / Next /
35
+ First / Last so consumers can target them individually if needed. */
36
+ .kj-pagination-item,
37
+ .kj-pagination-action {
38
+ display: inline-flex;
39
+ align-items: center;
40
+ justify-content: center;
41
+ min-width: var(--kj-pagination-item-size);
42
+ min-height: var(--kj-pagination-item-size);
43
+ padding: 0 var(--kj-space-sm);
44
+ background: var(--kj-pagination-item-bg);
45
+ color: var(--kj-pagination-item-fg);
46
+ border: var(--kj-pagination-item-border-width) solid var(--kj-pagination-item-border-color);
47
+ border-radius: var(--kj-pagination-item-radius);
48
+ font: var(--kj-pagination-item-font-size) / 1.2 var(--kj-pagination-item-font);
49
+ cursor: pointer;
50
+ user-select: none;
51
+ transition: var(--kj-transition);
52
+ }
53
+
54
+ .kj-pagination-item:hover,
55
+ .kj-pagination-action:hover {
56
+ background: var(--kj-pagination-item-bg-hover);
57
+ }
58
+
59
+ .kj-pagination-item:focus-visible,
60
+ .kj-pagination-action:focus-visible {
61
+ outline: 2px solid var(--kj-fg-primary);
62
+ outline-offset: 2px;
63
+ }
64
+
65
+ /* Active page — keep the touch target intact (don't shrink the active
66
+ state). The directive reflects `aria-current="page"` and
67
+ `data-current="true"`; we key off `aria-current` for AT correctness. */
68
+ .kj-pagination-item[aria-current='page'] {
69
+ background: var(--kj-pagination-item-bg-current);
70
+ color: var(--kj-pagination-item-fg-current);
71
+ border-color: var(--kj-pagination-item-bg-current);
72
+ cursor: default;
73
+ }
74
+ .kj-pagination-item[aria-current='page']:hover {
75
+ background: var(--kj-pagination-item-bg-current);
76
+ }
77
+
78
+ /* Disabled — `aria-disabled="true"` rather than the native attribute,
79
+ so the control stays focusable (kouji's WCAG 2.1 AAA stance). */
80
+ .kj-pagination-item[aria-disabled='true'],
81
+ .kj-pagination-action[aria-disabled='true'] {
82
+ opacity: 0.45;
83
+ cursor: not-allowed;
84
+ }
85
+ .kj-pagination-item[aria-disabled='true']:hover,
86
+ .kj-pagination-action[aria-disabled='true']:hover {
87
+ background: var(--kj-pagination-item-bg);
88
+ }
89
+
90
+ /* Ellipsis is non-interactive — no hover, no border, no touch target. */
91
+ .kj-pagination-ellipsis {
92
+ display: inline-flex;
93
+ align-items: center;
94
+ justify-content: center;
95
+ min-width: var(--kj-pagination-item-size);
96
+ min-height: var(--kj-pagination-item-size);
97
+ color: var(--kj-pagination-ellipsis-color);
98
+ user-select: none;
99
+ }
100
+
101
+ /* Info span — AT-readable, visible. */
102
+ .kj-pagination-info {
103
+ display: inline-flex;
104
+ align-items: center;
105
+ padding: 0 var(--kj-space-sm);
106
+ color: var(--kj-pagination-info-color);
107
+ font: var(--kj-pagination-item-font-size) / 1.2 var(--kj-pagination-item-font);
108
+ }
109
+
110
+ /* ── Variants ──────────────────────────────────────────────── */
111
+ /* The root cascades `data-variant` to children via the context; each
112
+ child's KjVariant host directive reflects its own. We target the
113
+ children directly so the wrapper variant flows through. */
114
+
115
+ .kj-pagination-item[data-variant='outline'],
116
+ .kj-pagination-action[data-variant='outline'] {
117
+ --kj-pagination-item-bg: transparent;
118
+ --kj-pagination-item-border-color: var(--kj-border-default);
119
+ }
120
+
121
+ .kj-pagination-item[data-variant='ghost'],
122
+ .kj-pagination-action[data-variant='ghost'] {
123
+ --kj-pagination-item-bg: transparent;
124
+ --kj-pagination-item-border-color: transparent;
125
+ }
126
+
127
+ /* ── Sizes ─────────────────────────────────────────────────── */
128
+ .kj-pagination-item[data-size='xs'],
129
+ .kj-pagination-action[data-size='xs'] {
130
+ min-width: 1.5rem;
131
+ min-height: 1.5rem;
132
+ --kj-pagination-item-font-size: 0.75rem;
133
+ }
134
+ .kj-pagination-item[data-size='sm'],
135
+ .kj-pagination-action[data-size='sm'] {
136
+ min-width: 2rem;
137
+ min-height: 2rem;
138
+ --kj-pagination-item-font-size: 0.8125rem;
139
+ }
140
+ .kj-pagination-item[data-size='lg'],
141
+ .kj-pagination-action[data-size='lg'] {
142
+ min-width: 3rem;
143
+ min-height: 3rem;
144
+ --kj-pagination-item-font-size: 1rem;
145
+ }
146
+ }
@@ -0,0 +1,155 @@
1
+ /* ─────────────────────────────────────────────────────────────
2
+ .kj-password-input — component-layer tokens + structural CSS
3
+ Same pattern as input.css / button.css: tokens default to the
4
+ shared layer; variants/states retarget the component tokens.
5
+ ──────────────────────────────────────────────────────────── */
6
+
7
+ @layer kj.component {
8
+ .kj-password-input {
9
+ --kj-password-input-bg: var(--kj-bg-field);
10
+ --kj-password-input-fg: var(--kj-fg-default);
11
+ --kj-password-input-border-color: var(--kj-border-default);
12
+ --kj-password-input-border-style: solid;
13
+ --kj-password-input-border-width: var(--kj-border);
14
+ --kj-password-input-radius: var(--kj-radius-field);
15
+ --kj-password-input-padding-x: var(--kj-space-md);
16
+ --kj-password-input-padding-y: var(--kj-space-sm);
17
+ --kj-password-input-font: var(--kj-font-sans);
18
+ --kj-password-input-font-size: var(--kj-text-sm);
19
+ --kj-password-input-placeholder-fg: var(--kj-fg-muted);
20
+ --kj-password-input-toggle-fg: var(--kj-fg-muted);
21
+ --kj-password-input-toggle-fg-hover: var(--kj-fg-default);
22
+ /* Explicit height contract (same pattern as input.css / button.css) so
23
+ password-input lines up with sibling inputs in a form row. */
24
+ --kj-password-input-height: 2.25rem;
25
+
26
+ display: inline-flex;
27
+ align-items: center;
28
+ gap: var(--kj-space-xs);
29
+ inline-size: 100%;
30
+ height: var(--kj-password-input-height);
31
+ background: var(--kj-password-input-bg);
32
+ color: var(--kj-password-input-fg);
33
+ border: var(--kj-password-input-border-width)
34
+ var(--kj-password-input-border-style)
35
+ var(--kj-password-input-border-color);
36
+ border-radius: var(--kj-password-input-radius);
37
+ padding: 0 var(--kj-password-input-padding-x);
38
+ font: var(--kj-password-input-font-size) / 1.4 var(--kj-password-input-font);
39
+ transition: var(--kj-transition);
40
+ }
41
+ .kj-password-input[data-size="sm"] { --kj-password-input-height: 2rem; --kj-password-input-font-size: 0.8125rem; }
42
+ .kj-password-input[data-size="lg"] { --kj-password-input-height: 2.75rem; --kj-password-input-font-size: 1rem; }
43
+
44
+ .kj-password-input__field {
45
+ flex: 1 1 auto;
46
+ inline-size: 100%;
47
+ height: 100%;
48
+ background: transparent;
49
+ color: inherit;
50
+ border: 0;
51
+ outline: 0;
52
+ padding: 0;
53
+ font: inherit;
54
+ }
55
+ .kj-password-input__field::placeholder {
56
+ color: var(--kj-password-input-placeholder-fg);
57
+ }
58
+
59
+ .kj-password-input:focus-within {
60
+ outline: 2px solid var(--kj-fg-primary);
61
+ outline-offset: 2px;
62
+ --kj-password-input-border-color: var(--kj-fg-primary);
63
+ }
64
+ .kj-password-input:has([aria-invalid="true"]) {
65
+ --kj-password-input-border-color: var(--kj-fg-danger);
66
+ }
67
+ .kj-password-input:has([aria-disabled="true"]),
68
+ .kj-password-input:has(:disabled) {
69
+ opacity: 0.55;
70
+ cursor: not-allowed;
71
+ }
72
+
73
+ /* eye-icon toggle button: ghost, square, in-row */
74
+ .kj-password-input__toggle {
75
+ display: inline-flex;
76
+ align-items: center;
77
+ justify-content: center;
78
+ inline-size: 1.75rem;
79
+ block-size: 1.75rem;
80
+ margin-inline-start: var(--kj-space-xs);
81
+ color: var(--kj-password-input-toggle-fg);
82
+ background: transparent;
83
+ border: 0;
84
+ border-radius: var(--kj-radius-field);
85
+ cursor: pointer;
86
+ }
87
+ .kj-password-input__toggle:hover,
88
+ .kj-password-input__toggle:focus-visible {
89
+ color: var(--kj-password-input-toggle-fg-hover);
90
+ }
91
+ .kj-password-input__toggle:focus-visible {
92
+ outline: 2px solid var(--kj-fg-primary);
93
+ outline-offset: 2px;
94
+ }
95
+
96
+ .kj-password-input__icon {
97
+ inline-size: 1rem;
98
+ block-size: 1rem;
99
+ fill: none;
100
+ stroke: currentColor;
101
+ stroke-width: 1.5;
102
+ stroke-linecap: round;
103
+ stroke-linejoin: round;
104
+ }
105
+
106
+ /* strength meter — 4-segment bar, colour ramp owned here */
107
+ .kj-password-strength {
108
+ display: flex;
109
+ gap: var(--kj-space-xs);
110
+ inline-size: 100%;
111
+ margin-block-start: var(--kj-space-xs);
112
+ }
113
+ .kj-password-strength__seg {
114
+ flex: 1 1 0;
115
+ block-size: 0.25rem;
116
+ background: var(--kj-bg-surface);
117
+ border-radius: 2px;
118
+ transition: background-color var(--kj-transition);
119
+ }
120
+ .kj-password-strength[data-score="1"] .kj-password-strength__seg:nth-child(-n+1),
121
+ .kj-password-strength[data-score="2"] .kj-password-strength__seg:nth-child(-n+2),
122
+ .kj-password-strength[data-score="3"] .kj-password-strength__seg:nth-child(-n+3),
123
+ .kj-password-strength[data-score="4"] .kj-password-strength__seg:nth-child(-n+4) {
124
+ background: var(--kj-password-strength-color, var(--kj-bg-primary));
125
+ }
126
+ .kj-password-strength[data-score="1"] {
127
+ --kj-password-strength-color: var(--kj-bg-danger);
128
+ }
129
+ .kj-password-strength[data-score="2"] {
130
+ --kj-password-strength-color: var(--kj-bg-warning, #d97706);
131
+ }
132
+ .kj-password-strength[data-score="3"] {
133
+ --kj-password-strength-color: var(--kj-bg-success, #059669);
134
+ }
135
+ .kj-password-strength[data-score="4"] {
136
+ --kj-password-strength-color: var(--kj-bg-success, #059669);
137
+ }
138
+
139
+ .kj-password-strength__label {
140
+ display: block;
141
+ margin-block-start: var(--kj-space-xs);
142
+ font-size: var(--kj-text-xs);
143
+ color: var(--kj-fg-muted);
144
+ }
145
+
146
+ .kj-password-caps-lock-warning {
147
+ display: block;
148
+ margin-block-start: var(--kj-space-xs);
149
+ font-size: var(--kj-text-xs);
150
+ color: var(--kj-fg-warning, #d97706);
151
+ }
152
+ .kj-password-caps-lock-warning[hidden] {
153
+ display: none;
154
+ }
155
+ }
@@ -0,0 +1,71 @@
1
+ @layer kj.component {
2
+ /* The wrapper hosts use display: contents — they don't add a box. The
3
+ panel is portal-mounted to <body> by the underlying directive; the
4
+ `.kj-popover-content` class lands on the panel via `kjPanelClass`. */
5
+
6
+ .kj-popover-content {
7
+ --kj-popover-bg: var(--kj-bg-elevated);
8
+ --kj-popover-fg: var(--kj-fg-default);
9
+ --kj-popover-border-color: var(--kj-border-default);
10
+ --kj-popover-radius: var(--kj-radius-box);
11
+ --kj-popover-padding-x: var(--kj-space-md);
12
+ --kj-popover-padding-y: var(--kj-space-md);
13
+ --kj-popover-shadow: var(--kj-shadow-md, 0 12px 32px rgb(0 0 0 / 0.18));
14
+ --kj-popover-arrow-size: 8px;
15
+
16
+ /* Layout — the directive applies position: fixed and writes top/left. */
17
+ display: block;
18
+ box-sizing: border-box;
19
+ max-width: min(28rem, calc(100vw - 2rem));
20
+ background: var(--kj-popover-bg);
21
+ color: var(--kj-popover-fg);
22
+ border: var(--kj-border) solid var(--kj-popover-border-color);
23
+ border-radius: var(--kj-popover-radius);
24
+ padding: var(--kj-popover-padding-y) var(--kj-popover-padding-x);
25
+ font: 0.875rem / 1.5 var(--kj-font-sans);
26
+ box-shadow: var(--kj-popover-shadow);
27
+ pointer-events: auto;
28
+ z-index: var(--kj-overlay-z, 1000);
29
+ word-wrap: break-word;
30
+ }
31
+
32
+ .kj-popover-content[hidden] { display: none; }
33
+
34
+ .kj-popover-title {
35
+ display: block;
36
+ font-size: 0.9375rem;
37
+ font-weight: 600;
38
+ margin: 0 0 var(--kj-space-xs);
39
+ color: var(--kj-fg-default);
40
+ }
41
+
42
+ /* Arrow positioning relative to the panel's resolved data-side. */
43
+ .kj-popover-arrow {
44
+ position: absolute;
45
+ width: var(--kj-popover-arrow-size, 8px);
46
+ height: var(--kj-popover-arrow-size, 8px);
47
+ background: var(--kj-popover-bg, var(--kj-bg-elevated));
48
+ border: var(--kj-border) solid var(--kj-popover-border-color, var(--kj-border-default));
49
+ transform: rotate(45deg);
50
+ }
51
+
52
+ .kj-popover-content[data-side="top"] .kj-popover-arrow { bottom: calc(var(--kj-popover-arrow-size) / -2); left: 50%; margin-left: calc(var(--kj-popover-arrow-size) / -2); border-top-color: transparent; border-left-color: transparent; }
53
+ .kj-popover-content[data-side="bottom"] .kj-popover-arrow { top: calc(var(--kj-popover-arrow-size) / -2); left: 50%; margin-left: calc(var(--kj-popover-arrow-size) / -2); border-bottom-color: transparent; border-right-color: transparent; }
54
+ .kj-popover-content[data-side="left"] .kj-popover-arrow { right: calc(var(--kj-popover-arrow-size) / -2); top: 50%; margin-top: calc(var(--kj-popover-arrow-size) / -2); border-left-color: transparent; border-bottom-color: transparent; }
55
+ .kj-popover-content[data-side="right"] .kj-popover-arrow { left: calc(var(--kj-popover-arrow-size) / -2); top: 50%; margin-top: calc(var(--kj-popover-arrow-size) / -2); border-right-color: transparent; border-top-color: transparent; }
56
+
57
+ /* Modal-mode body scroll lock. Multiple stacked modals coordinate via
58
+ a counter in KjOverlayService; this just hides body overflow when the
59
+ attribute is present. */
60
+ body[data-kj-scroll-lock="true"] {
61
+ overflow: hidden;
62
+ }
63
+
64
+ @media (prefers-reduced-motion: no-preference) {
65
+ .kj-popover-content {
66
+ transition: opacity 140ms ease-out, transform 140ms ease-out;
67
+ }
68
+ .kj-popover-content[data-state="closed"] { opacity: 0; transform: scale(0.98); }
69
+ .kj-popover-content[data-state="open"] { opacity: 1; transform: scale(1); }
70
+ }
71
+ }
@@ -0,0 +1,113 @@
1
+ /* ─────────────────────────────────────────────────────────────
2
+ .kj-progress-bar — component-layer tokens + structural CSS
3
+ The directive owns the value math + ARIA contract. The theme
4
+ only renders track + fill. Variant tokens flip the fill colour;
5
+ size tokens flip the bar height. The indeterminate stripe is a
6
+ pure CSS animation, neutralised under prefers-reduced-motion.
7
+ ──────────────────────────────────────────────────────────── */
8
+
9
+ @layer kj.component {
10
+ .kj-progress-bar {
11
+ /* component knobs — themes/users can override any of these */
12
+ --kj-progress-bar-track: var(--kj-bg-field);
13
+ --kj-progress-bar-fill: var(--kj-bg-primary);
14
+ --kj-progress-bar-radius: 9999px;
15
+ --kj-progress-bar-height: 0.5rem; /* md default — 8px */
16
+ --kj-progress-bar-transition: 200ms ease-out;
17
+ --kj-progress-bar-indeterminate-duration: 1.6s;
18
+
19
+ display: block;
20
+ position: relative;
21
+ width: 100%;
22
+ height: var(--kj-progress-bar-height);
23
+ background: var(--kj-progress-bar-track);
24
+ border-radius: var(--kj-progress-bar-radius);
25
+ overflow: hidden;
26
+ }
27
+
28
+ /* ── Sizes ── (data-size mirrored by KjSize host directive) */
29
+ .kj-progress-bar[data-size="xs"] { --kj-progress-bar-height: 0.125rem; } /* 2px */
30
+ .kj-progress-bar[data-size="sm"] { --kj-progress-bar-height: 0.25rem; } /* 4px */
31
+ .kj-progress-bar[data-size="md"] { --kj-progress-bar-height: 0.5rem; } /* 8px */
32
+ .kj-progress-bar[data-size="lg"] { --kj-progress-bar-height: 0.75rem; } /* 12px */
33
+
34
+ /* ── Variants ── (data-variant mirrored by KjVariant host directive) */
35
+ .kj-progress-bar[data-variant="primary"] { --kj-progress-bar-fill: var(--kj-bg-primary); }
36
+ .kj-progress-bar[data-variant="success"] { --kj-progress-bar-fill: var(--kj-bg-success); }
37
+ .kj-progress-bar[data-variant="warning"] { --kj-progress-bar-fill: var(--kj-bg-warning); }
38
+ .kj-progress-bar[data-variant="error"] { --kj-progress-bar-fill: var(--kj-bg-danger); }
39
+
40
+ /* ── Fill (determinate) ──
41
+ The directive sets --kj-progress-fraction (0..1) on the host AND on the
42
+ fill. Reading from the fill is one selector hop shorter and lets us scope
43
+ the transition cleanly. */
44
+ .kj-progress-bar__fill {
45
+ display: block;
46
+ width: 100%;
47
+ height: 100%;
48
+ background: var(--kj-progress-bar-fill);
49
+ border-radius: inherit;
50
+ transform-origin: left center;
51
+ transform: scaleX(var(--kj-progress-fraction, 0));
52
+ /* Transition the fill width on value changes — but never the colour, so
53
+ a variant flip mid-flight is instantaneous. */
54
+ transition: transform var(--kj-progress-bar-transition);
55
+ }
56
+
57
+ /* RTL: the fill grows from the right edge. */
58
+ [dir="rtl"] .kj-progress-bar__fill {
59
+ transform-origin: right center;
60
+ }
61
+
62
+ /* ── Fill (indeterminate) ──
63
+ When data-indeterminate="true", abandon the scaleX(fraction) transform
64
+ and run a stripe that travels left → right. The stripe is a 50%-wide
65
+ coloured band on a transparent track, which gives a clean "working"
66
+ read on every theme without needing the track itself to animate. */
67
+ .kj-progress-bar__fill[data-indeterminate="true"] {
68
+ transform: none;
69
+ background: linear-gradient(
70
+ 90deg,
71
+ transparent 0%,
72
+ var(--kj-progress-bar-fill) 50%,
73
+ transparent 100%
74
+ );
75
+ background-size: 50% 100%;
76
+ background-repeat: no-repeat;
77
+ animation: kj-progress-bar-indeterminate
78
+ var(--kj-progress-bar-indeterminate-duration) linear infinite;
79
+ }
80
+
81
+ @keyframes kj-progress-bar-indeterminate {
82
+ from { background-position: -50% 0; }
83
+ to { background-position: 150% 0; }
84
+ }
85
+
86
+ /* ── Reduced motion ──
87
+ APG / WCAG 2.3.3: animations from interactions must respect
88
+ prefers-reduced-motion. The directive reflects data-reduced-motion;
89
+ we also honour the media query directly so the rule kicks in even if
90
+ the directive's media-query subscription hasn't run yet (SSR, very
91
+ early paint). The indeterminate stripe collapses to a static, dimmed
92
+ fill — still visible as "something is happening", just not moving. */
93
+ .kj-progress-bar[data-reduced-motion="reduce"] .kj-progress-bar__fill,
94
+ .kj-progress-bar[data-reduced-motion="reduce"] .kj-progress-bar__fill[data-indeterminate="true"] {
95
+ animation: none;
96
+ transition: none;
97
+ }
98
+ .kj-progress-bar[data-reduced-motion="reduce"] .kj-progress-bar__fill[data-indeterminate="true"] {
99
+ background: var(--kj-progress-bar-fill);
100
+ opacity: 0.6;
101
+ }
102
+
103
+ @media (prefers-reduced-motion: reduce) {
104
+ .kj-progress-bar__fill {
105
+ transition: none;
106
+ }
107
+ .kj-progress-bar__fill[data-indeterminate="true"] {
108
+ animation: none;
109
+ background: var(--kj-progress-bar-fill);
110
+ opacity: 0.6;
111
+ }
112
+ }
113
+ }
@@ -0,0 +1,58 @@
1
+ @layer kj.component {
2
+ .kj-radio-group {
3
+ display: flex;
4
+ flex-direction: column;
5
+ gap: var(--kj-space-sm);
6
+ }
7
+ .kj-radio-group[data-orientation="horizontal"] { flex-direction: row; gap: var(--kj-space-lg); }
8
+
9
+ /* Outer host. Acts as a single click target whose label area forwards to
10
+ the inner dot. */
11
+ .kj-radio {
12
+ display: inline-flex;
13
+ }
14
+ .kj-radio-inner {
15
+ display: inline-flex;
16
+ align-items: center;
17
+ gap: var(--kj-space-sm);
18
+ cursor: pointer;
19
+ color: var(--kj-fg-default);
20
+ font: 0.875rem / 1.4 var(--kj-font-sans);
21
+ }
22
+ .kj-radio-label { user-select: none; }
23
+
24
+ /* The interactive dot (carries the kjRadio directive). */
25
+ .kj-radio-dot {
26
+ --kj-radio-size: 1rem;
27
+ --kj-radio-border: var(--kj-border-default);
28
+ --kj-radio-bg-checked: var(--kj-bg-primary);
29
+
30
+ display: inline-block;
31
+ width: var(--kj-radio-size);
32
+ height: var(--kj-radio-size);
33
+ flex-shrink: 0;
34
+ border: var(--kj-border) solid var(--kj-radio-border);
35
+ border-radius: 9999px;
36
+ background: var(--kj-bg-field);
37
+ position: relative;
38
+ transition: var(--kj-transition);
39
+ }
40
+ .kj-radio-dot[data-checked]::after {
41
+ content: '';
42
+ position: absolute;
43
+ inset: 25%;
44
+ border-radius: 9999px;
45
+ background: var(--kj-radio-bg-checked);
46
+ }
47
+ .kj-radio-dot[data-checked] { border-color: var(--kj-radio-bg-checked); }
48
+ .kj-radio-dot:focus-visible { outline: 2px solid var(--kj-fg-primary); outline-offset: 2px; }
49
+
50
+ /* Disabled — dim the whole group (dot + label text) for visual consistency
51
+ with native form controls. */
52
+ .kj-radio[data-disabled] .kj-radio-inner { cursor: not-allowed; }
53
+ .kj-radio[data-disabled] .kj-radio-dot,
54
+ .kj-radio[data-disabled] .kj-radio-label {
55
+ opacity: 0.45;
56
+ }
57
+ .kj-radio[data-disabled] .kj-radio-dot { pointer-events: none; }
58
+ }