@kolkrabbi/kol-component 0.60.0 → 0.62.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.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-component",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.62.0",
|
|
4
4
|
"description": "KOL design-system components — atoms through organisms, emitting canonical kol-* classes. Pairs with @kolkrabbi/kol-theme for styling.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"@floating-ui/react": "^0.27.19",
|
|
31
31
|
"embla-carousel-react": "^8.6.0",
|
|
32
32
|
"react-syntax-highlighter": "^16.1.1",
|
|
33
|
-
"@kolkrabbi/kol-icons": "^0.
|
|
33
|
+
"@kolkrabbi/kol-icons": "^0.18.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"framer-motion": "^12.0.0",
|
|
@@ -81,6 +81,8 @@ export default function SearchInput({
|
|
|
81
81
|
open,
|
|
82
82
|
onOpenChange,
|
|
83
83
|
expandedWidth = 280,
|
|
84
|
+
iconSize,
|
|
85
|
+
fieldHeight,
|
|
84
86
|
triggerLabel = 'Open search',
|
|
85
87
|
className = '',
|
|
86
88
|
...inputProps
|
|
@@ -100,10 +102,35 @@ export default function SearchInput({
|
|
|
100
102
|
|
|
101
103
|
/* the pinned squares, 05-control-chrome.md — sm 28 · md 32 · lg 36 */
|
|
102
104
|
const square = { sm: 28, md: 32, lg: 36 }[size] ?? 32
|
|
105
|
+
/* THE OPEN FIELD'S HEIGHT IS ITS OWN KNOB, defaulting to the square.
|
|
106
|
+
*
|
|
107
|
+
* /work's pill is `h-9` open AND closed, sitting level with a 36px toggle —
|
|
108
|
+
* that is the shipped design and the default must reproduce it. But a field
|
|
109
|
+
* beside two BARE 32px glyphs (ContentFilters) read as chunky at the full
|
|
110
|
+
* square, so that surface asks for a shorter one. Deriving it (`square - 4`)
|
|
111
|
+
* served the second case and silently broke the first. */
|
|
112
|
+
const fieldH = fieldHeight ?? square
|
|
113
|
+
|
|
114
|
+
/* Escape closes, and so does clicking away — a field that can only be closed
|
|
115
|
+
* by emptying it and blurring is a trap, and this one had neither wired. The
|
|
116
|
+
* listener only exists while open. */
|
|
117
|
+
const shellRef = useRef(null)
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
if (!expanding || !isOpen) return undefined
|
|
120
|
+
const away = (e) => { if (shellRef.current && !shellRef.current.contains(e.target)) setOpen(false) }
|
|
121
|
+
const esc = (e) => { if (e.key === 'Escape') setOpen(false) }
|
|
122
|
+
document.addEventListener('mousedown', away)
|
|
123
|
+
document.addEventListener('keydown', esc)
|
|
124
|
+
return () => {
|
|
125
|
+
document.removeEventListener('mousedown', away)
|
|
126
|
+
document.removeEventListener('keydown', esc)
|
|
127
|
+
}
|
|
128
|
+
}, [expanding, isOpen])
|
|
103
129
|
|
|
104
130
|
if (expanding) {
|
|
105
131
|
return (
|
|
106
132
|
<div
|
|
133
|
+
ref={shellRef}
|
|
107
134
|
/* THE FILL BELONGS TO THE FIELD, NOT THE TRIGGER. At rest this is a
|
|
108
135
|
glyph you click, and it must read as one — bare, exactly like the
|
|
109
136
|
filter icon it sits beside. `bg-fg-04` was unconditional, so the
|
|
@@ -114,19 +141,27 @@ export default function SearchInput({
|
|
|
114
141
|
hardcoded 36 — the LG square — so an expanding search sat beside a
|
|
115
142
|
`kol-btn-md` filter button at two different sizes. */
|
|
116
143
|
className={`kol-expand flex items-center rounded-full ${isOpen ? 'bg-fg-04' : ''} ${className}`.trim()}
|
|
117
|
-
style={{ height: square, width: isOpen ? expandedWidth : square }}
|
|
144
|
+
style={{ height: isOpen ? fieldH : square, width: isOpen ? expandedWidth : square }}
|
|
118
145
|
>
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
146
|
+
{/* THE GLYPH IS THE CLOSED STATE, and only that (user ruling
|
|
147
|
+
2026-08-15). Once the field is open the caret is the affordance;
|
|
148
|
+
keeping the magnifier there spends the widest part of the pill
|
|
149
|
+
restating what the blinking cursor already says. */}
|
|
150
|
+
{!isOpen && (
|
|
151
|
+
<button
|
|
152
|
+
type="button"
|
|
153
|
+
className="flex items-center justify-center rounded-full text-auto flex-shrink-0 border border-transparent transition-colors hover:border-oq-16"
|
|
154
|
+
style={{ width: square, height: square }}
|
|
155
|
+
onClick={() => setOpen(true)}
|
|
156
|
+
aria-label={triggerLabel}
|
|
157
|
+
aria-expanded={false}
|
|
158
|
+
>
|
|
159
|
+
{/* SOLO ladder, and NO ink class — it inherits from its chrome
|
|
160
|
+
exactly as IconFrame's glyph does, so it cannot drift from the
|
|
161
|
+
icon beside it. */}
|
|
162
|
+
<Icon name="search" size={iconSize ?? glyphSize(size, true)} />
|
|
163
|
+
</button>
|
|
164
|
+
)}
|
|
130
165
|
{isOpen && (
|
|
131
166
|
<input
|
|
132
167
|
ref={inputRef}
|
|
@@ -136,7 +171,7 @@ export default function SearchInput({
|
|
|
136
171
|
readOnly={!onChange || undefined}
|
|
137
172
|
placeholder={placeholder}
|
|
138
173
|
spellCheck={false}
|
|
139
|
-
className={`bg-transparent outline-none ${SIZE_TYPE[size] ?? SIZE_TYPE.md} flex-1 text-oq-80 caret-current
|
|
174
|
+
className={`bg-transparent outline-none ${SIZE_TYPE[size] ?? SIZE_TYPE.md} flex-1 text-oq-80 caret-current px-4 min-w-0 appearance-none [&::-webkit-search-cancel-button]:hidden`}
|
|
140
175
|
onKeyDown={(e) => { if (e.key === 'Escape') setOpen(false) }}
|
|
141
176
|
{...inputProps}
|
|
142
177
|
/>
|
|
@@ -171,7 +206,7 @@ export default function SearchInput({
|
|
|
171
206
|
className={`flex items-center shrink-0 ${bare ? 'text-fg-48' : 'text-auto opacity-50'}`}
|
|
172
207
|
>
|
|
173
208
|
{/* ADJACENT — this glyph sits in the field's line box beside the query */}
|
|
174
|
-
<Icon name="search" size={glyphSize(size)} />
|
|
209
|
+
<Icon name="search" size={iconSize ?? glyphSize(size)} />
|
|
175
210
|
</span>
|
|
176
211
|
<input
|
|
177
212
|
type="search"
|
|
@@ -81,8 +81,12 @@ const ContentFilters = ({
|
|
|
81
81
|
/* THE LOOK SEAMS. Every one defaults to what the component shipped, so
|
|
82
82
|
* passing nothing renders exactly as before — these exist because the
|
|
83
83
|
* hardcoded values were the wrong call for every consumer but one. */
|
|
84
|
+
/* The title is the SAME as RECENT/SAVED (user ruling 2026-08-15) — same
|
|
85
|
+
* rung, same casing, same tracking. It sits at the other end of the same
|
|
86
|
+
* row, so a different treatment reads as two unrelated things rather than
|
|
87
|
+
* one strip of chrome. */
|
|
84
88
|
titleClassName = 'kol-helper-14',
|
|
85
|
-
titleUppercase =
|
|
89
|
+
titleUppercase = true,
|
|
86
90
|
labelClassName = 'kol-helper-12 text-fg-96',
|
|
87
91
|
labelUppercase = true,
|
|
88
92
|
tagVariant = 'primary',
|
|
@@ -91,8 +95,13 @@ const ContentFilters = ({
|
|
|
91
95
|
tagRestClassName = 'text-fg-48',
|
|
92
96
|
viewClassName = 'kol-helper-14',
|
|
93
97
|
layoutClassName = 'kol-helper-12',
|
|
94
|
-
|
|
95
|
-
|
|
98
|
+
/* REST WAS TOO DARK (user ruling 2026-08-15). `fg-32` is a third of the ink
|
|
99
|
+
* and read as DISABLED rather than unselected — the same complaint the
|
|
100
|
+
* IconFrame `nav` variant already settled ("the dimmed row read as disabled
|
|
101
|
+
* beside the ThemeToggle"). Rest is now the ghost rung, oq-48, and the whole
|
|
102
|
+
* pair moves to the opaque tier so it never washes out over a tinted surface. */
|
|
103
|
+
stripActiveClassName = 'text-oq-96',
|
|
104
|
+
stripRestClassName = 'text-oq-48 hover:text-oq-64',
|
|
96
105
|
countClassName = 'kol-helper-12 text-fg-64',
|
|
97
106
|
}) => {
|
|
98
107
|
/* Icon seam — consumers on a local icon shelf pass their own component
|
|
@@ -261,7 +270,7 @@ const ContentFilters = ({
|
|
|
261
270
|
* states"); the span here was the same defect that promoted it. */}
|
|
262
271
|
<h2 className="flex items-center gap-2">
|
|
263
272
|
{titleIcon && <IconFrame name={titleIcon} variant="secondary" size="md" />}
|
|
264
|
-
<span className={titleClassName} style={titleUppercase ? { textTransform: 'uppercase' } : undefined}>{title}</span>
|
|
273
|
+
<span className={titleClassName} style={titleUppercase ? { textTransform: 'uppercase', letterSpacing: 1 } : undefined}>{title}</span>
|
|
265
274
|
</h2>
|
|
266
275
|
<Divider variant="vertical" className="self-stretch py-1" />
|
|
267
276
|
<div className="flex items-center gap-1">
|
|
@@ -275,9 +284,16 @@ const ContentFilters = ({
|
|
|
275
284
|
*
|
|
276
285
|
* `nav` rests at oq-64 and lights to full ink when the panel is
|
|
277
286
|
* open, so the toggle finally SHOWS that it is on. */}
|
|
287
|
+
{/* MD — 32px square, 20px glyph (user ruling 2026-08-15). The
|
|
288
|
+
* squares are 28 · 32 · 36, so 32 is the MIDDLE rung, not a large
|
|
289
|
+
* one; the search beside it takes the same. */}
|
|
278
290
|
<IconFrame
|
|
279
291
|
name="filter"
|
|
280
|
-
|
|
292
|
+
/* `nav` ALWAYS — no container. The open state is the panel
|
|
293
|
+
* appearing below; painting a filled square behind the glyph as
|
|
294
|
+
* well says the same thing twice and puts a box in a row that
|
|
295
|
+
* has none. */
|
|
296
|
+
variant="nav"
|
|
281
297
|
size="md"
|
|
282
298
|
onClick={() => setIsExpanded(!isExpanded)}
|
|
283
299
|
aria-label="Toggle filters"
|
|
@@ -305,6 +321,9 @@ const ContentFilters = ({
|
|
|
305
321
|
* The organism's own field never had one. */
|
|
306
322
|
placeholder=""
|
|
307
323
|
size="md"
|
|
324
|
+
/* a rung under the 32 square: this field sits beside two BARE
|
|
325
|
+
* glyphs, and at the full square it read as a chunky input. */
|
|
326
|
+
fieldHeight={28}
|
|
308
327
|
/>
|
|
309
328
|
{headerActions}
|
|
310
329
|
</div>
|
|
@@ -386,7 +405,9 @@ const ContentFilters = ({
|
|
|
386
405
|
* it always reads "N of N", a number that has never told anyone
|
|
387
406
|
* anything. Ink stays `text-fg-64`: static information, not a
|
|
388
407
|
* toggle, so it takes neither the 96 active nor the 32 rest. */}
|
|
389
|
-
|
|
408
|
+
{/* ONE LINE — the count sits BESIDE LIST/GRID, not stacked above it.
|
|
409
|
+
* They are the same strip of chrome reading left to right. */}
|
|
410
|
+
<div className="flex flex-shrink-0 items-center gap-4">
|
|
390
411
|
{showCount && isExpanded && (
|
|
391
412
|
<span className={countClassName} style={{ letterSpacing: 1 }}>
|
|
392
413
|
{filteredItems.length} of {totalCount}
|