@kolkrabbi/kol-component 0.9.0 → 0.10.1

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.1",
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.1"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "react": "^18.3.0 || ^19.0.0",
@@ -10,7 +10,7 @@ export default function AssetPlaceholder({
10
10
  category,
11
11
  name,
12
12
  aspectRatio = '16 / 9',
13
- note = 'missing',
13
+ note = 'MISSING',
14
14
  className = '',
15
15
  }) {
16
16
  const label = [category, name].filter(Boolean).join(' · ')
@@ -23,7 +23,8 @@ export default function AssetPlaceholder({
23
23
  >
24
24
  {/* note is single-line → helper scale; label can wrap (category · name
25
25
  * in a narrow tile) → line-height-bearing mono scale */}
26
- <span className="kol-asset-placeholder-note kol-helper-12 uppercase [letter-spacing:0.12em] text-fg-48">{note}</span>
26
+ {/* no text-transform (casing law) — note is authored in-case */}
27
+ <span className="kol-asset-placeholder-note kol-helper-12 [letter-spacing:0.12em] text-fg-48">{note}</span>
27
28
  {label && <span className="kol-asset-placeholder-label kol-mono-12 text-fg-40 [letter-spacing:0.04em]">{label}</span>}
28
29
  </div>
29
30
  )
@@ -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>
@@ -25,7 +25,6 @@ import { Icon } from '@kolkrabbi/kol-icons'
25
25
  * the shell at text-meta. aria-hidden — affordances, not labels.
26
26
  * iconLeft — name of a leading icon rendered inside the shell (e.g.
27
27
  * "search-16"). iconSize overrides the size-derived default.
28
- * uppercase — adds Tailwind `uppercase` to the input element only.
29
28
  *
30
29
  * Chrome (bg/border/padding/transition/disabled) comes from .kol-control;
31
30
  * Input owns prefix/suffix/icon layout + the inner <input> styling.
@@ -47,7 +46,6 @@ export default function Input({
47
46
  iconSize = null,
48
47
  placeholder,
49
48
  disabled = false,
50
- uppercase = false,
51
49
  width,
52
50
  className = '',
53
51
  inputClassName = '',
@@ -86,7 +84,6 @@ export default function Input({
86
84
  * the affordance than to the empty edge, reads off-balance. */
87
85
  prefix !== undefined && 'pr-1',
88
86
  suffix !== undefined && 'pl-1',
89
- uppercase && 'uppercase',
90
87
  isNumber && 'hide-number-spinners',
91
88
  inputClassName,
92
89
  ].filter(Boolean).join(' ')
@@ -44,7 +44,7 @@ export default function LabeledControl({
44
44
  <div className={`flex items-center gap-3 ${className}`}>
45
45
  {showLabel && (
46
46
  <span
47
- className="kol-helper-10 uppercase tracking-widest text-meta shrink-0"
47
+ className="kol-helper-10 tracking-widest text-meta shrink-0"
48
48
  style={{ width: labelWidth }}
49
49
  >
50
50
  {labelInner}
@@ -58,7 +58,7 @@ export default function LabeledControl({
58
58
  return (
59
59
  <div className={`flex flex-col gap-2 ${className}`}>
60
60
  {showLabel && (
61
- <span className="kol-helper-10 uppercase tracking-widest text-meta">{labelInner}</span>
61
+ <span className="kol-helper-10 tracking-widest text-meta">{labelInner}</span>
62
62
  )}
63
63
  {children}
64
64
  </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'
@@ -8,7 +8,7 @@ export default function Section({ label, children, className = '' }) {
8
8
  return (
9
9
  <div className={`flex flex-col gap-2 ${className}`}>
10
10
  {label && (
11
- <p className="kol-helper-10 uppercase tracking-widest text-meta">{label}</p>
11
+ <p className="kol-helper-10 tracking-widest text-meta">{label}</p>
12
12
  )}
13
13
  {children}
14
14
  </div>
package/src/atoms/Tag.jsx CHANGED
@@ -71,7 +71,7 @@ export default function Tag({
71
71
  className="tag-dismiss"
72
72
  onClick={handleRemove}
73
73
  >
74
- <Icon name="cross" size={iconSize} />
74
+ <Icon name="x" size={iconSize} />
75
75
  </span>
76
76
  )}
77
77
  </Element>
@@ -6,7 +6,7 @@ import { Icon } from '@kolkrabbi/kol-icons'
6
6
  * `--textarea` modifier (display: block).
7
7
  *
8
8
  * variant="filled" (default) — persistent solid bg
9
- * variant="ghost" — borderless at rest, reveals on hover/focus
9
+ * variant="ghost" — legacy alias, resolves to outline
10
10
  * variant="outline" — bordered, transparent bg
11
11
  *
12
12
  * size="sm" / "md" (default) / "lg" — padding + type class
@@ -25,8 +25,8 @@ const ToggleBracket = ({
25
25
  const isPlain = variant === 'plain'
26
26
 
27
27
  const cls = isPlain
28
- ? `inline-flex items-center gap-3 kol-mono-12 uppercase text-meta hover:text-emphasis transition-colors bg-transparent border-0 p-0 cursor-pointer ${className}`
29
- : `kol-control kol-control--filled kol-control-md kol-mono-12 uppercase justify-between gap-3 ${value ? 'toggle-bracket--active' : ''} ${className}`
28
+ ? `inline-flex items-center gap-3 kol-mono-12 text-meta hover:text-emphasis transition-colors bg-transparent border-0 p-0 cursor-pointer ${className}`
29
+ : `kol-control kol-control--filled kol-control-md kol-mono-12 justify-between gap-3 ${value ? 'toggle-bracket--active' : ''} ${className}`
30
30
 
31
31
  return (
32
32
  <button
@@ -29,9 +29,9 @@ const ToggleCheckbox = ({
29
29
  <polyline points="1 5 4 8 11 1" fill="none" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
30
30
  </svg>
31
31
  </span>
32
- <span className="toggle-checkbox-label kol-helper-12 uppercase tracking-[0.08em]">
32
+ <span className="toggle-checkbox-label kol-helper-12 tracking-[0.08em]">
33
33
  {label}
34
- {hint ? <span className="ml-2 opacity-60 normal-case tracking-normal kol-helper-10">{hint}</span> : null}
34
+ {hint ? <span className="ml-2 opacity-60 tracking-normal kol-helper-10">{hint}</span> : null}
35
35
  </span>
36
36
  </label>
37
37
  )
@@ -13,7 +13,7 @@ import { Icon } from '@kolkrabbi/kol-icons'
13
13
  * inactive = off (bare text). Use for compact on/off
14
14
  * where a segmented two-button toggle is overkill.
15
15
  *
16
- * Built on the .kol-control shell. Default options use grid-06 / list-01
16
+ * Built on the .kol-control shell. Default options use grid / view-list
17
17
  * icons; consumers can pass `options` to override. `iconVariant` picks the
18
18
  * icon cut for variant="icon" ('stroke' default; 'solid' reads better at
19
19
  * 14px). For `variant="single"`, the FIRST option in `options` is the "off"
@@ -25,8 +25,8 @@ const ViewToggle = ({
25
25
  variant = 'text',
26
26
  iconVariant = 'stroke',
27
27
  options = [
28
- { value: 'grid', label: 'Grid view', icon: 'grid-06' },
29
- { value: 'list', label: 'List view', icon: 'list-01' }
28
+ { value: 'grid', label: 'Grid view', icon: 'grid' },
29
+ { value: 'list', label: 'List view', icon: 'view-list' }
30
30
  ],
31
31
  className = ''
32
32
  }) => {
package/src/index.js CHANGED
@@ -117,6 +117,7 @@ export { Icon } from '@kolkrabbi/kol-icons'
117
117
 
118
118
  // graphics (SVG illustration loader — globs its own ./graphics/svg/**)
119
119
  export { default as Graphic, GRAPHICS } from './graphics/Graphic.jsx'
120
+ export { GRAPHIC_RAW } from './graphics/graphicData.js'
120
121
 
121
122
  // hooks
122
123
  export { default as usePrefersReducedMotion } from './hooks/usePrefersReducedMotion.js'
@@ -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>}
@@ -1,11 +1,27 @@
1
1
  import CopyButton from '../atoms/CopyButton.jsx'
2
2
 
3
- export default function CodeBlock({ children, language }) {
3
+ /**
4
+ * CodeBlock — a chrome'd code block with language chip, filename chip, and copy.
5
+ *
6
+ * Accepts three input shapes (first match wins per field):
7
+ * • Portable Text: `value={{ code, language, filename }}` — the Sanity code-block
8
+ * object can be passed straight through from a portable-text map.
9
+ * • Direct props: `code` / `language` / `filename`.
10
+ * • Children: `<CodeBlock language="js">{'…'}</CodeBlock>` (the original API).
11
+ *
12
+ * DOM order is copy → filename → lang → pre so the `.kol-codeblock-*` + pre
13
+ * adjacency rules in the chrome CSS can pad the pre when chips are present.
14
+ */
15
+ export default function CodeBlock({ children, code, language, filename, value }) {
16
+ const src = code ?? value?.code ?? children
17
+ const lang = language ?? value?.language
18
+ const file = filename ?? value?.filename
4
19
  return (
5
20
  <div className="kol-codeblock">
6
- {language && <span className="kol-codeblock-lang">{language}</span>}
7
- <CopyButton text={String(children)} className="kol-codeblock-copy" />
8
- <pre><code>{children}</code></pre>
21
+ <CopyButton text={String(src ?? '')} className="kol-codeblock-copy" />
22
+ {file && <span className="kol-codeblock-filename">{file}</span>}
23
+ {lang && <span className="kol-codeblock-lang">{lang}</span>}
24
+ <pre><code>{src}</code></pre>
9
25
  </div>
10
26
  )
11
27
  }
@@ -136,7 +136,6 @@ export default function ColorInputRow({
136
136
  prefix="#"
137
137
  chars={6}
138
138
  maxLength={6}
139
- uppercase
140
139
  value={digits}
141
140
  onChange={(e) => onChange?.('#' + e.target.value.replace(/^#/, '').toUpperCase())}
142
141
  disabled={disabled}
@@ -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>}
@@ -43,7 +43,7 @@ export default function TabsRow({ tabs = [], value, onChange, onClose, onMinimis
43
43
  className="text-meta hover:text-emphasis self-center"
44
44
  style={{ lineHeight: 0 }}
45
45
  >
46
- <Icon name="close" size={12} />
46
+ <Icon name="x" size={12} />
47
47
  </button>
48
48
  )}
49
49