@improba/page-builder 0.1.3 → 0.2.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.
Files changed (69) hide show
  1. package/README.md +2 -1
  2. package/dist/core.cjs +2 -0
  3. package/dist/core.cjs.map +1 -0
  4. package/dist/core.js +29 -0
  5. package/dist/core.js.map +1 -0
  6. package/dist/index-D79WbFRY.cjs +2 -0
  7. package/dist/index-D79WbFRY.cjs.map +1 -0
  8. package/dist/index-c6HOrx9r.js +523 -0
  9. package/dist/index-c6HOrx9r.js.map +1 -0
  10. package/dist/index.cjs +8490 -5
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.js +27958 -2680
  13. package/dist/index.js.map +1 -1
  14. package/dist/style.css +1 -0
  15. package/dist/types/built-in/PbColumn.vue.d.ts +41 -0
  16. package/dist/types/built-in/PbContainer.vue.d.ts +32 -0
  17. package/dist/types/built-in/PbImage.vue.d.ts +42 -0
  18. package/dist/types/built-in/PbRow.vue.d.ts +41 -0
  19. package/dist/types/built-in/PbSection.vue.d.ts +50 -0
  20. package/dist/types/built-in/PbText.vue.d.ts +25 -0
  21. package/dist/types/built-in/PbVideo.vue.d.ts +27 -0
  22. package/dist/types/built-in/index.d.ts +10 -0
  23. package/dist/types/components/PageBuilder.vue.d.ts +50 -0
  24. package/dist/types/components/editor/EditorCanvas.vue.d.ts +67 -0
  25. package/dist/types/components/editor/EditorToolbar.vue.d.ts +97 -0
  26. package/dist/types/components/editor/IframeCanvas.vue.d.ts +102 -0
  27. package/dist/types/components/editor/LeftDrawer.vue.d.ts +46 -0
  28. package/dist/types/components/editor/NodeContextMenu.vue.d.ts +66 -0
  29. package/dist/types/components/editor/PageEditor.vue.d.ts +20 -0
  30. package/dist/types/components/editor/PbIcon.vue.d.ts +25 -0
  31. package/dist/types/components/editor/RightDrawer.vue.d.ts +43 -0
  32. package/dist/types/components/editor/TreePanel.vue.d.ts +28 -0
  33. package/dist/types/components/editor/prop-editors/MediaPicker.vue.d.ts +20 -0
  34. package/dist/types/components/editor/prop-editors/PropBooleanEditor.vue.d.ts +18 -0
  35. package/dist/types/components/editor/prop-editors/PropColorEditor.vue.d.ts +18 -0
  36. package/dist/types/components/editor/prop-editors/PropNumberEditor.vue.d.ts +36 -0
  37. package/dist/types/components/editor/prop-editors/PropSelectEditor.vue.d.ts +31 -0
  38. package/dist/types/components/editor/prop-editors/PropTextEditor.vue.d.ts +27 -0
  39. package/dist/types/components/editor/prop-editors/RichTextEditor.vue.d.ts +27 -0
  40. package/dist/types/components/editor/prop-editors/index.d.ts +29 -0
  41. package/dist/types/components/reader/NodeRenderer.vue.d.ts +33 -0
  42. package/dist/types/components/reader/PageReader.vue.d.ts +14 -0
  43. package/dist/types/components/shared/ErrorBoundary.vue.d.ts +21 -0
  44. package/dist/types/composables/use-drag-drop.d.ts +23 -0
  45. package/dist/types/composables/use-editor.d.ts +40 -0
  46. package/dist/types/composables/use-node-tree.d.ts +23 -0
  47. package/dist/types/composables/use-page-builder.d.ts +28 -0
  48. package/dist/types/core/drop-slot.d.ts +12 -0
  49. package/dist/types/core/errors.d.ts +14 -0
  50. package/dist/types/core/iframe-bridge.d.ts +85 -0
  51. package/dist/types/core/index.d.ts +18 -0
  52. package/dist/types/core/registry.d.ts +43 -0
  53. package/dist/types/core/sanitize.d.ts +3 -0
  54. package/dist/types/core/tree.d.ts +56 -0
  55. package/dist/types/core/validation.d.ts +10 -0
  56. package/dist/types/core/virtual-tree.d.ts +44 -0
  57. package/dist/types/i18n/context.d.ts +13 -0
  58. package/dist/types/i18n/index.d.ts +3 -0
  59. package/dist/types/i18n/messages.d.ts +3 -0
  60. package/dist/types/i18n/translator.d.ts +14 -0
  61. package/dist/types/index.d.ts +27 -0
  62. package/dist/types/plugin.d.ts +18 -0
  63. package/dist/types/types/component.d.ts +68 -0
  64. package/dist/types/types/editor.d.ts +54 -0
  65. package/dist/types/types/index.d.ts +6 -0
  66. package/dist/types/types/keys.d.ts +13 -0
  67. package/dist/types/types/node.d.ts +54 -0
  68. package/package.json +10 -2
  69. package/dist/index.css +0 -1
@@ -0,0 +1,85 @@
1
+ export declare const IFRAME_BRIDGE_NAMESPACE = "@improba/page-builder/iframe-bridge";
2
+ export declare const IFRAME_BRIDGE_VERSION: 1;
3
+ type IframeBridgeChannel = 'keydown' | 'pointer' | 'lifecycle';
4
+ export type IframeBridgeSessionToken = string;
5
+ interface IframeBridgeEnvelope<TChannel extends IframeBridgeChannel, TPayload> {
6
+ namespace: typeof IFRAME_BRIDGE_NAMESPACE;
7
+ version: typeof IFRAME_BRIDGE_VERSION;
8
+ sessionToken: IframeBridgeSessionToken;
9
+ channel: TChannel;
10
+ payload: TPayload;
11
+ }
12
+ export interface IframeBridgeKeydownPayload {
13
+ key: string;
14
+ code: string;
15
+ ctrlKey: boolean;
16
+ metaKey: boolean;
17
+ shiftKey: boolean;
18
+ altKey: boolean;
19
+ defaultPrevented: boolean;
20
+ isEditable: boolean;
21
+ }
22
+ export type IframeBridgePointerInteraction = 'hover' | 'select' | 'context';
23
+ interface IframeBridgePointerBasePayload {
24
+ interaction: IframeBridgePointerInteraction;
25
+ nodeId: number | null;
26
+ }
27
+ interface IframeBridgePointerContextPayload extends IframeBridgePointerBasePayload {
28
+ interaction: 'context';
29
+ clientX: number;
30
+ clientY: number;
31
+ }
32
+ interface IframeBridgePointerHoverOrSelectPayload extends IframeBridgePointerBasePayload {
33
+ interaction: 'hover' | 'select';
34
+ }
35
+ export type IframeBridgePointerPayload = IframeBridgePointerContextPayload | IframeBridgePointerHoverOrSelectPayload;
36
+ export interface IframeBridgeLifecyclePayload {
37
+ state: 'ready';
38
+ }
39
+ export type IframeBridgeKeydownMessage = IframeBridgeEnvelope<'keydown', IframeBridgeKeydownPayload>;
40
+ export type IframeBridgePointerMessage = IframeBridgeEnvelope<'pointer', IframeBridgePointerPayload>;
41
+ export type IframeBridgeLifecycleMessage = IframeBridgeEnvelope<'lifecycle', IframeBridgeLifecyclePayload>;
42
+ export type IframeBridgeMessage = IframeBridgeKeydownMessage | IframeBridgePointerMessage | IframeBridgeLifecycleMessage;
43
+ export declare function createIframeBridgeSessionToken(): IframeBridgeSessionToken;
44
+ export declare function createIframeBridgeReadyMessage(sessionToken: IframeBridgeSessionToken): IframeBridgeLifecycleMessage;
45
+ export declare function createIframeBridgePointerMessage(payload: IframeBridgePointerPayload, sessionToken: IframeBridgeSessionToken): IframeBridgePointerMessage;
46
+ export declare function createIframeBridgeKeydownMessage(payload: IframeBridgeKeydownPayload, sessionToken: IframeBridgeSessionToken): IframeBridgeKeydownMessage;
47
+ export interface ParseIframeBridgeMessageOptions {
48
+ expectedSessionToken?: IframeBridgeSessionToken;
49
+ allowLegacyNoSessionToken?: boolean;
50
+ }
51
+ export declare function parseIframeBridgeMessage(value: unknown, options?: ParseIframeBridgeMessageOptions): IframeBridgeMessage | null;
52
+ export interface IframeBridgeParentOptions {
53
+ hostWindow: Window;
54
+ expectedSource: Window;
55
+ expectedOrigin: string;
56
+ expectedSessionToken?: IframeBridgeSessionToken;
57
+ allowLegacyNoSessionToken?: boolean;
58
+ onReady?: () => void;
59
+ onPointer?: (payload: IframeBridgePointerPayload) => void;
60
+ onKeydown?: (payload: IframeBridgeKeydownPayload) => void;
61
+ }
62
+ export interface IframeBridgeParent {
63
+ dispose: () => void;
64
+ }
65
+ export declare function createIframeBridgeParent(options: IframeBridgeParentOptions): IframeBridgeParent;
66
+ export interface IframeBridgeChild {
67
+ postReady: () => void;
68
+ postPointer: (payload: IframeBridgePointerPayload) => void;
69
+ postKeydown: (payload: IframeBridgeKeydownPayload) => void;
70
+ }
71
+ export interface IframeBridgeChildOptions {
72
+ targetWindow: Window;
73
+ targetOrigin: string;
74
+ sessionToken: IframeBridgeSessionToken;
75
+ }
76
+ export declare function createIframeBridgeChild(options: IframeBridgeChildOptions): IframeBridgeChild;
77
+ export interface MountIframeBridgeDomListenersOptions {
78
+ frameDocument: Document;
79
+ contentRoot: HTMLElement;
80
+ bridge: IframeBridgeChild;
81
+ resolveNodeId: (target: EventTarget | null) => number | null;
82
+ isEditableTarget: (target: EventTarget | null) => boolean;
83
+ }
84
+ export declare function mountIframeBridgeDomListeners(options: MountIframeBridgeDomListenersOptions): () => void;
85
+ export {};
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Vue-free core utilities for server-side (Node.js) usage.
3
+ *
4
+ * This barrel intentionally excludes:
5
+ * - registry.ts / drop-slot.ts (import Vue's Component type)
6
+ * - sanitizeRichTextHtml (requires DOM APIs)
7
+ * - iframe-bridge.ts (browser-only)
8
+ * - reportDevDiagnostic (uses import.meta.env.DEV)
9
+ */
10
+ export { cloneTree, findNodeById, findParent, removeNode, insertNode, moveNode, createNode, walkTree, countNodes, getMaxId, interpolateProps, extractPlainText, } from './tree';
11
+ export { validateNode, validatePageData } from './validation';
12
+ export type { IValidationError, IValidationResult } from './validation';
13
+ export { normalizeSafeHtmlTag, sanitizeUrlByKind } from './sanitize';
14
+ export { PageBuilderError, isPageBuilderError, createPageBuilderError, toErrorMessage, } from './errors';
15
+ export type { PageBuilderErrorCode, PageBuilderErrorOptions } from './errors';
16
+ export { flattenTree, computeWindowRange, sliceWindow, createStableNodeKey, createVirtualTreeIndexMaps, } from './virtual-tree';
17
+ export type { IVirtualTreeRow, IVirtualWindowRange, IVirtualTreeIndexMaps, IFlattenTreeOptions, } from './virtual-tree';
18
+ export type { INode, IPageData, IPageMeta, IPageSavePayload, } from '../types/node';
@@ -0,0 +1,43 @@
1
+ import type { Component } from 'vue';
2
+ import type { IComponentDefinition } from '@/types/component';
3
+ /**
4
+ * Register a component for use in the page builder.
5
+ * Throws if a component with the same name is already registered.
6
+ */
7
+ export declare function registerComponent(definition: IComponentDefinition): void;
8
+ /**
9
+ * Register multiple components at once.
10
+ */
11
+ export declare function registerComponents(definitions: IComponentDefinition[]): void;
12
+ /**
13
+ * Replace an existing component registration.
14
+ */
15
+ export declare function replaceComponent(definition: IComponentDefinition): void;
16
+ /**
17
+ * Unregister a component by name.
18
+ */
19
+ export declare function unregisterComponent(name: string): boolean;
20
+ /**
21
+ * Retrieve a component definition by name. Returns undefined if not found.
22
+ */
23
+ export declare function getComponent(name: string): IComponentDefinition | undefined;
24
+ /**
25
+ * Retrieve the Vue component for rendering. Throws if not found.
26
+ */
27
+ export declare function resolveComponent(name: string): Component;
28
+ /**
29
+ * Get all registered component definitions.
30
+ */
31
+ export declare function getRegisteredComponents(): IComponentDefinition[];
32
+ /**
33
+ * Get all registered component definitions grouped by category.
34
+ */
35
+ export declare function getComponentsByCategory(): Map<string, IComponentDefinition[]>;
36
+ /**
37
+ * Check if a component is registered.
38
+ */
39
+ export declare function hasComponent(name: string): boolean;
40
+ /**
41
+ * Clear all registered components. Mainly useful for testing.
42
+ */
43
+ export declare function clearRegistry(): void;
@@ -0,0 +1,3 @@
1
+ export declare function sanitizeRichTextHtml(html: string): string;
2
+ export declare function normalizeSafeHtmlTag(tag: unknown, fallback?: string): string;
3
+ export declare function sanitizeUrlByKind(url: string, kind: 'link' | 'media' | 'background'): string;
@@ -0,0 +1,56 @@
1
+ import type { INode } from '@/types/node';
2
+ /**
3
+ * Deep clone a node tree using structured clone.
4
+ */
5
+ export declare function cloneTree(node: INode): INode;
6
+ /**
7
+ * Find a node by ID in the tree. Returns undefined if not found.
8
+ */
9
+ export declare function findNodeById(root: INode, id: number): INode | undefined;
10
+ /**
11
+ * Find the parent of a node by the child's ID.
12
+ * Returns the parent node and the child's index, or undefined.
13
+ */
14
+ export declare function findParent(root: INode, childId: number): {
15
+ parent: INode;
16
+ index: number;
17
+ } | undefined;
18
+ /**
19
+ * Remove a node by ID from the tree. Returns the removed node or undefined.
20
+ */
21
+ export declare function removeNode(root: INode, id: number): INode | undefined;
22
+ /**
23
+ * Insert a node as a child of a target node at a specific index and slot.
24
+ */
25
+ export declare function insertNode(root: INode, parentId: number, node: INode, index: number, slot?: string): boolean;
26
+ /**
27
+ * Move a node within the tree to a new parent at a specific index.
28
+ */
29
+ export declare function moveNode(root: INode, nodeId: number, newParentId: number, index: number, slot?: string): boolean;
30
+ /**
31
+ * Create a new node with default values and a given ID.
32
+ */
33
+ export declare function createNode(id: number, name: string, options?: Partial<Pick<INode, 'slot' | 'props' | 'children' | 'readonly'>>): INode;
34
+ /**
35
+ * Walk the tree depth-first and call visitor for each node.
36
+ * Return `false` from visitor to stop the entire traversal (not just the subtree).
37
+ */
38
+ export declare function walkTree(root: INode, visitor: (node: INode, depth: number) => boolean | void, depth?: number): boolean;
39
+ /**
40
+ * Count the total number of nodes in the tree.
41
+ */
42
+ export declare function countNodes(root: INode): number;
43
+ /**
44
+ * Get the maximum ID in the tree.
45
+ */
46
+ export declare function getMaxId(root: INode): number;
47
+ /**
48
+ * Interpolate template variables in node props.
49
+ * Replaces `{{ VAR }}` patterns with values from the variables map.
50
+ */
51
+ export declare function interpolateProps(props: Record<string, unknown>, variables: Record<string, string>): Record<string, unknown>;
52
+ /**
53
+ * Extract plain text from a node tree by collecting text content
54
+ * from PbText (and similar) components and stripping HTML tags.
55
+ */
56
+ export declare function extractPlainText(node: INode): string;
@@ -0,0 +1,10 @@
1
+ export interface IValidationError {
2
+ path: string;
3
+ message: string;
4
+ }
5
+ export interface IValidationResult {
6
+ isValid: boolean;
7
+ errors: IValidationError[];
8
+ }
9
+ export declare function validateNode(node: unknown, path?: string): IValidationResult;
10
+ export declare function validatePageData(pageData: unknown): IValidationResult;
@@ -0,0 +1,44 @@
1
+ import type { INode } from '@/types/node';
2
+ export interface IVirtualTreeRow {
3
+ node: INode;
4
+ id: number;
5
+ key: string;
6
+ depth: number;
7
+ index: number;
8
+ parentId: number | null;
9
+ }
10
+ export interface IVirtualWindowRange {
11
+ start: number;
12
+ end: number;
13
+ size: number;
14
+ total: number;
15
+ }
16
+ export interface IVirtualTreeIndexMaps {
17
+ keyByIndex: string[];
18
+ indexByKey: Map<string, number>;
19
+ indexByNodeId: Map<number, number>;
20
+ }
21
+ export interface IFlattenTreeOptions {
22
+ createKey?: (node: INode) => string;
23
+ }
24
+ export declare function createStableNodeKey(nodeId: number): string;
25
+ /**
26
+ * Flatten a node tree in depth-first pre-order with depth metadata.
27
+ * Uses an iterative stack to avoid recursion depth issues on large trees.
28
+ */
29
+ export declare function flattenTree(root: INode, options?: IFlattenTreeOptions): IVirtualTreeRow[];
30
+ /**
31
+ * Compute a clamped window range over a flat list.
32
+ */
33
+ export declare function computeWindowRange(total: number, startIndex: number, windowSize: number, overscan?: number): IVirtualWindowRange;
34
+ /**
35
+ * Slice a list using a computed virtual window.
36
+ */
37
+ export declare function sliceWindow<T>(rows: readonly T[], startIndex: number, windowSize: number, overscan?: number): {
38
+ rows: T[];
39
+ range: IVirtualWindowRange;
40
+ };
41
+ /**
42
+ * Build stable key/index lookup maps from flattened tree rows.
43
+ */
44
+ export declare function createVirtualTreeIndexMaps(rows: readonly IVirtualTreeRow[]): IVirtualTreeIndexMaps;
@@ -0,0 +1,13 @@
1
+ import { type ComputedRef, type InjectionKey } from 'vue';
2
+ import { type TranslationDictionary, type Translator } from './translator';
3
+ export interface PageBuilderI18nOptions {
4
+ locale?: string;
5
+ messages?: TranslationDictionary;
6
+ }
7
+ export interface PageBuilderI18nContext {
8
+ locale: ComputedRef<string>;
9
+ t: Translator;
10
+ }
11
+ export declare const PAGE_BUILDER_I18N_OPTIONS_KEY: InjectionKey<PageBuilderI18nOptions>;
12
+ export declare const PAGE_BUILDER_I18N_KEY: InjectionKey<PageBuilderI18nContext>;
13
+ export declare function usePageBuilderI18n(): PageBuilderI18nContext;
@@ -0,0 +1,3 @@
1
+ export { DEFAULT_LOCALE, defaultTranslations } from './messages';
2
+ export { createTranslator, mergeTranslations, translate, type TranslationParams, type TranslationDictionary, type Translator, } from './translator';
3
+ export { PAGE_BUILDER_I18N_KEY, PAGE_BUILDER_I18N_OPTIONS_KEY, usePageBuilderI18n, type PageBuilderI18nOptions, type PageBuilderI18nContext, } from './context';
@@ -0,0 +1,3 @@
1
+ import type { TranslationDictionary } from './translator';
2
+ export declare const DEFAULT_LOCALE = "en";
3
+ export declare const defaultTranslations: TranslationDictionary;
@@ -0,0 +1,14 @@
1
+ export type TranslationParams = Record<string, string | number>;
2
+ export type TranslationDictionary = Record<string, Record<string, string>>;
3
+ export type Translator = (key: string, params?: TranslationParams) => string;
4
+ interface TranslateOptions {
5
+ locale: string;
6
+ dictionary: TranslationDictionary;
7
+ key: string;
8
+ params?: TranslationParams;
9
+ fallbackLocale?: string;
10
+ }
11
+ export declare function mergeTranslations(...dictionaries: Array<TranslationDictionary | undefined | null>): TranslationDictionary;
12
+ export declare function translate(options: TranslateOptions): string;
13
+ export declare function createTranslator(localeSource: string | (() => string), dictionarySource: TranslationDictionary | (() => TranslationDictionary), fallbackLocale?: string): Translator;
14
+ export {};
@@ -0,0 +1,27 @@
1
+ export { PageBuilderPlugin } from './plugin';
2
+ export type { PageBuilderPluginOptions } from './plugin';
3
+ export { DEFAULT_LOCALE, defaultTranslations, createTranslator, mergeTranslations, translate, usePageBuilderI18n, } from './i18n';
4
+ export type { TranslationDictionary, TranslationParams, Translator, PageBuilderI18nOptions } from './i18n';
5
+ export { default as PageBuilder } from './components/PageBuilder.vue';
6
+ export { default as PageReader } from './components/reader/PageReader.vue';
7
+ export { default as PageEditor } from './components/editor/PageEditor.vue';
8
+ export { default as NodeRenderer } from './components/reader/NodeRenderer.vue';
9
+ export { registerComponent, registerComponents, replaceComponent, unregisterComponent, getComponent, resolveComponent, getRegisteredComponents, getComponentsByCategory, hasComponent, clearRegistry, } from './core/registry';
10
+ export { cloneTree, findNodeById, findParent, removeNode, insertNode, moveNode, createNode, walkTree, countNodes, getMaxId, interpolateProps, extractPlainText, } from './core/tree';
11
+ export { PageBuilderError, isPageBuilderError, } from './core/errors';
12
+ export type { PageBuilderErrorCode, PageBuilderErrorOptions, } from './core/errors';
13
+ export { validateNode, validatePageData } from './core/validation';
14
+ export type { IValidationError, IValidationResult } from './core/validation';
15
+ export { sanitizeRichTextHtml, normalizeSafeHtmlTag, sanitizeUrlByKind } from './core/sanitize';
16
+ export { flattenTree, computeWindowRange, sliceWindow, createStableNodeKey, createVirtualTreeIndexMaps, } from './core/virtual-tree';
17
+ export type { IVirtualTreeRow, IVirtualWindowRange, IVirtualTreeIndexMaps, IFlattenTreeOptions, } from './core/virtual-tree';
18
+ export { usePageBuilder } from './composables/use-page-builder';
19
+ export type { UsePageBuilderOptions, UsePageBuilderReturn } from './composables/use-page-builder';
20
+ export { useEditor } from './composables/use-editor';
21
+ export { useNodeTree } from './composables/use-node-tree';
22
+ export type { UseNodeTreeOptions } from './composables/use-node-tree';
23
+ export { useDragDrop } from './composables/use-drag-drop';
24
+ export type { DragState } from './composables/use-drag-drop';
25
+ export { builtInComponents, PbColumn, PbRow, PbText, PbImage, PbVideo, PbSection, PbContainer } from './built-in';
26
+ export { VIEWPORT_PRESETS, builderOptionsPropType, PAGE_BUILDER_KEY, EDITOR_KEY, NODE_TREE_KEY, DRAG_DROP_KEY, } from './types';
27
+ export type { INode, IPageData, IPageMeta, IPageSavePayload, IComponentDefinition, ISlotDefinition, IPropDefinition, ComponentCategory, PropEditorType, PageBuilderMode, IEditorState, IEditorHistoryEntry, IPageBuilderEvents, ViewportPreset, IViewportDimensions, } from './types';
@@ -0,0 +1,18 @@
1
+ import type { App } from 'vue';
2
+ import type { IComponentDefinition } from '@/types/component';
3
+ import { type TranslationDictionary } from '@/i18n';
4
+ export interface PageBuilderPluginOptions {
5
+ /** Custom components to register in addition to built-in ones. */
6
+ components?: IComponentDefinition[];
7
+ /** If false, built-in components (PbColumn, PbRow, etc.) won't be registered. Default: true. */
8
+ registerBuiltIn?: boolean;
9
+ /** Global component name for <PageBuilder>. Default: 'PageBuilder'. Set to false to skip global registration. */
10
+ globalName?: string | false;
11
+ /** Default locale for editor UI text. Can be overridden per <PageBuilder> instance. */
12
+ locale?: string;
13
+ /** Additional/overridden translation messages grouped by locale. */
14
+ messages?: TranslationDictionary;
15
+ }
16
+ export declare const PageBuilderPlugin: {
17
+ install(app: App, options?: PageBuilderPluginOptions): void;
18
+ };
@@ -0,0 +1,68 @@
1
+ import type { Component, PropType } from 'vue';
2
+ /**
3
+ * Metadata describing a page builder component.
4
+ * Every component registered in the page builder must provide this.
5
+ */
6
+ export interface IComponentDefinition {
7
+ /** Unique name used as key in the registry and in INode.name. */
8
+ name: string;
9
+ /** Human-readable label shown in the editor palette. */
10
+ label: string;
11
+ /** Short description for the editor tooltip. */
12
+ description?: string;
13
+ /** Category for grouping in the component palette. */
14
+ category: ComponentCategory;
15
+ /** URL or import path of a preview icon/thumbnail for the palette. */
16
+ icon?: string;
17
+ /** The Vue component to render. */
18
+ component: Component;
19
+ /** Named slots this component exposes for child nodes. */
20
+ slots: ISlotDefinition[];
21
+ /** Editable props schema for the right-drawer property editor. */
22
+ editableProps: IPropDefinition[];
23
+ /** Default props applied when the component is first added. */
24
+ defaultProps?: Record<string, unknown>;
25
+ /** If true, this component cannot be added by users (used for internal wrappers). */
26
+ hidden?: boolean;
27
+ }
28
+ export type ComponentCategory = 'layout' | 'content' | 'media' | 'navigation' | 'form' | 'data' | 'custom';
29
+ export interface ISlotDefinition {
30
+ /** Slot name matching Vue's named slot system. */
31
+ name: string;
32
+ /** Human-readable label. */
33
+ label: string;
34
+ /** Component names allowed in this slot. Empty array = all allowed. */
35
+ allowedComponents?: string[];
36
+ }
37
+ export interface IPropDefinition {
38
+ /** Prop key (matches the component prop name). */
39
+ key: string;
40
+ /** Human-readable label for the property editor. */
41
+ label: string;
42
+ /** Editor widget type. */
43
+ type: PropEditorType;
44
+ /** Default value. */
45
+ defaultValue?: unknown;
46
+ /** Whether this prop is required. */
47
+ required?: boolean;
48
+ /** For 'select' type — available options. */
49
+ options?: {
50
+ label: string;
51
+ value: string | number | boolean;
52
+ }[];
53
+ /** Validation rules. */
54
+ validation?: {
55
+ min?: number;
56
+ max?: number;
57
+ pattern?: string;
58
+ message?: string;
59
+ };
60
+ }
61
+ export type PropEditorType = 'text' | 'textarea' | 'richtext' | 'number' | 'boolean' | 'select' | 'color' | 'image' | 'url' | 'json';
62
+ /**
63
+ * Vue prop type helper for builderOptions.
64
+ * Components expose this as a static property so the registry can read it.
65
+ */
66
+ export declare const builderOptionsPropType: {
67
+ type: PropType<IComponentDefinition>;
68
+ };
@@ -0,0 +1,54 @@
1
+ import type { INode, IPageSavePayload } from './node';
2
+ /** Operating mode for the page builder. */
3
+ export type PageBuilderMode = 'read' | 'edit';
4
+ /**
5
+ * Editor UI state managed by the useEditor composable.
6
+ * Drag-and-drop state is managed separately by useDragDrop.
7
+ */
8
+ export interface IEditorState {
9
+ /** Currently selected node, or null. */
10
+ selectedNodeId: number | null;
11
+ /** Currently hovered node, or null. */
12
+ hoveredNodeId: number | null;
13
+ /** Whether the left drawer (component palette) is open. */
14
+ leftDrawerOpen: boolean;
15
+ /** Whether the right drawer (property editor) is open. */
16
+ rightDrawerOpen: boolean;
17
+ /** Undo/redo history stack. */
18
+ history: IEditorHistoryEntry[];
19
+ /** Current position in the history stack. */
20
+ historyIndex: number;
21
+ /** Dirty flag — true if unsaved changes exist. */
22
+ isDirty: boolean;
23
+ /** Viewport scale for the canvas iframe. */
24
+ canvasScale: number;
25
+ /** Viewport preset. */
26
+ viewport: ViewportPreset;
27
+ }
28
+ export interface IEditorHistoryEntry {
29
+ timestamp: number;
30
+ label: string;
31
+ snapshot: string;
32
+ }
33
+ export type ViewportPreset = 'desktop' | 'tablet' | 'mobile' | 'custom';
34
+ export interface IViewportDimensions {
35
+ width: number;
36
+ height: number;
37
+ label: string;
38
+ }
39
+ export interface IViewportSize {
40
+ width: number;
41
+ height: number;
42
+ }
43
+ export declare const VIEWPORT_PRESETS: Record<ViewportPreset, IViewportDimensions>;
44
+ /**
45
+ * Events emitted by the page builder in edit mode.
46
+ */
47
+ export interface IPageBuilderEvents {
48
+ /** Emitted when the user saves (Ctrl+S or save button). */
49
+ save: [payload: IPageSavePayload];
50
+ /** Emitted when the tree structure changes. */
51
+ change: [tree: INode];
52
+ /** Emitted when a node is selected. */
53
+ select: [nodeId: number | null];
54
+ }
@@ -0,0 +1,6 @@
1
+ export type { INode, IPageData, IPageMeta, IPageSavePayload, } from './node';
2
+ export type { IComponentDefinition, ISlotDefinition, IPropDefinition, ComponentCategory, PropEditorType, } from './component';
3
+ export { builderOptionsPropType } from './component';
4
+ export type { PageBuilderMode, IEditorState, IEditorHistoryEntry, IPageBuilderEvents, ViewportPreset, IViewportDimensions, } from './editor';
5
+ export { VIEWPORT_PRESETS } from './editor';
6
+ export { PAGE_BUILDER_KEY, EDITOR_KEY, NODE_TREE_KEY, DRAG_DROP_KEY, } from './keys';
@@ -0,0 +1,13 @@
1
+ import type { InjectionKey } from 'vue';
2
+ import type { UsePageBuilderReturn } from '@/composables/use-page-builder';
3
+ import type { useEditor } from '@/composables/use-editor';
4
+ import type { useNodeTree } from '@/composables/use-node-tree';
5
+ import type { useDragDrop } from '@/composables/use-drag-drop';
6
+ /**
7
+ * Typed injection keys for provide/inject across the editor component tree.
8
+ * Use these instead of raw strings for type-safe inject() calls.
9
+ */
10
+ export declare const PAGE_BUILDER_KEY: InjectionKey<UsePageBuilderReturn>;
11
+ export declare const EDITOR_KEY: InjectionKey<ReturnType<typeof useEditor>>;
12
+ export declare const NODE_TREE_KEY: InjectionKey<ReturnType<typeof useNodeTree>>;
13
+ export declare const DRAG_DROP_KEY: InjectionKey<ReturnType<typeof useDragDrop>>;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Core tree node representing a single component instance in the page tree.
3
+ *
4
+ * The page structure is a recursive tree of INode objects.
5
+ * Each node maps to a registered component and can contain children
6
+ * distributed across named slots.
7
+ */
8
+ export interface INode {
9
+ /** Unique identifier within the tree. Used for selection, drag-drop, and reconciliation. */
10
+ id: number;
11
+ /** Component name — must match a key in the component registry. */
12
+ name: string;
13
+ /** Target slot name in the parent component. `null` for the root node. */
14
+ slot: string | null;
15
+ /** Props passed to the component instance. Supports template variables via `{{ VAR }}`. */
16
+ props: Record<string, unknown>;
17
+ /** Child nodes, rendered into the component's slots. */
18
+ children: INode[];
19
+ /** If true, this node cannot be edited, moved, or deleted in edit mode. */
20
+ readonly?: boolean;
21
+ }
22
+ /**
23
+ * Full page data structure returned by the backend.
24
+ * This is the single JSON payload the frontend receives.
25
+ */
26
+ export interface IPageData {
27
+ /** Page metadata */
28
+ meta: IPageMeta;
29
+ /** The root node of the page content tree. */
30
+ content: INode;
31
+ /** The root node of the page layout tree. Layout wraps content. */
32
+ layout: INode;
33
+ /** Maximum node ID used in the tree. Incremented when adding new nodes. */
34
+ maxId: number;
35
+ /** Variables injected into component props via `{{ VAR }}` syntax. */
36
+ variables: Record<string, string>;
37
+ }
38
+ export interface IPageMeta {
39
+ id: string;
40
+ name: string;
41
+ url: string;
42
+ status: 'draft' | 'published' | 'archived';
43
+ updatedAt?: string;
44
+ createdAt?: string;
45
+ }
46
+ /**
47
+ * Serialized page data sent back to the backend on save.
48
+ * Only the mutable parts are included.
49
+ */
50
+ export interface IPageSavePayload {
51
+ content: INode;
52
+ layout: INode;
53
+ maxId: number;
54
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@improba/page-builder",
3
- "version": "0.1.3",
3
+ "version": "0.2.1",
4
4
  "description": "A Vue 3 page builder component library — WYSIWYG editor with read/edit modes, custom component registry, and SSR support.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -12,6 +12,11 @@
12
12
  "import": "./dist/index.js",
13
13
  "require": "./dist/index.cjs"
14
14
  },
15
+ "./core": {
16
+ "types": "./dist/types/core/index.d.ts",
17
+ "import": "./dist/core.js",
18
+ "require": "./dist/core.cjs"
19
+ },
15
20
  "./style.css": "./dist/style.css"
16
21
  },
17
22
  "files": [
@@ -25,7 +30,7 @@
25
30
  "scripts": {
26
31
  "dev": "vite serve playground",
27
32
  "build": "vite build",
28
- "build:types": "vue-tsc --declaration --emitDeclarationOnly --outDir dist/types",
33
+ "build:types": "vue-tsc -p tsconfig.build.json",
29
34
  "docs:api": "typedoc --options typedoc.json",
30
35
  "docs:api:clean": "rm -rf docs/api && npm run docs:api",
31
36
  "docs:screenshots": "playwright test tests/e2e/capture-screenshots.spec.ts",
@@ -48,6 +53,9 @@
48
53
  "release": "./scripts/release.sh",
49
54
  "prepublishOnly": "npm run release:prepare"
50
55
  },
56
+ "dependencies": {
57
+ "lucide": "^1.8.0"
58
+ },
51
59
  "peerDependencies": {
52
60
  "vue": "^3.4.0"
53
61
  },
package/dist/index.css DELETED
@@ -1 +0,0 @@
1
- .ipb-error-boundary[data-v-5b429c0c]{width:100%;padding:.75rem;border:1px solid #f3d1d1;border-radius:.375rem;background-color:#fff5f5;color:#7a1f1f}.ipb-error-boundary__message[data-v-5b429c0c]{margin:0;font-size:.9rem;line-height:1.4}.ipb-error-boundary__details[data-v-5b429c0c]{margin:.5rem 0 0;font-size:.75rem;line-height:1.4;white-space:pre-wrap;word-break:break-word;color:#5f1b1b}.ipb-node-renderer__invalid[data-v-40a28160]{width:100%;padding:.75rem;border:1px solid #f3d1d1;border-radius:.375rem;background-color:#fff5f5;color:#7a1f1f;font-size:.825rem;line-height:1.35}.ipb-page-reader[data-v-d9bf9b29]{width:100%;min-height:100%}.ipb-page-reader__invalid[data-v-d9bf9b29]{padding:.75rem;border:1px solid #f3d1d1;border-radius:.375rem;background-color:#fff5f5;color:#7a1f1f;font-size:.875rem}.ipb-toolbar[data-v-7086d88b]{display:flex;align-items:center;justify-content:space-between;height:48px;padding:0 16px;background:var(--ipb-toolbar-bg, #fff);border-bottom:1px solid var(--ipb-border-color, #e0e0e0);flex-shrink:0}.ipb-toolbar__left[data-v-7086d88b],.ipb-toolbar__center[data-v-7086d88b],.ipb-toolbar__right[data-v-7086d88b]{display:flex;align-items:center;gap:8px}.ipb-toolbar__btn[data-v-7086d88b]{padding:6px 12px;border:1px solid var(--ipb-border-color, #e0e0e0);border-radius:4px;background:transparent;cursor:pointer;font-size:13px;transition:background .15s}.ipb-toolbar__btn[data-v-7086d88b]:focus-visible,.ipb-toolbar__size-input[data-v-7086d88b]:focus-visible{outline:2px solid var(--ipb-focus-color, #2563eb);outline-offset:1px}.ipb-toolbar__custom-size[data-v-7086d88b]{display:inline-flex;align-items:center;gap:8px;margin-left:8px;padding-left:8px;border-left:1px solid var(--ipb-border-color, #e0e0e0)}.ipb-toolbar__size-control[data-v-7086d88b]{display:inline-flex;align-items:center;gap:4px;font-size:12px;color:var(--ipb-text-secondary, #4b5563)}.ipb-toolbar__size-input[data-v-7086d88b]{width:72px;padding:4px 6px;border:1px solid var(--ipb-border-color, #e0e0e0);border-radius:4px;font-size:12px}.ipb-toolbar__viewport-size[data-v-7086d88b]{margin-left:8px;color:var(--ipb-text-secondary, #6b7280);font-size:12px;font-variant-numeric:tabular-nums}.ipb-toolbar__btn[data-v-7086d88b]:hover:not(:disabled){background:var(--ipb-hover-bg, #f5f5f5)}.ipb-toolbar__btn[data-v-7086d88b]:disabled{opacity:.4;cursor:not-allowed}.ipb-toolbar__btn--active[data-v-7086d88b]{background:var(--ipb-active-bg, #e8f0fe);border-color:var(--ipb-primary-color, #1a73e8);color:var(--ipb-primary-color, #1a73e8)}.ipb-toolbar__btn--primary[data-v-7086d88b]{background:var(--ipb-primary-color, #1a73e8);color:#fff;border-color:var(--ipb-primary-color, #1a73e8)}.ipb-toolbar__btn--primary[data-v-7086d88b]:hover{background:var(--ipb-primary-hover, #1557b0)}.ipb-toolbar__dirty-indicator[data-v-7086d88b]{color:var(--ipb-warning-color, #f59e0b);font-size:10px}.ipb-tree-panel[data-v-f50f228d]{min-width:0}.ipb-tree-panel__list[data-v-f50f228d]{display:flex;flex-direction:column;gap:2px}.ipb-tree-panel__row-wrapper[data-v-f50f228d]{display:flex;flex-direction:column;gap:0}.ipb-tree-panel__drop-line[data-v-f50f228d]{min-height:8px;margin:0 8px;padding:3px 0;flex-shrink:0;display:flex;align-items:center;cursor:default}.ipb-tree-panel__drop-line[data-v-f50f228d]:before{content:"";display:block;width:100%;height:2px;border-radius:1px;background:var(--ipb-primary-color, #1d4ed8)}.ipb-tree-panel__item[data-v-f50f228d]{display:flex;align-items:center;gap:6px;width:100%;min-width:0;padding:6px 8px;border:0;border-radius:4px;background:transparent;color:inherit;text-align:left;cursor:pointer;font-size:12px;line-height:1.3}.ipb-tree-panel__item[data-v-f50f228d]:hover{background:var(--ipb-hover-bg, #f5f5f5)}.ipb-tree-panel__item[data-v-f50f228d]:focus-visible{outline:2px solid var(--ipb-focus-color, #2563eb);outline-offset:-1px}.ipb-tree-panel__item--selected[data-v-f50f228d]{background:var(--ipb-primary-soft, rgba(37, 99, 235, .14));color:var(--ipb-primary-color, #1d4ed8)}.ipb-tree-panel__item--readonly[data-v-f50f228d]{opacity:.8}.ipb-tree-panel__item--drop-target[data-v-f50f228d]{background:var(--ipb-primary-soft, rgba(37, 99, 235, .2));outline:2px dashed var(--ipb-primary-color, #1d4ed8);outline-offset:2px}.ipb-tree-panel__item--draggable[data-v-f50f228d]{cursor:grab}.ipb-tree-panel__item--draggable[data-v-f50f228d]:active{cursor:grabbing}.ipb-tree-panel__name[data-v-f50f228d]{font-weight:500;white-space:nowrap}.ipb-tree-panel__id[data-v-f50f228d]{color:var(--ipb-text-muted, #6b7280);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:11px;white-space:nowrap}.ipb-tree-panel__readonly[data-v-f50f228d]{margin-left:auto;font-size:10px;text-transform:uppercase;letter-spacing:.03em;color:var(--ipb-text-muted, #6b7280);border:1px solid var(--ipb-border-color, #d1d5db);border-radius:3px;padding:1px 4px}.ipb-left-drawer[data-v-2bb04ccb]{width:48px;background:var(--ipb-drawer-bg, #fff);border-right:1px solid var(--ipb-border-color, #e0e0e0);transition:width .2s ease;overflow:hidden;flex-shrink:0;display:flex;flex-direction:column}.ipb-left-drawer--open[data-v-2bb04ccb]{width:260px}.ipb-left-drawer__header[data-v-2bb04ccb]{display:flex;align-items:center;justify-content:space-between;gap:8px;min-width:0;padding:12px;border-bottom:1px solid var(--ipb-border-color, #e0e0e0)}.ipb-left-drawer__title[data-v-2bb04ccb]{font-weight:600;font-size:14px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ipb-left-drawer__toggle[data-v-2bb04ccb]{flex-shrink:0;background:none;border:none;cursor:pointer;font-size:12px;padding:4px}.ipb-left-drawer__toggle[data-v-2bb04ccb]:focus-visible,.ipb-left-drawer__section-toggle[data-v-2bb04ccb]:focus-visible{outline:2px solid var(--ipb-focus-color, #2563eb);outline-offset:1px;border-radius:4px}.ipb-left-drawer__content[data-v-2bb04ccb]{flex:1;overflow-y:auto;padding:12px;display:flex;flex-direction:column;gap:12px}.ipb-left-drawer__section[data-v-2bb04ccb]{min-width:0}.ipb-left-drawer__search[data-v-2bb04ccb]{margin-top:8px;margin-bottom:8px}.ipb-left-drawer__search-input[data-v-2bb04ccb]{width:100%;padding:6px 8px;border:1px solid var(--ipb-border-color, #e0e0e0);border-radius:4px;background:var(--ipb-input-bg, #fff);color:var(--ipb-text-color, #111827);font-size:12px}.ipb-left-drawer__search-input[data-v-2bb04ccb]:focus-visible{outline:2px solid var(--ipb-focus-color, #2563eb);outline-offset:1px}.ipb-left-drawer__section--tree[data-v-2bb04ccb]{padding-top:8px;border-top:1px solid var(--ipb-border-color, #e0e0e0)}.ipb-left-drawer__section-header[data-v-2bb04ccb]{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.ipb-left-drawer__section-title[data-v-2bb04ccb]{margin:0;font-size:12px;text-transform:uppercase;letter-spacing:.05em;color:var(--ipb-text-muted, #6b7280)}.ipb-left-drawer__section-toggle[data-v-2bb04ccb]{background:none;border:none;cursor:pointer;padding:2px 4px;font-size:14px;line-height:1}.ipb-left-drawer__category-title[data-v-2bb04ccb]{font-size:11px;font-weight:600;margin:10px 0 8px}.ipb-left-drawer__component-list[data-v-2bb04ccb]{display:flex;flex-direction:column;gap:4px}.ipb-left-drawer__component-item[data-v-2bb04ccb]{display:flex;align-items:center;gap:8px;padding:8px;border-radius:4px;cursor:grab;transition:background .15s;font-size:13px;width:100%;border:0;background:transparent;color:inherit;text-align:left}.ipb-left-drawer__component-item[data-v-2bb04ccb]:hover{background:var(--ipb-hover-bg, #f5f5f5)}.ipb-left-drawer__component-item[data-v-2bb04ccb]:focus-visible{outline:2px solid var(--ipb-focus-color, #2563eb);outline-offset:-1px}.ipb-left-drawer__component-icon[data-v-2bb04ccb]{font-size:16px;width:24px;text-align:center}.ipb-left-drawer__empty[data-v-2bb04ccb]{margin:8px 0 0;font-size:12px;color:var(--ipb-text-muted, #6b7280)}.ipb-prop-editor[data-v-f48156f6]{width:100%;padding:6px 8px;border:1px solid var(--ipb-border-color, #e0e0e0);border-radius:4px;font-size:13px;box-sizing:border-box}.ipb-prop-editor[data-v-f48156f6]:focus-visible{outline:2px solid var(--ipb-focus-color, #2563eb);outline-offset:1px}.ipb-prop-editor[data-v-11786ace]{width:100%;padding:6px 8px;border:1px solid var(--ipb-border-color, #e0e0e0);border-radius:4px;font-size:13px;box-sizing:border-box}.ipb-prop-editor[data-v-11786ace]:focus-visible{outline:2px solid var(--ipb-focus-color, #2563eb);outline-offset:1px}.ipb-prop-editor--boolean[data-v-cfba94a9]{display:inline-flex;align-items:center;gap:8px;font-size:13px;-webkit-user-select:none;user-select:none}.ipb-prop-editor--boolean input[data-v-cfba94a9]:focus-visible{outline:2px solid var(--ipb-focus-color, #2563eb);outline-offset:2px}.ipb-prop-editor[data-v-8c477cbd]{width:100%;padding:6px 8px;border:1px solid var(--ipb-border-color, #e0e0e0);border-radius:4px;font-size:13px;box-sizing:border-box;background:var(--ipb-drawer-bg, #fff)}.ipb-prop-editor[data-v-8c477cbd]:focus-visible{outline:2px solid var(--ipb-focus-color, #2563eb);outline-offset:1px}.ipb-prop-editor--color[data-v-b0f904c5]{display:flex;align-items:center;gap:8px}.ipb-prop-editor__picker[data-v-b0f904c5]{width:40px;height:32px;padding:2px;border:1px solid var(--ipb-border-color, #e0e0e0);border-radius:4px;background:var(--ipb-drawer-bg, #fff)}.ipb-prop-editor__text[data-v-b0f904c5]{flex:1;min-width:0;padding:6px 8px;border:1px solid var(--ipb-border-color, #e0e0e0);border-radius:4px;font-size:13px;box-sizing:border-box}.ipb-prop-editor__picker[data-v-b0f904c5]:focus-visible,.ipb-prop-editor__text[data-v-b0f904c5]:focus-visible{outline:2px solid var(--ipb-focus-color, #2563eb);outline-offset:1px}.ipb-richtext-editor[data-v-d9cdfb46]{border:1px solid var(--ipb-border-color, #e0e0e0);border-radius:4px;overflow:hidden}.ipb-richtext-editor__toolbar[data-v-d9cdfb46]{display:flex;flex-wrap:wrap;gap:4px;padding:6px;border-bottom:1px solid var(--ipb-border-color, #e0e0e0);background:var(--ipb-surface-muted, #f8fafc)}.ipb-richtext-editor__toolbar button[data-v-d9cdfb46]{min-width:30px;height:28px;padding:0 8px;border:1px solid var(--ipb-border-color, #d1d5db);border-radius:4px;background:var(--ipb-drawer-bg, #fff);cursor:pointer;font-size:12px}.ipb-richtext-editor__toolbar button[data-v-d9cdfb46]:hover{background:var(--ipb-surface-hover, #f3f4f6)}.ipb-richtext-editor__toolbar button[data-v-d9cdfb46]:focus-visible,.ipb-richtext-editor__content[data-v-d9cdfb46]:focus-visible{outline:2px solid var(--ipb-focus-color, #2563eb);outline-offset:1px}.ipb-richtext-editor__content[data-v-d9cdfb46]{min-height:120px;padding:8px;font-size:13px;line-height:1.5;outline:none}.ipb-richtext-editor__content[data-v-d9cdfb46]:empty:before{content:attr(data-placeholder);color:var(--ipb-text-muted, #9ca3af);pointer-events:none}.ipb-media-picker[data-v-743fe73e]{display:flex;flex-direction:column;gap:8px}.ipb-media-picker__controls[data-v-743fe73e]{display:grid;grid-template-columns:minmax(0,1fr) auto auto;gap:8px;align-items:center}.ipb-media-picker__input[data-v-743fe73e]{min-width:0;width:100%;padding:6px 8px;border:1px solid var(--ipb-border-color, #e0e0e0);border-radius:4px;font-size:13px;box-sizing:border-box}.ipb-media-picker__btn[data-v-743fe73e]{padding:6px 8px;border:1px solid var(--ipb-border-color, #e0e0e0);border-radius:4px;background:var(--ipb-drawer-bg, #fff);cursor:pointer;font-size:12px}.ipb-media-picker__input[data-v-743fe73e]:focus-visible,.ipb-media-picker__btn[data-v-743fe73e]:focus-visible{outline:2px solid var(--ipb-focus-color, #2563eb);outline-offset:1px}.ipb-media-picker__btn[data-v-743fe73e]:disabled{opacity:.5;cursor:not-allowed}.ipb-media-picker__preview[data-v-743fe73e]{border:1px dashed var(--ipb-border-color, #e0e0e0);border-radius:4px;padding:6px;min-height:88px;display:flex;align-items:center;justify-content:center;background:var(--ipb-bg-subtle, #fafafa)}.ipb-media-picker__image[data-v-743fe73e]{max-width:100%;max-height:140px;object-fit:contain;display:block}.ipb-media-picker__empty[data-v-743fe73e]{margin:0;font-size:12px;color:var(--ipb-text-muted, #6b7280);text-align:center}.ipb-right-drawer[data-v-a25cd890]{width:48px;background:var(--ipb-drawer-bg, #fff);border-left:1px solid var(--ipb-border-color, #e0e0e0);transition:width .2s ease;overflow:hidden;flex-shrink:0;display:flex;flex-direction:column}.ipb-right-drawer--open[data-v-a25cd890]{width:320px}.ipb-right-drawer__header[data-v-a25cd890]{display:flex;align-items:center;justify-content:space-between;gap:8px;min-width:0;padding:12px;border-bottom:1px solid var(--ipb-border-color, #e0e0e0)}.ipb-right-drawer__title[data-v-a25cd890]{font-weight:600;font-size:14px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ipb-right-drawer__toggle[data-v-a25cd890]{flex-shrink:0;background:none;border:none;cursor:pointer;font-size:12px;padding:4px}.ipb-right-drawer__toggle[data-v-a25cd890]:focus-visible,.ipb-right-drawer__btn[data-v-a25cd890]:focus-visible{outline:2px solid var(--ipb-focus-color, #2563eb);outline-offset:1px}.ipb-right-drawer__content[data-v-a25cd890]{flex:1;overflow-y:auto;padding:16px}.ipb-right-drawer__section[data-v-a25cd890]{margin-bottom:20px}.ipb-right-drawer__section-title[data-v-a25cd890]{font-size:16px;font-weight:600;margin:0 0 4px}.ipb-right-drawer__section-subtitle[data-v-a25cd890]{font-size:12px;text-transform:uppercase;letter-spacing:.05em;color:var(--ipb-text-muted, #6b7280);margin:0 0 12px}.ipb-right-drawer__description[data-v-a25cd890]{font-size:13px;color:var(--ipb-text-muted, #6b7280);margin:0}.ipb-right-drawer__prop[data-v-a25cd890]{margin-bottom:12px}.ipb-right-drawer__prop-label[data-v-a25cd890]{display:block;font-size:12px;font-weight:500;margin-bottom:4px}.ipb-right-drawer__prop-editor[data-v-a25cd890]{width:100%}.ipb-right-drawer__actions[data-v-a25cd890]{display:flex;gap:8px;padding-top:16px;border-top:1px solid var(--ipb-border-color, #e0e0e0)}.ipb-right-drawer__btn[data-v-a25cd890]{flex:1;padding:8px;border:1px solid var(--ipb-border-color, #e0e0e0);border-radius:4px;background:transparent;cursor:pointer;font-size:13px}.ipb-right-drawer__btn--danger[data-v-a25cd890]{color:var(--ipb-danger-color, #dc2626);border-color:var(--ipb-danger-color, #dc2626)}.ipb-right-drawer__btn--danger[data-v-a25cd890]:disabled{opacity:.4;cursor:not-allowed}.ipb-right-drawer__empty[data-v-a25cd890]{padding:24px 16px;text-align:center;color:var(--ipb-text-muted, #6b7280);font-size:13px}.ipb-node-context-menu[data-v-7a59845f]{position:absolute;z-index:30;display:flex;flex-direction:column;min-width:148px;padding:6px;border:1px solid var(--ipb-border-color, #d1d5db);border-radius:8px;background:#fff;box-shadow:0 8px 24px #0f172a29}.ipb-node-context-menu__item[data-v-7a59845f]{border:0;border-radius:6px;background:transparent;color:var(--ipb-text-color, #111827);font-size:13px;line-height:1.4;text-align:left;padding:7px 10px;cursor:pointer}.ipb-node-context-menu__item[data-v-7a59845f]:hover:not(:disabled){background:#3b82f61a}.ipb-node-context-menu__item[data-v-7a59845f]:focus-visible{outline:2px solid var(--ipb-focus-color, #2563eb);outline-offset:-1px}.ipb-node-context-menu__item--danger[data-v-7a59845f]{color:var(--ipb-danger-color, #dc2626)}.ipb-node-context-menu__item[data-v-7a59845f]:disabled{opacity:.45;cursor:not-allowed}.ipb-canvas[data-v-3d599a49]{flex:1;display:flex;align-items:flex-start;justify-content:center;padding:24px;overflow:auto;background:var(--ipb-canvas-bg, #e5e7eb)}.ipb-canvas__viewport[data-v-3d599a49]{position:relative;flex:0 0 auto;background:#fff;box-shadow:0 1px 3px #0000001f,0 1px 2px #0000000f;border-radius:4px;overflow:hidden;transition:width .2s ease,height .2s ease}.ipb-iframe-canvas__stage[data-v-3d599a49]{position:relative;width:100%;height:100%;background:#fff}.ipb-iframe-canvas__frame[data-v-3d599a49]{width:100%;height:100%;border:0;display:block;background:#fff}.ipb-iframe-canvas__fallback[data-v-3d599a49]{position:relative;width:100%;height:100%;background:#fff}.ipb-canvas__overlay[data-v-3d599a49]{position:absolute;pointer-events:none;border-radius:4px;box-sizing:border-box}.ipb-canvas__overlay--hovered[data-v-3d599a49]{border:1px dashed rgba(59,130,246,.75);background:#3b82f614;z-index:10}.ipb-canvas__overlay--drop[data-v-3d599a49]{border:2px dashed rgba(16,185,129,.9);background:#10b98124;z-index:12}.ipb-canvas__overlay--selected[data-v-3d599a49]{border:2px solid rgba(37,99,235,.95);background:#2563eb24;box-shadow:0 0 0 1px #ffffffd9 inset;z-index:11}.ipb-page-editor[data-v-2a96c483]{display:flex;flex-direction:column;height:100vh;width:100vw;overflow:hidden;background:var(--ipb-editor-bg, #f0f2f5)}.ipb-page-editor__body[data-v-2a96c483]{display:flex;flex:1;overflow:hidden}.ipb-page-builder__warning[data-v-84b8d079]{margin-bottom:.5rem;padding:.625rem .75rem;border:1px solid #f8d7a8;border-radius:.375rem;background-color:#fff8ec;color:#7d4a00;font-size:.875rem}