@kolkrabbi/kol-shell 0.1.1 → 0.1.3
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 +1 -1
- package/src/ContentFilters.jsx +58 -42
- package/src/TabStrip.jsx +10 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-shell",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
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",
|
package/src/ContentFilters.jsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useMemo, useRef, useEffect } from 'react'
|
|
2
|
-
import {
|
|
2
|
+
import { Divider } from '@kolkrabbi/kol-component'
|
|
3
3
|
import { Icon } from '@kolkrabbi/kol-icons'
|
|
4
4
|
import TabStrip from './TabStrip.jsx'
|
|
5
5
|
|
|
@@ -132,29 +132,36 @@ const ContentFilters = ({
|
|
|
132
132
|
})
|
|
133
133
|
}, [items, activeFilters, customFilterKeys, searchText, searchKeys])
|
|
134
134
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
135
|
+
// A group is a COLUMN: label on top, values beneath it — never label-left /
|
|
136
|
+
// values-right on one line (user ruling 2026-08-15, second QA round). The
|
|
137
|
+
// label wears the full strip idiom too — tracked `kol-helper-12` at the
|
|
138
|
+
// strip's rest ink — so a category header and the LIST/GRID items read as one
|
|
139
|
+
// family ("LIST GRID text has style, that style also on TAGS and any other
|
|
140
|
+
// filter category"). Rest ink, not active: the label is not interactive.
|
|
141
|
+
const renderFilterGroup = (group) => {
|
|
142
|
+
const prefix = `${group.key}:`
|
|
143
|
+
const selected = new Set(
|
|
144
|
+
Array.from(activeFilters)
|
|
145
|
+
.filter((f) => f.startsWith(prefix))
|
|
146
|
+
.map((f) => f.slice(prefix.length))
|
|
147
|
+
)
|
|
142
148
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
149
|
+
return (
|
|
150
|
+
<div key={group.key} className="flex flex-col gap-3">
|
|
151
|
+
<h4 className="kol-helper-12 text-fg-32" style={{ letterSpacing: 1 }}>
|
|
152
|
+
{group.label}
|
|
153
|
+
</h4>
|
|
154
|
+
<TabStrip
|
|
155
|
+
options={group.values.map((v) => ({ value: v, label: v }))}
|
|
156
|
+
value={selected}
|
|
157
|
+
onChange={(v) => toggleFilter(group.key, v)}
|
|
158
|
+
size={12}
|
|
159
|
+
tracked
|
|
160
|
+
className="gap-4 flex-wrap"
|
|
161
|
+
/>
|
|
155
162
|
</div>
|
|
156
|
-
|
|
157
|
-
|
|
163
|
+
)
|
|
164
|
+
}
|
|
158
165
|
|
|
159
166
|
return (
|
|
160
167
|
<div className="w-full" style={{ display: 'flex', flexDirection: 'column', flex: 1 }}>
|
|
@@ -231,9 +238,35 @@ const ContentFilters = ({
|
|
|
231
238
|
{viewModeOptions && (
|
|
232
239
|
<TabStrip options={viewModeOptions} value={viewMode} onChange={handleViewModeChange} tracked />
|
|
233
240
|
)}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
<Divider className="mb-4" />
|
|
245
|
+
|
|
246
|
+
{/* BELOW the divider: filter groups as left-aligned COLUMNS (only while
|
|
247
|
+
the filter toggle is open), layout strip RIGHT and ALWAYS visible.
|
|
248
|
+
`items-start` is load-bearing — it pins the strip to the label row, so
|
|
249
|
+
the strip and the group labels read as one line and the values hang
|
|
250
|
+
beneath. 0.1.1 read "at the divider level" as the header row above the
|
|
251
|
+
divider; the strip never belongs in the header
|
|
252
|
+
(ShellHeaderFilterRefinements, 2026-08-15). */}
|
|
253
|
+
{(layoutOptions || isExpanded) && (
|
|
254
|
+
<div className="flex items-start justify-between gap-16 mb-4">
|
|
255
|
+
<div className="flex items-start gap-16">
|
|
256
|
+
{isExpanded && filterGroups.map((group) => renderFilterGroup(group))}
|
|
257
|
+
{isExpanded && activeFilters.size > 0 && (
|
|
258
|
+
// Strip rest recipe, not its own ink — this button shares the line
|
|
259
|
+
// with the labels and values, and that line carries exactly TWO
|
|
260
|
+
// ink states (user ruling 2026-08-15: "we are not maintaining 3
|
|
261
|
+
// opacity states for one line in a component").
|
|
262
|
+
<button
|
|
263
|
+
onClick={clearAllFilters}
|
|
264
|
+
className="kol-helper-12 transition-colors underline text-fg-32 hover:text-fg-48"
|
|
265
|
+
>
|
|
266
|
+
Clear all ({activeFilters.size})
|
|
267
|
+
</button>
|
|
268
|
+
)}
|
|
269
|
+
</div>
|
|
237
270
|
{layoutOptions && (
|
|
238
271
|
<TabStrip
|
|
239
272
|
options={layoutOptions}
|
|
@@ -245,23 +278,6 @@ const ContentFilters = ({
|
|
|
245
278
|
/>
|
|
246
279
|
)}
|
|
247
280
|
</div>
|
|
248
|
-
</div>
|
|
249
|
-
|
|
250
|
-
<Divider className="mb-4" />
|
|
251
|
-
|
|
252
|
-
{isExpanded && (
|
|
253
|
-
<div className="flex items-start gap-16">
|
|
254
|
-
{filterGroups.map((group) => renderFilterGroup(group))}
|
|
255
|
-
{activeFilters.size > 0 && (
|
|
256
|
-
<button
|
|
257
|
-
onClick={clearAllFilters}
|
|
258
|
-
className="kol-helper-12 transition-colors underline text-fg-48"
|
|
259
|
-
style={{ marginLeft: 'auto' }}
|
|
260
|
-
>
|
|
261
|
-
Clear all ({activeFilters.size})
|
|
262
|
-
</button>
|
|
263
|
-
)}
|
|
264
|
-
</div>
|
|
265
281
|
)}
|
|
266
282
|
|
|
267
283
|
{/* Render filtered items */}
|
package/src/TabStrip.jsx
CHANGED
|
@@ -8,16 +8,25 @@
|
|
|
8
8
|
* want GRID). `tracked` adds the 1px letter-spacing the view-mode strips
|
|
9
9
|
* carried.
|
|
10
10
|
*
|
|
11
|
+
* `value` takes a **Set** for a multi-select strip (ContentFilters' filter
|
|
12
|
+
* values, ruled 2026-08-15 to wear this exact idiom rather than Tag chips) or
|
|
13
|
+
* a scalar for the single-select tabs. One ink recipe, both behaviours — the
|
|
14
|
+
* alternative was a second copy of the active/rest classes, which is how the
|
|
15
|
+
* two shipped shells drifted in the first place.
|
|
16
|
+
*
|
|
11
17
|
* @param {Array} props.options `[{ value, label }]`
|
|
18
|
+
* @param {Set|*} props.value Set → multi-select · scalar → single-select
|
|
12
19
|
*/
|
|
13
20
|
export default function TabStrip({ options = [], value, onChange, size = 14, tracked = false, className = 'gap-6', style }) {
|
|
21
|
+
const isOn = (opt) => (value instanceof Set ? value.has(opt.value) : value === opt.value)
|
|
22
|
+
|
|
14
23
|
return (
|
|
15
24
|
<div className={`flex items-center ${className}`.trim()} style={style}>
|
|
16
25
|
{options.map((opt) => (
|
|
17
26
|
<span
|
|
18
27
|
key={opt.value}
|
|
19
28
|
onClick={() => onChange?.(opt.value)}
|
|
20
|
-
className={`kol-helper-${size} cursor-pointer select-none ${
|
|
29
|
+
className={`kol-helper-${size} cursor-pointer select-none ${isOn(opt) ? 'text-fg-96' : 'text-fg-32 hover:text-fg-48'}`}
|
|
21
30
|
style={tracked ? { letterSpacing: 1 } : undefined}
|
|
22
31
|
>
|
|
23
32
|
{opt.label}
|