@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
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { createPortal } from 'react-dom'
|
|
3
|
+
import Button from '../atoms/Button.jsx'
|
|
4
|
+
import Input from '../atoms/Input.jsx'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Modal — promise-based prompt + confirm dialogs.
|
|
8
|
+
*
|
|
9
|
+
* const { prompt, confirm } = useModal()
|
|
10
|
+
* const name = await prompt('Name this frame:', 'Untitled')
|
|
11
|
+
* const proceed = await confirm('Discard unsaved changes?')
|
|
12
|
+
*
|
|
13
|
+
* Returned promise resolves to:
|
|
14
|
+
* - prompt → string (value) on submit, `null` on cancel
|
|
15
|
+
* - confirm → boolean: `true` on confirm, `false` on cancel
|
|
16
|
+
*
|
|
17
|
+
* Mounted once at the app root (BrandLayout). Renders into `document.body`
|
|
18
|
+
* via portal, so it floats above any rail / scroll-context.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const ModalCtx = createContext(null)
|
|
22
|
+
|
|
23
|
+
export function ModalProvider({ children }) {
|
|
24
|
+
const [state, setState] = useState(null)
|
|
25
|
+
|
|
26
|
+
const closeWith = useCallback((value) => {
|
|
27
|
+
setState((s) => {
|
|
28
|
+
if (s) s.resolve(value)
|
|
29
|
+
return null
|
|
30
|
+
})
|
|
31
|
+
}, [])
|
|
32
|
+
|
|
33
|
+
const prompt = useCallback((title, defaultValue = '') =>
|
|
34
|
+
new Promise((resolve) => setState({ kind: 'prompt', title, defaultValue, resolve })),
|
|
35
|
+
[])
|
|
36
|
+
|
|
37
|
+
const confirm = useCallback((title) =>
|
|
38
|
+
new Promise((resolve) => setState({ kind: 'confirm', title, resolve })),
|
|
39
|
+
[])
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<ModalCtx.Provider value={{ prompt, confirm }}>
|
|
43
|
+
{children}
|
|
44
|
+
{state && typeof document !== 'undefined' && createPortal(
|
|
45
|
+
<ModalView state={state} closeWith={closeWith} />,
|
|
46
|
+
document.body,
|
|
47
|
+
)}
|
|
48
|
+
</ModalCtx.Provider>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function ModalView({ state, closeWith }) {
|
|
53
|
+
const [val, setVal] = useState(state.defaultValue ?? '')
|
|
54
|
+
const inputRef = useRef(null)
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (state.kind === 'prompt') inputRef.current?.focus()
|
|
58
|
+
const onKey = (e) => {
|
|
59
|
+
if (e.key === 'Escape') closeWith(state.kind === 'prompt' ? null : false)
|
|
60
|
+
if (e.key === 'Enter') closeWith(state.kind === 'prompt' ? val : true)
|
|
61
|
+
}
|
|
62
|
+
window.addEventListener('keydown', onKey)
|
|
63
|
+
return () => window.removeEventListener('keydown', onKey)
|
|
64
|
+
}, [state.kind, val, closeWith])
|
|
65
|
+
|
|
66
|
+
const submit = () => closeWith(state.kind === 'prompt' ? val : true)
|
|
67
|
+
const cancel = () => closeWith(state.kind === 'prompt' ? null : false)
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div
|
|
71
|
+
onMouseDown={cancel}
|
|
72
|
+
style={{
|
|
73
|
+
position: 'fixed', inset: 0, zIndex: 1000,
|
|
74
|
+
background: 'rgba(0,0,0,0.5)',
|
|
75
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
<div
|
|
79
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
80
|
+
className="kol-modal flex flex-col gap-4 p-5 rounded border border-fg-08"
|
|
81
|
+
style={{
|
|
82
|
+
background: 'var(--kol-surface-primary)',
|
|
83
|
+
color: 'var(--kol-surface-on-primary)',
|
|
84
|
+
minWidth: 320,
|
|
85
|
+
maxWidth: '90vw',
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
<p className="kol-helper-12 text-emphasis">{state.title}</p>
|
|
89
|
+
{state.kind === 'prompt' && (
|
|
90
|
+
<Input
|
|
91
|
+
ref={inputRef}
|
|
92
|
+
variant="outline"
|
|
93
|
+
size="sm"
|
|
94
|
+
value={val}
|
|
95
|
+
onChange={(e) => setVal(e.target.value)}
|
|
96
|
+
className="w-full"
|
|
97
|
+
/>
|
|
98
|
+
)}
|
|
99
|
+
<div className="flex gap-2 justify-end">
|
|
100
|
+
<Button variant="secondary" size="sm" onClick={cancel}>Cancel</Button>
|
|
101
|
+
<Button variant="primary" size="sm" onClick={submit}>OK</Button>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function useModal() {
|
|
109
|
+
const ctx = useContext(ModalCtx)
|
|
110
|
+
if (ctx) return ctx
|
|
111
|
+
/* No-context fallback — falls back to native prompt/confirm so callers
|
|
112
|
+
* don't need to null-check. */
|
|
113
|
+
return {
|
|
114
|
+
prompt: async (title, def = '') => {
|
|
115
|
+
if (typeof window === 'undefined') return null
|
|
116
|
+
const v = window.prompt(title, def)
|
|
117
|
+
return v
|
|
118
|
+
},
|
|
119
|
+
confirm: async (title) => {
|
|
120
|
+
if (typeof window === 'undefined') return false
|
|
121
|
+
return window.confirm(title)
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pill Component
|
|
3
|
+
*
|
|
4
|
+
* Small badge/tag component for labels, categories, or status indicators
|
|
5
|
+
* Uses existing pill classes from components.css (pill-outline, pill-subtle, pill-inverse)
|
|
6
|
+
*
|
|
7
|
+
* @param {Object} props
|
|
8
|
+
* @param {string} props.children - Text content of the pill
|
|
9
|
+
* @param {string} props.variant - Visual variant: 'outline' | 'subtle' | 'inverse' (default: 'outline')
|
|
10
|
+
* @param {string} props.size - Size variant: 'sm' | 'md' | 'lg' (default: 'md')
|
|
11
|
+
* @param {string} props.className - Additional classes for customization
|
|
12
|
+
*/
|
|
13
|
+
const Pill = ({ children, variant = 'outline', size = 'md', className = '' }) => {
|
|
14
|
+
const variantClasses = {
|
|
15
|
+
outline: 'pill-outline',
|
|
16
|
+
subtle: 'pill-subtle',
|
|
17
|
+
inverse: 'pill-inverse'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const sizeClasses = {
|
|
21
|
+
sm: 'pill-sm',
|
|
22
|
+
md: 'pill-md',
|
|
23
|
+
lg: 'pill-lg'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<span className={`${variantClasses[variant]} ${sizeClasses[size]} ${className}`.trim()}>
|
|
28
|
+
{children}
|
|
29
|
+
</span>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default Pill
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
useFloating,
|
|
4
|
+
autoUpdate,
|
|
5
|
+
offset as offsetMw,
|
|
6
|
+
flip as flipMw,
|
|
7
|
+
shift as shiftMw,
|
|
8
|
+
size as sizeMw,
|
|
9
|
+
FloatingPortal,
|
|
10
|
+
FloatingFocusManager,
|
|
11
|
+
useClick,
|
|
12
|
+
useHover,
|
|
13
|
+
useFocus,
|
|
14
|
+
useDismiss,
|
|
15
|
+
useRole,
|
|
16
|
+
useInteractions,
|
|
17
|
+
} from '@floating-ui/react'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Popover — anchored floating-element primitive built on @floating-ui/react.
|
|
21
|
+
* Two surfaces:
|
|
22
|
+
*
|
|
23
|
+
* 1. `usePopover({ open, onOpenChange, ... })` — full hook. Returns refs +
|
|
24
|
+
* `getReferenceProps` / `getFloatingProps` so the consumer wires the
|
|
25
|
+
* anchor and the floater explicitly. Use when the trigger is non-trivial
|
|
26
|
+
* (custom component, span wrapper, anchor on something other than a
|
|
27
|
+
* button).
|
|
28
|
+
*
|
|
29
|
+
* const popover = usePopover({ open, onOpenChange })
|
|
30
|
+
* <button ref={popover.refs.setReference} {...popover.getReferenceProps()} />
|
|
31
|
+
* <PopoverPanel popover={popover}>...</PopoverPanel>
|
|
32
|
+
*
|
|
33
|
+
* 2. `<PopoverPanel popover={...}>` — renders the floater into a
|
|
34
|
+
* `FloatingPortal` (escapes overflow:hidden ancestors), wraps in
|
|
35
|
+
* `FloatingFocusManager` (focus trap + return-focus), applies the
|
|
36
|
+
* `.kol-popover` panel chrome.
|
|
37
|
+
*
|
|
38
|
+
* Position middleware: `offset` (gap), `flip` (auto-flip on overflow),
|
|
39
|
+
* `shift` (slide along the cross-axis to stay in viewport), optional
|
|
40
|
+
* `size` (clamp to available width/height). `autoUpdate` keeps everything
|
|
41
|
+
* synced on scroll / resize / layout shift.
|
|
42
|
+
*
|
|
43
|
+
* Interaction defaults: click toggles, outside-click + Esc dismiss, role
|
|
44
|
+
* defaults to `dialog` (use `role: 'menu' | 'listbox' | 'tooltip'` for
|
|
45
|
+
* other shapes — also drives the right ARIA attrs on the reference).
|
|
46
|
+
*/
|
|
47
|
+
export function usePopover({
|
|
48
|
+
open,
|
|
49
|
+
onOpenChange,
|
|
50
|
+
placement = 'bottom-start',
|
|
51
|
+
offset = 6,
|
|
52
|
+
flip = true,
|
|
53
|
+
flipPadding = 8,
|
|
54
|
+
shiftPadding = 8,
|
|
55
|
+
matchReferenceWidth = false,
|
|
56
|
+
click = true,
|
|
57
|
+
hover = false,
|
|
58
|
+
hoverDelay = { open: 400, close: 100 },
|
|
59
|
+
focus = false,
|
|
60
|
+
dismiss = true,
|
|
61
|
+
role = 'dialog',
|
|
62
|
+
referenceElement = null,
|
|
63
|
+
} = {}) {
|
|
64
|
+
const middleware = [offsetMw(offset)]
|
|
65
|
+
if (flip) middleware.push(flipMw({ padding: flipPadding }))
|
|
66
|
+
middleware.push(shiftMw({ padding: shiftPadding }))
|
|
67
|
+
if (matchReferenceWidth) {
|
|
68
|
+
middleware.push(
|
|
69
|
+
sizeMw({
|
|
70
|
+
apply({ rects, elements }) {
|
|
71
|
+
Object.assign(elements.floating.style, {
|
|
72
|
+
minWidth: `${rects.reference.width}px`,
|
|
73
|
+
})
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* `elements.reference` lets the consumer anchor the popover to an
|
|
80
|
+
* external DOM node (e.g. a parent container ref) instead of wiring
|
|
81
|
+
* `setReference` onto the trigger. Used by TypeBlockToolbar to anchor
|
|
82
|
+
* to its TypeFrame parent. */
|
|
83
|
+
const data = useFloating({
|
|
84
|
+
open,
|
|
85
|
+
onOpenChange,
|
|
86
|
+
placement,
|
|
87
|
+
middleware,
|
|
88
|
+
whileElementsMounted: autoUpdate,
|
|
89
|
+
elements: referenceElement ? { reference: referenceElement } : undefined,
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const interactions = useInteractions([
|
|
93
|
+
useClick(data.context, { enabled: click }),
|
|
94
|
+
useHover(data.context, { enabled: hover, delay: hoverDelay, move: false }),
|
|
95
|
+
useFocus(data.context, { enabled: focus }),
|
|
96
|
+
useDismiss(data.context, { enabled: dismiss }),
|
|
97
|
+
useRole(data.context, { role }),
|
|
98
|
+
])
|
|
99
|
+
|
|
100
|
+
return { ...data, ...interactions, open }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Tooltip — hover-triggered popover with text content. Wraps any trigger
|
|
105
|
+
* element. Built on `usePopover` with `hover: true`, `click: false`,
|
|
106
|
+
* `role: 'tooltip'`. Includes keyboard focus trigger so tab-focused
|
|
107
|
+
* controls also reveal the tooltip.
|
|
108
|
+
*
|
|
109
|
+
* <Tooltip label="Pattern" shortcut="P" placement="bottom">
|
|
110
|
+
* <Button iconOnly="ptrn-checker" ... />
|
|
111
|
+
* </Tooltip>
|
|
112
|
+
*/
|
|
113
|
+
export function Tooltip({
|
|
114
|
+
label,
|
|
115
|
+
shortcut,
|
|
116
|
+
placement = 'bottom',
|
|
117
|
+
offset = 6,
|
|
118
|
+
children,
|
|
119
|
+
triggerClassName = 'inline-flex',
|
|
120
|
+
}) {
|
|
121
|
+
const [open, setOpen] = useState(false)
|
|
122
|
+
const popover = usePopover({
|
|
123
|
+
open,
|
|
124
|
+
onOpenChange: setOpen,
|
|
125
|
+
placement,
|
|
126
|
+
offset,
|
|
127
|
+
role: 'tooltip',
|
|
128
|
+
click: false,
|
|
129
|
+
hover: true,
|
|
130
|
+
focus: true,
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<>
|
|
135
|
+
<span
|
|
136
|
+
ref={popover.refs.setReference}
|
|
137
|
+
{...popover.getReferenceProps()}
|
|
138
|
+
className={triggerClassName}
|
|
139
|
+
>
|
|
140
|
+
{children}
|
|
141
|
+
</span>
|
|
142
|
+
<PopoverPanel
|
|
143
|
+
popover={popover}
|
|
144
|
+
focus={false}
|
|
145
|
+
panel={false}
|
|
146
|
+
className="kol-tooltip"
|
|
147
|
+
>
|
|
148
|
+
<span className="text-emphasis">{label}</span>
|
|
149
|
+
{shortcut && <span className="kol-tooltip-key">{shortcut}</span>}
|
|
150
|
+
</PopoverPanel>
|
|
151
|
+
</>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* PopoverPanel — renders the floater into a portal with default panel chrome.
|
|
157
|
+
*
|
|
158
|
+
* Props:
|
|
159
|
+
* popover — the value returned from `usePopover`
|
|
160
|
+
* panel — apply default `.kol-popover` chrome (default: true)
|
|
161
|
+
* modal — focus modality (default: false — non-modal popover)
|
|
162
|
+
* focus — wrap in FloatingFocusManager (default: true)
|
|
163
|
+
* className — extra classes on the panel
|
|
164
|
+
* style — extra inline styles merged with floatingStyles
|
|
165
|
+
*/
|
|
166
|
+
export function PopoverPanel({
|
|
167
|
+
popover,
|
|
168
|
+
children,
|
|
169
|
+
panel = true,
|
|
170
|
+
modal = false,
|
|
171
|
+
focus = true,
|
|
172
|
+
className = '',
|
|
173
|
+
style: extraStyle,
|
|
174
|
+
}) {
|
|
175
|
+
if (!popover.open) return null
|
|
176
|
+
|
|
177
|
+
const { refs, floatingStyles, context, getFloatingProps } = popover
|
|
178
|
+
const cls = [panel && 'kol-popover', className].filter(Boolean).join(' ')
|
|
179
|
+
|
|
180
|
+
/* `data-editor-keep-selection` mirrors the marker on the EditorShell
|
|
181
|
+
* root. Popovers render via FloatingPortal (mounted on <body>, outside
|
|
182
|
+
* the shell), so the editor's document-level click-away handler would
|
|
183
|
+
* otherwise treat clicks on popover content as "outside the editor"
|
|
184
|
+
* and deselect. Tagging the panel here keeps that handler happy for
|
|
185
|
+
* every popover — color picker, dropdowns, menus, tooltips. Harmless
|
|
186
|
+
* for non-editor consumers since they ignore the attr. */
|
|
187
|
+
const node = (
|
|
188
|
+
<div
|
|
189
|
+
ref={refs.setFloating}
|
|
190
|
+
style={extraStyle ? { ...floatingStyles, ...extraStyle } : floatingStyles}
|
|
191
|
+
className={cls}
|
|
192
|
+
data-editor-keep-selection
|
|
193
|
+
{...getFloatingProps()}
|
|
194
|
+
>
|
|
195
|
+
{children}
|
|
196
|
+
</div>
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<FloatingPortal>
|
|
201
|
+
{focus
|
|
202
|
+
? <FloatingFocusManager context={context} modal={modal}>{node}</FloatingFocusManager>
|
|
203
|
+
: node}
|
|
204
|
+
</FloatingPortal>
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export default PopoverPanel
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PropertyInput — stacked label + input for inspector-style property panels.
|
|
3
|
+
* type="number" → Stepper (chevron buttons)
|
|
4
|
+
* type="text" / "color" / etc → Input (filled variant)
|
|
5
|
+
*
|
|
6
|
+
* Use in a `grid grid-cols-2 gap-4` for x/y/width/height/rotation row pairs.
|
|
7
|
+
*/
|
|
8
|
+
import Label from '../atoms/Label'
|
|
9
|
+
import Input from '../atoms/Input.jsx'
|
|
10
|
+
import Stepper from '../atoms/Stepper'
|
|
11
|
+
|
|
12
|
+
export default function PropertyInput({
|
|
13
|
+
label,
|
|
14
|
+
value,
|
|
15
|
+
onChange,
|
|
16
|
+
type = 'text',
|
|
17
|
+
min,
|
|
18
|
+
max,
|
|
19
|
+
step,
|
|
20
|
+
className = '',
|
|
21
|
+
}) {
|
|
22
|
+
return (
|
|
23
|
+
<div className={`flex flex-col gap-2 ${className}`}>
|
|
24
|
+
<Label className="kol-helper-10">{label}</Label>
|
|
25
|
+
{type === 'number' ? (
|
|
26
|
+
<Stepper value={value} onChange={onChange} min={min} max={max} step={step} />
|
|
27
|
+
) : (
|
|
28
|
+
<Input type={type} value={value} onChange={onChange} className="w-full" />
|
|
29
|
+
)}
|
|
30
|
+
</div>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
const SIZE_MAP = {
|
|
4
|
+
sm: { fontSize: 11, paddingY: 12, paddingX: 24, radius: 20, icon: 10 },
|
|
5
|
+
md: { fontSize: 12, paddingY: 14, paddingX: 24, radius: 22, icon: 12 },
|
|
6
|
+
lg: { fontSize: 14, paddingY: 16, paddingX: 24, radius: 24, icon: 14 }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const QuantityInput = ({
|
|
10
|
+
value = 1,
|
|
11
|
+
onChange,
|
|
12
|
+
min = 1,
|
|
13
|
+
max = 99,
|
|
14
|
+
size,
|
|
15
|
+
className = ''
|
|
16
|
+
}) => {
|
|
17
|
+
const [resolvedSize, setResolvedSize] = useState('md')
|
|
18
|
+
const [componentWidth, setComponentWidth] = useState('180px')
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const determineSize = () => {
|
|
22
|
+
if (size) {
|
|
23
|
+
setResolvedSize(size)
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (typeof window === 'undefined') {
|
|
28
|
+
setResolvedSize('md')
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (window.innerWidth >= 1024) {
|
|
33
|
+
setResolvedSize('lg')
|
|
34
|
+
} else if (window.innerWidth >= 768) {
|
|
35
|
+
setResolvedSize('md')
|
|
36
|
+
} else {
|
|
37
|
+
setResolvedSize('sm')
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
determineSize()
|
|
42
|
+
window.addEventListener('resize', determineSize)
|
|
43
|
+
return () => window.removeEventListener('resize', determineSize)
|
|
44
|
+
}, [size])
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
const updateWidth = () => {
|
|
48
|
+
if (typeof window === 'undefined') return
|
|
49
|
+
|
|
50
|
+
if (window.innerWidth >= 1024) {
|
|
51
|
+
setComponentWidth('180px')
|
|
52
|
+
} else if (window.innerWidth >= 768) {
|
|
53
|
+
setComponentWidth('140px')
|
|
54
|
+
} else {
|
|
55
|
+
setComponentWidth('100px')
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
updateWidth()
|
|
59
|
+
window.addEventListener('resize', updateWidth)
|
|
60
|
+
return () => window.removeEventListener('resize', updateWidth)
|
|
61
|
+
}, [])
|
|
62
|
+
|
|
63
|
+
const metrics = SIZE_MAP[resolvedSize] || SIZE_MAP.md
|
|
64
|
+
|
|
65
|
+
const increment = () => {
|
|
66
|
+
if (value < max) onChange?.(value + 1)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const decrement = () => {
|
|
70
|
+
if (value > min) onChange?.(value - 1)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div
|
|
75
|
+
className={`relative block ${className}`}
|
|
76
|
+
>
|
|
77
|
+
{/* Border wrapper - EXPLICIT 42.5px height */}
|
|
78
|
+
<div
|
|
79
|
+
className="w-full flex items-center"
|
|
80
|
+
style={{
|
|
81
|
+
position: 'relative',
|
|
82
|
+
height: '42.5px',
|
|
83
|
+
border: '1px solid var(--kol-border-default)',
|
|
84
|
+
borderRadius: `${metrics.radius}px`,
|
|
85
|
+
backgroundColor: 'var(--kol-surface-primary)',
|
|
86
|
+
color: 'var(--kol-surface-on-primary)',
|
|
87
|
+
paddingLeft: `${metrics.paddingX}px`,
|
|
88
|
+
paddingRight: `${metrics.paddingX}px`,
|
|
89
|
+
fontSize: `${metrics.fontSize}px`,
|
|
90
|
+
lineHeight: '120%',
|
|
91
|
+
fontFamily: 'var(--kol-font-family-mono)',
|
|
92
|
+
boxSizing: 'border-box'
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
<span>{value}</span>
|
|
96
|
+
|
|
97
|
+
{/* Chevrons - absolute, OUTSIDE the padded content */}
|
|
98
|
+
<div
|
|
99
|
+
style={{
|
|
100
|
+
position: 'absolute',
|
|
101
|
+
right: `${metrics.paddingX}px`,
|
|
102
|
+
top: '50%',
|
|
103
|
+
transform: 'translateY(-50%)',
|
|
104
|
+
display: 'flex',
|
|
105
|
+
flexDirection: 'column'
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
<button
|
|
109
|
+
type="button"
|
|
110
|
+
onClick={increment}
|
|
111
|
+
disabled={value >= max}
|
|
112
|
+
style={{
|
|
113
|
+
backgroundColor: 'transparent',
|
|
114
|
+
border: 'none',
|
|
115
|
+
padding: '0',
|
|
116
|
+
cursor: value >= max ? 'not-allowed' : 'pointer',
|
|
117
|
+
opacity: value >= max ? 0.3 : 1,
|
|
118
|
+
lineHeight: 0,
|
|
119
|
+
display: 'block'
|
|
120
|
+
}}
|
|
121
|
+
aria-label="Increase quantity"
|
|
122
|
+
>
|
|
123
|
+
<svg
|
|
124
|
+
width={metrics.icon}
|
|
125
|
+
height={metrics.icon}
|
|
126
|
+
viewBox="0 0 12 12"
|
|
127
|
+
fill="none"
|
|
128
|
+
>
|
|
129
|
+
<path
|
|
130
|
+
d="m3 7 3-3 3 3"
|
|
131
|
+
stroke="currentColor"
|
|
132
|
+
strokeWidth="1.25"
|
|
133
|
+
strokeLinecap="round"
|
|
134
|
+
strokeLinejoin="round"
|
|
135
|
+
/>
|
|
136
|
+
</svg>
|
|
137
|
+
</button>
|
|
138
|
+
<button
|
|
139
|
+
type="button"
|
|
140
|
+
onClick={decrement}
|
|
141
|
+
disabled={value <= min}
|
|
142
|
+
style={{
|
|
143
|
+
backgroundColor: 'transparent',
|
|
144
|
+
border: 'none',
|
|
145
|
+
padding: '0',
|
|
146
|
+
cursor: value <= min ? 'not-allowed' : 'pointer',
|
|
147
|
+
opacity: value <= min ? 0.3 : 1,
|
|
148
|
+
lineHeight: 0,
|
|
149
|
+
display: 'block'
|
|
150
|
+
}}
|
|
151
|
+
aria-label="Decrease quantity"
|
|
152
|
+
>
|
|
153
|
+
<svg
|
|
154
|
+
width={metrics.icon}
|
|
155
|
+
height={metrics.icon}
|
|
156
|
+
viewBox="0 0 12 12"
|
|
157
|
+
fill="none"
|
|
158
|
+
>
|
|
159
|
+
<path
|
|
160
|
+
d="m3 5 3 3 3-3"
|
|
161
|
+
stroke="currentColor"
|
|
162
|
+
strokeWidth="1.25"
|
|
163
|
+
strokeLinecap="round"
|
|
164
|
+
strokeLinejoin="round"
|
|
165
|
+
/>
|
|
166
|
+
</svg>
|
|
167
|
+
</button>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export default QuantityInput
|