@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.60.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.17.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
- <button
120
- type="button"
121
- className={`flex items-center justify-center rounded-full text-auto flex-shrink-0 border border-transparent ${isOpen ? '' : 'transition-colors hover:border-oq-16'}`}
122
- style={{ width: square, height: square }}
123
- onClick={() => !isOpen && setOpen(true)}
124
- aria-label={triggerLabel}
125
- aria-expanded={isOpen}
126
- >
127
- {/* SOLO ladder — the glyph moves WITH the square, never independently */}
128
- <Icon name="search" size={glyphSize(size, true)} className="text-oq-80" />
129
- </button>
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 pr-4 min-w-0 appearance-none [&::-webkit-search-cancel-button]:hidden`}
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 = false,
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
- stripActiveClassName = 'text-fg-96',
95
- stripRestClassName = 'text-fg-32 hover:text-fg-48',
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
- variant={isExpanded ? 'primary' : 'nav'}
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
- <div className="flex flex-shrink-0 flex-col items-end gap-4">
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}