@kolkrabbi/kol-component 0.234.0 → 0.236.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.234.0",
3
+ "version": "0.236.0",
4
4
  "description": "The core component library",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -135,9 +135,20 @@ const readBack = (n) => {
135
135
  *
136
136
  * STATE is untouched: collapsed/expanded still persists via
137
137
  * `stateKey`, unconditionally, as it always did. The ticket says the
138
- * two are separate questions and only asks about the width. */
138
+ * two are separate questions and only asks about the width.
139
+ * variant — how the handle DRAWS and what a click does (2026-10-01, user: *"we should make another
140
+ * drag to resize variant: on hover this pointer, and either hover or click to highlight
141
+ * the border, with double click expanding (like if sidebar with icons)"*):
142
+ * 'pill' (default) — the short pill that wakes as the pointer nears; a CLICK toggles.
143
+ * 'line' — the whole edge lights up on hover and while dragging; a click does
144
+ * nothing and a DOUBLE-CLICK toggles expand ↔ collapse (the two
145
+ * cannot share one gesture — see "double-click reset is gone" above,
146
+ * which is why this is a variant and not an addition). */
139
147
  export default function useDragResize(ref, options = {}) {
140
- const { token = 'kol-sidenav', side = 'left', defaultCollapsed = false, persistWidth = false } = options
148
+ const { token = 'kol-sidenav', side = 'left', defaultCollapsed = false, persistWidth = false, variant = 'pill' } = options
149
+ const line = variant === 'line'
150
+ const lineRef = useRef(line)
151
+ lineRef.current = line
141
152
  /* -1 on a right-hand rail: the same rightward pointer travel that widens a
142
153
  * left rail must NARROW a right one, because its handle faces the canvas. */
143
154
  const dir = side === 'right' ? -1 : 1
@@ -221,7 +232,7 @@ export default function useDragResize(ref, options = {}) {
221
232
  root().removeAttribute(names.draggingAttr)
222
233
  document.body.style.cursor = ''
223
234
  document.body.style.userSelect = ''
224
- if (!moved) { toggleCollapsed(); return } // a click, not a drag
235
+ if (!moved) { if (!lineRef.current) toggleCollapsed(); return } // a click, not a drag — `line` toggles on double-click
225
236
  /* Snap-to-default: release near the stylesheet default clears the
226
237
  * override entirely. */
227
238
  const { collapsed: c, widthPx: w } = readBack(names)
@@ -304,7 +315,8 @@ export default function useDragResize(ref, options = {}) {
304
315
  /* the pill is drawn by `.kol-rail-grab` (kol-animation.css) — the hook
305
316
  * only supplies `is-near` and `--kol-rail-grab-y`. A consumer's own
306
317
  * className, spread after this, still wins. */
307
- className: 'kol-rail-grab',
318
+ className: line ? 'kol-rail-grab kol-rail-grab--line' : 'kol-rail-grab',
319
+ ...(line ? { onDoubleClick: toggleCollapsed } : {}),
308
320
  role: 'separator',
309
321
  'aria-orientation': 'vertical',
310
322
  'aria-label': 'Resize navigation',
package/src/index.js CHANGED
@@ -128,6 +128,8 @@ export { default as ToolPalette } from './organisms/ToolPalette.jsx'
128
128
  export { default as DocumentEditor } from './organisms/DocumentEditor.jsx'
129
129
  export { default as SwatchControls, SwatchStack, EyedropPick } from './molecules/SwatchControls.jsx'
130
130
  export { default as TabsRow } from './molecules/TabsRow.jsx'
131
+ /* TabChips — tabs as chips, for inside a toolbar (2026-10-01; was the showcase's local DocTabs) */
132
+ export { default as TabChips } from './molecules/TabChips.jsx'
131
133
 
132
134
  // organisms
133
135
  /* monorepo sets (P6–P10) — organism members. Foundry members live in the
@@ -0,0 +1,39 @@
1
+ /**
2
+ * TabChips — Tabs as a row of chips. the active tab is a filled chip, the rest are quiet text.
3
+ *
4
+ * WHY IT EXISTS (user, 2026-10-01, on the showcase's Preview / Code and pnpm / npm / yarn / bun rows:
5
+ * *"what is this button? its not DS?"* → *"we can just make a tab chip for this purpose exactly
6
+ * like this"*). It was `DocTabs`, a strip local to the showcase, built there because neither
7
+ * shipped tab control fits a toolbar: `SegmentedToggle` is a joined strip with radiogroup
8
+ * semantics, `TabsRow` is an underline strip that wants a rule beneath it. This is the third tab
9
+ * idiom — small, unframed, sits inside a bar — promoted as it was, class for class.
10
+ *
11
+ * Labels render as authored. Selection is the parent's.
12
+ *
13
+ * @param {Array} tabs `[{ id, label }]`
14
+ * @param {string} value id of the active tab (controlled)
15
+ * @param {Function} onChange `(id) => void`
16
+ * @param {string} [ariaLabel] accessible name for the strip
17
+ * @param {string} [className] extra classes on the row
18
+ */
19
+ export default function TabChips({ tabs = [], value, onChange, ariaLabel, className = '' }) {
20
+ return (
21
+ <div className={`flex items-center gap-1 ${className}`.trim()} role="tablist" aria-label={ariaLabel}>
22
+ {tabs.map((t) => {
23
+ const active = t.id === value
24
+ return (
25
+ <button
26
+ key={t.id}
27
+ type="button"
28
+ role="tab"
29
+ aria-selected={active}
30
+ onClick={() => onChange?.(t.id)}
31
+ className={`kol-mono-12 rounded-[var(--kol-radius-sm)] px-3 py-1 transition-colors [@media(pointer:coarse)]:min-h-8 ${active ? 'bg-fg-08 text-emphasis' : 'text-meta hover:text-emphasis'}`}
32
+ >
33
+ {t.label}
34
+ </button>
35
+ )
36
+ })}
37
+ </div>
38
+ )
39
+ }
@@ -69,7 +69,7 @@ export function HighlightMatch({ label, query, ranges }) {
69
69
  * The ⌘K binding itself lives in the shell's key handler, not here.
70
70
  *
71
71
  * Keyboard: ArrowUp/ArrowDown rove the active row (mouse hover roves too),
72
- * Enter selects it (index starts at 0 → Enter-selects-first preserved),
72
+ * Enter opens it (the top row from the first keystroke), ⌘/Ctrl+Enter opens every hit,
73
73
  * Escape closes. Focus trap: focus moves into the input on open, returns to
74
74
  * the opener on close, and Tab is pinned — rows are combobox options driven
75
75
  * via aria-activedescendant, never tab stops.
@@ -168,13 +168,12 @@ export default function ShellSearchOverlay({
168
168
  /* ⌘ENTER OPENS EVERY HIT (user 2026-09-30: *"enter would focus searched item, and maybe
169
169
  * command enter would open the results page"*) — only where the consumer has a place for them. */
170
170
  if ((e.metaKey || e.ctrlKey) && onOpenResults && query) { onOpenResults(query); onClose?.(); return }
171
- /* Enter COMMITS the query and expands. It only selects when the user has
172
- * actually arrowed to a row — `activeIndex` starts at 0, so "a row is
173
- * highlighted" is true from the first keystroke and testing `active >= 0`
174
- * made Enter navigate to whatever happened to be first. Committing a
175
- * query must never be a navigation you didn't choose. */
176
- /* An empty query has nothing to commit — Enter goes to the top suggestion. */
177
- if (active >= 0 && (navigated || !query)) select(results[active])
171
+ /* ENTER OPENS THE HIGHLIGHTED ROW (user 2026-10-01: *"I think enter should also take you to
172
+ * the atom but something like command enter take you to the index"* — reverses the
173
+ * 2026-08-01 "Enter commits, it only selects once you have arrowed"). The top row is drawn
174
+ * highlighted from the first keystroke, so Enter goes where the highlight says. With no
175
+ * rows there is nothing to open and Enter commits the query. */
176
+ if (active >= 0) select(results[active])
178
177
  else onExpand?.()
179
178
  } else if (e.key === 'Tab') {
180
179
  /* Focus trap — the input is the palette's only tab stop. */
@@ -283,17 +282,28 @@ export default function ShellSearchOverlay({
283
282
  </ul>
284
283
  )}
285
284
  {/* THE FOOTER SAYS WHAT ENTER DOES — always, not only once typing. */}
286
- {!expanded && ((!query && results.length > 0) || (query && enterLabel)) && (
285
+ {!expanded && (results.length > 0 || (query && (enterLabel || onOpenResults))) && (
287
286
  <div className="border-t border-fg-08 py-2">
288
- <p className="flex items-center gap-2 kol-helper-12 text-fg-48 mx-2 px-4 border-x border-transparent">
289
- <Kbd icon="corner-down-left" />
290
- {query ? enterLabel : 'Go to page'}
291
- </p>
287
+ {/* Enter's line names what Enter does NOW: a row is highlighted → it opens that row;
288
+ * no rows → it commits the query. */}
289
+ {(results.length > 0 || enterLabel) && (
290
+ <p className="flex items-center gap-2 kol-helper-12 text-fg-48 mx-2 px-4 border-x border-transparent">
291
+ <Kbd icon="corner-down-left" />
292
+ {results.length > 0 ? 'Go to page' : enterLabel}
293
+ </p>
294
+ )}
295
+ {/* THE RESULTS LINE IS A LINK (user 2026-10-01: *"make 'all results...' at the bottom
296
+ * also be a link to the results page"*) — the same door ⌘Enter opens. */}
292
297
  {query && onOpenResults && (
293
- <p className="flex items-center gap-2 kol-helper-12 text-fg-48 mx-2 px-4 border-x border-transparent pt-1">
298
+ <button
299
+ type="button"
300
+ className="flex items-center gap-2 kol-helper-12 text-fg-48 hover:text-fg-default transition-colors mx-2 px-4 border-x border-transparent pt-1"
301
+ onMouseDown={(e) => e.preventDefault()}
302
+ onClick={() => { onOpenResults(query); onClose?.() }}
303
+ >
294
304
  <Kbd icon="command"><Icon name="corner-down-left" size={12} /></Kbd>
295
305
  {resultsLabel}
296
- </p>
306
+ </button>
297
307
  )}
298
308
  </div>
299
309
  )}
@@ -33,7 +33,7 @@ const WIDTHS = {
33
33
  column: '',
34
34
  }
35
35
 
36
- const Table = ({ caption, columns, rows, variant = 'default', className = '', width = 'panel', rowClassName, compact = false }) => {
36
+ const Table = ({ caption, columns, rows, variant = 'default', className = '', width = 'panel', rowClassName, compact = false, lastRule = false }) => {
37
37
  const [sort, setSort] = useState({ key: null, dir: null })
38
38
 
39
39
  const cycle = (key) =>
@@ -58,7 +58,9 @@ const Table = ({ caption, columns, rows, variant = 'default', className = '', wi
58
58
  const variantClass = variant === 'simple' ? 'kol-table--simple' : ''
59
59
  /* compact — record-surface density (RecordManager, 2026-08-09): the default
60
60
  * 12/16 cell box read as bloated rows against the reference table. */
61
- const wrapperClass = ['kol-table-wrapper', variantClass, compact && 'kol-table--compact', WIDTHS[width] ?? WIDTHS.panel, className].filter(Boolean).join(' ')
61
+ /* lastRule — `simple` only: the rule under the LAST row. Off by default (2026-10-01): the
62
+ * table is usually followed by a divider of its own, and the two read as a double line. */
63
+ const wrapperClass = ['kol-table-wrapper', variantClass, lastRule && 'kol-table--last-rule', compact && 'kol-table--compact', WIDTHS[width] ?? WIDTHS.panel, className].filter(Boolean).join(' ')
62
64
  return (
63
65
  <div className={wrapperClass}>
64
66
  <table className="kol-table">