handzon-core 0.6.1 → 0.6.2

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,144 @@
1
+ /* Shared dropdown component (handzon-core/src/components/ui/Dropdown.tsx).
2
+ *
3
+ * Wraps Radix Select so we never ship the browser-native dropdown UI.
4
+ * Every dropdown in handzon-core inherits these styles — keep the look
5
+ * mono/brutalist (hard edges, uppercase mono label chip, accent border
6
+ * on focus / open) so dropdowns sit alongside the search input, pills,
7
+ * and pagination buttons without visual drift.
8
+ */
9
+
10
+ .hz-dd {
11
+ display: inline-flex;
12
+ align-items: stretch;
13
+ font-family: var(--font-mono);
14
+ font-size: 0.78em;
15
+ }
16
+
17
+ /* Uppercase mono label chip glued to the trigger. */
18
+ .hz-dd-label {
19
+ display: inline-flex;
20
+ align-items: center;
21
+ padding: 0 0.6rem;
22
+ border: 1px solid var(--color-border);
23
+ border-right: 0;
24
+ color: var(--color-muted);
25
+ text-transform: uppercase;
26
+ letter-spacing: 0.06em;
27
+ user-select: none;
28
+ }
29
+
30
+ .hz-dd-trigger {
31
+ display: inline-flex;
32
+ align-items: center;
33
+ gap: 0.45rem;
34
+ padding: 0.4rem 0.6rem;
35
+ /* Match the search input's natural height so the whole toolbar row
36
+ * shares a single baseline. Without this, the Sort dropdown reads
37
+ * ~6px shorter than the multi-select triggers which stretch via
38
+ * the toolbar's align-items: stretch. */
39
+ min-height: 2.5rem;
40
+ background: transparent;
41
+ border: 1px solid var(--color-border);
42
+ color: var(--color-fg);
43
+ font: inherit;
44
+ cursor: pointer;
45
+ outline: none;
46
+ transition: border-color 0.12s ease, color 0.12s ease;
47
+ }
48
+ .hz-dd-trigger:hover {
49
+ border-color: var(--color-accent);
50
+ }
51
+ .hz-dd-trigger:focus-visible,
52
+ .hz-dd-trigger[data-state="open"] {
53
+ border-color: var(--color-accent);
54
+ color: var(--color-accent);
55
+ }
56
+ .hz-dd-trigger[data-placeholder] {
57
+ color: var(--color-muted);
58
+ }
59
+
60
+ .hz-dd-tricon {
61
+ display: inline-flex;
62
+ align-items: center;
63
+ color: var(--color-muted);
64
+ }
65
+
66
+ .hz-dd-caret {
67
+ display: inline-flex;
68
+ align-items: center;
69
+ margin-left: 0.15rem;
70
+ color: var(--color-muted);
71
+ transition: transform 0.15s ease;
72
+ }
73
+ .hz-dd-trigger[data-state="open"] .hz-dd-caret {
74
+ transform: rotate(180deg);
75
+ color: var(--color-accent);
76
+ }
77
+
78
+ /* ---------- Popover ---------- */
79
+
80
+ .hz-dd-content {
81
+ /* Radix Portal renders this outside the normal flow — float it above
82
+ * the page chrome, including the sticky sidebar (z=10) and the fixed
83
+ * topbar (z=50). */
84
+ z-index: 100;
85
+ min-width: var(--radix-select-trigger-width);
86
+ max-height: var(--radix-select-content-available-height);
87
+ background: var(--color-bg);
88
+ border: 1px solid var(--color-border);
89
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
90
+ overflow: hidden;
91
+ }
92
+
93
+ .hz-dd-viewport {
94
+ padding: 0.25rem 0;
95
+ }
96
+
97
+ .hz-dd-item {
98
+ display: flex;
99
+ align-items: center;
100
+ gap: 0.5rem;
101
+ padding: 0.45rem 0.75rem;
102
+ color: var(--color-fg);
103
+ cursor: pointer;
104
+ user-select: none;
105
+ outline: none;
106
+ }
107
+ .hz-dd-item[data-highlighted] {
108
+ background: var(--color-surface);
109
+ color: var(--color-accent);
110
+ }
111
+ .hz-dd-item[data-state="checked"] {
112
+ color: var(--color-accent);
113
+ }
114
+ .hz-dd-item[data-disabled] {
115
+ opacity: 0.4;
116
+ pointer-events: none;
117
+ }
118
+
119
+ .hz-dd-icon {
120
+ display: inline-flex;
121
+ align-items: center;
122
+ color: var(--color-muted);
123
+ }
124
+ .hz-dd-item[data-highlighted] .hz-dd-icon,
125
+ .hz-dd-item[data-state="checked"] .hz-dd-icon {
126
+ color: var(--color-accent);
127
+ }
128
+
129
+ .hz-dd-check {
130
+ margin-left: auto;
131
+ display: inline-flex;
132
+ align-items: center;
133
+ color: var(--color-accent);
134
+ }
135
+
136
+ .hz-dd-scroll {
137
+ display: flex;
138
+ align-items: center;
139
+ justify-content: center;
140
+ height: 1.5rem;
141
+ color: var(--color-muted);
142
+ background: var(--color-bg);
143
+ cursor: default;
144
+ }
@@ -0,0 +1,128 @@
1
+ /* Home-page filter bar.
2
+ *
3
+ * One toolbar row with search + Level dropdown + Topics dropdown + a
4
+ * vertical divider + Sort dropdown. Beneath it: optional "popular
5
+ * topics" pill row (top tags by count) and an "active filters" chip
6
+ * row that only appears when at least one filter is set.
7
+ *
8
+ * The dropdowns themselves (their trigger chrome and popover content)
9
+ * are styled in dropdown.css and multiselect.css respectively — this
10
+ * file only handles the toolbar layout + the inline pill + chip rows.
11
+ */
12
+
13
+ .filterbar {
14
+ display: flex;
15
+ flex-direction: column;
16
+ gap: 0.75rem;
17
+ margin-top: 1.5rem;
18
+ }
19
+
20
+ /* ---------- Toolbar (search + facets + sort) ---------- */
21
+
22
+ .fb-toolbar {
23
+ display: flex;
24
+ flex-wrap: wrap;
25
+ align-items: stretch;
26
+ gap: 0.75rem;
27
+ }
28
+
29
+ .fb-toolbar .search {
30
+ flex: 1 1 16rem;
31
+ min-width: 12rem;
32
+ display: inline-flex;
33
+ align-items: center;
34
+ gap: 0.5rem;
35
+ padding: 0.45rem 0.7rem;
36
+ border: 1px solid var(--color-border);
37
+ color: var(--color-muted);
38
+ transition: border-color 0.12s ease, color 0.12s ease;
39
+ }
40
+ .fb-toolbar .search:focus-within {
41
+ border-color: var(--color-accent);
42
+ color: var(--color-fg);
43
+ }
44
+ .fb-toolbar .search input {
45
+ background: transparent;
46
+ border: 0;
47
+ color: var(--color-fg);
48
+ font: inherit;
49
+ outline: none;
50
+ width: 100%;
51
+ }
52
+
53
+ /* Visual separator marking "sort isn't a filter". The sort cluster
54
+ * pushes itself to the right edge so the divider falls in a natural
55
+ * spot. */
56
+ .fb-divider {
57
+ width: 0;
58
+ border-left: 1px solid var(--color-border);
59
+ align-self: stretch;
60
+ margin: 0 0.25rem;
61
+ }
62
+ .fb-sort-slot {
63
+ display: inline-flex;
64
+ align-items: center;
65
+ flex-shrink: 0;
66
+ }
67
+
68
+ /* On narrow viewports the toolbar wraps; sort drops to its own line.
69
+ * Keep the divider visible only when the row hasn't wrapped — when it
70
+ * has, the layout already reads as distinct rows. Cheap heuristic:
71
+ * hide the divider below ~640px viewport. */
72
+ @media (max-width: 640px) {
73
+ .fb-divider { display: none; }
74
+ }
75
+
76
+ /* ---------- Active filter chips ---------- */
77
+
78
+ .active-filters {
79
+ display: flex;
80
+ flex-wrap: wrap;
81
+ gap: 0.5rem;
82
+ padding-top: 0.75rem;
83
+ border-top: 1px solid var(--color-border);
84
+ }
85
+
86
+ .active-filter-chip {
87
+ display: inline-flex;
88
+ align-items: center;
89
+ gap: 0.35rem;
90
+ padding: 0.25rem 0.55rem;
91
+ background: transparent;
92
+ border: 1px solid var(--color-border);
93
+ color: var(--color-muted);
94
+ font-family: var(--font-mono);
95
+ font-size: 0.72em;
96
+ cursor: pointer;
97
+ transition: color 0.12s ease, border-color 0.12s ease;
98
+ }
99
+ .active-filter-chip:hover,
100
+ .active-filter-chip:focus-visible {
101
+ color: var(--color-accent);
102
+ border-color: var(--color-accent);
103
+ outline: none;
104
+ }
105
+ .active-filter-chip .afc-label { text-transform: lowercase; }
106
+
107
+ .active-filter-clear {
108
+ margin-left: 0.25rem;
109
+ padding: 0.25rem 0.55rem;
110
+ background: transparent;
111
+ border: 0;
112
+ color: var(--color-muted);
113
+ font-family: var(--font-mono);
114
+ font-size: 0.72em;
115
+ text-transform: uppercase;
116
+ letter-spacing: 0.06em;
117
+ cursor: pointer;
118
+ text-decoration: underline;
119
+ text-decoration-color: transparent;
120
+ text-underline-offset: 3px;
121
+ transition: color 0.12s ease, text-decoration-color 0.12s ease;
122
+ }
123
+ .active-filter-clear:hover,
124
+ .active-filter-clear:focus-visible {
125
+ color: var(--color-fg);
126
+ text-decoration-color: currentColor;
127
+ outline: none;
128
+ }
@@ -0,0 +1,206 @@
1
+ /* Multi-select dropdown (handzon-core/src/components/ui/MultiSelect.tsx).
2
+ *
3
+ * Wraps Radix Popover. Visual parity with the single-select Dropdown:
4
+ * trigger reuses .hz-dd-label + .hz-dd-trigger + .hz-dd-caret. The
5
+ * popover content (option list, in-popover search, count badges,
6
+ * footer Clear) is what's specific to multi-select.
7
+ */
8
+
9
+ /* The MultiSelect trigger is a single <button> wrapping the label
10
+ * chip + the inner trigger body. Radix Popover.Trigger asChild needs
11
+ * a *single* focusable element, so we collapse the structure into one
12
+ * button and let CSS draw the chip + body as siblings. */
13
+ .hz-ms-trigger {
14
+ background: transparent;
15
+ border: 0;
16
+ padding: 0;
17
+ /* The button is also a `.hz-dd` element. That sibling class sets
18
+ * font-size: 0.78em + the mono family. Don't redeclare font-size /
19
+ * font-weight / line-height / letter-spacing here — at equal
20
+ * specificity, this stylesheet loads after dropdown.css and would
21
+ * win, resolving `inherit` to the body's 16px and making the
22
+ * trigger taller than the single-select Sort dropdown. We only
23
+ * need to undo the UA button defaults that .hz-dd doesn't already
24
+ * cover (font-family + color). */
25
+ font-family: inherit;
26
+ color: inherit;
27
+ display: inline-flex;
28
+ align-items: stretch;
29
+ cursor: pointer;
30
+ }
31
+ .hz-ms-trigger-body {
32
+ display: inline-flex;
33
+ align-items: center;
34
+ gap: 0.45rem;
35
+ padding: 0.4rem 0.6rem;
36
+ min-height: 2.5rem;
37
+ background: transparent;
38
+ border: 1px solid var(--color-border);
39
+ color: var(--color-fg);
40
+ transition: border-color 0.12s ease, color 0.12s ease;
41
+ }
42
+ .hz-ms-trigger:hover .hz-ms-trigger-body,
43
+ .hz-ms-trigger:focus-visible .hz-ms-trigger-body,
44
+ .hz-ms-trigger[data-state="open"] .hz-ms-trigger-body {
45
+ border-color: var(--color-accent);
46
+ color: var(--color-accent);
47
+ }
48
+ .hz-ms-trigger:focus-visible { outline: none; }
49
+ .hz-ms-trigger[data-active] .hz-ms-trigger-body {
50
+ border-color: var(--color-accent);
51
+ color: var(--color-accent);
52
+ }
53
+
54
+ /* Icon sits inside the label chip on the LEFT segment, not inside
55
+ * the trigger body, so the chip reads "[icon] LEVEL" and the body
56
+ * carries only the current value. */
57
+ .hz-ms-label {
58
+ display: inline-flex;
59
+ align-items: center;
60
+ gap: 0.4rem;
61
+ /* Re-assert uppercase here because the inline-flex layout on the
62
+ * chip creates a nested span structure that loses the inheritance
63
+ * from .hz-dd-label in some rendering contexts. */
64
+ text-transform: uppercase;
65
+ }
66
+ .hz-ms-label-icon {
67
+ display: inline-flex;
68
+ align-items: center;
69
+ color: var(--color-muted);
70
+ }
71
+
72
+ /* Inherit the trigger's font (already mono + 0.78em via .hz-dd).
73
+ * No font-size override here — it would compound with the parent
74
+ * (0.78em × 0.78em ≈ 0.6em) and render as tiny ~10px text, out of
75
+ * step with the single-select Dropdown's value chrome.
76
+ *
77
+ * No muted color for the empty "Any" state either — the value text
78
+ * should read the same as the Sort dropdown's "Default" so the
79
+ * three trigger bodies share one visual rhythm. */
80
+ .hz-ms-value {
81
+ font-family: inherit;
82
+ }
83
+
84
+ /* ---------- Popover content ---------- */
85
+
86
+ .hz-ms-content {
87
+ z-index: 100;
88
+ min-width: 16rem;
89
+ max-width: 22rem;
90
+ display: flex;
91
+ flex-direction: column;
92
+ background: var(--color-bg);
93
+ border: 1px solid var(--color-border);
94
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
95
+ font-family: var(--font-mono);
96
+ font-size: 0.78em;
97
+ }
98
+
99
+ .hz-ms-search {
100
+ display: flex;
101
+ align-items: center;
102
+ gap: 0.5rem;
103
+ padding: 0.55rem 0.7rem;
104
+ border-bottom: 1px solid var(--color-border);
105
+ color: var(--color-muted);
106
+ }
107
+ .hz-ms-search input {
108
+ background: transparent;
109
+ border: 0;
110
+ color: var(--color-fg);
111
+ font: inherit;
112
+ outline: none;
113
+ width: 100%;
114
+ }
115
+ .hz-ms-search input::-webkit-search-cancel-button { display: none; }
116
+
117
+ .hz-ms-viewport {
118
+ max-height: 16rem;
119
+ overflow-y: auto;
120
+ padding: 0.25rem 0;
121
+ }
122
+
123
+ .hz-ms-empty {
124
+ padding: 0.75rem 0.85rem;
125
+ color: var(--color-muted);
126
+ text-align: center;
127
+ font-style: italic;
128
+ }
129
+
130
+ .hz-ms-option {
131
+ display: flex;
132
+ align-items: center;
133
+ gap: 0.55rem;
134
+ padding: 0.45rem 0.7rem;
135
+ cursor: pointer;
136
+ user-select: none;
137
+ outline: none;
138
+ color: var(--color-fg);
139
+ }
140
+ .hz-ms-option:hover,
141
+ .hz-ms-option:focus-visible,
142
+ .hz-ms-option[tabindex="0"]:focus {
143
+ background: var(--color-surface);
144
+ }
145
+ .hz-ms-option[data-checked] {
146
+ color: var(--color-accent);
147
+ }
148
+
149
+ .hz-ms-check {
150
+ display: inline-flex;
151
+ align-items: center;
152
+ justify-content: center;
153
+ width: 14px;
154
+ height: 14px;
155
+ border: 1px solid var(--color-border);
156
+ flex-shrink: 0;
157
+ color: var(--color-accent);
158
+ }
159
+ .hz-ms-option[data-checked] .hz-ms-check {
160
+ border-color: var(--color-accent);
161
+ background: color-mix(in oklab, var(--color-accent) 10%, transparent);
162
+ }
163
+
164
+ .hz-ms-option-label {
165
+ flex: 1;
166
+ text-transform: lowercase;
167
+ overflow: hidden;
168
+ text-overflow: ellipsis;
169
+ white-space: nowrap;
170
+ }
171
+
172
+ .hz-ms-count {
173
+ color: var(--color-muted);
174
+ font-size: 0.9em;
175
+ flex-shrink: 0;
176
+ }
177
+ .hz-ms-option[data-checked] .hz-ms-count { color: var(--color-accent); }
178
+
179
+ .hz-ms-footer {
180
+ padding: 0.45rem 0.55rem;
181
+ border-top: 1px solid var(--color-border);
182
+ display: flex;
183
+ justify-content: flex-end;
184
+ }
185
+
186
+ .hz-ms-clear {
187
+ display: inline-flex;
188
+ align-items: center;
189
+ gap: 0.35rem;
190
+ padding: 0.25rem 0.55rem;
191
+ background: transparent;
192
+ border: 1px solid var(--color-border);
193
+ color: var(--color-muted);
194
+ font: inherit;
195
+ cursor: pointer;
196
+ text-transform: uppercase;
197
+ letter-spacing: 0.06em;
198
+ font-size: 0.88em;
199
+ transition: color 0.12s ease, border-color 0.12s ease;
200
+ }
201
+ .hz-ms-clear:hover,
202
+ .hz-ms-clear:focus-visible {
203
+ color: var(--color-accent);
204
+ border-color: var(--color-accent);
205
+ outline: none;
206
+ }
@@ -9,6 +9,9 @@
9
9
  @import "./components/expressive-code.css";
10
10
  @import "./components/a11y.css";
11
11
  @import "./components/modal.css";
12
+ @import "./components/dropdown.css";
13
+ @import "./components/multiselect.css";
14
+ @import "./components/filterbar.css";
12
15
 
13
16
  /* Site chrome */
14
17
  @import "./components/progress.css";