@kolkrabbi/kol-component 0.1.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/README.md +24 -0
- package/package.json +46 -0
- package/src/atoms/Avatar.jsx +17 -0
- package/src/atoms/Button.jsx +172 -0
- package/src/atoms/ColorSwatch.jsx +126 -0
- package/src/atoms/Divider.jsx +34 -0
- package/src/atoms/Input.jsx +121 -0
- package/src/atoms/Label.jsx +12 -0
- package/src/atoms/Slider.jsx +129 -0
- package/src/atoms/Stepper.jsx +97 -0
- package/src/atoms/Textarea.jsx +70 -0
- package/src/atoms/ToggleCheckbox.jsx +40 -0
- package/src/atoms/ToggleSwitch.jsx +42 -0
- package/src/atoms/TransparentX.jsx +37 -0
- package/src/graphics/Graphic.jsx +53 -0
- package/src/graphics/svg/abstract/abstract-01.svg +3 -0
- package/src/graphics/svg/abstract/abstract-02.svg +3 -0
- package/src/graphics/svg/abstract/abstract-03.svg +3 -0
- package/src/graphics/svg/abstract/abstract-06.svg +4 -0
- package/src/graphics/svg/diagrams/.gitkeep +0 -0
- package/src/graphics/svg/diagrams/ac-structure.svg +90 -0
- package/src/graphics/svg/diagrams/rg-x-height.svg +3 -0
- package/src/graphics/svg/diagrams/structure-grid.svg +618 -0
- package/src/graphics/svg/diagrams/structure-logo.svg +23 -0
- package/src/graphics/svg/patterns/.gitkeep +0 -0
- package/src/graphics/svg/patterns/patt-01.svg +34 -0
- package/src/graphics/svg/patterns/patt-02.svg +34 -0
- package/src/graphics/svg/patterns/patt-03.svg +110 -0
- package/src/graphics/svg/patterns/patt-04.svg +10 -0
- package/src/graphics/svg/patterns/patt-05.svg +62 -0
- package/src/graphics/svg/patterns/patt-06.svg +66 -0
- package/src/graphics/svg/patterns/patt-07.svg +130 -0
- package/src/graphics/svg/patterns/patt-08.svg +434 -0
- package/src/graphics/svg/patterns/patt-09.svg +38 -0
- package/src/graphics/svg/patterns/patt-10.svg +20 -0
- package/src/graphics/svg/patterns/patt-11.svg +38 -0
- package/src/graphics/svg/patterns/patt-12.svg +58 -0
- package/src/graphics/svg/patterns/patt-13.svg +48 -0
- package/src/graphics/svg/patterns/patt-14.svg +90 -0
- package/src/graphics/svg/patterns/patt-15.svg +194 -0
- package/src/graphics/svg/patterns/patt-16.svg +12 -0
- package/src/graphics/svg/social/social-01.svg +18 -0
- package/src/graphics/svg/social/social-02.svg +18 -0
- package/src/graphics/svg/social/social-03.svg +18 -0
- package/src/graphics/svg/social/social-04.svg +18 -0
- package/src/graphics/svg/social/social-05.svg +18 -0
- package/src/graphics/svg/social/social-06.svg +18 -0
- package/src/graphics/svg/social/social-07.svg +22 -0
- package/src/graphics/svg/social/social-08.svg +22 -0
- package/src/graphics/svg/social/social-09.svg +22 -0
- package/src/graphics/svg/social/social-10.svg +6 -0
- package/src/graphics/svg/social/social-11.svg +6 -0
- package/src/graphics/svg/social/social-12.svg +6 -0
- package/src/graphics/svg/social/social-13.svg +9 -0
- package/src/graphics/svg/social/social-14.svg +9 -0
- package/src/graphics/svg/social/social-15.svg +9 -0
- package/src/graphics/svg/structure/diagram-grid-lockup-hori.svg +23 -0
- package/src/graphics/svg/structure/diagram-grid-lockup-vert.svg +25 -0
- package/src/graphics/svg/structure/diagram-grid-logomark.svg +20 -0
- package/src/graphics/svg/structure/diagram-grid-wordmark.svg +18 -0
- package/src/graphics/svg/structure/diagram-logo-lockup-hori.svg +9 -0
- package/src/graphics/svg/structure/diagram-logo-lockup-vert.svg +23 -0
- package/src/graphics/svg/structure/diagram-logo-logomark.svg +7 -0
- package/src/graphics/svg/structure/diagram-logo-wordmark.svg +3 -0
- package/src/graphics/svg/structure/diagram-x-lockup-hori.svg +5 -0
- package/src/graphics/svg/structure/diagram-x-lockup-vert.svg +10 -0
- package/src/graphics/svg/structure/diagram-x-logomark.svg +5 -0
- package/src/graphics/svg/structure/diagram-x-wordmark.svg +5 -0
- package/src/hooks/useReveal.js +28 -0
- package/src/hooks/useScrollSpy.js +56 -0
- package/src/index.js +59 -0
- package/src/molecules/Badge.jsx +51 -0
- package/src/molecules/ContentFilters.jsx +263 -0
- package/src/molecules/Dropdown.jsx +223 -0
- package/src/molecules/DropdownTagFilter.jsx +253 -0
- package/src/molecules/LabeledControl.jsx +66 -0
- package/src/molecules/MenuItem.jsx +148 -0
- package/src/molecules/MenuPopover.jsx +128 -0
- package/src/molecules/Modal.jsx +124 -0
- package/src/molecules/Pill.jsx +33 -0
- package/src/molecules/Popover.jsx +208 -0
- package/src/molecules/PropertyInput.jsx +32 -0
- package/src/molecules/QuantityInput.jsx +174 -0
- package/src/molecules/QuantityStepper.jsx +149 -0
- package/src/molecules/Section.jsx +16 -0
- package/src/molecules/SectionLabel.jsx +59 -0
- package/src/molecules/SegmentedToggle.jsx +56 -0
- package/src/molecules/Tag.jsx +79 -0
- package/src/molecules/ToggleBracket.jsx +45 -0
- package/src/molecules/ViewToggle.jsx +101 -0
- package/src/organisms/Table.jsx +46 -0
- package/src/primitives/Accordion.jsx +45 -0
- package/src/primitives/AssetPlaceholder.jsx +28 -0
- package/src/primitives/Carousel.jsx +50 -0
- package/src/primitives/CodeBlock.jsx +41 -0
- package/src/primitives/ExitPreview.jsx +12 -0
- package/src/primitives/FullscreenOverlay.jsx +39 -0
- package/src/primitives/Image.jsx +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# @kolkrabbi/kol-component
|
|
2
|
+
|
|
3
|
+
The KOL design-system component library — atoms through organisms. Components emit canonical `kol-*` classes; their styling lives in [`@kolkrabbi/kol-theme`](https://www.npmjs.com/package/@kolkrabbi/kol-theme).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm i @kolkrabbi/kol-component @kolkrabbi/kol-theme
|
|
9
|
+
# react, react-dom are peers; react-router-dom is an optional peer (some components)
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Requires a **Vite + Tailwind v4** app and the theme CSS imported (see the theme package).
|
|
13
|
+
|
|
14
|
+
## Use
|
|
15
|
+
|
|
16
|
+
```jsx
|
|
17
|
+
import { Button, Tag, Badge, Slider, Dropdown, Table } from '@kolkrabbi/kol-component'
|
|
18
|
+
import { Icon } from '@kolkrabbi/kol-loader'
|
|
19
|
+
|
|
20
|
+
<Button variant="primary" iconLeft="plus">New</Button>
|
|
21
|
+
<Badge variant="success">Active</Badge>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Atoms (Button, Input, Slider, Toggle\*, …), molecules (Dropdown, Tag, Badge, Modal, Popover, …), primitives (Accordion, Carousel, CodeBlock, Image, …), an organism (Table), graphics, and hooks (`useReveal`, `useScrollSpy`). See the [usage reference](https://github.com/kolkrabbi/kol-design-system/tree/main/docs/usage) for real examples of each.
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kolkrabbi/kol-component",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "KOL design-system components — atoms through organisms, emitting canonical kol-* classes. Pairs with @kolkrabbi/kol-theme for styling.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./src/index.js",
|
|
8
|
+
"module": "./src/index.js",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./src/index.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"src",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"keywords": [
|
|
17
|
+
"kol",
|
|
18
|
+
"kolkrabbi",
|
|
19
|
+
"design-system",
|
|
20
|
+
"react",
|
|
21
|
+
"components"
|
|
22
|
+
],
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@floating-ui/react": "^0.27.19",
|
|
25
|
+
"embla-carousel-react": "^8.6.0",
|
|
26
|
+
"@kolkrabbi/kol-loader": "0.1.0"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"react": "^18.3.0 || ^19.0.0",
|
|
30
|
+
"react-dom": "^18.3.0 || ^19.0.0",
|
|
31
|
+
"react-router-dom": "^6.0.0 || ^7.0.0"
|
|
32
|
+
},
|
|
33
|
+
"peerDependenciesMeta": {
|
|
34
|
+
"react-router-dom": {
|
|
35
|
+
"optional": true
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/kolkrabbi/kol-design-system.git",
|
|
41
|
+
"directory": "packages/component"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const SIZE_MAP = {
|
|
2
|
+
sm: 'w-8 h-8 text-xs',
|
|
3
|
+
md: 'w-10 h-10 text-sm',
|
|
4
|
+
lg: 'w-14 h-14 text-base',
|
|
5
|
+
xl: 'w-24 h-24 text-3xl',
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function Avatar({ initial, size = 'sm', className = '' }) {
|
|
9
|
+
const sizeCls = SIZE_MAP[size] ?? SIZE_MAP.sm
|
|
10
|
+
return (
|
|
11
|
+
<span
|
|
12
|
+
className={`kol-avatar inline-flex items-center justify-center rounded-full bg-surface-secondary text-emphasis font-narrow font-semibold shrink-0 ${sizeCls} ${className}`}
|
|
13
|
+
>
|
|
14
|
+
{initial}
|
|
15
|
+
</span>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { Icon } from '@kolkrabbi/kol-loader'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Button — canonical KOL button. Emits kol-btn* classes (CSS in @kol/theme).
|
|
5
|
+
*
|
|
6
|
+
* Supports link (href) and button (onClick / default) forms with optional
|
|
7
|
+
* icons (left / right / icon-only), per-icon hover swap, and a selected
|
|
8
|
+
* (toggle) state.
|
|
9
|
+
*
|
|
10
|
+
* @param {Object} props
|
|
11
|
+
* @param {ReactNode} props.children - Button content
|
|
12
|
+
* @param {'primary'|'secondary'|'accent'|'outline'|'ghost'|'control'} props.variant - Visual variant. `control` is an alias for `ghost` (legacy call-sites).
|
|
13
|
+
* @param {'sm'|'md'|'lg'} props.size - Button size (default: 'md')
|
|
14
|
+
* @param {string} props.iconLeft - Icon name to display on the left
|
|
15
|
+
* @param {string} props.iconRight - Icon name to display on the right
|
|
16
|
+
* @param {string} props.iconLeftHover - Icon to show on hover (left position)
|
|
17
|
+
* @param {string} props.iconRightHover - Icon to show on hover (right position)
|
|
18
|
+
* @param {string} props.iconOnly - Icon name for icon-only button
|
|
19
|
+
* @param {string} props.iconOnlyHover - Icon to show on hover (icon-only)
|
|
20
|
+
* @param {boolean} props.animateIcon - Disable default hover states to focus on icon animation
|
|
21
|
+
* @param {boolean} props.quiet - Dimmed at rest, brightens on hover; stays dimmed when disabled. For secondary icon-only chrome.
|
|
22
|
+
* @param {number} props.iconSize - Size of the icon in pixels (default: auto by size)
|
|
23
|
+
* @param {number} props.iconGap - Gap between icon and text (default: 8)
|
|
24
|
+
* @param {string} props.href - Link destination (makes it an <a>)
|
|
25
|
+
* @param {Function} props.onClick - Click handler (makes it a <button>)
|
|
26
|
+
* @param {string} props.className - Additional classes
|
|
27
|
+
* @param {Object} props.style - Inline styles
|
|
28
|
+
* @param {string} props.type - Button type attribute (default: 'button')
|
|
29
|
+
* @param {boolean} props.disabled - Disabled state
|
|
30
|
+
* @param {boolean} props.selected - Selected/active state (toggle highlight)
|
|
31
|
+
*/
|
|
32
|
+
const Button = ({
|
|
33
|
+
children,
|
|
34
|
+
variant = 'primary',
|
|
35
|
+
size = 'md',
|
|
36
|
+
iconLeft,
|
|
37
|
+
iconRight,
|
|
38
|
+
iconLeftHover,
|
|
39
|
+
iconRightHover,
|
|
40
|
+
iconOnly,
|
|
41
|
+
iconOnlyHover,
|
|
42
|
+
animateIcon = false,
|
|
43
|
+
quiet = false,
|
|
44
|
+
iconSize,
|
|
45
|
+
iconGap,
|
|
46
|
+
href,
|
|
47
|
+
onClick,
|
|
48
|
+
className = '',
|
|
49
|
+
style = {},
|
|
50
|
+
type = 'button',
|
|
51
|
+
disabled = false,
|
|
52
|
+
selected = false,
|
|
53
|
+
...props
|
|
54
|
+
}) => {
|
|
55
|
+
const resolvedIconSize = iconSize ?? (size === 'sm' ? 14 : size === 'lg' ? 18 : 16)
|
|
56
|
+
|
|
57
|
+
// `control` is a legacy alias for `ghost` (kept so web call-sites passing
|
|
58
|
+
// variant="control" keep working post-migration).
|
|
59
|
+
const resolvedVariant = variant === 'control' ? 'ghost' : variant
|
|
60
|
+
|
|
61
|
+
const variantClass = resolvedVariant === 'primary'
|
|
62
|
+
? 'kol-btn-primary'
|
|
63
|
+
: resolvedVariant === 'accent'
|
|
64
|
+
? 'kol-btn-accent'
|
|
65
|
+
: resolvedVariant === 'outline'
|
|
66
|
+
? 'kol-btn-outline'
|
|
67
|
+
: resolvedVariant === 'ghost'
|
|
68
|
+
? 'kol-btn-ghost'
|
|
69
|
+
: 'kol-btn-secondary'
|
|
70
|
+
|
|
71
|
+
// Add size class — pairs the padding rule with its mono type class.
|
|
72
|
+
const sizeClass = size === 'sm'
|
|
73
|
+
? 'kol-btn-sm kol-mono-12'
|
|
74
|
+
: size === 'lg'
|
|
75
|
+
? 'kol-btn-lg kol-mono-16'
|
|
76
|
+
: 'kol-btn-md kol-mono-14'
|
|
77
|
+
|
|
78
|
+
// Add kol-btn-animate class if animateIcon is true to disable default hover states
|
|
79
|
+
const animateClass = animateIcon ? 'kol-btn-animate' : ''
|
|
80
|
+
const quietClass = quiet ? 'kol-btn-quiet' : ''
|
|
81
|
+
const selectedClass = selected ? 'kol-btn-selected' : ''
|
|
82
|
+
|
|
83
|
+
const combinedClass = `kol-btn ${variantClass} ${sizeClass} ${animateClass} ${quietClass} ${selectedClass} ${className}`.trim().replace(/\s+/g, ' ')
|
|
84
|
+
|
|
85
|
+
// Render icon with optional hover state
|
|
86
|
+
const renderIcon = (iconName, iconHoverName) => {
|
|
87
|
+
if (!iconName && !iconHoverName) return null
|
|
88
|
+
|
|
89
|
+
// If no hover icon, render single icon
|
|
90
|
+
if (!iconHoverName) {
|
|
91
|
+
return <Icon name={iconName} size={resolvedIconSize} />
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Render both default and hover icons with positioning
|
|
95
|
+
return (
|
|
96
|
+
<span className="kol-icon-swap-container" style={{ position: 'relative', display: 'inline-flex', width: resolvedIconSize, height: resolvedIconSize, overflow: 'hidden' }}>
|
|
97
|
+
<Icon
|
|
98
|
+
name={iconName}
|
|
99
|
+
size={resolvedIconSize}
|
|
100
|
+
className="kol-icon-default"
|
|
101
|
+
style={{ position: 'absolute' }}
|
|
102
|
+
/>
|
|
103
|
+
<Icon
|
|
104
|
+
name={iconHoverName}
|
|
105
|
+
size={resolvedIconSize}
|
|
106
|
+
className="kol-icon-hover"
|
|
107
|
+
style={{ position: 'absolute' }}
|
|
108
|
+
/>
|
|
109
|
+
</span>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Render content with icons
|
|
114
|
+
const renderContent = () => {
|
|
115
|
+
// Icon-only button
|
|
116
|
+
if (iconOnly) {
|
|
117
|
+
return renderIcon(iconOnly, iconOnlyHover)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Button with icon(s) and text
|
|
121
|
+
if (iconLeft || iconRight || iconLeftHover || iconRightHover) {
|
|
122
|
+
return (
|
|
123
|
+
<span className="flex items-center" style={{ gap: iconGap ?? 8 }}>
|
|
124
|
+
{(iconLeft || iconLeftHover) && <span style={{ marginLeft: -2 }}>{renderIcon(iconLeft, iconLeftHover)}</span>}
|
|
125
|
+
{children}
|
|
126
|
+
{(iconRight || iconRightHover) && <span style={{ marginRight: -2 }}>{renderIcon(iconRight, iconRightHover)}</span>}
|
|
127
|
+
</span>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Text-only button
|
|
132
|
+
return children
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Merge icon-only specific styles with user-provided styles
|
|
136
|
+
const mergedStyle = iconOnly
|
|
137
|
+
? { lineHeight: 0, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', ...style }
|
|
138
|
+
: style
|
|
139
|
+
|
|
140
|
+
// Render as button
|
|
141
|
+
if (onClick || !href) {
|
|
142
|
+
return (
|
|
143
|
+
<button
|
|
144
|
+
onClick={onClick}
|
|
145
|
+
type={type}
|
|
146
|
+
className={combinedClass}
|
|
147
|
+
style={mergedStyle}
|
|
148
|
+
disabled={disabled}
|
|
149
|
+
aria-pressed={selected ? true : undefined}
|
|
150
|
+
aria-label={iconOnly ? (props['aria-label'] || 'Button') : undefined}
|
|
151
|
+
{...props}
|
|
152
|
+
>
|
|
153
|
+
{renderContent()}
|
|
154
|
+
</button>
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Render as link
|
|
159
|
+
return (
|
|
160
|
+
<a
|
|
161
|
+
href={href}
|
|
162
|
+
className={combinedClass}
|
|
163
|
+
style={mergedStyle}
|
|
164
|
+
aria-label={iconOnly ? (props['aria-label'] || 'Link') : undefined}
|
|
165
|
+
{...props}
|
|
166
|
+
>
|
|
167
|
+
{renderContent()}
|
|
168
|
+
</a>
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export default Button
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ColorSwatch — fixed-size color chip atom.
|
|
3
|
+
*
|
|
4
|
+
* onClick provided → renders <button> with hover + selected states
|
|
5
|
+
* onClick omitted → renders <span aria-hidden> — non-interactive preview
|
|
6
|
+
*
|
|
7
|
+
* Pass `showTransparent` to render the TransparentX overlay (for null /
|
|
8
|
+
* unset color slots). When true, bg goes transparent regardless of `hex`.
|
|
9
|
+
*
|
|
10
|
+
* Props:
|
|
11
|
+
* hex — color string, e.g. '#FF6F00'. Ignored if showTransparent.
|
|
12
|
+
* selected — adds active border + ring (border-fg-64 ring-1).
|
|
13
|
+
* size — number (px) for fixed-size, 'fill' (w-full aspect-square,
|
|
14
|
+
* for grid cells that should stay square), or 'stretch'
|
|
15
|
+
* (w-full h-full, for grid cells that stretch to row height).
|
|
16
|
+
* Default 24.
|
|
17
|
+
* radius — 'sm' (4px, default) | 'tight' (2px) | 'none' | 'full' (circle).
|
|
18
|
+
* frame — boolean (default true). When false, no border drawn —
|
|
19
|
+
* used by tightly-packed grid layouts.
|
|
20
|
+
* variant — 'default' (border-based chrome) |
|
|
21
|
+
* 'halo' (macOS-port double box-shadow halo). Halo
|
|
22
|
+
* overrides `frame` since it provides its own ring.
|
|
23
|
+
* hoverable — boolean (default true). When false, no hover border
|
|
24
|
+
* state is applied even if `onClick` is set. For static
|
|
25
|
+
* chips that just open a popover (e.g. inspector fill /
|
|
26
|
+
* stroke swatches).
|
|
27
|
+
* showTransparent — universal "disabled / no value / unset" indicator.
|
|
28
|
+
* Renders white background + TransparentX diagonal
|
|
29
|
+
* stroke; rounded corners clip the line cleanly via
|
|
30
|
+
* `overflow-hidden` on the swatch root.
|
|
31
|
+
* transparentTone — tone of the TransparentX stroke when `showTransparent`
|
|
32
|
+
* is true: 'warning' (default) | 'error' | 'info' |
|
|
33
|
+
* 'success'. Maps through to `var(--ui-{tone})`.
|
|
34
|
+
* onClick — if provided, renders as <button>; else <span>.
|
|
35
|
+
* title — passes through.
|
|
36
|
+
*/
|
|
37
|
+
import TransparentX from './TransparentX'
|
|
38
|
+
|
|
39
|
+
const SIZE_CLASSES = {
|
|
40
|
+
fill: 'w-full aspect-square',
|
|
41
|
+
stretch: 'w-full h-full',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const RADIUS_CLASSES = {
|
|
45
|
+
none: 'rounded-none',
|
|
46
|
+
tight: 'rounded-[2px]',
|
|
47
|
+
sm: 'rounded',
|
|
48
|
+
full: 'rounded-full',
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const HALO_SHADOW = '0 0 0 1px #000, 0 0 0 2px #505050'
|
|
52
|
+
|
|
53
|
+
export default function ColorSwatch({
|
|
54
|
+
hex,
|
|
55
|
+
selected = false,
|
|
56
|
+
size = 24,
|
|
57
|
+
radius = 'tight',
|
|
58
|
+
frame = true,
|
|
59
|
+
variant = 'default',
|
|
60
|
+
hoverable = true,
|
|
61
|
+
showTransparent = false,
|
|
62
|
+
transparentTone = 'warning',
|
|
63
|
+
onClick,
|
|
64
|
+
title,
|
|
65
|
+
className = '',
|
|
66
|
+
...rest
|
|
67
|
+
}) {
|
|
68
|
+
const interactive = typeof onClick === 'function'
|
|
69
|
+
const isNamed = typeof size === 'string'
|
|
70
|
+
const sizeCls = isNamed ? (SIZE_CLASSES[size] ?? '') : ''
|
|
71
|
+
const sizeStyle = isNamed ? null : { width: size, height: size }
|
|
72
|
+
|
|
73
|
+
const isHalo = variant === 'halo'
|
|
74
|
+
const radiusCls = RADIUS_CLASSES[radius] ?? RADIUS_CLASSES.sm
|
|
75
|
+
|
|
76
|
+
/* Halo provides its own ring via box-shadow → no border classes.
|
|
77
|
+
* frame=false → consumer wants a borderless chip (e.g. swatch grids
|
|
78
|
+
* that tile edge-to-edge). */
|
|
79
|
+
const showBorder = !isHalo && frame
|
|
80
|
+
|
|
81
|
+
/* Border policy:
|
|
82
|
+
* default state (not selected) — no border, clean chip
|
|
83
|
+
* selected — 2px border-fg-64 (the selection chrome)
|
|
84
|
+
* halo variant — handled separately via box-shadow
|
|
85
|
+
* `frame` and `hoverable` are kept on the API but no longer drive a
|
|
86
|
+
* default-state border; they're advisory for future variants. */
|
|
87
|
+
const cls = [
|
|
88
|
+
'relative shrink-0 inline-flex overflow-hidden',
|
|
89
|
+
radiusCls,
|
|
90
|
+
!isHalo && selected ? 'border-2 border-fg-64' : '',
|
|
91
|
+
interactive ? 'cursor-pointer' : '',
|
|
92
|
+
sizeCls,
|
|
93
|
+
className,
|
|
94
|
+
].filter(Boolean).join(' ')
|
|
95
|
+
|
|
96
|
+
const style = {
|
|
97
|
+
background: showTransparent ? '#FFFFFF' : (hex || 'transparent'),
|
|
98
|
+
...(isHalo && { boxShadow: HALO_SHADOW }),
|
|
99
|
+
...sizeStyle,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const inner = showTransparent ? <TransparentX tone={transparentTone} /> : null
|
|
103
|
+
|
|
104
|
+
if (interactive) {
|
|
105
|
+
return (
|
|
106
|
+
<button
|
|
107
|
+
{...rest}
|
|
108
|
+
type="button"
|
|
109
|
+
onClick={onClick}
|
|
110
|
+
title={title}
|
|
111
|
+
aria-label={rest['aria-label'] ?? hex ?? 'transparent'}
|
|
112
|
+
aria-pressed={selected}
|
|
113
|
+
className={cls}
|
|
114
|
+
style={style}
|
|
115
|
+
>
|
|
116
|
+
{inner}
|
|
117
|
+
</button>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<span {...rest} aria-hidden="true" title={title} className={cls} style={style}>
|
|
123
|
+
{inner}
|
|
124
|
+
</span>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Divider - Horizontal or vertical divider line
|
|
5
|
+
*
|
|
6
|
+
* Simple atom for creating separator lines
|
|
7
|
+
* Uses bg-fg-08 for consistent 8% opacity across themes by default
|
|
8
|
+
* Vertical variant includes wrapper div for proper flex behavior
|
|
9
|
+
*
|
|
10
|
+
* @param {Object} props
|
|
11
|
+
* @param {string} props.variant - 'horizontal' or 'vertical' (default: 'horizontal')
|
|
12
|
+
* @param {string} props.className - Additional classes
|
|
13
|
+
* @param {string} props.opacity - Opacity level (01, 02, 04, 08, 12, 16, 24, 32, 48, 64, 80, 88, 96) (default: '08')
|
|
14
|
+
*/
|
|
15
|
+
const Divider = ({ variant = 'horizontal', className = '', opacity = '08', inverse = false }) => {
|
|
16
|
+
const isVertical = variant === 'vertical'
|
|
17
|
+
const opacityClass = inverse ? `bg-fg-inverse-${opacity}` : `bg-fg-${opacity}`
|
|
18
|
+
|
|
19
|
+
if (isVertical) {
|
|
20
|
+
return (
|
|
21
|
+
<div className={`self-stretch flex justify-center items-center ${className}`.trim()}>
|
|
22
|
+
<div className={opacityClass} style={{ width: '1px', height: '100%' }} />
|
|
23
|
+
</div>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className={className}>
|
|
29
|
+
<div className={`${opacityClass} h-px w-full`} />
|
|
30
|
+
</div>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default Divider
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Icon } from '@kolkrabbi/kol-loader'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Input — single-input atom built on the .kol-control shell.
|
|
5
|
+
*
|
|
6
|
+
* variant="filled" (default) — persistent solid bg
|
|
7
|
+
* variant="ghost" — borderless at rest, reveals on hover/focus
|
|
8
|
+
* variant="outline" — bordered, transparent bg (also: the
|
|
9
|
+
* forced-reveal / "is-edited" state of a
|
|
10
|
+
* ghost field — flip variant to outline)
|
|
11
|
+
*
|
|
12
|
+
* size="sm" / "md" (default) / "lg" — matched padding + type class
|
|
13
|
+
*
|
|
14
|
+
* Sizing:
|
|
15
|
+
* chars — HTML `size` attribute on the inner input. When set, the inner
|
|
16
|
+
* <input> sizes to N characters and the shell hugs (padding + prefix +
|
|
17
|
+
* N chars + suffix + padding). Use for known-format values (hex,
|
|
18
|
+
* percentage, integer counts). Drops `flex-1` so the input doesn't
|
|
19
|
+
* overflow into empty trailing space.
|
|
20
|
+
* width — explicit shell width override (e.g. "100%", "240px"). For
|
|
21
|
+
* stretch behavior, set the shell width and let the inner input fill.
|
|
22
|
+
*
|
|
23
|
+
* Props:
|
|
24
|
+
* prefix / suffix — small static text (e.g. "#", "%") rendered inside
|
|
25
|
+
* the shell at text-meta. aria-hidden — affordances, not labels.
|
|
26
|
+
* iconLeft — name of a leading icon rendered inside the shell (e.g.
|
|
27
|
+
* "search-16"). iconSize overrides the size-derived default.
|
|
28
|
+
* uppercase — adds Tailwind `uppercase` to the input element only.
|
|
29
|
+
*
|
|
30
|
+
* Chrome (bg/border/padding/transition/disabled) comes from .kol-control;
|
|
31
|
+
* Input owns prefix/suffix/icon layout + the inner <input> styling.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const SIZE_TYPE = { sm: 'kol-mono-12', md: 'kol-mono-14', lg: 'kol-mono-16' }
|
|
35
|
+
const ICON_SIZE = { sm: 14, md: 14, lg: 18 }
|
|
36
|
+
|
|
37
|
+
export default function Input({
|
|
38
|
+
type = 'text',
|
|
39
|
+
value,
|
|
40
|
+
onChange,
|
|
41
|
+
variant = 'filled',
|
|
42
|
+
size = 'md',
|
|
43
|
+
chars,
|
|
44
|
+
prefix,
|
|
45
|
+
suffix,
|
|
46
|
+
iconLeft,
|
|
47
|
+
iconSize = null,
|
|
48
|
+
placeholder,
|
|
49
|
+
disabled = false,
|
|
50
|
+
uppercase = false,
|
|
51
|
+
width,
|
|
52
|
+
className = '',
|
|
53
|
+
inputClassName = '',
|
|
54
|
+
...inputProps
|
|
55
|
+
}) {
|
|
56
|
+
const isNumber = type === 'number'
|
|
57
|
+
const fixedChars = typeof chars === 'number'
|
|
58
|
+
const resolvedIconSize = iconSize ?? ICON_SIZE[size] ?? 14
|
|
59
|
+
|
|
60
|
+
const shellCls = [
|
|
61
|
+
'kol-control',
|
|
62
|
+
`kol-control--${variant}`,
|
|
63
|
+
`kol-control-${size}`,
|
|
64
|
+
SIZE_TYPE[size],
|
|
65
|
+
'cursor-text',
|
|
66
|
+
className,
|
|
67
|
+
].filter(Boolean).join(' ')
|
|
68
|
+
|
|
69
|
+
/* Pin inner input height to the typography token's line-height. Without
|
|
70
|
+
* this the `<input>` renders ~0.5px taller than the equivalent <button>
|
|
71
|
+
* or <label> at the same kol-mono-N — Chromium computes input height
|
|
72
|
+
* from the font's ascender+descender (font-metric), not strictly from
|
|
73
|
+
* CSS line-height. Result: kol-control-sm ends up 26.5px instead of 26.
|
|
74
|
+
* h-4 / h-[18px] / h-[22px] match the kol-mono-12 / -14 / -16 line-heights. */
|
|
75
|
+
const heightCls = size === 'sm' ? 'h-4' : size === 'md' ? 'h-[18px]' : 'h-[22px]'
|
|
76
|
+
|
|
77
|
+
const inputCls = [
|
|
78
|
+
'min-w-0 bg-transparent border-none outline-none text-auto',
|
|
79
|
+
heightCls,
|
|
80
|
+
!fixedChars && 'flex-1',
|
|
81
|
+
/* Balance the dim prefix/suffix visual weight with extra inner padding
|
|
82
|
+
* on the opposite side. Without this the bright value sits closer to
|
|
83
|
+
* the affordance than to the empty edge, reads off-balance. */
|
|
84
|
+
prefix !== undefined && 'pr-1',
|
|
85
|
+
suffix !== undefined && 'pl-1',
|
|
86
|
+
uppercase && 'uppercase',
|
|
87
|
+
isNumber && 'hide-number-spinners',
|
|
88
|
+
inputClassName,
|
|
89
|
+
].filter(Boolean).join(' ')
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<label
|
|
93
|
+
className={shellCls}
|
|
94
|
+
style={width ? { width: typeof width === 'number' ? `${width}px` : width } : undefined}
|
|
95
|
+
aria-disabled={disabled || undefined}
|
|
96
|
+
>
|
|
97
|
+
{iconLeft && (
|
|
98
|
+
<span aria-hidden="true" className="flex items-center text-auto opacity-50 shrink-0 pr-2">
|
|
99
|
+
<Icon name={iconLeft} size={resolvedIconSize} />
|
|
100
|
+
</span>
|
|
101
|
+
)}
|
|
102
|
+
{prefix !== undefined && (
|
|
103
|
+
<span aria-hidden="true" className="text-meta pr-1 shrink-0">{prefix}</span>
|
|
104
|
+
)}
|
|
105
|
+
<input
|
|
106
|
+
type={type}
|
|
107
|
+
value={value ?? ''}
|
|
108
|
+
onChange={onChange}
|
|
109
|
+
placeholder={placeholder}
|
|
110
|
+
disabled={disabled}
|
|
111
|
+
spellCheck={false}
|
|
112
|
+
size={fixedChars ? chars : undefined}
|
|
113
|
+
className={inputCls}
|
|
114
|
+
{...inputProps}
|
|
115
|
+
/>
|
|
116
|
+
{suffix !== undefined && (
|
|
117
|
+
<span aria-hidden="true" className="text-meta pl-1 shrink-0">{suffix}</span>
|
|
118
|
+
)}
|
|
119
|
+
</label>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Label — minimal form label. Inherits typography from parent (consumer
|
|
3
|
+
* supplies `kol-helper-N` or similar via className when needed). Defaults
|
|
4
|
+
* to `text-fg-48` for the muted-control look.
|
|
5
|
+
*/
|
|
6
|
+
export default function Label({ children, htmlFor, className = '' }) {
|
|
7
|
+
return (
|
|
8
|
+
<label htmlFor={htmlFor} className={`text-fg-48 ${className}`}>
|
|
9
|
+
{children}
|
|
10
|
+
</label>
|
|
11
|
+
)
|
|
12
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
2
|
+
import Input from './Input.jsx'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Slider — range slider with label and an editable value readout.
|
|
6
|
+
*
|
|
7
|
+
* Variants (track/shell styling):
|
|
8
|
+
* default — bordered track + boxed value.
|
|
9
|
+
* minimal — bare track + boxed value. 99% of real usage (foundry previews,
|
|
10
|
+
* inline controls). PRESERVED from web.
|
|
11
|
+
* subtle — filled rounded chip; for inspector-style controls.
|
|
12
|
+
*
|
|
13
|
+
* The value readout is an editable <Input> (type a value, commit on
|
|
14
|
+
* blur / Enter, revert on Escape). Track color is exposed as the
|
|
15
|
+
* `--kol-slider-track` CSS variable on `.slider-black`; override
|
|
16
|
+
* per-instance via style={{ '--kol-slider-track': '...' }}.
|
|
17
|
+
*
|
|
18
|
+
* @param {Object} props
|
|
19
|
+
* @param {string} props.label - Slider label text
|
|
20
|
+
* @param {number} props.min - Minimum value
|
|
21
|
+
* @param {number} props.max - Maximum value
|
|
22
|
+
* @param {number} props.value - Current value
|
|
23
|
+
* @param {Function} props.onChange - Change handler
|
|
24
|
+
* @param {'default'|'minimal'|'subtle'} props.variant - Visual variant (default: 'default')
|
|
25
|
+
* @param {string} props.className - Additional wrapper classes
|
|
26
|
+
* @param {number} props.displayWidth - Width of the value readout, in characters (default: 6)
|
|
27
|
+
* @param {string} props.fontSize - Font size for label/value (e.g., '11px')
|
|
28
|
+
* @param {number} props.step - Slider step increment (default: 1)
|
|
29
|
+
* @param {Function} props.formatValue - Optional formatter for displayed value
|
|
30
|
+
*/
|
|
31
|
+
const Slider = ({
|
|
32
|
+
label,
|
|
33
|
+
min = 0,
|
|
34
|
+
max = 100,
|
|
35
|
+
value = 0,
|
|
36
|
+
onChange,
|
|
37
|
+
variant = 'default',
|
|
38
|
+
className = '',
|
|
39
|
+
displayWidth = 6,
|
|
40
|
+
fontSize,
|
|
41
|
+
step = 1,
|
|
42
|
+
formatValue
|
|
43
|
+
}) => {
|
|
44
|
+
const handleChange = (e) => {
|
|
45
|
+
if (onChange) {
|
|
46
|
+
onChange(Number(e.target.value))
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const variantClass = variant === 'minimal'
|
|
51
|
+
? 'control-slider-minimal'
|
|
52
|
+
: variant === 'subtle'
|
|
53
|
+
? 'control-slider-subtle'
|
|
54
|
+
: 'control-slider'
|
|
55
|
+
|
|
56
|
+
const decimals = useMemo(() => {
|
|
57
|
+
if (formatValue) return null
|
|
58
|
+
if (!Number.isFinite(step)) return 0
|
|
59
|
+
if (step >= 1) return 0
|
|
60
|
+
const decimalPart = step.toString().split('.')[1]
|
|
61
|
+
return decimalPart ? decimalPart.length : 2
|
|
62
|
+
}, [formatValue, step])
|
|
63
|
+
|
|
64
|
+
const displayValue = useMemo(() => {
|
|
65
|
+
if (formatValue) return String(formatValue(value))
|
|
66
|
+
if (decimals && decimals > 0) {
|
|
67
|
+
return Number(value).toFixed(decimals)
|
|
68
|
+
}
|
|
69
|
+
return String(Math.round(value))
|
|
70
|
+
}, [decimals, formatValue, value])
|
|
71
|
+
|
|
72
|
+
/* Editable readout — local string state lets the user type intermediate
|
|
73
|
+
* values (e.g. "-" while entering a negative) without clamping mid-keystroke.
|
|
74
|
+
* Commits on blur / Enter; reverts to current value on Escape. */
|
|
75
|
+
const [draft, setDraft] = useState(displayValue)
|
|
76
|
+
const [editing, setEditing] = useState(false)
|
|
77
|
+
useEffect(() => { if (!editing) setDraft(displayValue) }, [displayValue, editing])
|
|
78
|
+
|
|
79
|
+
const commit = () => {
|
|
80
|
+
setEditing(false)
|
|
81
|
+
const parsed = Number(draft)
|
|
82
|
+
if (!Number.isFinite(parsed) || onChange == null) {
|
|
83
|
+
setDraft(displayValue)
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
const clamped = Math.max(min, Math.min(max, parsed))
|
|
87
|
+
onChange(clamped)
|
|
88
|
+
setDraft(String(clamped))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const onKeyDown = (e) => {
|
|
92
|
+
if (e.key === 'Enter') { e.currentTarget.blur() }
|
|
93
|
+
if (e.key === 'Escape') { setDraft(displayValue); setEditing(false); e.currentTarget.blur() }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div className={`${variantClass} gap-3 shadow-none ${className}`}>
|
|
98
|
+
{label && (
|
|
99
|
+
<label className="kol-helper-12 whitespace-nowrap shrink-0 w-fit" style={fontSize ? { fontSize } : undefined}>
|
|
100
|
+
{label}
|
|
101
|
+
</label>
|
|
102
|
+
)}
|
|
103
|
+
<input
|
|
104
|
+
type="range"
|
|
105
|
+
min={min}
|
|
106
|
+
max={max}
|
|
107
|
+
step={step}
|
|
108
|
+
value={value}
|
|
109
|
+
onChange={handleChange}
|
|
110
|
+
className="slider-black flex-1 w-full cursor-pointer"
|
|
111
|
+
/>
|
|
112
|
+
<Input
|
|
113
|
+
type="text"
|
|
114
|
+
inputMode="decimal"
|
|
115
|
+
variant="filled"
|
|
116
|
+
size="sm"
|
|
117
|
+
chars={displayWidth}
|
|
118
|
+
value={draft}
|
|
119
|
+
onFocus={(e) => { setEditing(true); e.target.select() }}
|
|
120
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
121
|
+
onBlur={commit}
|
|
122
|
+
onKeyDown={onKeyDown}
|
|
123
|
+
inputClassName="text-center"
|
|
124
|
+
/>
|
|
125
|
+
</div>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export default Slider
|