@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.60.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.17.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
- <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>
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 pr-4 min-w-0 appearance-none [&::-webkit-search-cancel-button]:hidden`}
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 = 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"
@@ -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
- <div className="flex flex-shrink-0 flex-col items-end gap-4">
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}