@hilum/designer 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hilum
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # @hilum/designer
2
+
3
+ Engine-agnostic canvas-editor chrome. Provides the shell, toolbar, side panels, collapsible panes, and generic hooks (undo/redo stack, keyboard shortcuts) — works with any kind of editing surface (free-positioned canvas, form builder, layout editor, code editor).
4
+
5
+ For the actual canvas (pan/zoom viewport, layers, drag/resize), use `@hilum/designer-canvas` on top.
6
+
7
+ ## Components
8
+
9
+ - `<DesignerShell>` — root layout (full viewport, themed)
10
+ - `<DesignerHeader>` / `<DesignerSidebar>` / `<DesignerPanel>` — chrome
11
+ - `<DesignerPane>` — collapsible property panel section with `showFor` predicate
12
+ - `<DesignerToolbar>` + `Group` / `Button` / `Separator` — composable toolbar
13
+
14
+ ## Hooks
15
+
16
+ - `useHistory<T>(initial)` — generic undo/redo stack
17
+ - `useKeybindings(config)` — keyboard shortcut registry
18
+ - `useShellContext()` — selection, active tool, read-only flag
@@ -0,0 +1,253 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { Dispatch, SetStateAction, ReactNode, ComponentType } from 'react';
3
+
4
+ /**
5
+ * The minimal contract a designer shell needs from the engine beneath it.
6
+ *
7
+ * The chrome (toolbar, panel, pane, sidebar) reads from this context.
8
+ * It does NOT know what is being edited — selection IDs are opaque strings
9
+ * (layer IDs, field IDs, node IDs — whatever the engine uses), and active
10
+ * tool is an open string the app defines (`select`, `hand`, `text`, …).
11
+ *
12
+ * @hilum/designer-canvas wires this up for canvas apps. A form-builder app
13
+ * could wire its own engine to ShellContext with the same primitives.
14
+ */
15
+ interface ShellContextValue {
16
+ /** IDs of currently-selected items. Engine-defined meaning. */
17
+ selectedIds: string[];
18
+ setSelectedIds: Dispatch<SetStateAction<string[]>>;
19
+ /** Active tool. App-defined enum (e.g. 'select' | 'hand' | 'text'). */
20
+ activeTool: string;
21
+ setActiveTool: Dispatch<SetStateAction<string>>;
22
+ /** When true, mutating actions should no-op. Used for previews. */
23
+ readOnly: boolean;
24
+ /**
25
+ * Optional resolver: given a `selectedId`, return its engine-defined "kind"
26
+ * (e.g. 'text', 'image'). DesignerPane.showFor uses this to decide whether
27
+ * to render. Apps that don't have typed selections can leave this undefined.
28
+ */
29
+ resolveKind?: (id: string) => string | undefined;
30
+ }
31
+ declare function useShellContext(): ShellContextValue;
32
+ interface ShellProviderProps {
33
+ /** Initial selected IDs. Default: []. */
34
+ initialSelectedIds?: string[];
35
+ /** Initial active tool. Default: 'select'. */
36
+ initialTool?: string;
37
+ /** Read-only flag. */
38
+ readOnly?: boolean;
39
+ /** Kind resolver — see ShellContextValue.resolveKind. */
40
+ resolveKind?: (id: string) => string | undefined;
41
+ /** Controlled override — pass a value to lift state out of the provider. */
42
+ value?: Partial<ShellContextValue>;
43
+ children: ReactNode;
44
+ }
45
+ declare function ShellProvider({ initialSelectedIds, initialTool, readOnly, resolveKind, value: controlled, children, }: ShellProviderProps): react_jsx_runtime.JSX.Element;
46
+
47
+ interface DesignerShellProps {
48
+ className?: string;
49
+ children: ReactNode;
50
+ }
51
+ /**
52
+ * Root layout for an editor app — full viewport, themed surface.
53
+ * Place a <DesignerHeader>, <DesignerSidebar>, <DesignerPanel>, and the
54
+ * canvas content as children.
55
+ */
56
+ declare function DesignerShell({ className, children }: DesignerShellProps): react_jsx_runtime.JSX.Element;
57
+
58
+ interface DesignerHeaderProps {
59
+ /** Left-aligned content — file name, breadcrumbs, project switcher. */
60
+ left?: ReactNode;
61
+ /** Center content — typically the active document title. */
62
+ center?: ReactNode;
63
+ /** Right-aligned content — share, export, presence, account. */
64
+ right?: ReactNode;
65
+ className?: string;
66
+ children?: ReactNode;
67
+ }
68
+ /**
69
+ * Top bar of an editor app. Slot-driven — the chrome doesn't know what
70
+ * goes in each region. Use <DesignerHeader left={...} center={...} right={...} />.
71
+ */
72
+ declare function DesignerHeader({ left, center, right, className, children }: DesignerHeaderProps): react_jsx_runtime.JSX.Element;
73
+
74
+ interface SidebarItem {
75
+ id: string;
76
+ label: string;
77
+ icon: ComponentType<{
78
+ size?: number;
79
+ className?: string;
80
+ }>;
81
+ onClick?: () => void;
82
+ /** Caller-computed active flag. */
83
+ active?: boolean;
84
+ disabled?: boolean;
85
+ badge?: ReactNode;
86
+ }
87
+ interface DesignerSidebarProps {
88
+ /** Top group of icon buttons. */
89
+ items: SidebarItem[];
90
+ /** Optional bottom group (settings, help, account). */
91
+ bottomItems?: SidebarItem[];
92
+ side?: "left" | "right";
93
+ className?: string;
94
+ children?: ReactNode;
95
+ }
96
+ /**
97
+ * Vertical icon rail used as the primary tool / navigation column in an
98
+ * editor. Driven by an `items` array — no engine coupling. Each item gets
99
+ * a tooltip showing its label.
100
+ */
101
+ declare function DesignerSidebar({ items, bottomItems, side, className, children, }: DesignerSidebarProps): react_jsx_runtime.JSX.Element;
102
+
103
+ interface DesignerPanelProps {
104
+ side: "left" | "right";
105
+ /** Width in pixels. Default: 240. */
106
+ width?: number;
107
+ /** Add a separator border on the inner edge. Default: true. */
108
+ bordered?: boolean;
109
+ className?: string;
110
+ children?: ReactNode;
111
+ }
112
+ /**
113
+ * Left or right side panel of an editor — typically holds layer lists,
114
+ * inspector / properties, history, comments, etc. Static-width for v1.
115
+ * (Resize handles arrive in a later iteration if needed.)
116
+ */
117
+ declare function DesignerPanel({ side, width, bordered, className, children, }: DesignerPanelProps): react_jsx_runtime.JSX.Element;
118
+
119
+ interface DesignerPaneProps {
120
+ /**
121
+ * Predicate or list of allowed kinds. The pane renders only when matched.
122
+ *
123
+ * - `string[]`: render when at least one selected ID resolves to one of
124
+ * these kinds (requires `ShellContext.resolveKind`).
125
+ * - `(selectedIds: string[]) => boolean`: full predicate.
126
+ * - omitted: always render.
127
+ */
128
+ showFor?: string[] | ((selectedIds: string[]) => boolean);
129
+ collapsible?: boolean;
130
+ defaultOpen?: boolean;
131
+ className?: string;
132
+ children: ReactNode;
133
+ }
134
+ interface DesignerPaneTitleProps {
135
+ className?: string;
136
+ children: ReactNode;
137
+ /** Right-aligned action / control. */
138
+ action?: ReactNode;
139
+ }
140
+ interface DesignerPaneContentProps {
141
+ className?: string;
142
+ children: ReactNode;
143
+ }
144
+ /**
145
+ * Collapsible inspector section. Use inside a <DesignerPanel>.
146
+ *
147
+ * <DesignerPane showFor={["text"]} collapsible>
148
+ * <DesignerPaneTitle>Typography</DesignerPaneTitle>
149
+ * <DesignerPaneContent>...</DesignerPaneContent>
150
+ * </DesignerPane>
151
+ */
152
+ declare function DesignerPane({ showFor, collapsible, defaultOpen, className, children, }: DesignerPaneProps): react_jsx_runtime.JSX.Element | null;
153
+ declare function DesignerPaneTitle({ className, children, action }: DesignerPaneTitleProps): react_jsx_runtime.JSX.Element;
154
+ declare function DesignerPaneContent({ className, children }: DesignerPaneContentProps): react_jsx_runtime.JSX.Element | null;
155
+
156
+ interface DesignerToolbarProps {
157
+ className?: string;
158
+ /** Position. Default: 'floating' (centered, floating above content). */
159
+ variant?: "floating" | "inline";
160
+ children: ReactNode;
161
+ }
162
+ declare function DesignerToolbar({ className, variant, children }: DesignerToolbarProps): react_jsx_runtime.JSX.Element;
163
+ interface DesignerToolbarGroupProps {
164
+ className?: string;
165
+ children: ReactNode;
166
+ }
167
+ declare function DesignerToolbarGroup({ className, children }: DesignerToolbarGroupProps): react_jsx_runtime.JSX.Element;
168
+ interface DesignerToolbarSeparatorProps {
169
+ className?: string;
170
+ }
171
+ declare function DesignerToolbarSeparator({ className }: DesignerToolbarSeparatorProps): react_jsx_runtime.JSX.Element;
172
+ interface DesignerToolbarButtonProps {
173
+ label: string;
174
+ icon?: ComponentType<{
175
+ size?: number;
176
+ className?: string;
177
+ }>;
178
+ onClick?: () => void;
179
+ active?: boolean;
180
+ disabled?: boolean;
181
+ /** Optional keyboard shortcut shown in the tooltip (e.g. 'V', 'Cmd+Z'). */
182
+ shortcut?: string;
183
+ className?: string;
184
+ children?: ReactNode;
185
+ }
186
+ declare function DesignerToolbarButton({ label, icon: Icon, onClick, active, disabled, shortcut, className, children, }: DesignerToolbarButtonProps): react_jsx_runtime.JSX.Element;
187
+
188
+ interface UseHistoryReturn<T> {
189
+ state: T;
190
+ /**
191
+ * Record a new state. Use when the user makes a logical edit. Pushes the
192
+ * current `state` into `past` and clears `future`. Pass `{ skipHistory: true }`
193
+ * (via a separate setter) to overwrite without pushing — apps that need that
194
+ * can set state directly via a wrapper.
195
+ */
196
+ setState: (next: T | ((prev: T) => T)) => void;
197
+ /** Replace `state` without pushing onto the history stack. */
198
+ replaceState: (next: T | ((prev: T) => T)) => void;
199
+ undo: () => void;
200
+ redo: () => void;
201
+ reset: (next: T) => void;
202
+ canUndo: boolean;
203
+ canRedo: boolean;
204
+ /** Number of past entries (undo depth). */
205
+ pastSize: number;
206
+ /** Number of future entries (redo depth). */
207
+ futureSize: number;
208
+ }
209
+ /**
210
+ * Generic, engine-agnostic undo/redo stack.
211
+ *
212
+ * @hilum/designer-canvas wires this with `useHistory<Layer[]>(layers)`.
213
+ * A form-builder app could wire `useHistory<FormSchema>(schema)`.
214
+ */
215
+ declare function useHistory<T>(initial: T): UseHistoryReturn<T>;
216
+
217
+ interface KeybindingConfig {
218
+ /** Lower-case key (e.g. 'z', 'arrowup', 'escape', '+', '-'). */
219
+ key: string;
220
+ ctrl?: boolean;
221
+ /** Cmd on macOS. Use both `meta` and `ctrl: true` if you want both. */
222
+ meta?: boolean;
223
+ shift?: boolean;
224
+ alt?: boolean;
225
+ /**
226
+ * Treat both ctrl and meta as the same modifier (for cross-platform
227
+ * shortcuts like Cmd+Z / Ctrl+Z). Default: false. Set to true to match
228
+ * either modifier.
229
+ */
230
+ mod?: boolean;
231
+ action: (event: KeyboardEvent) => void;
232
+ /** Don't run when target is an input / textarea / contenteditable. Default: true. */
233
+ skipInputs?: boolean;
234
+ /** Call event.preventDefault() before action. Default: true. */
235
+ preventDefault?: boolean;
236
+ }
237
+ interface UseKeybindingsOptions {
238
+ /** Skip all bindings entirely. */
239
+ disabled?: boolean;
240
+ /** Element to attach the listener to. Default: window. */
241
+ target?: Window | HTMLElement | null;
242
+ }
243
+ /**
244
+ * Generic keyboard shortcut registry. Engine-agnostic — the package doesn't
245
+ * know what an action does. Pass an array of bindings; the hook attaches
246
+ * a single keydown listener and dispatches on match.
247
+ *
248
+ * @hilum/designer-canvas wires its standard editor shortcuts (Cmd+Z, V, T,
249
+ * arrow nudge, etc.) on top of this hook.
250
+ */
251
+ declare function useKeybindings(bindings: KeybindingConfig[], { disabled, target }?: UseKeybindingsOptions): void;
252
+
253
+ export { DesignerHeader, type DesignerHeaderProps, DesignerPane, DesignerPaneContent, type DesignerPaneContentProps, type DesignerPaneProps, DesignerPaneTitle, type DesignerPaneTitleProps, DesignerPanel, type DesignerPanelProps, DesignerShell, type DesignerShellProps, DesignerSidebar, type DesignerSidebarProps, DesignerToolbar, DesignerToolbarButton, type DesignerToolbarButtonProps, DesignerToolbarGroup, type DesignerToolbarGroupProps, type DesignerToolbarProps, DesignerToolbarSeparator, type DesignerToolbarSeparatorProps, type KeybindingConfig, type ShellContextValue, ShellProvider, type SidebarItem, useHistory, useKeybindings, useShellContext };
package/dist/index.js ADDED
@@ -0,0 +1,360 @@
1
+ import { createContext, useContext, useState, useMemo, useCallback, useEffect } from 'react';
2
+ import { jsx, jsxs } from 'react/jsx-runtime';
3
+ import { cn, TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '@hilum/ui';
4
+ import { ChevronDown } from 'lucide-react';
5
+
6
+ // src/shell/ShellContext.tsx
7
+ var noop = () => {
8
+ };
9
+ var DEFAULT = {
10
+ selectedIds: [],
11
+ setSelectedIds: noop,
12
+ activeTool: "select",
13
+ setActiveTool: noop,
14
+ readOnly: false,
15
+ resolveKind: void 0
16
+ };
17
+ var ShellContext = createContext(DEFAULT);
18
+ function useShellContext() {
19
+ return useContext(ShellContext);
20
+ }
21
+ function ShellProvider({
22
+ initialSelectedIds = [],
23
+ initialTool = "select",
24
+ readOnly = false,
25
+ resolveKind,
26
+ value: controlled,
27
+ children
28
+ }) {
29
+ const [selectedIds, setSelectedIds] = useState(initialSelectedIds);
30
+ const [activeTool, setActiveTool] = useState(initialTool);
31
+ const value = useMemo(
32
+ () => ({
33
+ selectedIds: controlled?.selectedIds ?? selectedIds,
34
+ setSelectedIds: controlled?.setSelectedIds ?? setSelectedIds,
35
+ activeTool: controlled?.activeTool ?? activeTool,
36
+ setActiveTool: controlled?.setActiveTool ?? setActiveTool,
37
+ readOnly: controlled?.readOnly ?? readOnly,
38
+ resolveKind: controlled?.resolveKind ?? resolveKind
39
+ }),
40
+ [selectedIds, activeTool, readOnly, resolveKind, controlled]
41
+ );
42
+ return /* @__PURE__ */ jsx(ShellContext.Provider, { value, children });
43
+ }
44
+ function DesignerShell({ className, children }) {
45
+ return /* @__PURE__ */ jsx(
46
+ "div",
47
+ {
48
+ className: cn(
49
+ "flex flex-col h-screen w-screen overflow-hidden bg-taupe-50 text-taupe-900",
50
+ className
51
+ ),
52
+ children
53
+ }
54
+ );
55
+ }
56
+ function DesignerHeader({ left, center, right, className, children }) {
57
+ return /* @__PURE__ */ jsxs(
58
+ "header",
59
+ {
60
+ className: cn(
61
+ "flex h-12 items-center justify-between gap-3 border-b border-taupe-100 bg-white px-3 shrink-0",
62
+ className
63
+ ),
64
+ children: [
65
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2 min-w-0", children: left }),
66
+ center && /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2 min-w-0", children: center }),
67
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2 min-w-0", children: right }),
68
+ children
69
+ ]
70
+ }
71
+ );
72
+ }
73
+ function DesignerSidebar({
74
+ items,
75
+ bottomItems,
76
+ side = "left",
77
+ className,
78
+ children
79
+ }) {
80
+ return /* @__PURE__ */ jsx(
81
+ "aside",
82
+ {
83
+ className: cn(
84
+ "flex flex-col w-12 bg-white shrink-0",
85
+ side === "left" ? "border-r" : "border-l",
86
+ "border-taupe-100",
87
+ className
88
+ ),
89
+ children: /* @__PURE__ */ jsxs(TooltipProvider, { children: [
90
+ /* @__PURE__ */ jsx("div", { className: "flex flex-col items-center gap-0.5 p-1.5", children: items.map((item) => /* @__PURE__ */ jsx(SidebarButton, { item }, item.id)) }),
91
+ children,
92
+ bottomItems && bottomItems.length > 0 && /* @__PURE__ */ jsx("div", { className: "mt-auto flex flex-col items-center gap-0.5 p-1.5", children: bottomItems.map((item) => /* @__PURE__ */ jsx(SidebarButton, { item }, item.id)) })
93
+ ] })
94
+ }
95
+ );
96
+ }
97
+ function SidebarButton({ item }) {
98
+ const Icon = item.icon;
99
+ return /* @__PURE__ */ jsxs(Tooltip, { children: [
100
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
101
+ "button",
102
+ {
103
+ type: "button",
104
+ onClick: item.onClick,
105
+ disabled: item.disabled,
106
+ "aria-label": item.label,
107
+ "aria-pressed": item.active,
108
+ className: cn(
109
+ "relative flex size-9 items-center justify-center rounded-md transition-colors",
110
+ item.active ? "bg-taupe-900 text-white" : "text-taupe-500 hover:bg-taupe-100 hover:text-taupe-900",
111
+ item.disabled && "opacity-50 cursor-not-allowed"
112
+ ),
113
+ children: [
114
+ /* @__PURE__ */ jsx(Icon, { size: 16 }),
115
+ item.badge != null && /* @__PURE__ */ jsx("span", { className: "absolute -top-0.5 -right-0.5 caption-xs", children: item.badge })
116
+ ]
117
+ }
118
+ ) }),
119
+ /* @__PURE__ */ jsx(TooltipContent, { side: "right", children: item.label })
120
+ ] });
121
+ }
122
+ function DesignerPanel({
123
+ side,
124
+ width = 240,
125
+ bordered = true,
126
+ className,
127
+ children
128
+ }) {
129
+ return /* @__PURE__ */ jsx(
130
+ "aside",
131
+ {
132
+ className: cn(
133
+ "flex flex-col bg-white shrink-0 overflow-hidden",
134
+ bordered && (side === "left" ? "border-r" : "border-l"),
135
+ bordered && "border-taupe-100",
136
+ className
137
+ ),
138
+ style: { width },
139
+ children: /* @__PURE__ */ jsx("div", { className: "flex flex-col flex-1 overflow-y-auto", children })
140
+ }
141
+ );
142
+ }
143
+ function DesignerPane({
144
+ showFor,
145
+ collapsible = false,
146
+ defaultOpen = true,
147
+ className,
148
+ children
149
+ }) {
150
+ const { selectedIds, resolveKind } = useShellContext();
151
+ const [open, setOpen] = useState(defaultOpen);
152
+ let visible = true;
153
+ if (typeof showFor === "function") {
154
+ visible = showFor(selectedIds);
155
+ } else if (Array.isArray(showFor)) {
156
+ if (!resolveKind) {
157
+ visible = selectedIds.length > 0;
158
+ } else {
159
+ visible = selectedIds.some((id) => {
160
+ const kind = resolveKind(id);
161
+ return kind != null && showFor.includes(kind);
162
+ });
163
+ }
164
+ }
165
+ if (!visible) return null;
166
+ return /* @__PURE__ */ jsx(PaneContext.Provider, { value: { open, toggle: () => setOpen((v) => !v), collapsible }, children: /* @__PURE__ */ jsx("section", { className: cn("flex flex-col border-b border-taupe-100 last:border-b-0", className), children }) });
167
+ }
168
+ function DesignerPaneTitle({ className, children, action }) {
169
+ const { open, toggle, collapsible } = usePaneContext();
170
+ const Tag = collapsible ? "button" : "div";
171
+ return /* @__PURE__ */ jsxs(
172
+ Tag,
173
+ {
174
+ type: collapsible ? "button" : void 0,
175
+ onClick: collapsible ? toggle : void 0,
176
+ className: cn(
177
+ "flex w-full items-center justify-between gap-2 px-3 py-2 text-left",
178
+ "caption-xs uppercase tracking-wider font-semibold text-taupe-500",
179
+ collapsible && "hover:text-taupe-900 transition-colors",
180
+ className
181
+ ),
182
+ children: [
183
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5", children: [
184
+ collapsible && /* @__PURE__ */ jsx(
185
+ ChevronDown,
186
+ {
187
+ size: 12,
188
+ className: cn("transition-transform duration-150", !open && "-rotate-90")
189
+ }
190
+ ),
191
+ children
192
+ ] }),
193
+ action && /* @__PURE__ */ jsx("span", { className: "ml-auto", children: action })
194
+ ]
195
+ }
196
+ );
197
+ }
198
+ function DesignerPaneContent({ className, children }) {
199
+ const { open } = usePaneContext();
200
+ if (!open) return null;
201
+ return /* @__PURE__ */ jsx("div", { className: cn("flex flex-col gap-2 px-3 pb-3", className), children });
202
+ }
203
+ var PaneContext = createContext({ open: true, toggle: () => {
204
+ }, collapsible: false });
205
+ function usePaneContext() {
206
+ return useContext(PaneContext);
207
+ }
208
+ function DesignerToolbar({ className, variant = "floating", children }) {
209
+ return /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsx(
210
+ "div",
211
+ {
212
+ role: "toolbar",
213
+ className: cn(
214
+ "flex items-center gap-0.5 rounded-lg bg-white shadow-natural border border-taupe-100 p-1",
215
+ variant === "floating" && "fixed bottom-4 left-1/2 -translate-x-1/2 z-30",
216
+ className
217
+ ),
218
+ children
219
+ }
220
+ ) });
221
+ }
222
+ function DesignerToolbarGroup({ className, children }) {
223
+ return /* @__PURE__ */ jsx("div", { className: cn("flex items-center gap-0.5", className), children });
224
+ }
225
+ function DesignerToolbarSeparator({ className }) {
226
+ return /* @__PURE__ */ jsx("div", { className: cn("mx-1 h-5 w-px bg-taupe-100", className), role: "separator" });
227
+ }
228
+ function DesignerToolbarButton({
229
+ label,
230
+ icon: Icon,
231
+ onClick,
232
+ active,
233
+ disabled,
234
+ shortcut,
235
+ className,
236
+ children
237
+ }) {
238
+ return /* @__PURE__ */ jsxs(Tooltip, { children: [
239
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
240
+ "button",
241
+ {
242
+ type: "button",
243
+ onClick,
244
+ disabled,
245
+ "aria-label": label,
246
+ "aria-pressed": active,
247
+ className: cn(
248
+ "flex h-8 min-w-8 items-center justify-center gap-1 rounded-md px-1.5 transition-colors caption",
249
+ active ? "bg-taupe-900 text-white" : "text-taupe-600 hover:bg-taupe-100 hover:text-taupe-900",
250
+ disabled && "opacity-50 cursor-not-allowed",
251
+ className
252
+ ),
253
+ children: [
254
+ Icon && /* @__PURE__ */ jsx(Icon, { size: 16 }),
255
+ children
256
+ ]
257
+ }
258
+ ) }),
259
+ /* @__PURE__ */ jsxs(TooltipContent, { side: "top", children: [
260
+ /* @__PURE__ */ jsx("span", { children: label }),
261
+ shortcut && /* @__PURE__ */ jsx("span", { className: "ml-2 caption-xs text-taupe-300", children: shortcut })
262
+ ] })
263
+ ] });
264
+ }
265
+ function useHistory(initial) {
266
+ const [history, setHistory] = useState({
267
+ past: [],
268
+ present: initial,
269
+ future: []
270
+ });
271
+ const setState = useCallback((next) => {
272
+ setHistory((prev) => {
273
+ const value = typeof next === "function" ? next(prev.present) : next;
274
+ if (Object.is(value, prev.present)) return prev;
275
+ return {
276
+ past: [...prev.past, prev.present],
277
+ present: value,
278
+ future: []
279
+ };
280
+ });
281
+ }, []);
282
+ const replaceState = useCallback((next) => {
283
+ setHistory((prev) => {
284
+ const value = typeof next === "function" ? next(prev.present) : next;
285
+ return { ...prev, present: value };
286
+ });
287
+ }, []);
288
+ const undo = useCallback(() => {
289
+ setHistory((prev) => {
290
+ if (prev.past.length === 0) return prev;
291
+ const past = prev.past.slice(0, -1);
292
+ const previous = prev.past[prev.past.length - 1];
293
+ return {
294
+ past,
295
+ present: previous,
296
+ future: [prev.present, ...prev.future]
297
+ };
298
+ });
299
+ }, []);
300
+ const redo = useCallback(() => {
301
+ setHistory((prev) => {
302
+ if (prev.future.length === 0) return prev;
303
+ const [next, ...rest] = prev.future;
304
+ return {
305
+ past: [...prev.past, prev.present],
306
+ present: next,
307
+ future: rest
308
+ };
309
+ });
310
+ }, []);
311
+ const reset = useCallback((next) => {
312
+ setHistory({ past: [], present: next, future: [] });
313
+ }, []);
314
+ return {
315
+ state: history.present,
316
+ setState,
317
+ replaceState,
318
+ undo,
319
+ redo,
320
+ reset,
321
+ canUndo: history.past.length > 0,
322
+ canRedo: history.future.length > 0,
323
+ pastSize: history.past.length,
324
+ futureSize: history.future.length
325
+ };
326
+ }
327
+ function useKeybindings(bindings, { disabled, target } = {}) {
328
+ useEffect(() => {
329
+ if (disabled) return;
330
+ const node = target ?? (typeof window !== "undefined" ? window : null);
331
+ if (!node) return;
332
+ const handler = (rawEvent) => {
333
+ const e = rawEvent;
334
+ const tgt = e.target;
335
+ const isInput = tgt instanceof HTMLInputElement || tgt instanceof HTMLTextAreaElement || tgt instanceof HTMLSelectElement || tgt?.isContentEditable === true;
336
+ const key = e.key.toLowerCase();
337
+ for (const b of bindings) {
338
+ if (b.key.toLowerCase() !== key) continue;
339
+ if (b.skipInputs !== false && isInput) continue;
340
+ const ctrlMatches = b.ctrl ? e.ctrlKey : !e.ctrlKey || b.mod;
341
+ const metaMatches = b.meta ? e.metaKey : !e.metaKey || b.mod;
342
+ const modMatches = b.mod ? e.ctrlKey || e.metaKey : true;
343
+ const shiftMatches = b.shift ? e.shiftKey : !e.shiftKey;
344
+ const altMatches = b.alt ? e.altKey : !e.altKey;
345
+ if (!modMatches) continue;
346
+ if (!b.mod && (!ctrlMatches || !metaMatches)) continue;
347
+ if (!shiftMatches || !altMatches) continue;
348
+ if (b.preventDefault !== false) e.preventDefault();
349
+ b.action(e);
350
+ return;
351
+ }
352
+ };
353
+ node.addEventListener("keydown", handler);
354
+ return () => node.removeEventListener("keydown", handler);
355
+ }, [bindings, disabled, target]);
356
+ }
357
+
358
+ export { DesignerHeader, DesignerPane, DesignerPaneContent, DesignerPaneTitle, DesignerPanel, DesignerShell, DesignerSidebar, DesignerToolbar, DesignerToolbarButton, DesignerToolbarGroup, DesignerToolbarSeparator, ShellProvider, useHistory, useKeybindings, useShellContext };
359
+ //# sourceMappingURL=index.js.map
360
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shell/ShellContext.tsx","../src/components/DesignerShell.tsx","../src/components/DesignerHeader.tsx","../src/components/DesignerSidebar.tsx","../src/components/DesignerPanel.tsx","../src/components/DesignerPane.tsx","../src/components/DesignerToolbar.tsx","../src/hooks/useHistory.ts","../src/hooks/useKeybindings.ts"],"names":["jsx","cn","jsxs","useState","createContext","useContext","TooltipProvider","Tooltip","TooltipTrigger","TooltipContent"],"mappings":"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","file":"index.js","sourcesContent":["import { createContext, useContext, useMemo, useState } from \"react\";\nimport type { Dispatch, ReactNode, SetStateAction } from \"react\";\n\n/**\n * The minimal contract a designer shell needs from the engine beneath it.\n *\n * The chrome (toolbar, panel, pane, sidebar) reads from this context.\n * It does NOT know what is being edited — selection IDs are opaque strings\n * (layer IDs, field IDs, node IDs — whatever the engine uses), and active\n * tool is an open string the app defines (`select`, `hand`, `text`, …).\n *\n * @hilum/designer-canvas wires this up for canvas apps. A form-builder app\n * could wire its own engine to ShellContext with the same primitives.\n */\nexport interface ShellContextValue {\n /** IDs of currently-selected items. Engine-defined meaning. */\n selectedIds: string[];\n setSelectedIds: Dispatch<SetStateAction<string[]>>;\n\n /** Active tool. App-defined enum (e.g. 'select' | 'hand' | 'text'). */\n activeTool: string;\n setActiveTool: Dispatch<SetStateAction<string>>;\n\n /** When true, mutating actions should no-op. Used for previews. */\n readOnly: boolean;\n\n /**\n * Optional resolver: given a `selectedId`, return its engine-defined \"kind\"\n * (e.g. 'text', 'image'). DesignerPane.showFor uses this to decide whether\n * to render. Apps that don't have typed selections can leave this undefined.\n */\n resolveKind?: (id: string) => string | undefined;\n}\n\nconst noop = () => {};\n\nconst DEFAULT: ShellContextValue = {\n selectedIds: [],\n setSelectedIds: noop as Dispatch<SetStateAction<string[]>>,\n activeTool: \"select\",\n setActiveTool: noop as Dispatch<SetStateAction<string>>,\n readOnly: false,\n resolveKind: undefined,\n};\n\nconst ShellContext = createContext<ShellContextValue>(DEFAULT);\n\nexport function useShellContext(): ShellContextValue {\n return useContext(ShellContext);\n}\n\ninterface ShellProviderProps {\n /** Initial selected IDs. Default: []. */\n initialSelectedIds?: string[];\n /** Initial active tool. Default: 'select'. */\n initialTool?: string;\n /** Read-only flag. */\n readOnly?: boolean;\n /** Kind resolver — see ShellContextValue.resolveKind. */\n resolveKind?: (id: string) => string | undefined;\n /** Controlled override — pass a value to lift state out of the provider. */\n value?: Partial<ShellContextValue>;\n children: ReactNode;\n}\n\nexport function ShellProvider({\n initialSelectedIds = [],\n initialTool = \"select\",\n readOnly = false,\n resolveKind,\n value: controlled,\n children,\n}: ShellProviderProps) {\n const [selectedIds, setSelectedIds] = useState<string[]>(initialSelectedIds);\n const [activeTool, setActiveTool] = useState<string>(initialTool);\n\n const value = useMemo<ShellContextValue>(\n () => ({\n selectedIds: controlled?.selectedIds ?? selectedIds,\n setSelectedIds: controlled?.setSelectedIds ?? setSelectedIds,\n activeTool: controlled?.activeTool ?? activeTool,\n setActiveTool: controlled?.setActiveTool ?? setActiveTool,\n readOnly: controlled?.readOnly ?? readOnly,\n resolveKind: controlled?.resolveKind ?? resolveKind,\n }),\n [selectedIds, activeTool, readOnly, resolveKind, controlled],\n );\n\n return <ShellContext.Provider value={value}>{children}</ShellContext.Provider>;\n}\n","import type { ReactNode } from \"react\";\nimport { cn } from \"@hilum/ui\";\n\ninterface DesignerShellProps {\n className?: string;\n children: ReactNode;\n}\n\n/**\n * Root layout for an editor app — full viewport, themed surface.\n * Place a <DesignerHeader>, <DesignerSidebar>, <DesignerPanel>, and the\n * canvas content as children.\n */\nfunction DesignerShell({ className, children }: DesignerShellProps) {\n return (\n <div\n className={cn(\n \"flex flex-col h-screen w-screen overflow-hidden bg-taupe-50 text-taupe-900\",\n className,\n )}\n >\n {children}\n </div>\n );\n}\n\nexport { DesignerShell };\nexport type { DesignerShellProps };\n","import type { ReactNode } from \"react\";\nimport { cn } from \"@hilum/ui\";\n\ninterface DesignerHeaderProps {\n /** Left-aligned content — file name, breadcrumbs, project switcher. */\n left?: ReactNode;\n /** Center content — typically the active document title. */\n center?: ReactNode;\n /** Right-aligned content — share, export, presence, account. */\n right?: ReactNode;\n className?: string;\n children?: ReactNode;\n}\n\n/**\n * Top bar of an editor app. Slot-driven — the chrome doesn't know what\n * goes in each region. Use <DesignerHeader left={...} center={...} right={...} />.\n */\nfunction DesignerHeader({ left, center, right, className, children }: DesignerHeaderProps) {\n return (\n <header\n className={cn(\n \"flex h-12 items-center justify-between gap-3 border-b border-taupe-100 bg-white px-3 shrink-0\",\n className,\n )}\n >\n <div className=\"flex items-center gap-2 min-w-0\">{left}</div>\n {center && <div className=\"flex items-center gap-2 min-w-0\">{center}</div>}\n <div className=\"flex items-center gap-2 min-w-0\">{right}</div>\n {children}\n </header>\n );\n}\n\nexport { DesignerHeader };\nexport type { DesignerHeaderProps };\n","import type { ComponentType, ReactNode } from \"react\";\nimport { cn, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from \"@hilum/ui\";\n\ninterface SidebarItem {\n id: string;\n label: string;\n icon: ComponentType<{ size?: number; className?: string }>;\n onClick?: () => void;\n /** Caller-computed active flag. */\n active?: boolean;\n disabled?: boolean;\n badge?: ReactNode;\n}\n\ninterface DesignerSidebarProps {\n /** Top group of icon buttons. */\n items: SidebarItem[];\n /** Optional bottom group (settings, help, account). */\n bottomItems?: SidebarItem[];\n side?: \"left\" | \"right\";\n className?: string;\n children?: ReactNode;\n}\n\n/**\n * Vertical icon rail used as the primary tool / navigation column in an\n * editor. Driven by an `items` array — no engine coupling. Each item gets\n * a tooltip showing its label.\n */\nfunction DesignerSidebar({\n items,\n bottomItems,\n side = \"left\",\n className,\n children,\n}: DesignerSidebarProps) {\n return (\n <aside\n className={cn(\n \"flex flex-col w-12 bg-white shrink-0\",\n side === \"left\" ? \"border-r\" : \"border-l\",\n \"border-taupe-100\",\n className,\n )}\n >\n <TooltipProvider>\n <div className=\"flex flex-col items-center gap-0.5 p-1.5\">\n {items.map((item) => (\n <SidebarButton key={item.id} item={item} />\n ))}\n </div>\n\n {children}\n\n {bottomItems && bottomItems.length > 0 && (\n <div className=\"mt-auto flex flex-col items-center gap-0.5 p-1.5\">\n {bottomItems.map((item) => (\n <SidebarButton key={item.id} item={item} />\n ))}\n </div>\n )}\n </TooltipProvider>\n </aside>\n );\n}\n\nfunction SidebarButton({ item }: { item: SidebarItem }) {\n const Icon = item.icon;\n return (\n <Tooltip>\n <TooltipTrigger asChild>\n <button\n type=\"button\"\n onClick={item.onClick}\n disabled={item.disabled}\n aria-label={item.label}\n aria-pressed={item.active}\n className={cn(\n \"relative flex size-9 items-center justify-center rounded-md transition-colors\",\n item.active\n ? \"bg-taupe-900 text-white\"\n : \"text-taupe-500 hover:bg-taupe-100 hover:text-taupe-900\",\n item.disabled && \"opacity-50 cursor-not-allowed\",\n )}\n >\n <Icon size={16} />\n {item.badge != null && (\n <span className=\"absolute -top-0.5 -right-0.5 caption-xs\">{item.badge}</span>\n )}\n </button>\n </TooltipTrigger>\n <TooltipContent side=\"right\">{item.label}</TooltipContent>\n </Tooltip>\n );\n}\n\nexport { DesignerSidebar };\nexport type { DesignerSidebarProps, SidebarItem };\n","import type { ReactNode } from \"react\";\nimport { cn } from \"@hilum/ui\";\n\ninterface DesignerPanelProps {\n side: \"left\" | \"right\";\n /** Width in pixels. Default: 240. */\n width?: number;\n /** Add a separator border on the inner edge. Default: true. */\n bordered?: boolean;\n className?: string;\n children?: ReactNode;\n}\n\n/**\n * Left or right side panel of an editor — typically holds layer lists,\n * inspector / properties, history, comments, etc. Static-width for v1.\n * (Resize handles arrive in a later iteration if needed.)\n */\nfunction DesignerPanel({\n side,\n width = 240,\n bordered = true,\n className,\n children,\n}: DesignerPanelProps) {\n return (\n <aside\n className={cn(\n \"flex flex-col bg-white shrink-0 overflow-hidden\",\n bordered && (side === \"left\" ? \"border-r\" : \"border-l\"),\n bordered && \"border-taupe-100\",\n className,\n )}\n style={{ width }}\n >\n <div className=\"flex flex-col flex-1 overflow-y-auto\">{children}</div>\n </aside>\n );\n}\n\nexport { DesignerPanel };\nexport type { DesignerPanelProps };\n","import { useState, type ReactNode } from \"react\";\nimport { ChevronDown } from \"lucide-react\";\nimport { cn } from \"@hilum/ui\";\nimport { useShellContext } from \"../shell/ShellContext\";\n\ninterface DesignerPaneProps {\n /**\n * Predicate or list of allowed kinds. The pane renders only when matched.\n *\n * - `string[]`: render when at least one selected ID resolves to one of\n * these kinds (requires `ShellContext.resolveKind`).\n * - `(selectedIds: string[]) => boolean`: full predicate.\n * - omitted: always render.\n */\n showFor?: string[] | ((selectedIds: string[]) => boolean);\n collapsible?: boolean;\n defaultOpen?: boolean;\n className?: string;\n children: ReactNode;\n}\n\ninterface DesignerPaneTitleProps {\n className?: string;\n children: ReactNode;\n /** Right-aligned action / control. */\n action?: ReactNode;\n}\n\ninterface DesignerPaneContentProps {\n className?: string;\n children: ReactNode;\n}\n\n/**\n * Collapsible inspector section. Use inside a <DesignerPanel>.\n *\n * <DesignerPane showFor={[\"text\"]} collapsible>\n * <DesignerPaneTitle>Typography</DesignerPaneTitle>\n * <DesignerPaneContent>...</DesignerPaneContent>\n * </DesignerPane>\n */\nfunction DesignerPane({\n showFor,\n collapsible = false,\n defaultOpen = true,\n className,\n children,\n}: DesignerPaneProps) {\n const { selectedIds, resolveKind } = useShellContext();\n const [open, setOpen] = useState(defaultOpen);\n\n // Visibility check.\n let visible = true;\n if (typeof showFor === \"function\") {\n visible = showFor(selectedIds);\n } else if (Array.isArray(showFor)) {\n if (!resolveKind) {\n // No resolver: best effort — show only if any IDs are selected.\n visible = selectedIds.length > 0;\n } else {\n visible = selectedIds.some((id) => {\n const kind = resolveKind(id);\n return kind != null && showFor.includes(kind);\n });\n }\n }\n if (!visible) return null;\n\n return (\n <PaneContext.Provider value={{ open, toggle: () => setOpen((v) => !v), collapsible }}>\n <section className={cn(\"flex flex-col border-b border-taupe-100 last:border-b-0\", className)}>\n {children}\n </section>\n </PaneContext.Provider>\n );\n}\n\nfunction DesignerPaneTitle({ className, children, action }: DesignerPaneTitleProps) {\n const { open, toggle, collapsible } = usePaneContext();\n\n const Tag = collapsible ? \"button\" : \"div\";\n return (\n <Tag\n type={collapsible ? \"button\" : undefined}\n onClick={collapsible ? toggle : undefined}\n className={cn(\n \"flex w-full items-center justify-between gap-2 px-3 py-2 text-left\",\n \"caption-xs uppercase tracking-wider font-semibold text-taupe-500\",\n collapsible && \"hover:text-taupe-900 transition-colors\",\n className,\n )}\n >\n <span className=\"flex items-center gap-1.5\">\n {collapsible && (\n <ChevronDown\n size={12}\n className={cn(\"transition-transform duration-150\", !open && \"-rotate-90\")}\n />\n )}\n {children}\n </span>\n {action && <span className=\"ml-auto\">{action}</span>}\n </Tag>\n );\n}\n\nfunction DesignerPaneContent({ className, children }: DesignerPaneContentProps) {\n const { open } = usePaneContext();\n if (!open) return null;\n return <div className={cn(\"flex flex-col gap-2 px-3 pb-3\", className)}>{children}</div>;\n}\n\n// --- internal pane context (so title and content stay in sync) ---\n\nimport { createContext, useContext } from \"react\";\n\ninterface PaneCtx {\n open: boolean;\n toggle: () => void;\n collapsible: boolean;\n}\nconst PaneContext = createContext<PaneCtx>({ open: true, toggle: () => {}, collapsible: false });\nfunction usePaneContext() {\n return useContext(PaneContext);\n}\n\nexport { DesignerPane, DesignerPaneTitle, DesignerPaneContent };\nexport type { DesignerPaneProps, DesignerPaneTitleProps, DesignerPaneContentProps };\n","import type { ComponentType, ReactNode } from \"react\";\nimport { cn, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from \"@hilum/ui\";\n\n/* ============================================================== *\n * Toolbar — outer container *\n * ============================================================== */\n\ninterface DesignerToolbarProps {\n className?: string;\n /** Position. Default: 'floating' (centered, floating above content). */\n variant?: \"floating\" | \"inline\";\n children: ReactNode;\n}\n\nfunction DesignerToolbar({ className, variant = \"floating\", children }: DesignerToolbarProps) {\n return (\n <TooltipProvider>\n <div\n role=\"toolbar\"\n className={cn(\n \"flex items-center gap-0.5 rounded-lg bg-white shadow-natural border border-taupe-100 p-1\",\n variant === \"floating\" && \"fixed bottom-4 left-1/2 -translate-x-1/2 z-30\",\n className,\n )}\n >\n {children}\n </div>\n </TooltipProvider>\n );\n}\n\n/* ============================================================== *\n * ToolbarGroup — visual group of buttons *\n * ============================================================== */\n\ninterface DesignerToolbarGroupProps {\n className?: string;\n children: ReactNode;\n}\n\nfunction DesignerToolbarGroup({ className, children }: DesignerToolbarGroupProps) {\n return <div className={cn(\"flex items-center gap-0.5\", className)}>{children}</div>;\n}\n\n/* ============================================================== *\n * ToolbarSeparator *\n * ============================================================== */\n\ninterface DesignerToolbarSeparatorProps {\n className?: string;\n}\n\nfunction DesignerToolbarSeparator({ className }: DesignerToolbarSeparatorProps) {\n return <div className={cn(\"mx-1 h-5 w-px bg-taupe-100\", className)} role=\"separator\" />;\n}\n\n/* ============================================================== *\n * ToolbarButton — single tool / action *\n * ============================================================== */\n\ninterface DesignerToolbarButtonProps {\n label: string;\n icon?: ComponentType<{ size?: number; className?: string }>;\n onClick?: () => void;\n active?: boolean;\n disabled?: boolean;\n /** Optional keyboard shortcut shown in the tooltip (e.g. 'V', 'Cmd+Z'). */\n shortcut?: string;\n className?: string;\n children?: ReactNode;\n}\n\nfunction DesignerToolbarButton({\n label,\n icon: Icon,\n onClick,\n active,\n disabled,\n shortcut,\n className,\n children,\n}: DesignerToolbarButtonProps) {\n return (\n <Tooltip>\n <TooltipTrigger asChild>\n <button\n type=\"button\"\n onClick={onClick}\n disabled={disabled}\n aria-label={label}\n aria-pressed={active}\n className={cn(\n \"flex h-8 min-w-8 items-center justify-center gap-1 rounded-md px-1.5 transition-colors caption\",\n active\n ? \"bg-taupe-900 text-white\"\n : \"text-taupe-600 hover:bg-taupe-100 hover:text-taupe-900\",\n disabled && \"opacity-50 cursor-not-allowed\",\n className,\n )}\n >\n {Icon && <Icon size={16} />}\n {children}\n </button>\n </TooltipTrigger>\n <TooltipContent side=\"top\">\n <span>{label}</span>\n {shortcut && <span className=\"ml-2 caption-xs text-taupe-300\">{shortcut}</span>}\n </TooltipContent>\n </Tooltip>\n );\n}\n\nexport { DesignerToolbar, DesignerToolbarGroup, DesignerToolbarSeparator, DesignerToolbarButton };\nexport type {\n DesignerToolbarProps,\n DesignerToolbarGroupProps,\n DesignerToolbarSeparatorProps,\n DesignerToolbarButtonProps,\n};\n","import { useCallback, useState } from \"react\";\n\ninterface HistoryState<T> {\n past: T[];\n present: T;\n future: T[];\n}\n\ninterface UseHistoryReturn<T> {\n state: T;\n /**\n * Record a new state. Use when the user makes a logical edit. Pushes the\n * current `state` into `past` and clears `future`. Pass `{ skipHistory: true }`\n * (via a separate setter) to overwrite without pushing — apps that need that\n * can set state directly via a wrapper.\n */\n setState: (next: T | ((prev: T) => T)) => void;\n /** Replace `state` without pushing onto the history stack. */\n replaceState: (next: T | ((prev: T) => T)) => void;\n undo: () => void;\n redo: () => void;\n reset: (next: T) => void;\n canUndo: boolean;\n canRedo: boolean;\n /** Number of past entries (undo depth). */\n pastSize: number;\n /** Number of future entries (redo depth). */\n futureSize: number;\n}\n\n/**\n * Generic, engine-agnostic undo/redo stack.\n *\n * @hilum/designer-canvas wires this with `useHistory<Layer[]>(layers)`.\n * A form-builder app could wire `useHistory<FormSchema>(schema)`.\n */\nexport function useHistory<T>(initial: T): UseHistoryReturn<T> {\n const [history, setHistory] = useState<HistoryState<T>>({\n past: [],\n present: initial,\n future: [],\n });\n\n const setState = useCallback((next: T | ((prev: T) => T)) => {\n setHistory((prev) => {\n const value = typeof next === \"function\" ? (next as (p: T) => T)(prev.present) : next;\n if (Object.is(value, prev.present)) return prev;\n return {\n past: [...prev.past, prev.present],\n present: value,\n future: [],\n };\n });\n }, []);\n\n const replaceState = useCallback((next: T | ((prev: T) => T)) => {\n setHistory((prev) => {\n const value = typeof next === \"function\" ? (next as (p: T) => T)(prev.present) : next;\n return { ...prev, present: value };\n });\n }, []);\n\n const undo = useCallback(() => {\n setHistory((prev) => {\n if (prev.past.length === 0) return prev;\n const past = prev.past.slice(0, -1);\n const previous = prev.past[prev.past.length - 1]!;\n return {\n past,\n present: previous,\n future: [prev.present, ...prev.future],\n };\n });\n }, []);\n\n const redo = useCallback(() => {\n setHistory((prev) => {\n if (prev.future.length === 0) return prev;\n const [next, ...rest] = prev.future;\n return {\n past: [...prev.past, prev.present],\n present: next!,\n future: rest,\n };\n });\n }, []);\n\n const reset = useCallback((next: T) => {\n setHistory({ past: [], present: next, future: [] });\n }, []);\n\n return {\n state: history.present,\n setState,\n replaceState,\n undo,\n redo,\n reset,\n canUndo: history.past.length > 0,\n canRedo: history.future.length > 0,\n pastSize: history.past.length,\n futureSize: history.future.length,\n };\n}\n","import { useEffect } from \"react\";\n\nexport interface KeybindingConfig {\n /** Lower-case key (e.g. 'z', 'arrowup', 'escape', '+', '-'). */\n key: string;\n ctrl?: boolean;\n /** Cmd on macOS. Use both `meta` and `ctrl: true` if you want both. */\n meta?: boolean;\n shift?: boolean;\n alt?: boolean;\n /**\n * Treat both ctrl and meta as the same modifier (for cross-platform\n * shortcuts like Cmd+Z / Ctrl+Z). Default: false. Set to true to match\n * either modifier.\n */\n mod?: boolean;\n action: (event: KeyboardEvent) => void;\n /** Don't run when target is an input / textarea / contenteditable. Default: true. */\n skipInputs?: boolean;\n /** Call event.preventDefault() before action. Default: true. */\n preventDefault?: boolean;\n}\n\ninterface UseKeybindingsOptions {\n /** Skip all bindings entirely. */\n disabled?: boolean;\n /** Element to attach the listener to. Default: window. */\n target?: Window | HTMLElement | null;\n}\n\n/**\n * Generic keyboard shortcut registry. Engine-agnostic — the package doesn't\n * know what an action does. Pass an array of bindings; the hook attaches\n * a single keydown listener and dispatches on match.\n *\n * @hilum/designer-canvas wires its standard editor shortcuts (Cmd+Z, V, T,\n * arrow nudge, etc.) on top of this hook.\n */\nexport function useKeybindings(\n bindings: KeybindingConfig[],\n { disabled, target }: UseKeybindingsOptions = {},\n) {\n useEffect(() => {\n if (disabled) return;\n const node = target ?? (typeof window !== \"undefined\" ? window : null);\n if (!node) return;\n\n const handler = (rawEvent: Event) => {\n const e = rawEvent as KeyboardEvent;\n\n const tgt = e.target as HTMLElement | null;\n const isInput =\n tgt instanceof HTMLInputElement ||\n tgt instanceof HTMLTextAreaElement ||\n tgt instanceof HTMLSelectElement ||\n tgt?.isContentEditable === true;\n\n const key = e.key.toLowerCase();\n\n for (const b of bindings) {\n if (b.key.toLowerCase() !== key) continue;\n if (b.skipInputs !== false && isInput) continue;\n\n const ctrlMatches = b.ctrl ? e.ctrlKey : !e.ctrlKey || b.mod;\n const metaMatches = b.meta ? e.metaKey : !e.metaKey || b.mod;\n const modMatches = b.mod ? e.ctrlKey || e.metaKey : true;\n const shiftMatches = b.shift ? e.shiftKey : !e.shiftKey;\n const altMatches = b.alt ? e.altKey : !e.altKey;\n\n if (!modMatches) continue;\n if (!b.mod && (!ctrlMatches || !metaMatches)) continue;\n if (!shiftMatches || !altMatches) continue;\n\n if (b.preventDefault !== false) e.preventDefault();\n b.action(e);\n return;\n }\n };\n\n (node as Window).addEventListener(\"keydown\", handler);\n return () => (node as Window).removeEventListener(\"keydown\", handler);\n }, [bindings, disabled, target]);\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@hilum/designer",
3
+ "version": "0.1.1",
4
+ "private": false,
5
+ "description": "Engine-agnostic canvas-editor chrome — DesignerShell, Toolbar, Panel, Pane + generic useHistory and useKeybindings hooks",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+ssh://git@github.com/hilum-labs/hilum-ui.git",
10
+ "directory": "packages/designer"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "type": "module",
16
+ "exports": {
17
+ ".": {
18
+ "import": "./dist/index.js",
19
+ "types": "./dist/index.d.ts"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "README.md",
25
+ "LICENSE"
26
+ ],
27
+ "//": "Tests for this package live in src/**/*.test.ts and run from the root via `pnpm test`.",
28
+ "peerDependencies": {
29
+ "react": "^19.0.0",
30
+ "react-dom": "^19.0.0",
31
+ "@hilum/ui": "0.1.1"
32
+ },
33
+ "dependencies": {
34
+ "lucide-react": "^0.511.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/react": "^19.0.0",
38
+ "@types/react-dom": "^19.0.0",
39
+ "tailwindcss": "^4.2.0",
40
+ "tsup": "^8.3.0",
41
+ "typescript": "^5.7.0",
42
+ "@hilum/ui": "0.1.1"
43
+ },
44
+ "scripts": {
45
+ "build": "tsup",
46
+ "dev": "tsup --watch",
47
+ "typecheck": "tsc --noEmit",
48
+ "lint": "eslint src",
49
+ "clean": "rm -rf dist .turbo"
50
+ }
51
+ }