@kolkrabbi/kol-component 0.9.0 → 0.10.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.9.0",
3
+ "version": "0.10.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",
@@ -23,7 +23,7 @@
23
23
  "dependencies": {
24
24
  "@floating-ui/react": "^0.27.19",
25
25
  "embla-carousel-react": "^8.6.0",
26
- "@kolkrabbi/kol-icons": "0.5.0"
26
+ "@kolkrabbi/kol-icons": "0.6.0"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "react": "^18.3.0 || ^19.0.0",
@@ -10,7 +10,7 @@ import { Icon } from '@kolkrabbi/kol-icons'
10
10
  *
11
11
  * @param {Object} props
12
12
  * @param {ReactNode} props.children - Button content
13
- * @param {'primary'|'secondary'|'accent'|'outline'|'ghost'|'control'} props.variant - Visual variant. `control` is an alias for `ghost` (legacy call-sites).
13
+ * @param {'primary'|'secondary'|'accent'|'outline'|'ghost'|'danger'|'grey'|'control'} props.variant - Visual variant. `danger` is the destructive treatment (--ui-error fill); `control` is an alias for `ghost` (legacy call-sites).
14
14
  * @param {'sm'|'md'|'lg'} props.size - Button size (default: 'md')
15
15
  * @param {string} props.iconLeft - Icon name to display on the left
16
16
  * @param {string} props.iconRight - Icon name to display on the right
@@ -71,6 +71,10 @@ const Button = ({
71
71
  ? 'kol-btn-outline'
72
72
  : resolvedVariant === 'ghost'
73
73
  ? 'kol-btn-ghost'
74
+ : resolvedVariant === 'danger'
75
+ ? 'kol-btn-danger'
76
+ : resolvedVariant === 'grey'
77
+ ? 'kol-btn-grey'
74
78
  : 'kol-btn-secondary'
75
79
 
76
80
  // Add size class — pairs the padding rule with its mono type class.
@@ -1,6 +1,6 @@
1
1
  import { useEffect, useRef } from 'react'
2
2
 
3
- export default function FullscreenOverlay({ open, onClose, children }) {
3
+ export default function FullscreenOverlay({ open, onClose, closeButton = true, children }) {
4
4
  const sheetRef = useRef(null)
5
5
 
6
6
  useEffect(() => {
@@ -24,14 +24,16 @@ export default function FullscreenOverlay({ open, onClose, children }) {
24
24
  return (
25
25
  <div className="kol-overlay" role="dialog" aria-modal="true" onMouseDown={onBackdropClick}>
26
26
  <div ref={sheetRef} className="kol-overlay-sheet">
27
- <button
28
- type="button"
29
- className="kol-overlay-close"
30
- onClick={onClose}
31
- aria-label="Close"
32
- >
33
- ×
34
- </button>
27
+ {closeButton && (
28
+ <button
29
+ type="button"
30
+ className="kol-overlay-close"
31
+ onClick={onClose}
32
+ aria-label="Close"
33
+ >
34
+ ×
35
+ </button>
36
+ )}
35
37
  {children}
36
38
  </div>
37
39
  </div>
@@ -1,3 +1,4 @@
1
+ import { useEffect, useRef, useState } from 'react'
1
2
  import { Icon } from '@kolkrabbi/kol-icons'
2
3
 
3
4
  /**
@@ -7,6 +8,14 @@ import { Icon } from '@kolkrabbi/kol-icons'
7
8
  * icon, then two mutually exclusive trailing affordances: a caller-authored
8
9
  * shortcut kbd chip while the value is empty, a clear × while it isn't.
9
10
  *
11
+ * `expanding` is the third body plan (after the shell and `bare`): a round
12
+ * icon-button pill that animates open into an inline field — the /work
13
+ * navbar pattern, extracted from kol-content's WorkViewToggle (2026-07-15).
14
+ * `open` is controllable so a parent can choreograph siblings around it
15
+ * (collapse a toggle, reveal a close button); uncontrolled works too.
16
+ * Escape reports `onOpenChange(false)` — clearing on close stays the
17
+ * parent's call.
18
+ *
10
19
  * Always controlled (`value` defaults to ''). Controlled + no onChange =
11
20
  * deliberate display-only → readOnly, same convention as Input. Extra props
12
21
  * (onKeyDown, autoFocus, role, aria-*) spread onto the inner <input> —
@@ -23,13 +32,19 @@ import { Icon } from '@kolkrabbi/kol-icons'
23
32
  * @param {string} shortcutHint caller-authored kbd chip (e.g. "⌘K"); shown while the value is empty
24
33
  * @param {Function} onFocus (event) => void — input focus
25
34
  * @param {string} size 'sm' | 'md' — kol-control size + matched mono type class
26
- * @param {string} variant 'filled' | 'ghost' | 'outline' — kol-control variant, same chrome as Input (ignored when bare)
35
+ * @param {string} variant 'filled' | 'ghost' | 'outline' — kol-control variant, same chrome as Input (ignored when bare/expanding)
27
36
  * @param {boolean} bare borderless inline field for overlay panels (full width, no shell chrome; keeps icon/clear/chip slots)
37
+ * @param {boolean} expanding round icon-button pill that animates open into an inline field
38
+ * @param {boolean} open (expanding) controlled open state; omit for internal state
39
+ * @param {Function} onOpenChange (expanding) (bool) => void — trigger click / Escape
40
+ * @param {number} expandedWidth (expanding) open pill width in px (default 280)
41
+ * @param {string} triggerLabel (expanding) aria-label on the icon trigger (default 'Open search')
28
42
  * @param {string} className extra classes on the shell
29
43
  */
30
44
 
31
45
  const SIZE_TYPE = { sm: 'kol-mono-12', md: 'kol-mono-14' }
32
46
  const ICON_SIZE = { sm: 14, md: 14 }
47
+ const CUBIC_EASE = 'cubic-bezier(0.16, 1, 0.3, 1)'
33
48
 
34
49
  export default function SearchInput({
35
50
  value = '',
@@ -41,9 +56,59 @@ export default function SearchInput({
41
56
  size = 'md',
42
57
  variant = 'filled',
43
58
  bare = false,
59
+ expanding = false,
60
+ open,
61
+ onOpenChange,
62
+ expandedWidth = 280,
63
+ triggerLabel = 'Open search',
44
64
  className = '',
45
65
  ...inputProps
46
66
  }) {
67
+ /* expanding body plan — hooks stay unconditional (expanding never flips at runtime) */
68
+ const [internalOpen, setInternalOpen] = useState(false)
69
+ const inputRef = useRef(null)
70
+ const isOpen = open !== undefined ? open : internalOpen
71
+ const setOpen = (next) => {
72
+ if (open === undefined) setInternalOpen(next)
73
+ onOpenChange?.(next)
74
+ }
75
+
76
+ useEffect(() => {
77
+ if (expanding && isOpen) inputRef.current?.focus()
78
+ }, [expanding, isOpen])
79
+
80
+ if (expanding) {
81
+ return (
82
+ <div
83
+ className={`flex items-center bg-fg-04 rounded-full h-9 ${className}`.trim()}
84
+ style={{ width: isOpen ? expandedWidth : 36, transition: `width 600ms ${CUBIC_EASE}` }}
85
+ >
86
+ <button
87
+ type="button"
88
+ className={`flex items-center justify-center w-9 h-9 rounded-full text-auto flex-shrink-0 border border-transparent ${isOpen ? '' : 'transition-colors hover:border-fg-12'}`}
89
+ onClick={() => !isOpen && setOpen(true)}
90
+ aria-label={triggerLabel}
91
+ aria-expanded={isOpen}
92
+ >
93
+ <Icon name="search" size={16} className="text-fg-80" />
94
+ </button>
95
+ {isOpen && (
96
+ <input
97
+ ref={inputRef}
98
+ type="search"
99
+ value={value ?? ''}
100
+ onChange={onChange}
101
+ readOnly={!onChange || undefined}
102
+ placeholder={placeholder}
103
+ spellCheck={false}
104
+ className="bg-transparent outline-none kol-helper-14 flex-1 text-fg-80 caret-current pr-4 min-w-0 appearance-none [&::-webkit-search-cancel-button]:hidden"
105
+ onKeyDown={(e) => { if (e.key === 'Escape') setOpen(false) }}
106
+ {...inputProps}
107
+ />
108
+ )}
109
+ </div>
110
+ )
111
+ }
47
112
  const shellCls = [
48
113
  bare
49
114
  ? 'flex w-full gap-2.5 px-4 py-3'
@@ -38,8 +38,10 @@ export function AccordionPanel({
38
38
  onClick={toggle}
39
39
  aria-expanded={open}
40
40
  >
41
- <span className="kol-accordion-title kol-helper-12 uppercase tracking-widest">{title}</span>
42
- {meta && <span className="kol-accordion-meta kol-helper-12 uppercase tracking-widest text-fg-48 ml-auto mr-3">{meta}</span>}
41
+ {/* no text-transform (casing law 2026-07-15): titles render as authored —
42
+ * kol-helper-12 already carries the tracked-caps letterspacing */}
43
+ <span className="kol-accordion-title kol-helper-12">{title}</span>
44
+ {meta && <span className="kol-accordion-meta kol-helper-12 text-fg-48 ml-auto mr-3">{meta}</span>}
43
45
  <span className="kol-accordion-chevron kol-helper-16 text-fg-48 min-w-3 transition-colors duration-[120ms] ml-auto" aria-hidden="true">{open ? '−' : '+'}</span>
44
46
  </button>
45
47
  {open && <div className="kol-accordion-body pt-2 pb-6">{children}</div>}
@@ -13,6 +13,8 @@ import { PopoverPanel, usePopover } from '../atoms/Popover.jsx'
13
13
  *
14
14
  * variant="primary" (default) — filled trigger; open panel continues the
15
15
  * same fill (one piece: no border, no gap, hairline divider inside)
16
+ * variant="grey" — oq-12 filled trigger (opaque per the fill
17
+ * law); panel continues it
16
18
  * variant="outline" — bordered trigger; open panel carries the
17
19
  * same border, trigger's bottom edge acts as the divider
18
20
  *
@@ -110,7 +112,9 @@ const Dropdown = ({
110
112
 
111
113
  const triggerCls = [
112
114
  'kol-btn',
113
- `kol-btn-${resolvedVariant}`,
115
+ /* grey is dropdown-only chrome (2026-07-15) — no kol-btn-* class so it
116
+ * never leaks into Button's variant set */
117
+ resolvedVariant === 'grey' ? 'kol-dd-trigger--grey' : `kol-btn-${resolvedVariant}`,
114
118
  `kol-btn-${resolvedSize}`,
115
119
  SIZE_TYPE[resolvedSize],
116
120
  'kol-dd-trigger',
@@ -154,7 +158,7 @@ const Dropdown = ({
154
158
  focus={false}
155
159
  className={`kol-dd-panel kol-dd-panel--${resolvedVariant}`}
156
160
  >
157
- {resolvedVariant === 'primary' && <div className="kol-dd-div" />}
161
+ {(resolvedVariant === 'primary' || resolvedVariant === 'grey') && <div className="kol-dd-div" />}
158
162
 
159
163
  <div className="flex max-h-[300px] flex-col items-stretch overflow-y-auto" role="listbox">
160
164
  {options.map((option) => {
@@ -95,7 +95,7 @@ export function MenuDropdownItem({ onClick, disabled, prefix, iconLeft, shortcut
95
95
  onClick={onClick}
96
96
  disabled={disabled}
97
97
  role="menuitem"
98
- className="w-full kol-helper-12 px-3 h-8 inline-flex items-center gap-2 text-body hover:text-emphasis disabled:opacity-40 disabled:cursor-not-allowed text-left"
98
+ className="w-full kol-helper-12 px-3 h-8 shrink-0 inline-flex items-center gap-2 text-body hover:text-emphasis disabled:opacity-40 disabled:cursor-not-allowed text-left"
99
99
  >
100
100
  {prefix && <span className="shrink-0 inline-flex items-center">{prefix}</span>}
101
101
  {iconLeft && <span className="shrink-0 w-4 inline-flex items-center justify-center">{iconLeft}</span>}