@kolkrabbi/kol-component 0.60.0 → 0.61.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.61.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",
|
|
@@ -100,10 +100,32 @@ export default function SearchInput({
|
|
|
100
100
|
|
|
101
101
|
/* the pinned squares, 05-control-chrome.md — sm 28 · md 32 · lg 36 */
|
|
102
102
|
const square = { sm: 28, md: 32, lg: 36 }[size] ?? 32
|
|
103
|
+
/* THE OPEN FIELD IS SHORTER THAN THE TRIGGER'S HIT AREA. The square is the
|
|
104
|
+
* click target and it is INVISIBLE at rest (no fill), so the pill does not
|
|
105
|
+
* have to inherit its height — at 32 the field read as a chunky input beside
|
|
106
|
+
* two bare glyphs. It sits one rung down and centres inside the same box. */
|
|
107
|
+
const fieldH = square - 4
|
|
108
|
+
|
|
109
|
+
/* Escape closes, and so does clicking away — a field that can only be closed
|
|
110
|
+
* by emptying it and blurring is a trap, and this one had neither wired. The
|
|
111
|
+
* listener only exists while open. */
|
|
112
|
+
const shellRef = useRef(null)
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (!expanding || !isOpen) return undefined
|
|
115
|
+
const away = (e) => { if (shellRef.current && !shellRef.current.contains(e.target)) setOpen(false) }
|
|
116
|
+
const esc = (e) => { if (e.key === 'Escape') setOpen(false) }
|
|
117
|
+
document.addEventListener('mousedown', away)
|
|
118
|
+
document.addEventListener('keydown', esc)
|
|
119
|
+
return () => {
|
|
120
|
+
document.removeEventListener('mousedown', away)
|
|
121
|
+
document.removeEventListener('keydown', esc)
|
|
122
|
+
}
|
|
123
|
+
}, [expanding, isOpen])
|
|
103
124
|
|
|
104
125
|
if (expanding) {
|
|
105
126
|
return (
|
|
106
127
|
<div
|
|
128
|
+
ref={shellRef}
|
|
107
129
|
/* THE FILL BELONGS TO THE FIELD, NOT THE TRIGGER. At rest this is a
|
|
108
130
|
glyph you click, and it must read as one — bare, exactly like the
|
|
109
131
|
filter icon it sits beside. `bg-fg-04` was unconditional, so the
|
|
@@ -114,19 +136,27 @@ export default function SearchInput({
|
|
|
114
136
|
hardcoded 36 — the LG square — so an expanding search sat beside a
|
|
115
137
|
`kol-btn-md` filter button at two different sizes. */
|
|
116
138
|
className={`kol-expand flex items-center rounded-full ${isOpen ? 'bg-fg-04' : ''} ${className}`.trim()}
|
|
117
|
-
style={{ height: square, width: isOpen ? expandedWidth : square }}
|
|
139
|
+
style={{ height: isOpen ? fieldH : square, width: isOpen ? expandedWidth : square }}
|
|
118
140
|
>
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
141
|
+
{/* THE GLYPH IS THE CLOSED STATE, and only that (user ruling
|
|
142
|
+
2026-08-15). Once the field is open the caret is the affordance;
|
|
143
|
+
keeping the magnifier there spends the widest part of the pill
|
|
144
|
+
restating what the blinking cursor already says. */}
|
|
145
|
+
{!isOpen && (
|
|
146
|
+
<button
|
|
147
|
+
type="button"
|
|
148
|
+
className="flex items-center justify-center rounded-full text-auto flex-shrink-0 border border-transparent transition-colors hover:border-oq-16"
|
|
149
|
+
style={{ width: square, height: square }}
|
|
150
|
+
onClick={() => setOpen(true)}
|
|
151
|
+
aria-label={triggerLabel}
|
|
152
|
+
aria-expanded={false}
|
|
153
|
+
>
|
|
154
|
+
{/* SOLO ladder, and NO ink class — it inherits from its chrome
|
|
155
|
+
exactly as IconFrame's glyph does, so it cannot drift from the
|
|
156
|
+
icon beside it. */}
|
|
157
|
+
<Icon name="search" size={glyphSize(size, true)} />
|
|
158
|
+
</button>
|
|
159
|
+
)}
|
|
130
160
|
{isOpen && (
|
|
131
161
|
<input
|
|
132
162
|
ref={inputRef}
|
|
@@ -136,7 +166,7 @@ export default function SearchInput({
|
|
|
136
166
|
readOnly={!onChange || undefined}
|
|
137
167
|
placeholder={placeholder}
|
|
138
168
|
spellCheck={false}
|
|
139
|
-
className={`bg-transparent outline-none ${SIZE_TYPE[size] ?? SIZE_TYPE.md} flex-1 text-oq-80 caret-current
|
|
169
|
+
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
170
|
onKeyDown={(e) => { if (e.key === 'Escape') setOpen(false) }}
|
|
141
171
|
{...inputProps}
|
|
142
172
|
/>
|
|
@@ -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"
|
|
@@ -386,7 +402,9 @@ const ContentFilters = ({
|
|
|
386
402
|
* it always reads "N of N", a number that has never told anyone
|
|
387
403
|
* anything. Ink stays `text-fg-64`: static information, not a
|
|
388
404
|
* toggle, so it takes neither the 96 active nor the 32 rest. */}
|
|
389
|
-
|
|
405
|
+
{/* ONE LINE — the count sits BESIDE LIST/GRID, not stacked above it.
|
|
406
|
+
* They are the same strip of chrome reading left to right. */}
|
|
407
|
+
<div className="flex flex-shrink-0 items-center gap-4">
|
|
390
408
|
{showCount && isExpanded && (
|
|
391
409
|
<span className={countClassName} style={{ letterSpacing: 1 }}>
|
|
392
410
|
{filteredItems.length} of {totalCount}
|