@kolkrabbi/kol-shell 0.1.2 → 0.2.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-shell",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
5
  "description": "KOL application shell — fixed 48px NavRail + AppShell layout root, PageShell/PageHeader scaffolds, ContentFilters catalog organism, GridCard, SettingsScaffold, WalkthroughPanel, ShortcutsOverlay. App chrome (kol-framework owns site chrome). Nav items, content, shortcuts and settings are consumer-injected. Sits above @kolkrabbi/kol-{theme,component,framework}.",
6
6
  "license": "MIT",
@@ -1,4 +1,4 @@
1
- import { useState, useMemo, useRef, useEffect } from 'react'
1
+ import { Fragment, useState, useMemo, useRef, useEffect } from 'react'
2
2
  import { Divider } from '@kolkrabbi/kol-component'
3
3
  import { Icon } from '@kolkrabbi/kol-icons'
4
4
  import TabStrip from './TabStrip.jsx'
@@ -27,6 +27,22 @@ import TabStrip from './TabStrip.jsx'
27
27
  * @param {Array} props.mutuallyExclusiveFilters - filter keys that self-clear
28
28
  * @param {Array} props.customFilterKeys - keys renderItem handles, not this organism
29
29
  * @param {ElementType} props.iconComponent - icon seam (defaults to DS Icon; needs `filter` + `search`)
30
+ * @param {Function} props.renderFilterValue - `(value, isActive, toggle) => node`, one filter value
31
+ * @param {string} props.labelClassName - REPLACES the group label's class (never stacks)
32
+ *
33
+ * **The two rendering seams (0.2.0).** How a filter value and a group label look
34
+ * is the consumer's call, not this organism's. Three publishes in one day went
35
+ * into re-ruling that here — outlined pills vs bare strip items vs chips — none
36
+ * of which is a design-system question: it is what a given app's filter bar
37
+ * should look like. `renderFilterValue` and `labelClassName` end that class of
38
+ * change without a publish.
39
+ *
40
+ * `labelClassName` REPLACES rather than stacks, per the 2026-07-30 law: two
41
+ * equal-specificity type classes on one element are decided by sheet order, so
42
+ * a stacked seam produces different renders in different consumers with no
43
+ * version difference. Same contract as ListingCard's `titleClassName`.
44
+ *
45
+ * Defaults reproduce 0.1.3 exactly — a bump moves nothing.
30
46
  */
31
47
  const ContentFilters = ({
32
48
  items,
@@ -47,6 +63,8 @@ const ContentFilters = ({
47
63
  headerActions,
48
64
  showCountOnlyWhenFiltering = false,
49
65
  iconComponent,
66
+ renderFilterValue,
67
+ labelClassName,
50
68
  }) => {
51
69
  const [activeFilters, setActiveFilters] = useState(new Set())
52
70
  const [isExpanded, setIsExpanded] = useState(false)
@@ -132,9 +150,10 @@ const ContentFilters = ({
132
150
  })
133
151
  }, [items, activeFilters, customFilterKeys, searchText, searchKeys])
134
152
 
135
- // Filter values wear the LIST/GRID strip idiom, not Tag chips user ruling
136
- // 2026-08-15 ("TAGS should be same style as LIST and GRID"). Label and values
137
- // sit on ONE line so the group shares the row with the layout strip.
153
+ // A group is a COLUMN: label on top, values beneath it (user ruling
154
+ // 2026-08-15). How a value and a label RENDER is the consumer's three
155
+ // publishes in one day went into re-deciding that here, which is the defect
156
+ // the seams below end. Defaults are 0.1.3 exactly, so nothing moves on bump.
138
157
  const renderFilterGroup = (group) => {
139
158
  const prefix = `${group.key}:`
140
159
  const selected = new Set(
@@ -144,16 +163,31 @@ const ContentFilters = ({
144
163
  )
145
164
 
146
165
  return (
147
- <div key={group.key} className="flex items-center gap-4">
148
- <h4 className="kol-helper-12 text-fg-48">{group.label}</h4>
149
- <TabStrip
150
- options={group.values.map((v) => ({ value: v, label: v }))}
151
- value={selected}
152
- onChange={(v) => toggleFilter(group.key, v)}
153
- size={12}
154
- tracked
155
- className="gap-4"
156
- />
166
+ <div key={group.key} className="flex flex-col gap-3">
167
+ <h4
168
+ className={labelClassName || 'kol-helper-12 text-fg-32'}
169
+ style={labelClassName ? undefined : { letterSpacing: 1 }}
170
+ >
171
+ {group.label}
172
+ </h4>
173
+ {renderFilterValue ? (
174
+ <div className="flex flex-wrap items-center gap-4">
175
+ {group.values.map((v) => (
176
+ <Fragment key={v}>
177
+ {renderFilterValue(v, selected.has(v), () => toggleFilter(group.key, v))}
178
+ </Fragment>
179
+ ))}
180
+ </div>
181
+ ) : (
182
+ <TabStrip
183
+ options={group.values.map((v) => ({ value: v, label: v }))}
184
+ value={selected}
185
+ onChange={(v) => toggleFilter(group.key, v)}
186
+ size={12}
187
+ tracked
188
+ className="gap-4 flex-wrap"
189
+ />
190
+ )}
157
191
  </div>
158
192
  )
159
193
  }
@@ -238,19 +272,25 @@ const ContentFilters = ({
238
272
 
239
273
  <Divider className="mb-4" />
240
274
 
241
- {/* ONE row BELOW the divider: filter groups on the LEFT (only while the
242
- filter toggle is open), layout strip on the RIGHT and ALWAYS visible.
243
- 0.1.1 read "at the divider level" as the header row above it the
244
- filer's verdict corrected that; the strip never belongs in the header
275
+ {/* BELOW the divider: filter groups as left-aligned COLUMNS (only while
276
+ the filter toggle is open), layout strip RIGHT and ALWAYS visible.
277
+ `items-start` is load-bearing it pins the strip to the label row, so
278
+ the strip and the group labels read as one line and the values hang
279
+ beneath. 0.1.1 read "at the divider level" as the header row above the
280
+ divider; the strip never belongs in the header
245
281
  (ShellHeaderFilterRefinements, 2026-08-15). */}
246
282
  {(layoutOptions || isExpanded) && (
247
- <div className="flex items-center justify-between gap-16 mb-4">
248
- <div className="flex items-center gap-16">
283
+ <div className="flex items-start justify-between gap-16 mb-4">
284
+ <div className="flex items-start gap-16">
249
285
  {isExpanded && filterGroups.map((group) => renderFilterGroup(group))}
250
286
  {isExpanded && activeFilters.size > 0 && (
287
+ // Strip rest recipe, not its own ink — this button shares the line
288
+ // with the labels and values, and that line carries exactly TWO
289
+ // ink states (user ruling 2026-08-15: "we are not maintaining 3
290
+ // opacity states for one line in a component").
251
291
  <button
252
292
  onClick={clearAllFilters}
253
- className="kol-helper-12 transition-colors underline text-fg-48"
293
+ className="kol-helper-12 transition-colors underline text-fg-32 hover:text-fg-48"
254
294
  >
255
295
  Clear all ({activeFilters.size})
256
296
  </button>