@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.
|
|
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.
|
|
26
|
+
"@kolkrabbi/kol-icons": "0.6.0"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
29
|
"react": "^18.3.0 || ^19.0.0",
|
package/src/atoms/Button.jsx
CHANGED
|
@@ -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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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>}
|