@kolkrabbi/kol-component 0.43.0 → 0.44.1

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.43.0",
3
+ "version": "0.44.1",
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.16.0"
33
+ "@kolkrabbi/kol-icons": "^0.17.0"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "framer-motion": "^12.0.0",
@@ -24,6 +24,7 @@ import IconFrame from '../atoms/IconFrame.jsx'
24
24
  * @param {Function} props.onFilterChange — optional callback when filters change
25
25
  * @param {Array} props.mutuallyExclusiveFilters — filter keys that should be mutually exclusive
26
26
  * @param {Array} props.customFilterKeys — filter keys handled by renderItem, not by ContentFilters
27
+ * @param {ElementType} props.iconComponent — icon seam (defaults to DS Icon; needs `filter` + `search`)
27
28
  */
28
29
  const ContentFilters = ({
29
30
  items,
@@ -44,8 +45,12 @@ const ContentFilters = ({
44
45
  searchKeys = ['label', 'name', 'title', 'type'],
45
46
  headerActions,
46
47
  showCountOnlyWhenFiltering = false,
48
+ iconComponent,
47
49
  className = '',
48
50
  }) => {
51
+ /* Icon seam — consumers on a local icon shelf pass their own component
52
+ * rather than being forced onto the DS set. Needs `filter` + `search`. */
53
+ const IconSeam = iconComponent || Icon
49
54
  const [activeFilters, setActiveFilters] = useState(new Set())
50
55
  const [isExpanded, setIsExpanded] = useState(false)
51
56
  const [internalViewMode, setInternalViewMode] = useState(defaultViewMode)
@@ -115,21 +120,45 @@ const ContentFilters = ({
115
120
  })
116
121
  }, [items, activeFilters, customFilterKeys, searchText, searchKeys])
117
122
 
123
+ /* THE FILTER VALUE IS A TAG — the atom's whole reason to exist ("a Tag with
124
+ * no handler is a Pill wearing the wrong name"). Three defects lived here
125
+ * until 2026-08-15, all of them working around the atom instead of using it:
126
+ *
127
+ * variant="default" — not a declared variant; `VARIANTS[v] ?? primary`
128
+ * silently rendered the FILLED chip. The outlined
129
+ * chip these want is `secondary`.
130
+ * className border-* — hand-rolled active state beside the atom's own
131
+ * `active` prop, which is what drives `.is-active`.
132
+ * <div onClick> — the handler on a wrapper, so Tag rendered a <span>
133
+ * and the interactive chip was not interactive.
134
+ *
135
+ * Casing is the atom's too: `.kol-tag` carries `text-transform: uppercase`
136
+ * as the component tier's ONE documented exception to the no-casing law,
137
+ * because a filter value is data with no authoring site. Consumers must not
138
+ * uppercase these themselves. */
118
139
  const renderFilterGroup = (group) => (
119
- <div key={group.key}>
120
- <h4 className="kol-helper-12 text-fg-48">{group.label}</h4>
121
- <div className="flex flex-wrap gap-2 pt-3">
122
- {group.values.map((value) => {
123
- const filterKey = `${group.key}:${value}`
124
- const isActive = activeFilters.has(filterKey)
125
- return (
126
- <div key={value} onClick={() => toggleFilter(group.key, value)}>
127
- <Tag size="sm" variant="default" hash={false} className={isActive ? 'border-fg-32' : 'border-fg-08'}>
128
- {value}
129
- </Tag>
130
- </div>
131
- )
132
- })}
140
+ <div key={group.key} className="flex flex-col gap-3">
141
+ {/* The group label wears the layout strip's exact text treatment —
142
+ * `kol-helper-12` at 1px tracking — so TAGS / TYPES and LIST / GRID read
143
+ * as one family across the row (user ruling 2026-08-15: "LIST GRID text
144
+ * has style, that style also on TAGS and any other filter category").
145
+ * Rest ink, not active: the label is a heading, not a state. */}
146
+ <h4 className="kol-helper-12 text-fg-32" style={{ letterSpacing: 1 }}>
147
+ {group.label}
148
+ </h4>
149
+ <div className="flex flex-wrap gap-2">
150
+ {group.values.map((value) => (
151
+ <Tag
152
+ key={value}
153
+ size="sm"
154
+ variant="secondary"
155
+ hash={false}
156
+ active={activeFilters.has(`${group.key}:${value}`)}
157
+ onClick={() => toggleFilter(group.key, value)}
158
+ >
159
+ {value}
160
+ </Tag>
161
+ ))}
133
162
  </div>
134
163
  </div>
135
164
  )
@@ -158,7 +187,7 @@ const ContentFilters = ({
158
187
  aria-label="Toggle filters"
159
188
  style={{ background: 'transparent', border: 'none', cursor: 'pointer', color: 'inherit' }}
160
189
  >
161
- <Icon name="filter" size={20} />
190
+ <IconSeam name="filter" size={20} />
162
191
  </button>
163
192
  <div
164
193
  className="flex items-center justify-center rounded-sm cursor-pointer hover:bg-fg-04 transition-colors"
@@ -186,7 +215,7 @@ const ContentFilters = ({
186
215
  position: searchOpen ? 'absolute' : 'relative',
187
216
  }}
188
217
  >
189
- <Icon name="search" size={20} />
218
+ <IconSeam name="search" size={20} />
190
219
  </span>
191
220
  {searchOpen && (
192
221
  <input
@@ -237,37 +266,47 @@ const ContentFilters = ({
237
266
 
238
267
  <Divider className="mb-4" />
239
268
 
240
- {isExpanded && (
241
- <div className="flex items-start gap-16 pb-4">
242
- {filterGroups.map((group) => renderFilterGroup(group))}
243
- {activeFilters.size > 0 && (
244
- <button
245
- type="button"
246
- onClick={clearAllFilters}
247
- className="kol-helper-12 transition-colors underline text-fg-48"
248
- style={{ marginLeft: 'auto', background: 'transparent', border: 'none', cursor: 'pointer' }}
249
- >
250
- Clear all ({activeFilters.size})
251
- </button>
269
+ {/* ONE ROW BELOW THE DIVIDER (user ruling 2026-08-15, four rounds of QA
270
+ * against a fork of this file). Filter groups are left-aligned COLUMNS —
271
+ * label above values — and appear only while the filter toggle is open.
272
+ * The layout strip is right-aligned and ALWAYS visible.
273
+ *
274
+ * `items-start` is load-bearing: it pins the strip to the label row so
275
+ * the values hang beneath it. The strip previously rendered in its own
276
+ * row AFTER this block, which is why expanding a filter group pushed it
277
+ * down the page the defect that started the whole ticket. */}
278
+ {(layoutOptions || isExpanded) && (
279
+ <div className="flex items-start justify-between gap-16 pb-4">
280
+ <div className="flex items-start gap-16">
281
+ {isExpanded && filterGroups.map((group) => renderFilterGroup(group))}
282
+ {isExpanded && activeFilters.size > 0 && (
283
+ <button
284
+ type="button"
285
+ onClick={clearAllFilters}
286
+ className="kol-helper-12 transition-colors underline text-fg-48"
287
+ style={{ background: 'transparent', border: 'none', cursor: 'pointer' }}
288
+ >
289
+ Clear all ({activeFilters.size})
290
+ </button>
291
+ )}
292
+ </div>
293
+ {layoutOptions && (
294
+ <div className="flex items-center gap-4 flex-shrink-0">
295
+ {layoutOptions.map((opt) => (
296
+ <span
297
+ key={opt.value}
298
+ onClick={() => setLayout(opt.value)}
299
+ className={`kol-helper-12 cursor-pointer select-none ${layout === opt.value ? 'text-fg-96' : 'text-fg-32 hover:text-fg-48'}`}
300
+ style={{ letterSpacing: 1 }}
301
+ >
302
+ {opt.label}
303
+ </span>
304
+ ))}
305
+ </div>
252
306
  )}
253
307
  </div>
254
308
  )}
255
309
 
256
- {layoutOptions && (
257
- <div className="flex items-center justify-end gap-4 mt-4">
258
- {layoutOptions.map((opt) => (
259
- <span
260
- key={opt.value}
261
- onClick={() => setLayout(opt.value)}
262
- className={`kol-helper-12 cursor-pointer select-none ${layout === opt.value ? 'text-fg-96' : 'text-fg-32 hover:text-fg-48'}`}
263
- style={{ letterSpacing: 1 }}
264
- >
265
- {opt.label}
266
- </span>
267
- ))}
268
- </div>
269
- )}
270
-
271
310
  <div className="mt-8" style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }}>
272
311
  {renderItem(filteredItems, viewMode, layout)}
273
312
  </div>