@openpen/module-api 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/dist/_chunks/{AppButtonDropdown.vue_vue_type_style_index_0_lang-DBVFUpxc.js → AppButtonDropdown.vue_vue_type_style_index_0_lang-B9dHU-LE.js} +3 -2
- package/dist/_chunks/AppButtonDropdown.vue_vue_type_style_index_0_lang-B9dHU-LE.js.map +1 -0
- package/dist/host/index.d.ts +1 -1
- package/dist/host/index.js +2 -0
- package/dist/host/index.js.map +1 -1
- package/dist/host/stroke-store.d.ts +1 -0
- package/dist/host/types.d.ts +12 -0
- package/dist/uikit/components/AppToggle.vue.d.ts +2 -0
- package/dist/uikit/index.js +25 -11
- package/dist/uikit/index.js.map +1 -1
- package/dist/uikit/internal.js +1 -1
- package/dist/uikit/module-api.css +23 -23
- package/package.json +3 -3
- package/dist/_chunks/AppButtonDropdown.vue_vue_type_style_index_0_lang-DBVFUpxc.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.3.0](https://github.com/openpen-platform/openpen/compare/module-api@1.2.0...module-api@1.3.0) (2026-06-11)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **module-api:** add commitStroke host API for async-placement tools ([c8b1807](https://github.com/openpen-platform/openpen/commit/c8b18072ffff8ecaa14d70e237c069b55b218035))
|
|
9
|
+
|
|
3
10
|
## [1.2.0](https://github.com/openpen-platform/openpen/compare/v1.1.1...v1.2.0) (2026-05-24)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -127,6 +127,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
127
127
|
createVNode(unref(DialogContent), {
|
|
128
128
|
ref: "content",
|
|
129
129
|
class: normalizeClass(["openpen-modal-content openpen-interactive", { "openpen-modal-danger": __props.danger }]),
|
|
130
|
+
"data-testid": __props.danger ? "modal-dialog-danger" : "modal-dialog",
|
|
130
131
|
onInteractOutside: _cache[0] || (_cache[0] = (e) => __props.persistent && e.preventDefault()),
|
|
131
132
|
onEscapeKeyDown: _cache[1] || (_cache[1] = (e) => __props.persistent && e.preventDefault())
|
|
132
133
|
}, {
|
|
@@ -168,7 +169,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
168
169
|
})
|
|
169
170
|
]),
|
|
170
171
|
_: 3
|
|
171
|
-
}, 8, ["class"])
|
|
172
|
+
}, 8, ["class", "data-testid"])
|
|
172
173
|
]),
|
|
173
174
|
_: 3
|
|
174
175
|
})
|
|
@@ -242,4 +243,4 @@ export {
|
|
|
242
243
|
_resolveActive as b,
|
|
243
244
|
useDialog as u
|
|
244
245
|
};
|
|
245
|
-
//# sourceMappingURL=AppButtonDropdown.vue_vue_type_style_index_0_lang-
|
|
246
|
+
//# sourceMappingURL=AppButtonDropdown.vue_vue_type_style_index_0_lang-B9dHU-LE.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AppButtonDropdown.vue_vue_type_style_index_0_lang-B9dHU-LE.js","sources":["../../src/uikit/components/AppDialog.vue","../../src/uikit/composables/useDialog.ts","../../src/uikit/composables/dialog-internal-keys.ts"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * AppDialog — High-level wrapper (Layer 1).\n *\n * Plugin authors use this component without needing to know about inject\n * keys, Reka UI, modal manager mutual exclusion, or the animating guard.\n *\n * Public API is stable: switching the underlying headless library in the\n * future does not break the plugin author's code.\n *\n * Implementation notes:\n * - `open` prop is the single source of truth (controlled mode). The\n * component emits `update:open` for two-way binding. Internally it also\n * registers with the modal manager for mutual exclusion (same pattern as\n * AppPopover): when `open` turns true we call manager.open(); when\n * another panel causes the manager to close us we emit update:open(false).\n * - `persistent`: when true, ESC and backdrop clicks are suppressed via\n * @interact-outside.prevent and @escape-key-down.prevent on DialogContent.\n * - `danger`: adds class `openpen-modal-danger` on the content element as a\n * styling hook for destructive-action dialogs (no default style yet).\n * - animating guard: auto-close during ControlBar scaleY animation.\n * - usePassthroughGuard: registered on the dialog content element so the\n * transparent Electron window remains clickable when dialog is open.\n * - Styles are unscoped because DialogContent is teleported outside the\n * component's DOM subtree (same reason as AppPopover).\n *\n * CSS class names (.openpen-modal-*) are intentionally kept as the carried-over\n * internal styling tokens — renaming carries no DX benefit and risks accidental\n * style breakage in any code path still referencing them.\n */\nimport { computed, inject, onBeforeUnmount, ref, useTemplateRef, watch } from 'vue'\nimport {\n DialogRoot,\n DialogTrigger,\n DialogPortal,\n DialogContent,\n DialogOverlay,\n DialogClose,\n DialogTitle,\n DialogDescription,\n} from '../primitives'\n// Inject keys MUST come from the package barrel — see AppPopover.vue\n// header comment for why relative imports break Symbol identity in prod.\nimport {\n MODAL_MANAGER_KEY,\n CONTROL_BAR_ANIMATING_KEY,\n HOST_DIALOG_OPEN_COUNT_KEY,\n} from '@openpen/module-api'\nimport { usePassthroughGuard } from '../../host/passthrough'\n\n// ── OpenPen-owned types (MUST NOT reference reka-ui types) ───────────────────\n\nexport interface AppDialogProps {\n /** Globally unique modal id; used for modal-stack mutual exclusion. */\n modalId: string\n /** Dialog title displayed in the dialog header. */\n title: string\n /** Controlled open state. The parent must handle update:open to toggle. */\n open: boolean\n /**\n * When true, ESC key and backdrop clicks do not close the dialog.\n * Use for confirmations that require an explicit button press.\n */\n persistent?: boolean\n /**\n * When true, adds the `openpen-modal-danger` CSS class on the content\n * element — a hook for styling destructive-action dialogs.\n */\n danger?: boolean\n}\n\nexport interface AppDialogSlots {\n /**\n * Trigger slot; scope exposes active state + toggle / open / close actions.\n * MUST NOT write @click=\"toggle\" on the trigger — DialogTrigger's as-child\n * handles activation automatically; toggle/open/close are escape hatches for\n * programmatic control only.\n */\n trigger: (scope: {\n active: boolean\n toggle: () => void\n open: () => void\n close: () => void\n }) => unknown\n /** Dialog body content slot. */\n default: () => unknown\n /** Optional footer slot (action buttons etc.). */\n footer?: () => unknown\n}\n\nconst props = withDefaults(defineProps<AppDialogProps>(), {\n persistent: false,\n danger: false,\n})\n\nconst emit = defineEmits<{\n 'update:open': [value: boolean]\n}>()\n\ndefineSlots<AppDialogSlots>()\n\n// ── 1. Inject host context (plugin author never sees these) ───────────────────\n\nconst modalManager = inject(MODAL_MANAGER_KEY)\nconst animating = inject(CONTROL_BAR_ANIMATING_KEY)\nconst dialogOpenCount = inject(HOST_DIALOG_OPEN_COUNT_KEY, null)\n\n// Track whether THIS instance currently holds a count, so we never double\n// increment / decrement (e.g. on rapid open / close cycles or unmount-while-open).\nlet counted = false\n\nfunction acquireCount() {\n if (counted || !dialogOpenCount) return\n dialogOpenCount.value += 1\n counted = true\n}\n\nfunction releaseCount() {\n if (!counted || !dialogOpenCount) return\n dialogOpenCount.value -= 1\n counted = false\n}\n\nonBeforeUnmount(releaseCount)\n\n// ── 2. Modal manager mutual exclusion + controlled open prop ─────────────────\n//\n// The `open` prop is the canonical state. We watch it and push changes into\n// the modal manager. When the manager closes us (e.g. another panel opens),\n// we detect that via the computed isManagerOpen and emit update:open(false).\n\nfunction openDialog() {\n modalManager?.open(props.modalId)\n emit('update:open', true)\n}\n\nfunction closeDialog() {\n modalManager?.close(props.modalId)\n emit('update:open', false)\n}\n\nfunction toggleDialog() {\n if (props.open) {\n closeDialog()\n } else {\n openDialog()\n }\n}\n\n// When the prop turns true: register with the manager + bump host dialog count.\n// When it turns false: unregister + release count. Keeps everything in sync.\nwatch(\n () => props.open,\n (newOpen) => {\n if (newOpen) {\n modalManager?.open(props.modalId)\n acquireCount()\n } else {\n modalManager?.close(props.modalId)\n releaseCount()\n }\n },\n { immediate: true }\n)\n\n// When the manager closes us (another panel opened), emit update:open(false)\n// so the parent's v-model updates.\nconst isManagerOpen = computed(() => modalManager?.isOpen(props.modalId) ?? props.open)\n\nwatch(isManagerOpen, (managerOpen) => {\n if (!managerOpen && props.open) {\n emit('update:open', false)\n }\n})\n\n// Effective open state passed to Reka UI's DialogRoot.\nconst isOpen = computed(() => props.open && isManagerOpen.value)\n\n// Sync Reka UI's internal close events (ESC, overlay click) back to caller\n// unless `persistent` suppresses them.\nfunction onUpdateOpen(v: boolean) {\n if (!v && props.persistent) {\n // Persistent mode: ignore Reka UI close attempts; stay open.\n return\n }\n if (v) {\n openDialog()\n } else {\n closeDialog()\n }\n}\n\n// ── 3. ControlBar animation guard — auto-close during transitions ─────────────\n\nwatch(\n () => animating?.value,\n (isAnimating) => {\n if (isAnimating && props.open) {\n closeDialog()\n }\n }\n)\n\n// ── 4. Passthrough guard integration ─────────────────────────────────────────\n//\n// DialogContent is a Vue component; normalize to HTMLElement via watcher.\n\nconst contentInstance = useTemplateRef<HTMLElement | { $el?: HTMLElement } | null>('content')\nconst contentEl = ref<HTMLElement | null>(null)\n\nwatch(contentInstance, (instance) => {\n if (!instance) {\n contentEl.value = null\n } else if (instance instanceof HTMLElement) {\n contentEl.value = instance\n } else if (typeof instance === 'object' && '$el' in instance && instance.$el instanceof HTMLElement) {\n contentEl.value = instance.$el\n } else {\n contentEl.value = null\n }\n})\n\nusePassthroughGuard(contentEl)\n</script>\n\n<template>\n <DialogRoot :open=\"isOpen\" @update:open=\"onUpdateOpen\">\n <DialogTrigger as-child>\n <slot\n name=\"trigger\"\n :active=\"isOpen\"\n :toggle=\"toggleDialog\"\n :open=\"openDialog\"\n :close=\"closeDialog\"\n />\n </DialogTrigger>\n <DialogPortal>\n <DialogOverlay class=\"openpen-modal-overlay\" />\n <DialogContent\n ref=\"content\"\n class=\"openpen-modal-content openpen-interactive\"\n :class=\"{ 'openpen-modal-danger': danger }\"\n :data-testid=\"danger ? 'modal-dialog-danger' : 'modal-dialog'\"\n @interact-outside=\"(e: Event) => persistent && e.preventDefault()\"\n @escape-key-down=\"(e: KeyboardEvent) => persistent && e.preventDefault()\"\n >\n <DialogTitle class=\"openpen-modal-title\">{{ title }}</DialogTitle>\n <DialogDescription class=\"openpen-modal-description sr-only\" />\n <div class=\"openpen-modal-body\">\n <slot />\n </div>\n <div v-if=\"$slots.footer\" class=\"openpen-modal-footer\">\n <slot name=\"footer\" />\n </div>\n <DialogClose class=\"openpen-modal-close\" aria-label=\"Close\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M1 1L13 13M13 1L1 13\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"/>\n </svg>\n </DialogClose>\n </DialogContent>\n </DialogPortal>\n </DialogRoot>\n</template>\n\n<!-- Styles MUST be unscoped — Vue scoped CSS does not follow Teleport targets,\n so .openpen-modal-* classes would not receive the scoped data-v-* attribute\n when rendered inside the portal. -->\n<style>\n/* ── Overlay (transparent click absorber, NOT a dim backdrop) ─────────────────\n OpenPen's overlay window is transparent + frameless. A traditional dim/blur\n backdrop renders as a small dark patch only over the visible chrome — visually\n jarring. The overlay is therefore kept fully transparent here; its only job\n is to absorb outside clicks (so Reka can dispatch interact-outside → close\n when not persistent) and to protect the canvas from stray clicks while the\n dialog is open. Host containers (e.g. ControlBar) render their own \"locked\"\n visual state via HOST_DIALOG_OPEN_COUNT_KEY. */\n.openpen-modal-overlay {\n position: fixed;\n inset: 0;\n background: transparent;\n z-index: 200;\n pointer-events: auto;\n}\n\n/* ── Dialog content panel ────────────────────────────────────────────────── */\n.openpen-modal-content {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n z-index: 201;\n min-width: 320px;\n max-width: min(480px, calc(100vw - 48px));\n max-height: calc(100vh - 96px);\n overflow-y: auto;\n\n background: var(--openpen-color-surface-popup);\n border: 1px solid var(--openpen-color-border-hi);\n border-radius: var(--openpen-radius-lg);\n box-shadow: var(--openpen-shadow);\n backdrop-filter: var(--openpen-blur);\n -webkit-backdrop-filter: var(--openpen-blur);\n\n color: var(--openpen-color-text-primary);\n font-size: 13px;\n padding: var(--openpen-space-lg);\n pointer-events: auto;\n}\n\n/* ── Title ───────────────────────────────────────────────────────────────── */\n.openpen-modal-title {\n font-size: 15px;\n font-weight: 600;\n color: var(--openpen-color-text-primary);\n margin: 0 0 var(--openpen-space-md) 0;\n padding-right: 28px; /* leave room for close button */\n line-height: 1.4;\n}\n\n/* ── Visually hidden description (a11y) ──────────────────────────────────── */\n.openpen-modal-description.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border-width: 0;\n}\n\n/* ── Body ────────────────────────────────────────────────────────────────── */\n.openpen-modal-body {\n color: var(--openpen-color-text-primary);\n}\n\n/* ── Footer ──────────────────────────────────────────────────────────────── */\n.openpen-modal-footer {\n display: flex;\n justify-content: flex-end;\n gap: var(--openpen-space-sm);\n margin-top: var(--openpen-space-lg);\n padding-top: var(--openpen-space-md);\n border-top: 1px solid var(--openpen-color-border);\n}\n\n/* ── Close button ────────────────────────────────────────────────────────── */\n.openpen-modal-close {\n position: absolute;\n top: var(--openpen-space-md);\n right: var(--openpen-space-md);\n width: 24px;\n height: 24px;\n display: flex;\n align-items: center;\n justify-content: center;\n border: none;\n background: transparent;\n color: var(--openpen-color-text-dim);\n border-radius: var(--openpen-radius-sm);\n cursor: pointer;\n transition: background var(--openpen-duration-fast), color var(--openpen-duration-fast);\n}\n\n.openpen-modal-close:hover {\n background: var(--openpen-color-control-hover);\n color: var(--openpen-color-text-primary);\n}\n\n/* ── Open / close animations ─────────────────────────────────────────────────\n Reka UI's Presence keeps the element in the DOM until the CSS animation\n ends, so [data-state=\"closed\"] animations DO play on close.\n Pattern matches AppPopover keyframes (200ms bounce enter, 150ms ease leave). */\n\n/* Overlay is always transparent — no fade-in/out animation needed.\n Only the dialog content panel animates. */\n\n.openpen-modal-content[data-state='open'] {\n animation: openpen-modal-in 200ms cubic-bezier(0.34, 1.56, 0.64, 1);\n}\n.openpen-modal-content[data-state='closed'] {\n animation: openpen-modal-out 150ms ease forwards;\n}\n\n@keyframes openpen-modal-in {\n from { opacity: 0; transform: translate(-50%, -50%) scale(0.92); }\n to { opacity: 1; transform: translate(-50%, -50%) scale(1); }\n}\n@keyframes openpen-modal-out {\n from { opacity: 1; transform: translate(-50%, -50%) scale(1); }\n to { opacity: 0; transform: translate(-50%, -50%) scale(0.92); }\n}\n</style>\n","/**\n * useDialog — imperative dialog API for OpenPen modules.\n *\n * Returns a DialogApi with confirm/alert/prompt/custom methods that each\n * return a Promise. At most one imperative dialog is open at a time; extra\n * calls are queued and executed sequentially.\n *\n * The active request is stored in a module-scoped reactive store. DialogHost\n * (mounted by the host at bootstrap) reads this store and renders the dialog.\n *\n * Usage:\n * const dialog = useDialog()\n * const ok = await dialog.confirm({ title: 'Delete?', message: '...' })\n */\nimport type { Component } from 'vue'\nimport { ref } from 'vue'\n\n// ── Public option types ───────────────────────────────────────────────────────\n\ninterface BaseDialogOptions {\n title: string\n persistent?: boolean\n danger?: boolean\n}\n\nexport interface DialogConfirmOptions extends BaseDialogOptions {\n message: string\n okLabel?: string\n cancelLabel?: string\n}\n\nexport interface DialogAlertOptions extends BaseDialogOptions {\n message: string\n okLabel?: string\n}\n\nexport interface DialogPromptOptions extends BaseDialogOptions {\n message: string\n defaultValue?: string\n placeholder?: string\n okLabel?: string\n cancelLabel?: string\n}\n\nexport interface DialogCustomOptions<T = unknown> extends BaseDialogOptions {\n component: Component\n componentProps?: Record<string, unknown>\n /** Phantom type field for TypeScript to infer the resolution payload type. */\n __payloadType?: T\n}\n\n// ── Internal request types (tagged union) ─────────────────────────────────────\n\ntype DialogRequest =\n | { kind: 'confirm'; opts: DialogConfirmOptions; resolve: (v: boolean) => void }\n | { kind: 'alert'; opts: DialogAlertOptions; resolve: () => void }\n | { kind: 'prompt'; opts: DialogPromptOptions; resolve: (v: string | null) => void }\n | { kind: 'custom'; opts: DialogCustomOptions<unknown>; resolve: (v: unknown) => void }\n\n// ── Module-scoped store ───────────────────────────────────────────────────────\n//\n// Single reactive ref holds the currently rendered request. DialogHost watches\n// this and renders the appropriate dialog. When DialogHost resolves a request,\n// it calls resolveActive() which drains the queue.\n\nexport const _activeRequest = ref<DialogRequest | null>(null)\n\nconst _queue: (() => void)[] = []\nlet _processing = false\n\nfunction _enqueue(run: () => void): void {\n if (_processing) {\n _queue.push(run)\n } else {\n _processing = true\n run()\n }\n}\n\n/**\n * Called by DialogHost when the dialog is resolved (ok/cancel/dismiss).\n * Advances the queue if there are pending requests.\n */\nexport function _resolveActive(): void {\n _activeRequest.value = null\n const next = _queue.shift()\n if (next) {\n next()\n } else {\n _processing = false\n }\n}\n\n// ── Public API ────────────────────────────────────────────────────────────────\n\nexport interface DialogApi {\n confirm(opts: DialogConfirmOptions): Promise<boolean>\n alert(opts: DialogAlertOptions): Promise<void>\n prompt(opts: DialogPromptOptions): Promise<string | null>\n custom<T = unknown>(opts: DialogCustomOptions<T>): Promise<T | null>\n}\n\nexport function useDialog(): DialogApi {\n return {\n confirm(opts: DialogConfirmOptions): Promise<boolean> {\n return new Promise<boolean>((resolve) => {\n _enqueue(() => {\n _activeRequest.value = { kind: 'confirm', opts, resolve }\n })\n })\n },\n\n alert(opts: DialogAlertOptions): Promise<void> {\n return new Promise<void>((resolve) => {\n _enqueue(() => {\n _activeRequest.value = { kind: 'alert', opts, resolve }\n })\n })\n },\n\n prompt(opts: DialogPromptOptions): Promise<string | null> {\n return new Promise<string | null>((resolve) => {\n _enqueue(() => {\n _activeRequest.value = { kind: 'prompt', opts, resolve }\n })\n })\n },\n\n custom<T = unknown>(opts: DialogCustomOptions<T>): Promise<T | null> {\n return new Promise<T | null>((resolve) => {\n _enqueue(() => {\n _activeRequest.value = {\n kind: 'custom',\n opts: opts as DialogCustomOptions<unknown>,\n resolve: resolve as (v: unknown) => void,\n }\n })\n })\n },\n }\n}\n","/**\n * dialog-internal-keys.ts — Internal injection keys for the imperative dialog system.\n *\n * HOST-INTERNAL: Do not export from any public barrel. Plugin authors must\n * never see or depend on these keys; they exist only so DialogHost can\n * provide the handle that useDialogPluginComponent() reads.\n */\nimport type { InjectionKey } from 'vue'\nimport type { DialogPluginComponentHandle } from './useDialogPluginComponent'\n\n/**\n * Injection key that DialogHost provides to custom dialog components.\n * Components rendered via useDialog().custom() use useDialogPluginComponent()\n * which injects this key to get ok/cancel/dismiss callbacks.\n */\nexport const DIALOG_PLUGIN_HANDLE_KEY: InjectionKey<DialogPluginComponentHandle<unknown>> =\n Symbol('openpen:dialog-plugin-handle')\n"],"names":["_createBlock","_unref","_createVNode","_renderSlot","_normalizeClass","_createElementVNode","$slots","_openBlock","_createElementBlock"],"mappings":"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"}
|
package/dist/host/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
export { useStrokeStyle } from './stroke-style';
|
|
15
15
|
export type { StrokeColor } from './stroke-style';
|
|
16
16
|
export { resolveColorStyle, hexToRgb, rgbToHex, hsvToRgb, rgbToHsv, hexToHsv, hsvToHex, isValidHex, } from './color-utils';
|
|
17
|
-
export { getAllStrokes, removeStrokeById, pushCommand } from './stroke-store';
|
|
17
|
+
export { getAllStrokes, removeStrokeById, pushCommand, commitStroke } from './stroke-store';
|
|
18
18
|
export type { Stroke } from './stroke-store';
|
|
19
19
|
export { usePopupAnchor, calculatePopupAnchor } from './popup-anchor';
|
|
20
20
|
export { usePassthroughGuard } from './passthrough';
|
package/dist/host/index.js
CHANGED
|
@@ -13,6 +13,7 @@ const isValidHex = (hex) => _useHost().colorUtils.isValidHex(hex);
|
|
|
13
13
|
const getAllStrokes = () => _useHost().getAllStrokes();
|
|
14
14
|
const removeStrokeById = (id) => _useHost().removeStrokeById(id);
|
|
15
15
|
const pushCommand = (command) => _useHost().pushCommand(command);
|
|
16
|
+
const commitStroke = (stroke) => _useHost().commitStroke(stroke);
|
|
16
17
|
const usePopupAnchor = (options) => _useHost().usePopupAnchor(options);
|
|
17
18
|
const calculatePopupAnchor = (options) => _useHost().calculatePopupAnchor(options);
|
|
18
19
|
const hostCommands = new Proxy({}, {
|
|
@@ -26,6 +27,7 @@ const getSlotEntries = (slotKey) => _useHost().getSlotEntries(slotKey);
|
|
|
26
27
|
export {
|
|
27
28
|
calculatePopupAnchor,
|
|
28
29
|
c as clearModuleContext,
|
|
30
|
+
commitStroke,
|
|
29
31
|
emit,
|
|
30
32
|
getAllStrokes,
|
|
31
33
|
getSlotEntries,
|
package/dist/host/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../../src/host/stroke-style.ts","../../src/host/color-utils.ts","../../src/host/stroke-store.ts","../../src/host/popup-anchor.ts","../../src/host/host-commands.ts","../../src/host/event-bus.ts","../../src/host/slot-entries.ts"],"sourcesContent":["/**\n * @openpen/module-api/host — Stroke Style\n *\n * Proxies the useStrokeStyle composable through the host registry so\n * module components can read + write the global stroke style without\n * reaching past the package boundary. The reactive refs returned by\n * useStrokeStyle stay live across the proxy because the host registry\n * returns the singleton refs the host owns.\n *\n * initStrokeStyleFromSettings is host-boot-only and intentionally\n * NOT exposed here.\n */\nimport { _useHost } from './registry'\nimport type { ModuleHost } from './types'\n\nexport const useStrokeStyle: ModuleHost['useStrokeStyle'] = () =>\n _useHost().useStrokeStyle()\n\n// Re-export the StrokeColor type via the public SDK types (not host internals).\nexport type { StrokeColor } from '../types/tool'\n","/**\n * @openpen/module-api/host — Color Utilities\n *\n * Proxies color manipulation helpers through the host registry. These\n * helpers are pure functions (no host state), so a future refactor MAY\n * lift the implementations into this package itself; for now they flow\n * through injection for consistency with the rest of the host surface.\n */\nimport { _useHost } from './registry'\nimport type { ModuleHost } from './types'\n\ntype Utils = ModuleHost['colorUtils']\n\nexport const resolveColorStyle: Utils['resolveColorStyle'] = (ctx, color, startPoint, endPoint) =>\n _useHost().colorUtils.resolveColorStyle(ctx, color, startPoint, endPoint)\n\nexport const hexToRgb: Utils['hexToRgb'] = (hex) =>\n _useHost().colorUtils.hexToRgb(hex)\n\nexport const rgbToHex: Utils['rgbToHex'] = (r, g, b) =>\n _useHost().colorUtils.rgbToHex(r, g, b)\n\nexport const hsvToRgb: Utils['hsvToRgb'] = (h, s, v) =>\n _useHost().colorUtils.hsvToRgb(h, s, v)\n\nexport const rgbToHsv: Utils['rgbToHsv'] = (r, g, b) =>\n _useHost().colorUtils.rgbToHsv(r, g, b)\n\nexport const hexToHsv: Utils['hexToHsv'] = (hex) =>\n _useHost().colorUtils.hexToHsv(hex)\n\nexport const hsvToHex: Utils['hsvToHex'] = (h, s, v) =>\n _useHost().colorUtils.hsvToHex(h, s, v)\n\nexport const isValidHex: Utils['isValidHex'] = (hex) =>\n _useHost().colorUtils.isValidHex(hex)\n","/**\n * @openpen/module-api/host — Stroke Store\n *\n * Proxies the stroke store read + history APIs through the host registry.\n *
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../src/host/stroke-style.ts","../../src/host/color-utils.ts","../../src/host/stroke-store.ts","../../src/host/popup-anchor.ts","../../src/host/host-commands.ts","../../src/host/event-bus.ts","../../src/host/slot-entries.ts"],"sourcesContent":["/**\n * @openpen/module-api/host — Stroke Style\n *\n * Proxies the useStrokeStyle composable through the host registry so\n * module components can read + write the global stroke style without\n * reaching past the package boundary. The reactive refs returned by\n * useStrokeStyle stay live across the proxy because the host registry\n * returns the singleton refs the host owns.\n *\n * initStrokeStyleFromSettings is host-boot-only and intentionally\n * NOT exposed here.\n */\nimport { _useHost } from './registry'\nimport type { ModuleHost } from './types'\n\nexport const useStrokeStyle: ModuleHost['useStrokeStyle'] = () =>\n _useHost().useStrokeStyle()\n\n// Re-export the StrokeColor type via the public SDK types (not host internals).\nexport type { StrokeColor } from '../types/tool'\n","/**\n * @openpen/module-api/host — Color Utilities\n *\n * Proxies color manipulation helpers through the host registry. These\n * helpers are pure functions (no host state), so a future refactor MAY\n * lift the implementations into this package itself; for now they flow\n * through injection for consistency with the rest of the host surface.\n */\nimport { _useHost } from './registry'\nimport type { ModuleHost } from './types'\n\ntype Utils = ModuleHost['colorUtils']\n\nexport const resolveColorStyle: Utils['resolveColorStyle'] = (ctx, color, startPoint, endPoint) =>\n _useHost().colorUtils.resolveColorStyle(ctx, color, startPoint, endPoint)\n\nexport const hexToRgb: Utils['hexToRgb'] = (hex) =>\n _useHost().colorUtils.hexToRgb(hex)\n\nexport const rgbToHex: Utils['rgbToHex'] = (r, g, b) =>\n _useHost().colorUtils.rgbToHex(r, g, b)\n\nexport const hsvToRgb: Utils['hsvToRgb'] = (h, s, v) =>\n _useHost().colorUtils.hsvToRgb(h, s, v)\n\nexport const rgbToHsv: Utils['rgbToHsv'] = (r, g, b) =>\n _useHost().colorUtils.rgbToHsv(r, g, b)\n\nexport const hexToHsv: Utils['hexToHsv'] = (hex) =>\n _useHost().colorUtils.hexToHsv(hex)\n\nexport const hsvToHex: Utils['hsvToHex'] = (h, s, v) =>\n _useHost().colorUtils.hsvToHex(h, s, v)\n\nexport const isValidHex: Utils['isValidHex'] = (hex) =>\n _useHost().colorUtils.isValidHex(hex)\n","/**\n * @openpen/module-api/host — Stroke Store\n *\n * Proxies the stroke store read + history APIs through the host registry.\n * The exposed surface:\n * - getAllStrokes / removeStrokeById / pushCommand: read + history access\n * required by stroke-eraser (and similar tools).\n * - commitStroke: atomic add + ADD_STROKE history + redraw for async-placement\n * tools (e.g. text) that finalise a stroke outside the synchronous\n * onPointerUp return. Not idempotent — calling it twice appends twice.\n *\n * Host-internal state mutation helpers (clearAll, addStroke, undo, redo)\n * are intentionally NOT exposed here — modules must use hostCommands for\n * undo/redo and canvas clear.\n */\nimport { _useHost } from './registry'\nimport type { ModuleHost } from './types'\n\nexport const getAllStrokes: ModuleHost['getAllStrokes'] = () =>\n _useHost().getAllStrokes()\n\nexport const removeStrokeById: ModuleHost['removeStrokeById'] = (id) =>\n _useHost().removeStrokeById(id)\n\nexport const pushCommand: ModuleHost['pushCommand'] = (command) =>\n _useHost().pushCommand(command)\n\nexport const commitStroke: ModuleHost['commitStroke'] = (stroke) =>\n _useHost().commitStroke(stroke)\n\n// Re-export Stroke type via the public SDK types, not host internals.\nexport type { Stroke } from '../types/tool'\n","/**\n * @openpen/module-api/host — Popup Anchor\n *\n * Proxies the `usePopupAnchor` composable and the `calculatePopupAnchor`\n * pure helper through the host registry so module components can position\n * popups relative to anchor elements without reaching past the package\n * boundary.\n */\nimport { _useHost } from './registry'\nimport type { ModuleHost } from './types'\n\nexport const usePopupAnchor: ModuleHost['usePopupAnchor'] = (options) =>\n _useHost().usePopupAnchor(options)\n\nexport const calculatePopupAnchor: ModuleHost['calculatePopupAnchor'] = (options) =>\n _useHost().calculatePopupAnchor(options)\n","/**\n * @openpen/module-api/host — Host Commands\n *\n * Proxies hostCommands through the host registry so module components\n * can trigger host infrastructure actions (pin toggle, canvas clear,\n * undo/redo, drawing mode toggle) without reaching past the package\n * boundary. The exported object is a Proxy so every property access\n * fetches the live `hostCommands` from the registered host.\n */\nimport { _useHost } from './registry'\nimport type { ModuleHost } from './types'\n\nexport const hostCommands = new Proxy({} as ModuleHost['hostCommands'], {\n get(_target, prop) {\n return Reflect.get(_useHost().hostCommands as object, prop)\n },\n})\n","/**\n * @openpen/module-api/host — Domain Event Bus\n *\n * Proxies the renderer-side event-bus emit/on through the host registry so\n * module components can publish and subscribe without reaching into host\n * runtime internals. The bus is a process-local pub/sub for cross-module\n * coordination.\n *\n * **Host-emitted events (consume via `on(name, cb)`):**\n *\n * | Event name | Payload shape | Fires when |\n * |---------------------------|-------------------------------------------------|----------------------------------------------------------------------|\n * | `'tool-changed'` | `{ tool: string } & Partial<ToolConfig>` | The active drawing tool changes (user click, IPC, or shortcut) |\n * | `'stroke-style-changed'` | `{ color?: StrokeColor; lineWidth?: number }` | Color or stroke width changes |\n * | `'canvas-redraw-requested'` | (no payload) | A module wants the canvas to repaint (eraser, transformer, etc.) |\n *\n * **Module-emitted events:** modules MAY emit any event name they\n * choose; the bus is namespace-free. Use a prefix (e.g. `'my-plugin:'`)\n * to avoid colliding with another module's events.\n *\n * `on(name, cb)` returns an unsubscribe function — register it via\n * `ctx.onDispose(unsubscribe)` so the listener is cleaned up when the\n * module unloads.\n *\n * @example\n * ```ts\n * import { on } from '@openpen/module-api/host'\n *\n * setup(ctx) {\n * const off = on('tool-changed', (payload) => {\n * console.log('tool now', payload.tool)\n * })\n * ctx.onDispose(off)\n * }\n * ```\n */\n\nimport { _useHost } from './registry'\nimport type { ModuleHost } from './types'\n\nexport const emit: ModuleHost['emit'] = (event, payload) =>\n _useHost().emit(event, payload)\n\nexport const on: ModuleHost['on'] = (event, handler) =>\n _useHost().on(event, handler)\n","/**\n * @openpen/module-api/host — Slot Entries Reader\n *\n * Proxies getSlotEntries through the host registry so module components\n * can render UI driven by other modules' contributions to the same slot\n * (for example, the shape-picker rendering all canvas.shapes contributions).\n */\nimport { _useHost } from './registry'\nimport type { ModuleHost } from './types'\n\nexport const getSlotEntries: ModuleHost['getSlotEntries'] = <T = unknown>(slotKey: string) =>\n _useHost().getSlotEntries<T>(slotKey)\n"],"names":["s"],"mappings":";;;AAeO,MAAM,iBAA+C,MAC1D,SAAA,EAAW,eAAA;ACHN,MAAM,oBAAgD,CAAC,KAAK,OAAO,YAAY,aACpF,WAAW,WAAW,kBAAkB,KAAK,OAAO,YAAY,QAAQ;AAEnE,MAAM,WAA8B,CAAC,QAC1C,WAAW,WAAW,SAAS,GAAG;AAE7B,MAAM,WAA8B,CAAC,GAAG,GAAG,MAChD,WAAW,WAAW,SAAS,GAAG,GAAG,CAAC;AAEjC,MAAM,WAA8B,CAAC,GAAGA,IAAG,MAChD,WAAW,WAAW,SAAS,GAAGA,IAAG,CAAC;AAEjC,MAAM,WAA8B,CAAC,GAAG,GAAG,MAChD,WAAW,WAAW,SAAS,GAAG,GAAG,CAAC;AAEjC,MAAM,WAA8B,CAAC,QAC1C,WAAW,WAAW,SAAS,GAAG;AAE7B,MAAM,WAA8B,CAAC,GAAGA,IAAG,MAChD,WAAW,WAAW,SAAS,GAAGA,IAAG,CAAC;AAEjC,MAAM,aAAkC,CAAC,QAC9C,WAAW,WAAW,WAAW,GAAG;ACjB/B,MAAM,gBAA6C,MACxD,SAAA,EAAW,cAAA;AAEN,MAAM,mBAAmD,CAAC,OAC/D,SAAA,EAAW,iBAAiB,EAAE;AAEzB,MAAM,cAAyC,CAAC,YACrD,SAAA,EAAW,YAAY,OAAO;AAEzB,MAAM,eAA2C,CAAC,WACvD,SAAA,EAAW,aAAa,MAAM;ACjBzB,MAAM,iBAA+C,CAAC,YAC3D,SAAA,EAAW,eAAe,OAAO;AAE5B,MAAM,uBAA2D,CAAC,YACvE,SAAA,EAAW,qBAAqB,OAAO;ACHlC,MAAM,eAAe,IAAI,MAAM,IAAkC;AAAA,EACtE,IAAI,SAAS,MAAM;AACjB,WAAO,QAAQ,IAAI,SAAA,EAAW,cAAwB,IAAI;AAAA,EAC5D;AACF,CAAC;ACwBM,MAAM,OAA2B,CAAC,OAAO,YAC9C,WAAW,KAAK,OAAO,OAAO;AAEzB,MAAM,KAAuB,CAAC,OAAO,YAC1C,WAAW,GAAG,OAAO,OAAO;AClCvB,MAAM,iBAA+C,CAAc,YACxE,SAAA,EAAW,eAAkB,OAAO;"}
|
|
@@ -2,4 +2,5 @@ import type { ModuleHost } from './types';
|
|
|
2
2
|
export declare const getAllStrokes: ModuleHost['getAllStrokes'];
|
|
3
3
|
export declare const removeStrokeById: ModuleHost['removeStrokeById'];
|
|
4
4
|
export declare const pushCommand: ModuleHost['pushCommand'];
|
|
5
|
+
export declare const commitStroke: ModuleHost['commitStroke'];
|
|
5
6
|
export type { Stroke } from '../types/tool';
|
package/dist/host/types.d.ts
CHANGED
|
@@ -153,6 +153,18 @@ export interface ModuleHost {
|
|
|
153
153
|
getAllStrokes: () => Stroke[];
|
|
154
154
|
removeStrokeById: (id: string) => boolean;
|
|
155
155
|
pushCommand: (command: HistoryCommand) => void;
|
|
156
|
+
/**
|
|
157
|
+
* Atomically commit a finished stroke: append it to the store, record an
|
|
158
|
+
* ADD_STROKE history command, and request a canvas redraw. The commit path
|
|
159
|
+
* for tools that produce strokes outside the synchronous pointer-up return
|
|
160
|
+
* (e.g. async-placement tools that finalise after a deferred interaction).
|
|
161
|
+
*
|
|
162
|
+
* Semantically equivalent to the host's synchronous onPointerUp commit and
|
|
163
|
+
* fully participates in undo/redo. NOT idempotent: calling it twice with the
|
|
164
|
+
* same stroke appends two strokes and two history entries. The stroke shape
|
|
165
|
+
* is the caller's responsibility — v1 is full-trust and performs no sanitize.
|
|
166
|
+
*/
|
|
167
|
+
commitStroke: (stroke: Stroke) => void;
|
|
156
168
|
hostCommands: HostCommandsAPI;
|
|
157
169
|
colorUtils: ColorUtilsAPI;
|
|
158
170
|
useStrokeStyle: () => StrokeStyleAPI;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
type __VLS_Props = {
|
|
2
2
|
modelValue: boolean;
|
|
3
3
|
ariaLabel?: string;
|
|
4
|
+
testid?: string;
|
|
4
5
|
};
|
|
5
6
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
6
7
|
"update:modelValue": (value: boolean) => any;
|
|
@@ -8,6 +9,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
|
|
|
8
9
|
"onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
|
|
9
10
|
}>, {
|
|
10
11
|
ariaLabel: string;
|
|
12
|
+
testid: string;
|
|
11
13
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
12
14
|
declare const _default: typeof __VLS_export;
|
|
13
15
|
export default _default;
|
package/dist/uikit/index.js
CHANGED
|
@@ -5,8 +5,8 @@ import { W as WRAPPER_EL_KEY, M as MODAL_MANAGER_KEY, c as CONTROL_BAR_ANIMATING
|
|
|
5
5
|
import "dompurify";
|
|
6
6
|
import "zod";
|
|
7
7
|
import { u as usePassthroughGuard } from "../_chunks/passthrough-DKU7JYuQ.js";
|
|
8
|
-
import { D as DIALOG_PLUGIN_HANDLE_KEY } from "../_chunks/AppButtonDropdown.vue_vue_type_style_index_0_lang-
|
|
9
|
-
import { _, a, b, u } from "../_chunks/AppButtonDropdown.vue_vue_type_style_index_0_lang-
|
|
8
|
+
import { D as DIALOG_PLUGIN_HANDLE_KEY } from "../_chunks/AppButtonDropdown.vue_vue_type_style_index_0_lang-B9dHU-LE.js";
|
|
9
|
+
import { _, a, b, u } from "../_chunks/AppButtonDropdown.vue_vue_type_style_index_0_lang-B9dHU-LE.js";
|
|
10
10
|
const _sfc_main$9 = /* @__PURE__ */ defineComponent({
|
|
11
11
|
__name: "AppPopover",
|
|
12
12
|
props: {
|
|
@@ -97,7 +97,8 @@ const _sfc_main$9 = /* @__PURE__ */ defineComponent({
|
|
|
97
97
|
side: rekaSide.value,
|
|
98
98
|
"avoid-collisions": true,
|
|
99
99
|
"side-offset": props.gap,
|
|
100
|
-
class: "openpen-popover-content openpen-interactive"
|
|
100
|
+
class: "openpen-popover-content openpen-interactive",
|
|
101
|
+
"data-testid": "controlbar-popover"
|
|
101
102
|
}, {
|
|
102
103
|
default: withCtx(() => [
|
|
103
104
|
renderSlot(_ctx.$slots, "content"),
|
|
@@ -168,6 +169,7 @@ const _sfc_main$8 = /* @__PURE__ */ defineComponent({
|
|
|
168
169
|
return (_ctx, _cache) => {
|
|
169
170
|
return openBlock(), createBlock(unref(SliderRoot), {
|
|
170
171
|
class: normalizeClass(["app-slider-root", { "app-slider-root--vertical": isVertical.value, "app-slider-root--inverted": __props.inverted }]),
|
|
172
|
+
"data-testid": "app-slider-root",
|
|
171
173
|
style: normalizeStyle(rootStyle.value),
|
|
172
174
|
"model-value": [__props.modelValue],
|
|
173
175
|
min: __props.min,
|
|
@@ -178,13 +180,22 @@ const _sfc_main$8 = /* @__PURE__ */ defineComponent({
|
|
|
178
180
|
"onUpdate:modelValue": onValueChange
|
|
179
181
|
}, {
|
|
180
182
|
default: withCtx(() => [
|
|
181
|
-
createVNode(unref(SliderTrack), {
|
|
183
|
+
createVNode(unref(SliderTrack), {
|
|
184
|
+
class: "app-slider-track",
|
|
185
|
+
"data-testid": "app-slider-track"
|
|
186
|
+
}, {
|
|
182
187
|
default: withCtx(() => [
|
|
183
|
-
createVNode(unref(SliderRange), {
|
|
188
|
+
createVNode(unref(SliderRange), {
|
|
189
|
+
class: "app-slider-range",
|
|
190
|
+
"data-testid": "app-slider-range"
|
|
191
|
+
})
|
|
184
192
|
]),
|
|
185
193
|
_: 1
|
|
186
194
|
}),
|
|
187
|
-
createVNode(unref(SliderThumb), {
|
|
195
|
+
createVNode(unref(SliderThumb), {
|
|
196
|
+
class: "app-slider-thumb",
|
|
197
|
+
"data-testid": "app-slider-thumb"
|
|
198
|
+
})
|
|
188
199
|
]),
|
|
189
200
|
_: 1
|
|
190
201
|
}, 8, ["class", "style", "model-value", "min", "max", "step", "orientation", "inverted"]);
|
|
@@ -198,12 +209,13 @@ const _export_sfc = (sfc, props) => {
|
|
|
198
209
|
}
|
|
199
210
|
return target;
|
|
200
211
|
};
|
|
201
|
-
const AppSlider = /* @__PURE__ */ _export_sfc(_sfc_main$8, [["__scopeId", "data-v-
|
|
212
|
+
const AppSlider = /* @__PURE__ */ _export_sfc(_sfc_main$8, [["__scopeId", "data-v-ae09a023"]]);
|
|
202
213
|
const _sfc_main$7 = /* @__PURE__ */ defineComponent({
|
|
203
214
|
__name: "AppToggle",
|
|
204
215
|
props: {
|
|
205
216
|
modelValue: { type: Boolean },
|
|
206
|
-
ariaLabel: { default: "" }
|
|
217
|
+
ariaLabel: { default: "" },
|
|
218
|
+
testid: { default: void 0 }
|
|
207
219
|
},
|
|
208
220
|
emits: ["update:modelValue"],
|
|
209
221
|
setup(__props, { emit: __emit }) {
|
|
@@ -217,17 +229,18 @@ const _sfc_main$7 = /* @__PURE__ */ defineComponent({
|
|
|
217
229
|
"model-value": __props.modelValue,
|
|
218
230
|
"aria-label": __props.ariaLabel,
|
|
219
231
|
"aria-pressed": __props.modelValue,
|
|
232
|
+
"data-testid": __props.testid,
|
|
220
233
|
"onUpdate:modelValue": onCheckedChange
|
|
221
234
|
}, {
|
|
222
235
|
default: withCtx(() => [
|
|
223
236
|
createVNode(unref(SwitchThumb), { class: "app-toggle-thumb" })
|
|
224
237
|
]),
|
|
225
238
|
_: 1
|
|
226
|
-
}, 8, ["class", "model-value", "aria-label", "aria-pressed"]);
|
|
239
|
+
}, 8, ["class", "model-value", "aria-label", "aria-pressed", "data-testid"]);
|
|
227
240
|
};
|
|
228
241
|
}
|
|
229
242
|
});
|
|
230
|
-
const AppToggle = /* @__PURE__ */ _export_sfc(_sfc_main$7, [["__scopeId", "data-v-
|
|
243
|
+
const AppToggle = /* @__PURE__ */ _export_sfc(_sfc_main$7, [["__scopeId", "data-v-d23dd615"]]);
|
|
231
244
|
const _hoisted_1$3 = ["data-testid"];
|
|
232
245
|
const _hoisted_2$1 = ["innerHTML"];
|
|
233
246
|
const _sfc_main$6 = /* @__PURE__ */ defineComponent({
|
|
@@ -248,6 +261,7 @@ const _sfc_main$6 = /* @__PURE__ */ defineComponent({
|
|
|
248
261
|
return (_ctx, _cache) => {
|
|
249
262
|
return openBlock(), createBlock(unref(RadioGroupRoot), {
|
|
250
263
|
class: normalizeClass(["app-seg", { "app-seg--disabled": __props.disabled }]),
|
|
264
|
+
"data-testid": "app-seg",
|
|
251
265
|
"model-value": __props.modelValue,
|
|
252
266
|
disabled: __props.disabled,
|
|
253
267
|
orientation: "horizontal",
|
|
@@ -283,7 +297,7 @@ const _sfc_main$6 = /* @__PURE__ */ defineComponent({
|
|
|
283
297
|
};
|
|
284
298
|
}
|
|
285
299
|
});
|
|
286
|
-
const AppSegmented = /* @__PURE__ */ _export_sfc(_sfc_main$6, [["__scopeId", "data-v-
|
|
300
|
+
const AppSegmented = /* @__PURE__ */ _export_sfc(_sfc_main$6, [["__scopeId", "data-v-eaacab1f"]]);
|
|
287
301
|
function useDialogPluginComponent() {
|
|
288
302
|
const handle = inject(DIALOG_PLUGIN_HANDLE_KEY);
|
|
289
303
|
if (!handle) {
|
package/dist/uikit/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../../src/uikit/components/AppPopover.vue","../../src/uikit/components/AppSlider.vue","../../src/uikit/components/AppToggle.vue","../../src/uikit/components/AppSegmented.vue","../../src/uikit/composables/useDialogPluginComponent.ts","../../src/uikit/components/AppSelect.vue","../../src/uikit/components/AppTooltip.vue","../../src/uikit/components/AppTabs.vue","../../src/uikit/components/AppBanner.vue","../../src/uikit/components/AppButton.vue","../../src/uikit/components/AppButtonDropdown.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * AppPopover — High-level wrapper (Layer 1).\n *\n * Plugin authors use this component without needing to know about inject\n * keys, Reka UI, modal manager mutual exclusion, or the animating guard.\n *\n * Public API is stable: switching the underlying headless library in the\n * future does not break the plugin author's code.\n *\n * Implementation notes:\n * - placement 'auto' picks the initial Reka UI side based on snap edge:\n * vbar-left → 'right', vbar-right → 'left', horizontal → 'bottom'.\n * Reka UI's built-in flip middleware then handles viewport-edge collision.\n * - usePassthroughGuard: called with the content element ref. The contentEl\n * ref is passed to usePassthroughGuard, which attaches mousemove/leave\n * handlers that toggle mouse passthrough based on pointer position.\n */\nimport { computed, inject, onMounted, ref, useTemplateRef, watch } from 'vue'\nimport {\n PopoverRoot,\n PopoverTrigger,\n PopoverPortal,\n PopoverContent,\n PopoverArrow,\n} from '../primitives'\n// Inject keys MUST come from the package barrel — the runtime build\n// externalises '@openpen/module-api' so host + uikit share the same Symbol\n// instances. Importing from the relative '../../inject-keys' path bundles a\n// duplicate Symbol into module-api-uikit.js, causing host's provide() and\n// uikit's inject() to miss each other (popover never opens, etc).\nimport {\n WRAPPER_EL_KEY,\n MODAL_MANAGER_KEY,\n CONTROL_BAR_ANIMATING_KEY,\n POPOVER_PLACEMENT_HINT_KEY,\n} from '@openpen/module-api'\nimport { usePassthroughGuard } from '../../host/passthrough'\n\n// ── OpenPen-owned type (MUST NOT reference reka-ui types) ─────────────────────\n\n/** Preferred placement for the popover content relative to the trigger. */\nexport type Placement = 'auto' | 'top' | 'bottom' | 'left' | 'right'\n\nexport interface AppPopoverProps {\n /** Globally unique popover id; used for modal-stack mutual exclusion. */\n popoverId: string\n /**\n * Popover placement direction.\n * - 'auto' (default, recommended): the host's snap-edge hint picks the side that\n * keeps the popover on-screen across all control-bar modes.\n * - explicit values ('top' | 'bottom' | 'left' | 'right'): bypass the auto hint.\n * In dev mode a console warning is emitted because explicit values can position\n * popovers off-screen or away from the trigger button when the control bar is\n * in a vertical mode. Only use when you have verified the placement stays\n * on-screen across all snap layouts.\n */\n placement?: Placement\n /** Gap between trigger and content in px. Default 8. */\n gap?: number\n}\n\nexport interface AppPopoverSlots {\n /** Trigger slot; scope exposes active state + toggle / open / close actions. */\n trigger: (scope: {\n active: boolean\n toggle: () => void\n open: () => void\n close: () => void\n }) => unknown\n /** Popover content slot. */\n content: () => unknown\n}\n\nconst props = withDefaults(defineProps<AppPopoverProps>(), {\n placement: 'auto',\n gap: 8,\n})\n\ndefineSlots<AppPopoverSlots>()\n\n// ── 0. Dev-mode guard: warn plugin authors about risky explicit placement ─────\nonMounted(() => {\n if (!import.meta.env.DEV) return\n if (props.placement !== 'auto' && props.placement !== undefined) {\n console.warn(\n `[AppPopover] Explicit placement=\"${props.placement}\" bypasses ` +\n `the host's snap-edge auto detection. In vertical control-bar modes ` +\n `this can position popups off-screen or away from the trigger button. ` +\n `Prefer placement=\"auto\" (default) and let the host pick the side; ` +\n `use an explicit value only when you have verified it stays on-screen ` +\n `across snap-left / snap-right / snap-free / horizontal layouts.`\n )\n }\n})\n\n// ── 1. Inject host context (plugin author never sees these) ───────────────────\n\nconst wrapperElRef = inject(WRAPPER_EL_KEY)\nconst wrapperEl = computed<HTMLElement | null>(() => wrapperElRef?.value ?? null)\nconst modalManager = inject(MODAL_MANAGER_KEY)\nconst animating = inject(CONTROL_BAR_ANIMATING_KEY)\nconst popoverPlacementHint = inject(POPOVER_PLACEMENT_HINT_KEY)\n\n// ── 2. Modal manager mutual exclusion ────────────────────────────────────────\n\nconst isOpen = computed(() => modalManager?.isOpen(props.popoverId) ?? false)\n\nfunction open() {\n modalManager?.open(props.popoverId)\n}\n\nfunction close() {\n modalManager?.close(props.popoverId)\n}\n\nfunction toggle() {\n if (isOpen.value) {\n close()\n } else {\n open()\n }\n}\n\n// Sync Reka UI's controlled open state back to modal manager.\n// Reka UI may close via Escape / outside-click; we must keep manager in sync.\nfunction onUpdateOpen(v: boolean) {\n if (v) {\n open()\n } else {\n close()\n }\n}\n\n// ── 3. ControlBar animation guard — auto-close during transitions ────────────\n\nwatch(\n () => animating?.value,\n (isAnimating) => {\n if (isAnimating && isOpen.value) {\n close()\n }\n }\n)\n\n// ── 4. placement → Reka UI side mapping ──────────────────────────────────────\n//\n// 'auto' uses the POPOVER_PLACEMENT_HINT_KEY provided by ControlBar. The hint\n// encodes a two-tier UX rule:\n// - Snap edge set → open away from the edge (left→right, right→left, etc.)\n// - Snap off → open toward the side with more screen space on the relevant axis\n// (horizontal bar: top/bottom; vertical bar: left/right)\n// Reka UI's built-in flip middleware handles secondary overflow. The window is\n// workArea-sized, so the viewport equals the workArea and floating-ui's collision\n// detection is correct without any boundary-proxy workaround.\n\ntype RekaSide = 'top' | 'bottom' | 'left' | 'right'\n\nconst rekaSide = computed<RekaSide>(() => {\n if (props.placement !== 'auto') return props.placement as RekaSide\n // Fall back to 'bottom' only if no hint is provided (e.g. standalone use outside ControlBar).\n return popoverPlacementHint?.value ?? 'bottom'\n})\n\n// ── 5. Passthrough guard integration ─────────────────────────────────────────\n//\n// usePassthroughGuard is called with the content element ref. Reka UI's\n// PopoverContent is a Vue component, so the template ref resolves to a\n// component instance (with `$el`) rather than a raw HTMLElement. We normalize\n// via a watcher so the guard receives a plain element ref.\n// openpen-interactive class enables the ControlBar passthrough guard's CSS selector path.\nconst contentInstance = useTemplateRef<HTMLElement | { $el?: HTMLElement } | null>('content')\nconst contentEl = ref<HTMLElement | null>(null)\nwatch(contentInstance, (instance) => {\n if (!instance) {\n contentEl.value = null\n } else if (instance instanceof HTMLElement) {\n contentEl.value = instance\n } else if (typeof instance === 'object' && '$el' in instance && instance.$el instanceof HTMLElement) {\n contentEl.value = instance.$el\n } else {\n contentEl.value = null\n }\n})\nusePassthroughGuard(contentEl)\n</script>\n\n<template>\n <PopoverRoot :open=\"isOpen\" @update:open=\"onUpdateOpen\">\n <PopoverTrigger as-child>\n <slot\n name=\"trigger\"\n :active=\"isOpen\"\n :toggle=\"toggle\"\n :open=\"open\"\n :close=\"close\"\n />\n </PopoverTrigger>\n <PopoverPortal :to=\"wrapperEl ?? undefined\">\n <PopoverContent\n ref=\"content\"\n :side=\"rekaSide\"\n :avoid-collisions=\"true\"\n :side-offset=\"props.gap\"\n class=\"openpen-popover-content openpen-interactive\"\n >\n <slot name=\"content\" />\n <!-- Arrow geometry: 14×8 sharp triangle. Fill shares\n --openpen-color-popover-frame with the popup border so the arrow\n reads as a continuous extension of the frame. See tokens.css for\n per-theme values. -->\n <PopoverArrow class=\"openpen-popover-arrow\" :width=\"14\" :height=\"8\" />\n </PopoverContent>\n </PopoverPortal>\n </PopoverRoot>\n</template>\n\n<!-- Styles for teleported PopoverContent MUST be unscoped — Vue scoped CSS\n does not follow Teleport targets, so .openpen-popover-content would not\n receive the scoped data-v-* attribute when rendered inside the portal. -->\n<style>\n.openpen-popover-content {\n background: var(--openpen-color-surface-popup);\n border: 1px solid var(--openpen-color-popover-frame);\n border-radius: var(--openpen-radius-lg);\n padding: 14px 16px;\n color: var(--openpen-color-text-primary);\n font-size: 13px;\n box-shadow: var(--openpen-shadow);\n backdrop-filter: var(--openpen-blur);\n -webkit-backdrop-filter: var(--openpen-blur);\n z-index: 100;\n /* Prevent pointer events from leaking through transparent gaps. */\n pointer-events: auto;\n}\n\n.openpen-popover-arrow {\n fill: var(--openpen-color-popover-frame);\n}\n\n/* ── Open / close animations ────────────────────────────────────────────────\n Reka UI's Presence keeps the element in the DOM until the running CSS\n animation ends, so animations targeting [data-state=\"closed\"] DO play\n on close. transform-origin tracks data-side so the popover scales out\n from the trigger instead of from its own center.\n\n Timings: 200ms easeOutBack enter, 150ms ease leave. */\n.openpen-popover-content[data-state='open'] {\n animation: openpen-popover-in 200ms cubic-bezier(0.34, 1.56, 0.64, 1);\n}\n.openpen-popover-content[data-state='closed'] {\n animation: openpen-popover-out 150ms ease forwards;\n}\n\n.openpen-popover-content[data-side='bottom'] { transform-origin: top center; }\n.openpen-popover-content[data-side='top'] { transform-origin: bottom center; }\n.openpen-popover-content[data-side='left'] { transform-origin: right center; }\n.openpen-popover-content[data-side='right'] { transform-origin: left center; }\n\n@keyframes openpen-popover-in {\n from { opacity: 0; transform: scale(0.92); }\n to { opacity: 1; transform: scale(1); }\n}\n@keyframes openpen-popover-out {\n from { opacity: 1; transform: scale(1); }\n to { opacity: 0; transform: scale(0.92); }\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * AppSlider — High-level wrapper (Layer 1).\n *\n * Wraps Reka UI Slider (SliderRoot / SliderTrack / SliderRange / SliderThumb).\n *\n * Visual: 4px track, 14×14 white thumb, accent fill,\n * hover scale(1.2) at var(--openpen-duration-fast) easing.\n *\n * Styles use --openpen-* design tokens.\n * No Reka UI types are leaked in the public API.\n */\nimport { computed } from 'vue'\nimport {\n SliderRoot,\n SliderTrack,\n SliderRange,\n SliderThumb,\n} from '../primitives'\n\n// ── Public API (MUST NOT reference reka-ui types) ─────────────────────────────\n\nconst props = withDefaults(defineProps<{\n modelValue: number\n min?: number\n max?: number\n step?: number\n /** Container width (e.g. '100%', '72px'). In vertical mode this sets the height. */\n width?: string\n orientation?: 'horizontal' | 'vertical'\n trackHeight?: string\n trackRadius?: string\n thumbWidth?: string\n thumbHeight?: string\n thumbRadius?: string\n /**\n * Reverse the value-to-position mapping. With orientation='vertical', sets\n * top=min and bottom=max (drag down to increase). With orientation='horizontal',\n * sets right=min and left=max (drag left to increase). Useful when an icon\n * legend implies the opposite direction of the underlying control.\n *\n * Delegates to reka-ui SliderRoot's native `inverted` prop, which swaps\n * startEdge/endEdge on SliderRange — no manual value transformation needed.\n */\n inverted?: boolean\n}>(), {\n min: 0,\n max: 100,\n step: 1,\n width: '100%',\n orientation: 'horizontal',\n trackHeight: '4px',\n trackRadius: '2px',\n thumbWidth: '14px',\n thumbHeight: '14px',\n thumbRadius: '50%',\n inverted: false,\n})\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: number]\n}>()\n\n// ── Internal state ────────────────────────────────────────────────────────────\n\nconst isVertical = computed(() => props.orientation === 'vertical')\n\n/** Root element inline style */\nconst rootStyle = computed(() => {\n if (isVertical.value) {\n return {\n '--thumb-w': props.thumbWidth,\n '--thumb-h': props.thumbHeight,\n '--thumb-r': props.thumbRadius,\n '--track-height': props.trackHeight,\n '--track-radius': props.trackRadius,\n height: props.width,\n width: '16px',\n }\n }\n return {\n '--thumb-w': props.thumbWidth,\n '--thumb-h': props.thumbHeight,\n '--thumb-r': props.thumbRadius,\n '--track-height': props.trackHeight,\n '--track-radius': props.trackRadius,\n width: props.width,\n }\n})\n\nfunction onValueChange(val: number[] | undefined) {\n if (val && val.length > 0) {\n emit('update:modelValue', val[0])\n }\n}\n</script>\n\n<template>\n <SliderRoot\n class=\"app-slider-root\"\n :class=\"{ 'app-slider-root--vertical': isVertical, 'app-slider-root--inverted': inverted }\"\n :style=\"rootStyle\"\n :model-value=\"[modelValue]\"\n :min=\"min\"\n :max=\"max\"\n :step=\"step\"\n :orientation=\"orientation\"\n :inverted=\"inverted\"\n @update:model-value=\"onValueChange\"\n >\n <SliderTrack class=\"app-slider-track\">\n <SliderRange class=\"app-slider-range\" />\n </SliderTrack>\n <SliderThumb class=\"app-slider-thumb\" />\n </SliderRoot>\n</template>\n\n<style scoped>\n/* ── Root container ────────────────────────────────────────────────────────── */\n.app-slider-root {\n position: relative;\n display: flex;\n align-items: center;\n user-select: none;\n touch-action: none;\n flex-shrink: 0;\n cursor: pointer;\n}\n\n.app-slider-root[data-orientation='horizontal'] {\n height: var(--track-height, 4px);\n}\n\n.app-slider-root[data-orientation='vertical'] {\n flex-direction: column;\n width: var(--track-height, 4px);\n}\n\n/* ── Track ─────────────────────────────────────────────────────────────────── */\n.app-slider-track {\n position: relative;\n flex-grow: 1;\n border-radius: var(--track-radius, 2px);\n background: var(--openpen-color-border-hi);\n}\n\n.app-slider-root[data-orientation='horizontal'] .app-slider-track {\n height: var(--track-height, 4px);\n}\n\n.app-slider-root[data-orientation='vertical'] .app-slider-track {\n width: var(--track-height, 4px);\n}\n\n/* ── Range (filled portion) ─────────────────────────────────────────────────── */\n.app-slider-range {\n position: absolute;\n border-radius: var(--track-radius, 2px);\n background: var(--openpen-color-accent);\n}\n\n.app-slider-root[data-orientation='horizontal'] .app-slider-range {\n height: 100%;\n top: 0;\n left: 0;\n}\n\n.app-slider-root[data-orientation='vertical'] .app-slider-range {\n width: 100%;\n bottom: 0;\n left: 0;\n}\n\n/* ── Thumb ─────────────────────────────────────────────────────────────────── */\n.app-slider-thumb {\n display: block;\n width: var(--thumb-w, 14px);\n height: var(--thumb-h, 14px);\n border-radius: var(--thumb-r, 50%);\n background: #fff;\n box-shadow: 0 1px 6px rgba(0, 0, 0, 0.50);\n cursor: pointer;\n outline: none;\n transition: transform var(--openpen-duration-fast) var(--openpen-easing-standard);\n}\n\n.app-slider-thumb:hover {\n transform: scale(1.2);\n}\n\n.app-slider-thumb:focus-visible {\n box-shadow: 0 0 0 3px var(--openpen-color-accent-glow);\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * AppToggle — High-level wrapper (Layer 1).\n *\n * Wraps Reka UI Switch (SwitchRoot / SwitchThumb).\n *\n * Visual: 40×24px, off=toggle-off-bg, on=accent,\n * 18×18 white thumb, bounce transition.\n *\n * Styles use --openpen-* design tokens.\n * No Reka UI types are leaked in the public API.\n *\n * DOM notes:\n * - SwitchRoot renders a <button role=\"switch\"> with class \"app-toggle\"\n * - \"on\" class is added when modelValue is true\n * - aria-pressed is added explicitly because Reka UI's SwitchRoot uses\n * aria-checked; both attributes co-exist for maximum a11y surface.\n */\nimport {\n SwitchRoot,\n SwitchThumb,\n} from '../primitives'\n\n// ── Public API (MUST NOT reference reka-ui types) ─────────────────────────────\n\nwithDefaults(defineProps<{\n modelValue: boolean\n ariaLabel?: string\n}>(), { ariaLabel: '' })\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: boolean]\n}>()\n\nfunction onCheckedChange(val: boolean) {\n emit('update:modelValue', val)\n}\n</script>\n\n<template>\n <SwitchRoot\n class=\"app-toggle\"\n :class=\"{ on: modelValue }\"\n :model-value=\"modelValue\"\n :aria-label=\"ariaLabel\"\n :aria-pressed=\"modelValue\"\n @update:model-value=\"onCheckedChange\"\n >\n <SwitchThumb class=\"app-toggle-thumb\" />\n </SwitchRoot>\n</template>\n\n<style scoped>\n/* Root — the toggle track */\n.app-toggle {\n width: 40px;\n height: 24px;\n border-radius: 12px;\n border: none;\n cursor: pointer;\n position: relative;\n flex-shrink: 0;\n background: var(--openpen-color-toggle-off);\n transition: background var(--openpen-duration-base) var(--openpen-easing-standard);\n /* Remove default button styles */\n padding: 0;\n appearance: none;\n -webkit-appearance: none;\n display: inline-flex;\n align-items: center;\n}\n\n.app-toggle.on {\n background: var(--openpen-color-accent);\n}\n\n/* Thumb — the white circular knob */\n.app-toggle-thumb {\n position: absolute;\n top: 3px;\n left: 3px;\n width: 18px;\n height: 18px;\n border-radius: 50%;\n background: #fff;\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);\n transition: left var(--openpen-duration-bounce) var(--openpen-easing-bounce);\n display: block;\n pointer-events: none;\n}\n\n.app-toggle.on .app-toggle-thumb,\n.app-toggle[data-state='checked'] .app-toggle-thumb {\n left: 19px;\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * AppSegmented — High-level wrapper (Layer 1).\n *\n * Wraps Reka UI RadioGroup (RadioGroupRoot / RadioGroupItem).\n *\n * Visual: flex gap 4px, 1.5px border var(--border-hi),\n * active = var(--accent-bg) background + var(--accent) color.\n *\n * Styles use --openpen-* design tokens.\n * No Reka UI types are leaked in the public API.\n */\nimport {\n RadioGroupRoot,\n RadioGroupItem,\n} from '../primitives'\n\n// ── Public API (MUST NOT reference reka-ui types) ─────────────────────────────\n\ninterface Option {\n value: string\n label: string\n icon?: string\n testid?: string\n}\n\ndefineProps<{\n modelValue: string\n options: Option[]\n /** When true, all options are non-interactive and visually dimmed. */\n disabled?: boolean\n}>()\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: string]\n}>()\n\n// AcceptableValue from Reka UI can be null; we narrow to string so the public API has no Reka UI type leakage.\nfunction onValueChange(val: unknown) {\n if (typeof val === 'string') {\n emit('update:modelValue', val)\n }\n}\n</script>\n\n<template>\n <RadioGroupRoot\n class=\"app-seg\"\n :class=\"{ 'app-seg--disabled': disabled }\"\n :model-value=\"modelValue\"\n :disabled=\"disabled\"\n orientation=\"horizontal\"\n @update:model-value=\"onValueChange\"\n >\n <RadioGroupItem\n v-for=\"opt in options\"\n :key=\"opt.value\"\n :value=\"opt.value\"\n as-child\n >\n <button\n class=\"app-seg-btn\"\n :class=\"{ active: modelValue === opt.value }\"\n :data-testid=\"opt.testid\"\n >\n <span\n v-if=\"opt.icon\"\n class=\"app-seg-icon\"\n aria-hidden=\"true\"\n v-html=\"opt.icon\"\n />\n <span>{{ opt.label }}</span>\n </button>\n </RadioGroupItem>\n </RadioGroupRoot>\n</template>\n\n<style scoped>\n.app-seg {\n display: flex;\n gap: var(--openpen-space-xs);\n}\n\n.app-seg-btn {\n flex: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n padding: 6px var(--openpen-space-lg);\n border: 1.5px solid var(--openpen-color-border-hi);\n border-radius: var(--openpen-radius-sm);\n background: var(--openpen-color-input-bg);\n color: var(--openpen-color-text-dim);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n white-space: nowrap;\n transition:\n background var(--openpen-duration-fast) var(--openpen-easing-standard),\n color var(--openpen-duration-fast) var(--openpen-easing-standard),\n border-color var(--openpen-duration-fast) var(--openpen-easing-standard);\n}\n\n.app-seg-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n line-height: 0;\n}\n\n.app-seg-btn:hover {\n color: var(--openpen-color-text-primary);\n background: var(--openpen-color-control-hover);\n}\n\n.app-seg-btn.active {\n background: var(--openpen-color-accent-bg);\n color: var(--openpen-color-accent);\n border-color: color-mix(in srgb, var(--openpen-color-accent) 40%, transparent);\n}\n\n.app-seg--disabled {\n opacity: 0.45;\n pointer-events: none;\n cursor: not-allowed;\n}\n</style>\n","/**\n * useDialogPluginComponent — composable for components rendered via useDialog().custom().\n *\n * The custom component calls this to get ok/cancel/dismiss callbacks, which\n * resolve the Promise returned by useDialog().custom().\n */\nimport { inject } from 'vue'\nimport { DIALOG_PLUGIN_HANDLE_KEY } from './dialog-internal-keys'\n\nexport interface DialogPluginComponentHandle<T = unknown> {\n /** Resolve with a payload value and close the dialog. */\n ok(payload?: T): void\n /** Resolve with null and close the dialog. */\n cancel(): void\n /** Alias for cancel() — resolves with null. */\n dismiss(): void\n}\n\nexport function useDialogPluginComponent<T = unknown>(): DialogPluginComponentHandle<T> {\n const handle = inject(DIALOG_PLUGIN_HANDLE_KEY)\n if (!handle) {\n throw new Error(\n 'useDialogPluginComponent() must be called inside a component rendered via useDialog().custom()'\n )\n }\n return handle as DialogPluginComponentHandle<T>\n}\n","<script setup lang=\"ts\">\n/**\n * AppSelect — High-level wrapper (Layer 1).\n *\n * Plugin authors use this component without needing to know about Reka UI's\n * Select primitives. The API surface is OpenPen-owned and stable.\n *\n * Public API is stable: switching the underlying headless library in the\n * future does not break the plugin author's code.\n *\n * Implementation notes:\n * - Controlled via modelValue / update:modelValue (standard Vue v-model pattern).\n * - options[] is an OpenPen-owned type; no Reka UI types leak to the public API.\n * - Popup styling mirrors surface-popup tokens (same as AppPopover content).\n * - Styles are unscoped because SelectContent is teleported via SelectPortal.\n */\nimport {\n SelectRoot,\n SelectTrigger,\n SelectPortal,\n SelectContent,\n SelectViewport,\n SelectItem,\n SelectItemText,\n SelectItemIndicator,\n SelectValue,\n SelectIcon,\n} from '../primitives'\n\n// ── OpenPen-owned types (MUST NOT reference reka-ui types) ───────────────────\n\n/** A single option in the dropdown list. */\nexport interface SelectOption {\n /** The value emitted on selection. */\n value: string\n /** The label shown to the user. */\n label: string\n}\n\nexport interface AppSelectProps {\n /** Currently selected value (bind with v-model). */\n modelValue: string\n /** List of selectable options. */\n options: SelectOption[]\n /** Placeholder shown when no option is selected. */\n placeholder: string\n /** Whether the select is disabled. */\n disabled?: boolean\n}\n\nconst props = withDefaults(defineProps<AppSelectProps>(), {\n disabled: false,\n})\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: string]\n}>()\n\nfunction onValueChange(v: string) {\n emit('update:modelValue', v)\n}\n</script>\n\n<template>\n <SelectRoot\n :model-value=\"modelValue\"\n :disabled=\"disabled\"\n @update:model-value=\"onValueChange\"\n >\n <SelectTrigger class=\"openpen-select-trigger\">\n <SelectValue :placeholder=\"placeholder\" />\n <SelectIcon class=\"openpen-select-icon\" aria-hidden=\"true\">\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" fill=\"none\">\n <path d=\"M2 4L6 8L10 4\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </SelectIcon>\n </SelectTrigger>\n <SelectPortal>\n <SelectContent class=\"openpen-select-content\" position=\"popper\" :side-offset=\"4\">\n <SelectViewport class=\"openpen-select-viewport\">\n <SelectItem\n v-for=\"option in options\"\n :key=\"option.value\"\n :value=\"option.value\"\n class=\"openpen-select-item\"\n >\n <SelectItemIndicator class=\"openpen-select-item-indicator\">\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M2 6L5 9L10 3\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </SelectItemIndicator>\n <SelectItemText>{{ option.label }}</SelectItemText>\n </SelectItem>\n </SelectViewport>\n </SelectContent>\n </SelectPortal>\n </SelectRoot>\n</template>\n\n<!-- Styles MUST be unscoped — Vue scoped CSS does not follow Teleport targets,\n so .openpen-select-* classes would not receive the scoped data-v-* attribute\n when rendered inside the portal. -->\n<style>\n/* ── Trigger ─────────────────────────────────────────────────────────────── */\n.openpen-select-trigger {\n display: inline-flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--openpen-space-sm);\n min-width: 120px;\n height: 32px;\n padding: 0 var(--openpen-space-md);\n background: var(--openpen-color-input-bg);\n border: 1px solid var(--openpen-color-border-hi);\n border-radius: var(--openpen-radius-md);\n color: var(--openpen-color-text-primary);\n font-size: 13px;\n cursor: pointer;\n transition:\n background var(--openpen-duration-fast),\n border-color var(--openpen-duration-fast);\n outline: none;\n white-space: nowrap;\n user-select: none;\n}\n\n.openpen-select-trigger:hover {\n background: var(--openpen-color-control-hover);\n border-color: var(--openpen-color-border-hi);\n}\n\n.openpen-select-trigger:focus-visible {\n border-color: var(--openpen-color-accent);\n box-shadow: 0 0 0 2px var(--openpen-color-accent-glow);\n}\n\n.openpen-select-trigger[data-disabled] {\n opacity: 0.45;\n cursor: not-allowed;\n}\n\n/* ── Trigger icon (chevron) ──────────────────────────────────────────────── */\n.openpen-select-icon {\n color: var(--openpen-color-text-dim);\n flex-shrink: 0;\n display: flex;\n align-items: center;\n transition: transform var(--openpen-duration-fast);\n}\n\n.openpen-select-trigger[data-state='open'] .openpen-select-icon {\n transform: rotate(180deg);\n}\n\n/* ── Dropdown content ────────────────────────────────────────────────────── */\n.openpen-select-content {\n background: var(--openpen-color-surface-popup);\n border: 1px solid var(--openpen-color-border-hi);\n border-radius: var(--openpen-radius-md);\n box-shadow: var(--openpen-shadow);\n backdrop-filter: var(--openpen-blur);\n -webkit-backdrop-filter: var(--openpen-blur);\n z-index: 100;\n pointer-events: auto;\n /* Match trigger width */\n min-width: var(--reka-select-trigger-width, 120px);\n max-height: 240px;\n overflow: hidden;\n}\n\n/* ── Viewport scrollable area ────────────────────────────────────────────── */\n.openpen-select-viewport {\n padding: var(--openpen-space-xs);\n}\n\n/* ── Individual items ────────────────────────────────────────────────────── */\n.openpen-select-item {\n display: flex;\n align-items: center;\n gap: var(--openpen-space-sm);\n height: 30px;\n padding: 0 var(--openpen-space-md);\n padding-left: 28px; /* indent to align text past indicator */\n border-radius: var(--openpen-radius-sm);\n font-size: 13px;\n color: var(--openpen-color-text-primary);\n cursor: pointer;\n position: relative;\n user-select: none;\n outline: none;\n transition: background var(--openpen-duration-fast);\n}\n\n.openpen-select-item[data-highlighted] {\n background: var(--openpen-color-control-hover);\n color: var(--openpen-color-text-primary);\n}\n\n.openpen-select-item[data-state='checked'] {\n color: var(--openpen-color-accent);\n}\n\n.openpen-select-item[data-disabled] {\n opacity: 0.45;\n cursor: not-allowed;\n}\n\n/* ── Selected item indicator (checkmark) ─────────────────────────────────── */\n.openpen-select-item-indicator {\n position: absolute;\n left: var(--openpen-space-sm);\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--openpen-color-accent);\n}\n\n/* ── Open / close animations ─────────────────────────────────────────────────\n Pattern matches AppPopover keyframes (200ms bounce enter, 150ms ease leave). */\n.openpen-select-content[data-state='open'] {\n animation: openpen-select-in 200ms cubic-bezier(0.34, 1.56, 0.64, 1);\n}\n.openpen-select-content[data-state='closed'] {\n animation: openpen-select-out 150ms ease forwards;\n}\n\n@keyframes openpen-select-in {\n from { opacity: 0; transform: scale(0.95); }\n to { opacity: 1; transform: scale(1); }\n}\n@keyframes openpen-select-out {\n from { opacity: 1; transform: scale(1); }\n to { opacity: 0; transform: scale(0.95); }\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * AppTooltip — High-level wrapper (Layer 1).\n *\n * Wraps Reka UI Tooltip (TooltipProvider / TooltipRoot / TooltipTrigger /\n * TooltipPortal / TooltipContent / TooltipArrow).\n *\n * Plugin authors get hover-triggered tooltips with OpenPen styling without\n * knowing about Reka UI internals or inject keys.\n *\n * Styles use --openpen-* design tokens.\n * No Reka UI types are leaked in the public API.\n *\n * Usage:\n * <AppTooltip content=\"Save file\" side=\"top\">\n * <button>Save</button>\n * </AppTooltip>\n */\nimport {\n TooltipProvider,\n TooltipRoot,\n TooltipTrigger,\n TooltipPortal,\n TooltipContent,\n TooltipArrow,\n} from '../primitives'\n\n// ── OpenPen-owned types (MUST NOT reference reka-ui types) ────────────────────\n\n/** Preferred side for tooltip content relative to the trigger. */\nexport type TooltipSide = 'top' | 'bottom' | 'left' | 'right'\n\nexport interface AppTooltipProps {\n /** Tooltip text content. */\n content: string\n /** Preferred placement side relative to the trigger. Default 'top'. */\n side?: TooltipSide\n /** Delay before tooltip appears on hover, in ms. Default 200. */\n delay?: number\n}\n\nexport interface AppTooltipSlots {\n /** Trigger element. Any element that receives hover events. */\n default: () => unknown\n}\n\nconst props = withDefaults(defineProps<AppTooltipProps>(), {\n side: 'top',\n delay: 200,\n})\n\ndefineSlots<AppTooltipSlots>()\n</script>\n\n<template>\n <TooltipProvider :delay-duration=\"props.delay\">\n <TooltipRoot>\n <TooltipTrigger as-child>\n <slot />\n </TooltipTrigger>\n <TooltipPortal>\n <TooltipContent\n :side=\"props.side\"\n :side-offset=\"6\"\n :avoid-collisions=\"true\"\n :collision-padding=\"8\"\n class=\"openpen-tooltip-content\"\n >\n {{ props.content }}\n <TooltipArrow class=\"openpen-tooltip-arrow\" :width=\"10\" :height=\"5\" />\n </TooltipContent>\n </TooltipPortal>\n </TooltipRoot>\n </TooltipProvider>\n</template>\n\n<!-- Styles for teleported TooltipContent MUST be unscoped — Vue scoped CSS\n does not follow Teleport targets (same rationale as AppPopover). -->\n<style>\n.openpen-tooltip-content {\n background: var(--openpen-color-tooltip-bg);\n border: 1px solid var(--openpen-color-border-hi);\n border-radius: var(--openpen-radius-sm);\n padding: 5px 9px;\n color: var(--openpen-color-tooltip-text);\n font-size: 12px;\n line-height: 1.4;\n /* Prevent text wrap for short tooltip labels */\n white-space: nowrap;\n box-shadow: var(--openpen-shadow-sm);\n z-index: 200;\n pointer-events: none;\n}\n\n.openpen-tooltip-arrow {\n fill: var(--openpen-color-tooltip-bg);\n}\n\n/* ── Open / close animations ────────────────────────────────────────────────\n Reka UI Presence keeps the element in the DOM until the CSS animation ends,\n so data-state=\"delayed-open\" / \"instant-open\" / \"closed\" all animate. */\n.openpen-tooltip-content[data-state='delayed-open'],\n.openpen-tooltip-content[data-state='instant-open'] {\n animation: openpen-tooltip-in var(--openpen-duration-fast) var(--openpen-easing-standard);\n}\n.openpen-tooltip-content[data-state='closed'] {\n animation: openpen-tooltip-out var(--openpen-duration-fast) ease forwards;\n}\n\n@keyframes openpen-tooltip-in {\n from { opacity: 0; transform: scale(0.95); }\n to { opacity: 1; transform: scale(1); }\n}\n@keyframes openpen-tooltip-out {\n from { opacity: 1; transform: scale(1); }\n to { opacity: 0; transform: scale(0.95); }\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * AppTabs — High-level wrapper (Layer 1).\n *\n * Wraps Reka UI Tabs (TabsRoot / TabsList / TabsTrigger / TabsContent).\n * Intended for settings UI tabbed navigation and any plugin tab panel.\n *\n * Plugin authors get a controlled tab switcher with OpenPen styling without\n * knowing about Reka UI internals.\n *\n * Styles use --openpen-* design tokens.\n * No Reka UI types are leaked in the public API.\n *\n * Usage:\n * <AppTabs\n * v-model=\"activeTab\"\n * :tabs=\"[{ id: 'general', label: 'General' }, { id: 'advanced', label: 'Advanced' }]\"\n * >\n * <template #default=\"{ activeTabId }\">\n * <div v-if=\"activeTabId === 'general'\">...</div>\n * <div v-if=\"activeTabId === 'advanced'\">...</div>\n * </template>\n * </AppTabs>\n */\nimport {\n TabsRoot,\n TabsList,\n TabsTrigger,\n TabsContent,\n} from '../primitives'\n\n// ── OpenPen-owned types (MUST NOT reference reka-ui types) ────────────────────\n\nexport interface AppTabItem {\n /** Unique identifier for this tab. Used as the v-model value. */\n id: string\n /** Human-readable label rendered in the tab trigger button. */\n label: string\n}\n\nexport interface AppTabsProps {\n /** Currently active tab id (controlled mode). */\n modelValue: string\n /** Ordered list of tab descriptors. */\n tabs: AppTabItem[]\n}\n\nexport interface AppTabsSlots {\n /**\n * Default slot for tab content area.\n * Receives the currently active tab id via scoped slot.\n */\n default: (scope: { activeTabId: string }) => unknown\n}\n\nconst props = defineProps<AppTabsProps>()\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: string]\n}>()\n\ndefineSlots<AppTabsSlots>()\n\nfunction onTabChange(val: string | number | undefined) {\n if (typeof val === 'string') {\n emit('update:modelValue', val)\n }\n}\n</script>\n\n<template>\n <TabsRoot\n :model-value=\"props.modelValue\"\n class=\"openpen-tabs\"\n @update:model-value=\"onTabChange\"\n >\n <TabsList class=\"openpen-tabs-list\">\n <TabsTrigger\n v-for=\"tab in props.tabs\"\n :key=\"tab.id\"\n :value=\"tab.id\"\n class=\"openpen-tabs-trigger\"\n >\n {{ tab.label }}\n </TabsTrigger>\n </TabsList>\n\n <!-- Single content outlet: plugin author renders tab content via scoped slot. -->\n <TabsContent\n v-for=\"tab in props.tabs\"\n :key=\"tab.id\"\n :value=\"tab.id\"\n class=\"openpen-tabs-content\"\n >\n <slot :active-tab-id=\"tab.id\" />\n </TabsContent>\n </TabsRoot>\n</template>\n\n<style scoped>\n/* ── Tab root ───────────────────────────────────────────────────────────────── */\n.openpen-tabs {\n display: flex;\n flex-direction: column;\n gap: var(--openpen-space-sm);\n}\n\n/* ── Tab list (trigger bar) ─────────────────────────────────────────────────── */\n.openpen-tabs-list {\n display: flex;\n flex-direction: row;\n gap: 2px;\n padding: 3px;\n background: var(--openpen-color-control-group);\n border: 1px solid var(--openpen-color-border);\n border-radius: var(--openpen-radius-md);\n}\n\n/* ── Individual trigger ─────────────────────────────────────────────────────── */\n.openpen-tabs-trigger {\n flex: 1;\n padding: var(--openpen-space-xs) var(--openpen-space-sm);\n border: none;\n border-radius: calc(var(--openpen-radius-md) - 3px);\n background: transparent;\n color: var(--openpen-color-text-dim);\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n transition:\n background var(--openpen-duration-fast) var(--openpen-easing-standard),\n color var(--openpen-duration-fast) var(--openpen-easing-standard);\n white-space: nowrap;\n /* Remove default button outline; focus-visible ring below handles a11y. */\n outline: none;\n}\n\n.openpen-tabs-trigger:hover {\n background: var(--openpen-color-control-hover);\n color: var(--openpen-color-text-primary);\n}\n\n.openpen-tabs-trigger[data-state='active'] {\n background: var(--openpen-color-accent-bg);\n color: var(--openpen-color-accent);\n box-shadow: 0 0 0 1px var(--openpen-color-accent-glow);\n}\n\n.openpen-tabs-trigger:focus-visible {\n box-shadow: 0 0 0 2px var(--openpen-color-accent);\n}\n\n/* ── Tab content ────────────────────────────────────────────────────────────── */\n.openpen-tabs-content {\n /* Content area has no chrome by default; plugin authors add their own. */\n outline: none;\n}\n\n.openpen-tabs-content:focus-visible {\n box-shadow: 0 0 0 2px var(--openpen-color-accent);\n border-radius: var(--openpen-radius-sm);\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * AppBanner — High-level wrapper (Layer 1).\n *\n * Presents info / warning / success / error status messages using the\n * --openpen-color-state-* design tokens. The icon is auto-rendered based on\n * the variant; message content goes in the default slot; optional inline action\n * buttons (e.g. restart / dismiss) go in the `#actions` slot.\n *\n * Inline mode (compact single-line) is enabled via the `inline` prop and is\n * suited for form validation feedback and status lines inside dialogs.\n */\n\nexport type BannerVariant = 'info' | 'warning' | 'success' | 'error'\n\nwithDefaults(defineProps<{\n variant: BannerVariant\n /** Compact single-line layout: smaller padding, vertically centered. */\n inline?: boolean\n}>(), { inline: false })\n</script>\n\n<template>\n <div\n :class=\"['app-banner', `app-banner-${variant}`, { 'app-banner-inline': inline }]\"\n :role=\"variant === 'error' ? 'alert' : 'status'\"\n >\n <span class=\"app-banner-icon\" aria-hidden=\"true\">\n <!-- info -->\n <svg v-if=\"variant === 'info'\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"12\" cy=\"12\" r=\"10\"/><line x1=\"12\" y1=\"8\" x2=\"12\" y2=\"12\"/><line x1=\"12\" y1=\"16\" x2=\"12.01\" y2=\"16\"/>\n </svg>\n <!-- warning -->\n <svg v-else-if=\"variant === 'warning'\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/><line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"/><line x1=\"12\" y1=\"17\" x2=\"12.01\" y2=\"17\"/>\n </svg>\n <!-- success -->\n <svg v-else-if=\"variant === 'success'\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M22 11.08V12a10 10 0 1 1-5.93-9.14\"/><polyline points=\"22 4 12 14.01 9 11.01\"/>\n </svg>\n <!-- error -->\n <svg v-else width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"12\" cy=\"12\" r=\"10\"/><line x1=\"15\" y1=\"9\" x2=\"9\" y2=\"15\"/><line x1=\"9\" y1=\"9\" x2=\"15\" y2=\"15\"/>\n </svg>\n </span>\n\n <span class=\"app-banner-body\"><slot /></span>\n\n <span v-if=\"$slots.actions\" class=\"app-banner-actions\">\n <slot name=\"actions\" />\n </span>\n </div>\n</template>\n\n<style scoped>\n.app-banner {\n display: flex;\n align-items: flex-start;\n gap: 10px;\n padding: 10px 14px;\n border-radius: var(--openpen-radius-md, 10px);\n border: 1px solid;\n font-size: 13px;\n line-height: 1.55;\n}\n\n.app-banner-inline {\n padding: 7px 12px;\n align-items: center;\n font-size: 12px;\n}\n\n.app-banner-icon {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n margin-top: 1px;\n}\n\n.app-banner-inline .app-banner-icon { margin-top: 0; }\n\n.app-banner-body {\n flex: 1;\n min-width: 0;\n}\n\n.app-banner-actions {\n display: flex;\n align-items: center;\n gap: 6px;\n flex-shrink: 0;\n margin-left: auto;\n}\n\n/* Variants */\n.app-banner-info {\n background: var(--openpen-color-state-info-bg);\n border-color: var(--openpen-color-state-info-border);\n color: var(--openpen-color-state-info-text);\n}\n.app-banner-info .app-banner-icon {\n color: var(--openpen-color-state-info-icon);\n}\n\n.app-banner-warning {\n background: var(--openpen-color-state-warning-bg);\n border-color: var(--openpen-color-state-warning-border);\n color: var(--openpen-color-state-warning-text);\n}\n.app-banner-warning .app-banner-icon {\n color: var(--openpen-color-state-warning-icon);\n}\n\n.app-banner-success {\n background: var(--openpen-color-state-success-bg);\n border-color: var(--openpen-color-state-success-border);\n color: var(--openpen-color-state-success-text);\n}\n.app-banner-success .app-banner-icon {\n color: var(--openpen-color-state-success-icon);\n}\n\n.app-banner-error {\n background: var(--openpen-color-state-error-bg);\n border-color: var(--openpen-color-state-error-border);\n color: var(--openpen-color-state-error-text);\n}\n.app-banner-error .app-banner-icon {\n color: var(--openpen-color-state-error-icon);\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * AppButton — control-bar button wrapper (Layer 1).\n *\n * Provides the standard 36×36 rounded button appearance that matches the\n * host's control-bar buttons, so plugins do not need to replicate the\n * visual design manually.\n *\n * Disabled state uses aria-disabled + a CSS class rather than the HTML\n * `disabled` attribute, because `disabled` removes pointer events and would\n * swallow the tooltip hover that this component is expected to keep alive.\n *\n * In a vertical control bar the button shrinks to 34×34 and the tooltip\n * points away from the snapped edge (snap-left → right side; snap-right →\n * left side; vbar-free → right side). In a horizontal bar the tooltip sits\n * above the button by default, but flips below when the bar is near the\n * workArea top edge (the host raises TOOLTIP_FLIP_DOWN_KEY in that case).\n *\n * All orientation signals come from the host via injection\n * (IS_VERTICAL_KEY / SNAP_EDGE_KEY / TOOLTIP_FLIP_DOWN_KEY) so plugin\n * buttons render identically to the host's own control-bar buttons.\n */\n\nimport { computed, inject } from 'vue'\nimport { SNAP_EDGE_KEY, IS_VERTICAL_KEY, TOOLTIP_FLIP_DOWN_KEY } from '../../inject-keys'\n\n// ── Public API ────────────────────────────────────────────────────────────────\n\nconst props = withDefaults(defineProps<{\n variant?: 'default' | 'danger'\n active?: boolean\n disabled?: boolean\n tooltip?: string\n ariaLabel?: string\n}>(), {\n variant: 'default',\n active: false,\n disabled: false,\n tooltip: undefined,\n ariaLabel: undefined,\n})\n\nconst emit = defineEmits<{\n click: []\n}>()\n\nfunction handleClick() {\n if (!props.disabled) {\n emit('click')\n }\n}\n\n// ── Orientation context (host-injected) ───────────────────────────────────────\n\nconst isVertical = inject(IS_VERTICAL_KEY, undefined)\nconst snapEdge = inject(SNAP_EDGE_KEY, undefined)\nconst tooltipFlipDown = inject(TOOLTIP_FLIP_DOWN_KEY, undefined)\n\nconst tooltipSide = computed<'top' | 'top-flip' | 'right' | 'left'>(() => {\n if (isVertical?.value) {\n return snapEdge?.value === 'right' ? 'left' : 'right'\n }\n return tooltipFlipDown?.value ? 'top-flip' : 'top'\n})\n</script>\n\n<template>\n <button\n class=\"app-btn\"\n :class=\"[\n `app-btn--tooltip-${tooltipSide}`,\n {\n 'app-btn--vertical': isVertical,\n active,\n danger: variant === 'danger',\n 'app-btn-disabled': disabled,\n },\n ]\"\n :data-tip=\"tooltip\"\n :aria-label=\"ariaLabel\"\n :aria-disabled=\"disabled || undefined\"\n @click=\"handleClick\"\n >\n <slot />\n </button>\n</template>\n\n<style scoped>\n.app-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 36px;\n height: 36px;\n border: none;\n border-radius: var(--openpen-radius-md);\n background: transparent;\n color: var(--openpen-color-text-dim);\n cursor: pointer;\n transition: background var(--openpen-duration-fast), color var(--openpen-duration-fast), transform var(--openpen-duration-fast);\n position: relative;\n flex-shrink: 0;\n padding: 0;\n}\n\n/* Vertical bars shrink the button to 34×34 to match the host's\n `.control-bar.is-vertical .cb-btn` rule. */\n.app-btn.app-btn--vertical {\n width: 34px;\n height: 34px;\n}\n\n.app-btn:hover {\n background: var(--openpen-color-control-hover);\n color: var(--openpen-color-text-primary);\n}\n\n.app-btn:active {\n transform: scale(0.88);\n}\n\n.app-btn.active {\n background: var(--openpen-color-accent-bg);\n color: var(--openpen-color-accent);\n box-shadow: 0 0 0 1px var(--openpen-color-accent-glow);\n}\n\n.app-btn.danger {\n color: var(--openpen-color-danger);\n}\n\n.app-btn.danger:hover {\n background: var(--openpen-color-danger-hover-bg);\n color: var(--openpen-color-danger-hover);\n}\n\n/* Disabled: keeps pointer events alive so the tooltip ::after hover still fires. */\n.app-btn.app-btn-disabled {\n cursor: not-allowed;\n}\n\n/* Dim only the button content — tooltip pseudo-element stays at full opacity. */\n.app-btn.app-btn-disabled > * {\n opacity: 0.30;\n}\n\n/* Tooltip — horizontal mode default (above the button). */\n.app-btn[data-tip]::after {\n content: attr(data-tip);\n position: absolute;\n bottom: calc(100% + 8px);\n left: 50%;\n transform: translateX(-50%);\n background: var(--openpen-color-tooltip-bg);\n border: 1px solid var(--openpen-color-tooltip-border);\n color: var(--openpen-color-tooltip-text);\n font-size: 11.5px;\n font-weight: 500;\n white-space: nowrap;\n padding: 5px 10px;\n border-radius: var(--openpen-radius-sm);\n pointer-events: none;\n opacity: 0;\n transition: opacity var(--openpen-duration-fast);\n box-shadow: var(--openpen-shadow-sm);\n z-index: 41;\n}\n\n/* Horizontal mode near workArea top: tooltip flips below the button so\n it does not extend off-screen. The host sets TOOLTIP_FLIP_DOWN_KEY when\n the ball is within ~60 px of the top edge. */\n.app-btn.app-btn--tooltip-top-flip[data-tip]::after {\n bottom: auto;\n top: calc(100% + 8px);\n}\n\n/* Vertical bar, snap-left or vbar-free: tooltip points right (inward). */\n.app-btn.app-btn--tooltip-right[data-tip]::after {\n top: 50%;\n bottom: auto;\n left: calc(100% + 8px);\n right: auto;\n transform: translateY(-50%);\n}\n\n/* Vertical bar, snap-right: tooltip points left (inward). */\n.app-btn.app-btn--tooltip-left[data-tip]::after {\n top: 50%;\n bottom: auto;\n left: auto;\n right: calc(100% + 8px);\n transform: translateY(-50%);\n}\n\n.app-btn[data-tip]:hover::after {\n opacity: 1;\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * AppButtonDropdown — split-mode dropdown button wrapper (Layer 1).\n *\n * Pairs an AppButton (main action) with a narrow caret button that toggles\n * an AppPopover. Modelled after Quasar's QBtnDropdown split-mode and shadcn\n * Button + DropdownMenu composition: main click and caret click are\n * independent events so the host can activate a tool on the main click\n * and open the dropdown on the caret click (or activate AND open from\n * the caret when the tool is currently inactive).\n *\n * Caret icon rotation tracks the host's snap edge and vertical layout via\n * the same injection keys AppButton uses — plugin authors get the right\n * rotation behaviour without configuring anything.\n *\n * Visual identity:\n * - Main button: identical to AppButton (36×36 horizontal, 34×34 vertical)\n * - Caret: 16 wide × 36 tall in horizontal mode (14 × 30 in vertical),\n * hover background + accent on active, chevron rotates toward the\n * popover side\n * - Wrap: flex row in horizontal mode, flex column in vertical\n *\n * The structural classes (`app-btn-dropdown-wrap`, `app-btn-dropdown-caret`,\n * `app-btn-dropdown-caret-icon`) are deliberately stable so host CSS can\n * apply contextual overrides (e.g. `.cb-group--inset .app-btn-dropdown-caret`\n * shrinks the caret inside inset groups).\n */\nimport { computed, inject } from 'vue'\nimport { SNAP_EDGE_KEY, IS_VERTICAL_KEY } from '../../inject-keys'\nimport AppButton from './AppButton.vue'\nimport AppPopover from './AppPopover.vue'\n\n// ── Public API ────────────────────────────────────────────────────────────────\n\nconst props = withDefaults(defineProps<{\n /** Globally unique popover id (passed through to the inner AppPopover). */\n popoverId: string\n /** Preferred popover placement; defaults to 'auto'. */\n popoverPlacement?: 'auto' | 'top' | 'bottom' | 'left' | 'right'\n /** Main button active state (drives accent highlight). */\n active?: boolean\n /** Disables both main and caret buttons; tooltip remains active. */\n disabled?: boolean\n /** Main button variant (passed through to AppButton). */\n variant?: 'default' | 'danger'\n /** Tooltip shown above the main button. */\n mainTooltip?: string\n /** Main button aria-label. */\n mainAriaLabel?: string\n /** Caret button aria-label. Required for screen-reader users. */\n caretAriaLabel?: string\n /** Optional test ids forwarded to the rendered buttons. */\n mainTestid?: string\n caretTestid?: string\n}>(), {\n popoverPlacement: 'auto',\n active: false,\n disabled: false,\n variant: 'default',\n mainTooltip: undefined,\n mainAriaLabel: undefined,\n caretAriaLabel: undefined,\n mainTestid: undefined,\n caretTestid: undefined,\n})\n\nconst emit = defineEmits<{\n /** Fires when the main button is clicked (unless `disabled`). */\n mainClick: []\n /**\n * Fires when the caret button is clicked (unless `disabled`).\n * AppPopover handles open/close via its own trigger logic; this event\n * lets the host run side-effects (e.g. activate the tool if it isn't\n * already active before opening the dropdown).\n */\n caretClick: []\n}>()\n\n// ── Orientation context (host-injected) ───────────────────────────────────────\n\nconst isVertical = inject(IS_VERTICAL_KEY, undefined)\nconst snapEdge = inject(SNAP_EDGE_KEY, undefined)\n\n/**\n * Caret chevron rotation when the popover is open.\n *\n * - inactive → down (resting state, points at the popover that will appear)\n * - horizontal + active → up (popover is above, chevron points to it)\n * - vertical + snap-left or vbar-free + active → right (popover opens right)\n * - vertical + snap-right + active → left (popover opens left)\n */\nfunction caretRotation(open: boolean): 'down' | 'up' | 'left' | 'right' {\n if (!open) return 'down'\n if (!isVertical?.value) return 'up'\n return snapEdge?.value === 'right' ? 'left' : 'right'\n}\n\nfunction onMainClick(): void {\n if (!props.disabled) emit('mainClick')\n}\n\nfunction onCaretClick(): void {\n if (!props.disabled) emit('caretClick')\n}\n</script>\n\n<template>\n <div\n class=\"app-btn-dropdown-wrap\"\n :class=\"{ 'app-btn-dropdown-wrap--vertical': isVertical }\"\n >\n <AppButton\n :variant=\"variant\"\n :active=\"active\"\n :disabled=\"disabled\"\n :tooltip=\"mainTooltip\"\n :aria-label=\"mainAriaLabel\"\n :data-testid=\"mainTestid\"\n @click=\"onMainClick\"\n >\n <slot name=\"main-content\" />\n </AppButton>\n\n <AppPopover :popover-id=\"popoverId\" :placement=\"popoverPlacement\">\n <template #trigger=\"{ active: open }\">\n <button\n type=\"button\"\n class=\"app-btn-dropdown-caret\"\n :class=\"{\n active: open,\n 'app-btn-dropdown-caret--vertical': isVertical,\n 'app-btn-dropdown-caret--disabled': disabled,\n }\"\n :aria-label=\"caretAriaLabel\"\n :aria-disabled=\"disabled || undefined\"\n :data-testid=\"caretTestid\"\n @click=\"onCaretClick\"\n >\n <svg\n class=\"app-btn-dropdown-caret-icon\"\n :class=\"`app-btn-dropdown-caret-icon--${caretRotation(open)}`\"\n width=\"10\"\n height=\"10\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2.5\"\n >\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </button>\n </template>\n <template #content>\n <slot name=\"popover-content\" />\n </template>\n </AppPopover>\n </div>\n</template>\n\n<style>\n/* Structural classes ship UNSCOPED on purpose so host CSS can apply\n contextual overrides (e.g. `.cb-group--inset .app-btn-dropdown-caret`\n shrinks the caret inside inset groups in the host control bar). The\n classes are deliberately prefixed `app-btn-dropdown-*` to avoid\n collisions in plugin code. */\n.app-btn-dropdown-wrap {\n display: flex;\n align-items: center;\n gap: 0;\n -webkit-app-region: no-drag;\n}\n\n.app-btn-dropdown-wrap--vertical {\n flex-direction: column;\n gap: 2px;\n}\n\n.app-btn-dropdown-caret {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 16px;\n height: 36px;\n border: none;\n background: transparent;\n color: var(--openpen-color-text-dim);\n cursor: pointer;\n padding: 0;\n border-radius: 8px;\n transition: color var(--openpen-duration-fast), background var(--openpen-duration-fast);\n}\n\n.app-btn-dropdown-caret--vertical {\n width: 30px;\n height: 14px;\n}\n\n.app-btn-dropdown-caret:hover {\n color: var(--openpen-color-text-primary);\n background: var(--openpen-color-control-hover);\n}\n\n.app-btn-dropdown-caret.active {\n color: var(--openpen-color-accent);\n background: var(--openpen-color-accent-bg);\n}\n\n.app-btn-dropdown-caret--disabled {\n cursor: not-allowed;\n}\n\n.app-btn-dropdown-caret--disabled > * {\n opacity: 0.30;\n}\n\n.app-btn-dropdown-caret-icon {\n transition: transform 180ms cubic-bezier(0.34, 1.56, 0.64, 1);\n}\n\n.app-btn-dropdown-caret-icon--down { transform: rotate(0deg); }\n.app-btn-dropdown-caret-icon--up { transform: rotate(180deg); }\n.app-btn-dropdown-caret-icon--right { transform: rotate(-90deg); }\n.app-btn-dropdown-caret-icon--left { transform: rotate(90deg); }\n</style>\n"],"names":["_createBlock","_unref","_createVNode","_renderSlot","_normalizeClass","_createElementBlock","_Fragment","_renderList","_createElementVNode","_toDisplayString","_createTextVNode","_openBlock","$slots","AppPopover","_withCtx"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../src/uikit/components/AppPopover.vue","../../src/uikit/components/AppSlider.vue","../../src/uikit/components/AppToggle.vue","../../src/uikit/components/AppSegmented.vue","../../src/uikit/composables/useDialogPluginComponent.ts","../../src/uikit/components/AppSelect.vue","../../src/uikit/components/AppTooltip.vue","../../src/uikit/components/AppTabs.vue","../../src/uikit/components/AppBanner.vue","../../src/uikit/components/AppButton.vue","../../src/uikit/components/AppButtonDropdown.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * AppPopover — High-level wrapper (Layer 1).\n *\n * Plugin authors use this component without needing to know about inject\n * keys, Reka UI, modal manager mutual exclusion, or the animating guard.\n *\n * Public API is stable: switching the underlying headless library in the\n * future does not break the plugin author's code.\n *\n * Implementation notes:\n * - placement 'auto' picks the initial Reka UI side based on snap edge:\n * vbar-left → 'right', vbar-right → 'left', horizontal → 'bottom'.\n * Reka UI's built-in flip middleware then handles viewport-edge collision.\n * - usePassthroughGuard: called with the content element ref. The contentEl\n * ref is passed to usePassthroughGuard, which attaches mousemove/leave\n * handlers that toggle mouse passthrough based on pointer position.\n */\nimport { computed, inject, onMounted, ref, useTemplateRef, watch } from 'vue'\nimport {\n PopoverRoot,\n PopoverTrigger,\n PopoverPortal,\n PopoverContent,\n PopoverArrow,\n} from '../primitives'\n// Inject keys MUST come from the package barrel — the runtime build\n// externalises '@openpen/module-api' so host + uikit share the same Symbol\n// instances. Importing from the relative '../../inject-keys' path bundles a\n// duplicate Symbol into module-api-uikit.js, causing host's provide() and\n// uikit's inject() to miss each other (popover never opens, etc).\nimport {\n WRAPPER_EL_KEY,\n MODAL_MANAGER_KEY,\n CONTROL_BAR_ANIMATING_KEY,\n POPOVER_PLACEMENT_HINT_KEY,\n} from '@openpen/module-api'\nimport { usePassthroughGuard } from '../../host/passthrough'\n\n// ── OpenPen-owned type (MUST NOT reference reka-ui types) ─────────────────────\n\n/** Preferred placement for the popover content relative to the trigger. */\nexport type Placement = 'auto' | 'top' | 'bottom' | 'left' | 'right'\n\nexport interface AppPopoverProps {\n /** Globally unique popover id; used for modal-stack mutual exclusion. */\n popoverId: string\n /**\n * Popover placement direction.\n * - 'auto' (default, recommended): the host's snap-edge hint picks the side that\n * keeps the popover on-screen across all control-bar modes.\n * - explicit values ('top' | 'bottom' | 'left' | 'right'): bypass the auto hint.\n * In dev mode a console warning is emitted because explicit values can position\n * popovers off-screen or away from the trigger button when the control bar is\n * in a vertical mode. Only use when you have verified the placement stays\n * on-screen across all snap layouts.\n */\n placement?: Placement\n /** Gap between trigger and content in px. Default 8. */\n gap?: number\n}\n\nexport interface AppPopoverSlots {\n /** Trigger slot; scope exposes active state + toggle / open / close actions. */\n trigger: (scope: {\n active: boolean\n toggle: () => void\n open: () => void\n close: () => void\n }) => unknown\n /** Popover content slot. */\n content: () => unknown\n}\n\nconst props = withDefaults(defineProps<AppPopoverProps>(), {\n placement: 'auto',\n gap: 8,\n})\n\ndefineSlots<AppPopoverSlots>()\n\n// ── 0. Dev-mode guard: warn plugin authors about risky explicit placement ─────\nonMounted(() => {\n if (!import.meta.env.DEV) return\n if (props.placement !== 'auto' && props.placement !== undefined) {\n console.warn(\n `[AppPopover] Explicit placement=\"${props.placement}\" bypasses ` +\n `the host's snap-edge auto detection. In vertical control-bar modes ` +\n `this can position popups off-screen or away from the trigger button. ` +\n `Prefer placement=\"auto\" (default) and let the host pick the side; ` +\n `use an explicit value only when you have verified it stays on-screen ` +\n `across snap-left / snap-right / snap-free / horizontal layouts.`\n )\n }\n})\n\n// ── 1. Inject host context (plugin author never sees these) ───────────────────\n\nconst wrapperElRef = inject(WRAPPER_EL_KEY)\nconst wrapperEl = computed<HTMLElement | null>(() => wrapperElRef?.value ?? null)\nconst modalManager = inject(MODAL_MANAGER_KEY)\nconst animating = inject(CONTROL_BAR_ANIMATING_KEY)\nconst popoverPlacementHint = inject(POPOVER_PLACEMENT_HINT_KEY)\n\n// ── 2. Modal manager mutual exclusion ────────────────────────────────────────\n\nconst isOpen = computed(() => modalManager?.isOpen(props.popoverId) ?? false)\n\nfunction open() {\n modalManager?.open(props.popoverId)\n}\n\nfunction close() {\n modalManager?.close(props.popoverId)\n}\n\nfunction toggle() {\n if (isOpen.value) {\n close()\n } else {\n open()\n }\n}\n\n// Sync Reka UI's controlled open state back to modal manager.\n// Reka UI may close via Escape / outside-click; we must keep manager in sync.\nfunction onUpdateOpen(v: boolean) {\n if (v) {\n open()\n } else {\n close()\n }\n}\n\n// ── 3. ControlBar animation guard — auto-close during transitions ────────────\n\nwatch(\n () => animating?.value,\n (isAnimating) => {\n if (isAnimating && isOpen.value) {\n close()\n }\n }\n)\n\n// ── 4. placement → Reka UI side mapping ──────────────────────────────────────\n//\n// 'auto' uses the POPOVER_PLACEMENT_HINT_KEY provided by ControlBar. The hint\n// encodes a two-tier UX rule:\n// - Snap edge set → open away from the edge (left→right, right→left, etc.)\n// - Snap off → open toward the side with more screen space on the relevant axis\n// (horizontal bar: top/bottom; vertical bar: left/right)\n// Reka UI's built-in flip middleware handles secondary overflow. The window is\n// workArea-sized, so the viewport equals the workArea and floating-ui's collision\n// detection is correct without any boundary-proxy workaround.\n\ntype RekaSide = 'top' | 'bottom' | 'left' | 'right'\n\nconst rekaSide = computed<RekaSide>(() => {\n if (props.placement !== 'auto') return props.placement as RekaSide\n // Fall back to 'bottom' only if no hint is provided (e.g. standalone use outside ControlBar).\n return popoverPlacementHint?.value ?? 'bottom'\n})\n\n// ── 5. Passthrough guard integration ─────────────────────────────────────────\n//\n// usePassthroughGuard is called with the content element ref. Reka UI's\n// PopoverContent is a Vue component, so the template ref resolves to a\n// component instance (with `$el`) rather than a raw HTMLElement. We normalize\n// via a watcher so the guard receives a plain element ref.\n// openpen-interactive class enables the ControlBar passthrough guard's CSS selector path.\nconst contentInstance = useTemplateRef<HTMLElement | { $el?: HTMLElement } | null>('content')\nconst contentEl = ref<HTMLElement | null>(null)\nwatch(contentInstance, (instance) => {\n if (!instance) {\n contentEl.value = null\n } else if (instance instanceof HTMLElement) {\n contentEl.value = instance\n } else if (typeof instance === 'object' && '$el' in instance && instance.$el instanceof HTMLElement) {\n contentEl.value = instance.$el\n } else {\n contentEl.value = null\n }\n})\nusePassthroughGuard(contentEl)\n</script>\n\n<template>\n <PopoverRoot :open=\"isOpen\" @update:open=\"onUpdateOpen\">\n <PopoverTrigger as-child>\n <slot\n name=\"trigger\"\n :active=\"isOpen\"\n :toggle=\"toggle\"\n :open=\"open\"\n :close=\"close\"\n />\n </PopoverTrigger>\n <PopoverPortal :to=\"wrapperEl ?? undefined\">\n <PopoverContent\n ref=\"content\"\n :side=\"rekaSide\"\n :avoid-collisions=\"true\"\n :side-offset=\"props.gap\"\n class=\"openpen-popover-content openpen-interactive\"\n data-testid=\"controlbar-popover\"\n >\n <slot name=\"content\" />\n <!-- Arrow geometry: 14×8 sharp triangle. Fill shares\n --openpen-color-popover-frame with the popup border so the arrow\n reads as a continuous extension of the frame. See tokens.css for\n per-theme values. -->\n <PopoverArrow class=\"openpen-popover-arrow\" :width=\"14\" :height=\"8\" />\n </PopoverContent>\n </PopoverPortal>\n </PopoverRoot>\n</template>\n\n<!-- Styles for teleported PopoverContent MUST be unscoped — Vue scoped CSS\n does not follow Teleport targets, so .openpen-popover-content would not\n receive the scoped data-v-* attribute when rendered inside the portal. -->\n<style>\n.openpen-popover-content {\n background: var(--openpen-color-surface-popup);\n border: 1px solid var(--openpen-color-popover-frame);\n border-radius: var(--openpen-radius-lg);\n padding: 14px 16px;\n color: var(--openpen-color-text-primary);\n font-size: 13px;\n box-shadow: var(--openpen-shadow);\n backdrop-filter: var(--openpen-blur);\n -webkit-backdrop-filter: var(--openpen-blur);\n z-index: 100;\n /* Prevent pointer events from leaking through transparent gaps. */\n pointer-events: auto;\n}\n\n.openpen-popover-arrow {\n fill: var(--openpen-color-popover-frame);\n}\n\n/* ── Open / close animations ────────────────────────────────────────────────\n Reka UI's Presence keeps the element in the DOM until the running CSS\n animation ends, so animations targeting [data-state=\"closed\"] DO play\n on close. transform-origin tracks data-side so the popover scales out\n from the trigger instead of from its own center.\n\n Timings: 200ms easeOutBack enter, 150ms ease leave. */\n.openpen-popover-content[data-state='open'] {\n animation: openpen-popover-in 200ms cubic-bezier(0.34, 1.56, 0.64, 1);\n}\n.openpen-popover-content[data-state='closed'] {\n animation: openpen-popover-out 150ms ease forwards;\n}\n\n.openpen-popover-content[data-side='bottom'] { transform-origin: top center; }\n.openpen-popover-content[data-side='top'] { transform-origin: bottom center; }\n.openpen-popover-content[data-side='left'] { transform-origin: right center; }\n.openpen-popover-content[data-side='right'] { transform-origin: left center; }\n\n@keyframes openpen-popover-in {\n from { opacity: 0; transform: scale(0.92); }\n to { opacity: 1; transform: scale(1); }\n}\n@keyframes openpen-popover-out {\n from { opacity: 1; transform: scale(1); }\n to { opacity: 0; transform: scale(0.92); }\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * AppSlider — High-level wrapper (Layer 1).\n *\n * Wraps Reka UI Slider (SliderRoot / SliderTrack / SliderRange / SliderThumb).\n *\n * Visual: 4px track, 14×14 white thumb, accent fill,\n * hover scale(1.2) at var(--openpen-duration-fast) easing.\n *\n * Styles use --openpen-* design tokens.\n * No Reka UI types are leaked in the public API.\n */\nimport { computed } from 'vue'\nimport {\n SliderRoot,\n SliderTrack,\n SliderRange,\n SliderThumb,\n} from '../primitives'\n\n// ── Public API (MUST NOT reference reka-ui types) ─────────────────────────────\n\nconst props = withDefaults(defineProps<{\n modelValue: number\n min?: number\n max?: number\n step?: number\n /** Container width (e.g. '100%', '72px'). In vertical mode this sets the height. */\n width?: string\n orientation?: 'horizontal' | 'vertical'\n trackHeight?: string\n trackRadius?: string\n thumbWidth?: string\n thumbHeight?: string\n thumbRadius?: string\n /**\n * Reverse the value-to-position mapping. With orientation='vertical', sets\n * top=min and bottom=max (drag down to increase). With orientation='horizontal',\n * sets right=min and left=max (drag left to increase). Useful when an icon\n * legend implies the opposite direction of the underlying control.\n *\n * Delegates to reka-ui SliderRoot's native `inverted` prop, which swaps\n * startEdge/endEdge on SliderRange — no manual value transformation needed.\n */\n inverted?: boolean\n}>(), {\n min: 0,\n max: 100,\n step: 1,\n width: '100%',\n orientation: 'horizontal',\n trackHeight: '4px',\n trackRadius: '2px',\n thumbWidth: '14px',\n thumbHeight: '14px',\n thumbRadius: '50%',\n inverted: false,\n})\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: number]\n}>()\n\n// ── Internal state ────────────────────────────────────────────────────────────\n\nconst isVertical = computed(() => props.orientation === 'vertical')\n\n/** Root element inline style */\nconst rootStyle = computed(() => {\n if (isVertical.value) {\n return {\n '--thumb-w': props.thumbWidth,\n '--thumb-h': props.thumbHeight,\n '--thumb-r': props.thumbRadius,\n '--track-height': props.trackHeight,\n '--track-radius': props.trackRadius,\n height: props.width,\n width: '16px',\n }\n }\n return {\n '--thumb-w': props.thumbWidth,\n '--thumb-h': props.thumbHeight,\n '--thumb-r': props.thumbRadius,\n '--track-height': props.trackHeight,\n '--track-radius': props.trackRadius,\n width: props.width,\n }\n})\n\nfunction onValueChange(val: number[] | undefined) {\n if (val && val.length > 0) {\n emit('update:modelValue', val[0])\n }\n}\n</script>\n\n<template>\n <SliderRoot\n class=\"app-slider-root\"\n data-testid=\"app-slider-root\"\n :class=\"{ 'app-slider-root--vertical': isVertical, 'app-slider-root--inverted': inverted }\"\n :style=\"rootStyle\"\n :model-value=\"[modelValue]\"\n :min=\"min\"\n :max=\"max\"\n :step=\"step\"\n :orientation=\"orientation\"\n :inverted=\"inverted\"\n @update:model-value=\"onValueChange\"\n >\n <SliderTrack class=\"app-slider-track\" data-testid=\"app-slider-track\">\n <SliderRange class=\"app-slider-range\" data-testid=\"app-slider-range\" />\n </SliderTrack>\n <SliderThumb class=\"app-slider-thumb\" data-testid=\"app-slider-thumb\" />\n </SliderRoot>\n</template>\n\n<style scoped>\n/* ── Root container ────────────────────────────────────────────────────────── */\n.app-slider-root {\n position: relative;\n display: flex;\n align-items: center;\n user-select: none;\n touch-action: none;\n flex-shrink: 0;\n cursor: pointer;\n}\n\n.app-slider-root[data-orientation='horizontal'] {\n height: var(--track-height, 4px);\n}\n\n.app-slider-root[data-orientation='vertical'] {\n flex-direction: column;\n width: var(--track-height, 4px);\n}\n\n/* ── Track ─────────────────────────────────────────────────────────────────── */\n.app-slider-track {\n position: relative;\n flex-grow: 1;\n border-radius: var(--track-radius, 2px);\n background: var(--openpen-color-border-hi);\n}\n\n.app-slider-root[data-orientation='horizontal'] .app-slider-track {\n height: var(--track-height, 4px);\n}\n\n.app-slider-root[data-orientation='vertical'] .app-slider-track {\n width: var(--track-height, 4px);\n}\n\n/* ── Range (filled portion) ─────────────────────────────────────────────────── */\n.app-slider-range {\n position: absolute;\n border-radius: var(--track-radius, 2px);\n background: var(--openpen-color-accent);\n}\n\n.app-slider-root[data-orientation='horizontal'] .app-slider-range {\n height: 100%;\n top: 0;\n left: 0;\n}\n\n.app-slider-root[data-orientation='vertical'] .app-slider-range {\n width: 100%;\n bottom: 0;\n left: 0;\n}\n\n/* ── Thumb ─────────────────────────────────────────────────────────────────── */\n.app-slider-thumb {\n display: block;\n width: var(--thumb-w, 14px);\n height: var(--thumb-h, 14px);\n border-radius: var(--thumb-r, 50%);\n background: #fff;\n box-shadow: 0 1px 6px rgba(0, 0, 0, 0.50);\n cursor: pointer;\n outline: none;\n transition: transform var(--openpen-duration-fast) var(--openpen-easing-standard);\n}\n\n.app-slider-thumb:hover {\n transform: scale(1.2);\n}\n\n.app-slider-thumb:focus-visible {\n box-shadow: 0 0 0 3px var(--openpen-color-accent-glow);\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * AppToggle — High-level wrapper (Layer 1).\n *\n * Wraps Reka UI Switch (SwitchRoot / SwitchThumb).\n *\n * Visual: 40×24px, off=toggle-off-bg, on=accent,\n * 18×18 white thumb, bounce transition.\n *\n * Styles use --openpen-* design tokens.\n * No Reka UI types are leaked in the public API.\n *\n * DOM notes:\n * - SwitchRoot renders a <button role=\"switch\"> with class \"app-toggle\"\n * - \"on\" class is added when modelValue is true\n * - aria-pressed is added explicitly because Reka UI's SwitchRoot uses\n * aria-checked; both attributes co-exist for maximum a11y surface.\n */\nimport {\n SwitchRoot,\n SwitchThumb,\n} from '../primitives'\n\n// ── Public API (MUST NOT reference reka-ui types) ─────────────────────────────\n\nwithDefaults(defineProps<{\n modelValue: boolean\n ariaLabel?: string\n testid?: string\n}>(), { ariaLabel: '', testid: undefined })\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: boolean]\n}>()\n\nfunction onCheckedChange(val: boolean) {\n emit('update:modelValue', val)\n}\n</script>\n\n<template>\n <SwitchRoot\n class=\"app-toggle\"\n :class=\"{ on: modelValue }\"\n :model-value=\"modelValue\"\n :aria-label=\"ariaLabel\"\n :aria-pressed=\"modelValue\"\n :data-testid=\"testid\"\n @update:model-value=\"onCheckedChange\"\n >\n <SwitchThumb class=\"app-toggle-thumb\" />\n </SwitchRoot>\n</template>\n\n<style scoped>\n/* Root — the toggle track */\n.app-toggle {\n width: 40px;\n height: 24px;\n border-radius: 12px;\n border: none;\n cursor: pointer;\n position: relative;\n flex-shrink: 0;\n background: var(--openpen-color-toggle-off);\n transition: background var(--openpen-duration-base) var(--openpen-easing-standard);\n /* Remove default button styles */\n padding: 0;\n appearance: none;\n -webkit-appearance: none;\n display: inline-flex;\n align-items: center;\n}\n\n.app-toggle.on {\n background: var(--openpen-color-accent);\n}\n\n/* Thumb — the white circular knob */\n.app-toggle-thumb {\n position: absolute;\n top: 3px;\n left: 3px;\n width: 18px;\n height: 18px;\n border-radius: 50%;\n background: #fff;\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);\n transition: left var(--openpen-duration-bounce) var(--openpen-easing-bounce);\n display: block;\n pointer-events: none;\n}\n\n.app-toggle.on .app-toggle-thumb,\n.app-toggle[data-state='checked'] .app-toggle-thumb {\n left: 19px;\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * AppSegmented — High-level wrapper (Layer 1).\n *\n * Wraps Reka UI RadioGroup (RadioGroupRoot / RadioGroupItem).\n *\n * Visual: flex gap 4px, 1.5px border var(--border-hi),\n * active = var(--accent-bg) background + var(--accent) color.\n *\n * Styles use --openpen-* design tokens.\n * No Reka UI types are leaked in the public API.\n */\nimport {\n RadioGroupRoot,\n RadioGroupItem,\n} from '../primitives'\n\n// ── Public API (MUST NOT reference reka-ui types) ─────────────────────────────\n\ninterface Option {\n value: string\n label: string\n icon?: string\n testid?: string\n}\n\ndefineProps<{\n modelValue: string\n options: Option[]\n /** When true, all options are non-interactive and visually dimmed. */\n disabled?: boolean\n}>()\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: string]\n}>()\n\n// AcceptableValue from Reka UI can be null; we narrow to string so the public API has no Reka UI type leakage.\nfunction onValueChange(val: unknown) {\n if (typeof val === 'string') {\n emit('update:modelValue', val)\n }\n}\n</script>\n\n<template>\n <RadioGroupRoot\n class=\"app-seg\"\n data-testid=\"app-seg\"\n :class=\"{ 'app-seg--disabled': disabled }\"\n :model-value=\"modelValue\"\n :disabled=\"disabled\"\n orientation=\"horizontal\"\n @update:model-value=\"onValueChange\"\n >\n <RadioGroupItem\n v-for=\"opt in options\"\n :key=\"opt.value\"\n :value=\"opt.value\"\n as-child\n >\n <button\n class=\"app-seg-btn\"\n :class=\"{ active: modelValue === opt.value }\"\n :data-testid=\"opt.testid\"\n >\n <span\n v-if=\"opt.icon\"\n class=\"app-seg-icon\"\n aria-hidden=\"true\"\n v-html=\"opt.icon\"\n />\n <span>{{ opt.label }}</span>\n </button>\n </RadioGroupItem>\n </RadioGroupRoot>\n</template>\n\n<style scoped>\n.app-seg {\n display: flex;\n gap: var(--openpen-space-xs);\n}\n\n.app-seg-btn {\n flex: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n padding: 6px var(--openpen-space-lg);\n border: 1.5px solid var(--openpen-color-border-hi);\n border-radius: var(--openpen-radius-sm);\n background: var(--openpen-color-input-bg);\n color: var(--openpen-color-text-dim);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n white-space: nowrap;\n transition:\n background var(--openpen-duration-fast) var(--openpen-easing-standard),\n color var(--openpen-duration-fast) var(--openpen-easing-standard),\n border-color var(--openpen-duration-fast) var(--openpen-easing-standard);\n}\n\n.app-seg-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n line-height: 0;\n}\n\n.app-seg-btn:hover {\n color: var(--openpen-color-text-primary);\n background: var(--openpen-color-control-hover);\n}\n\n.app-seg-btn.active {\n background: var(--openpen-color-accent-bg);\n color: var(--openpen-color-accent);\n border-color: color-mix(in srgb, var(--openpen-color-accent) 40%, transparent);\n}\n\n.app-seg--disabled {\n opacity: 0.45;\n pointer-events: none;\n cursor: not-allowed;\n}\n</style>\n","/**\n * useDialogPluginComponent — composable for components rendered via useDialog().custom().\n *\n * The custom component calls this to get ok/cancel/dismiss callbacks, which\n * resolve the Promise returned by useDialog().custom().\n */\nimport { inject } from 'vue'\nimport { DIALOG_PLUGIN_HANDLE_KEY } from './dialog-internal-keys'\n\nexport interface DialogPluginComponentHandle<T = unknown> {\n /** Resolve with a payload value and close the dialog. */\n ok(payload?: T): void\n /** Resolve with null and close the dialog. */\n cancel(): void\n /** Alias for cancel() — resolves with null. */\n dismiss(): void\n}\n\nexport function useDialogPluginComponent<T = unknown>(): DialogPluginComponentHandle<T> {\n const handle = inject(DIALOG_PLUGIN_HANDLE_KEY)\n if (!handle) {\n throw new Error(\n 'useDialogPluginComponent() must be called inside a component rendered via useDialog().custom()'\n )\n }\n return handle as DialogPluginComponentHandle<T>\n}\n","<script setup lang=\"ts\">\n/**\n * AppSelect — High-level wrapper (Layer 1).\n *\n * Plugin authors use this component without needing to know about Reka UI's\n * Select primitives. The API surface is OpenPen-owned and stable.\n *\n * Public API is stable: switching the underlying headless library in the\n * future does not break the plugin author's code.\n *\n * Implementation notes:\n * - Controlled via modelValue / update:modelValue (standard Vue v-model pattern).\n * - options[] is an OpenPen-owned type; no Reka UI types leak to the public API.\n * - Popup styling mirrors surface-popup tokens (same as AppPopover content).\n * - Styles are unscoped because SelectContent is teleported via SelectPortal.\n */\nimport {\n SelectRoot,\n SelectTrigger,\n SelectPortal,\n SelectContent,\n SelectViewport,\n SelectItem,\n SelectItemText,\n SelectItemIndicator,\n SelectValue,\n SelectIcon,\n} from '../primitives'\n\n// ── OpenPen-owned types (MUST NOT reference reka-ui types) ───────────────────\n\n/** A single option in the dropdown list. */\nexport interface SelectOption {\n /** The value emitted on selection. */\n value: string\n /** The label shown to the user. */\n label: string\n}\n\nexport interface AppSelectProps {\n /** Currently selected value (bind with v-model). */\n modelValue: string\n /** List of selectable options. */\n options: SelectOption[]\n /** Placeholder shown when no option is selected. */\n placeholder: string\n /** Whether the select is disabled. */\n disabled?: boolean\n}\n\nconst props = withDefaults(defineProps<AppSelectProps>(), {\n disabled: false,\n})\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: string]\n}>()\n\nfunction onValueChange(v: string) {\n emit('update:modelValue', v)\n}\n</script>\n\n<template>\n <SelectRoot\n :model-value=\"modelValue\"\n :disabled=\"disabled\"\n @update:model-value=\"onValueChange\"\n >\n <SelectTrigger class=\"openpen-select-trigger\">\n <SelectValue :placeholder=\"placeholder\" />\n <SelectIcon class=\"openpen-select-icon\" aria-hidden=\"true\">\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" fill=\"none\">\n <path d=\"M2 4L6 8L10 4\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </SelectIcon>\n </SelectTrigger>\n <SelectPortal>\n <SelectContent class=\"openpen-select-content\" position=\"popper\" :side-offset=\"4\">\n <SelectViewport class=\"openpen-select-viewport\">\n <SelectItem\n v-for=\"option in options\"\n :key=\"option.value\"\n :value=\"option.value\"\n class=\"openpen-select-item\"\n >\n <SelectItemIndicator class=\"openpen-select-item-indicator\">\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M2 6L5 9L10 3\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </SelectItemIndicator>\n <SelectItemText>{{ option.label }}</SelectItemText>\n </SelectItem>\n </SelectViewport>\n </SelectContent>\n </SelectPortal>\n </SelectRoot>\n</template>\n\n<!-- Styles MUST be unscoped — Vue scoped CSS does not follow Teleport targets,\n so .openpen-select-* classes would not receive the scoped data-v-* attribute\n when rendered inside the portal. -->\n<style>\n/* ── Trigger ─────────────────────────────────────────────────────────────── */\n.openpen-select-trigger {\n display: inline-flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--openpen-space-sm);\n min-width: 120px;\n height: 32px;\n padding: 0 var(--openpen-space-md);\n background: var(--openpen-color-input-bg);\n border: 1px solid var(--openpen-color-border-hi);\n border-radius: var(--openpen-radius-md);\n color: var(--openpen-color-text-primary);\n font-size: 13px;\n cursor: pointer;\n transition:\n background var(--openpen-duration-fast),\n border-color var(--openpen-duration-fast);\n outline: none;\n white-space: nowrap;\n user-select: none;\n}\n\n.openpen-select-trigger:hover {\n background: var(--openpen-color-control-hover);\n border-color: var(--openpen-color-border-hi);\n}\n\n.openpen-select-trigger:focus-visible {\n border-color: var(--openpen-color-accent);\n box-shadow: 0 0 0 2px var(--openpen-color-accent-glow);\n}\n\n.openpen-select-trigger[data-disabled] {\n opacity: 0.45;\n cursor: not-allowed;\n}\n\n/* ── Trigger icon (chevron) ──────────────────────────────────────────────── */\n.openpen-select-icon {\n color: var(--openpen-color-text-dim);\n flex-shrink: 0;\n display: flex;\n align-items: center;\n transition: transform var(--openpen-duration-fast);\n}\n\n.openpen-select-trigger[data-state='open'] .openpen-select-icon {\n transform: rotate(180deg);\n}\n\n/* ── Dropdown content ────────────────────────────────────────────────────── */\n.openpen-select-content {\n background: var(--openpen-color-surface-popup);\n border: 1px solid var(--openpen-color-border-hi);\n border-radius: var(--openpen-radius-md);\n box-shadow: var(--openpen-shadow);\n backdrop-filter: var(--openpen-blur);\n -webkit-backdrop-filter: var(--openpen-blur);\n z-index: 100;\n pointer-events: auto;\n /* Match trigger width */\n min-width: var(--reka-select-trigger-width, 120px);\n max-height: 240px;\n overflow: hidden;\n}\n\n/* ── Viewport scrollable area ────────────────────────────────────────────── */\n.openpen-select-viewport {\n padding: var(--openpen-space-xs);\n}\n\n/* ── Individual items ────────────────────────────────────────────────────── */\n.openpen-select-item {\n display: flex;\n align-items: center;\n gap: var(--openpen-space-sm);\n height: 30px;\n padding: 0 var(--openpen-space-md);\n padding-left: 28px; /* indent to align text past indicator */\n border-radius: var(--openpen-radius-sm);\n font-size: 13px;\n color: var(--openpen-color-text-primary);\n cursor: pointer;\n position: relative;\n user-select: none;\n outline: none;\n transition: background var(--openpen-duration-fast);\n}\n\n.openpen-select-item[data-highlighted] {\n background: var(--openpen-color-control-hover);\n color: var(--openpen-color-text-primary);\n}\n\n.openpen-select-item[data-state='checked'] {\n color: var(--openpen-color-accent);\n}\n\n.openpen-select-item[data-disabled] {\n opacity: 0.45;\n cursor: not-allowed;\n}\n\n/* ── Selected item indicator (checkmark) ─────────────────────────────────── */\n.openpen-select-item-indicator {\n position: absolute;\n left: var(--openpen-space-sm);\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--openpen-color-accent);\n}\n\n/* ── Open / close animations ─────────────────────────────────────────────────\n Pattern matches AppPopover keyframes (200ms bounce enter, 150ms ease leave). */\n.openpen-select-content[data-state='open'] {\n animation: openpen-select-in 200ms cubic-bezier(0.34, 1.56, 0.64, 1);\n}\n.openpen-select-content[data-state='closed'] {\n animation: openpen-select-out 150ms ease forwards;\n}\n\n@keyframes openpen-select-in {\n from { opacity: 0; transform: scale(0.95); }\n to { opacity: 1; transform: scale(1); }\n}\n@keyframes openpen-select-out {\n from { opacity: 1; transform: scale(1); }\n to { opacity: 0; transform: scale(0.95); }\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * AppTooltip — High-level wrapper (Layer 1).\n *\n * Wraps Reka UI Tooltip (TooltipProvider / TooltipRoot / TooltipTrigger /\n * TooltipPortal / TooltipContent / TooltipArrow).\n *\n * Plugin authors get hover-triggered tooltips with OpenPen styling without\n * knowing about Reka UI internals or inject keys.\n *\n * Styles use --openpen-* design tokens.\n * No Reka UI types are leaked in the public API.\n *\n * Usage:\n * <AppTooltip content=\"Save file\" side=\"top\">\n * <button>Save</button>\n * </AppTooltip>\n */\nimport {\n TooltipProvider,\n TooltipRoot,\n TooltipTrigger,\n TooltipPortal,\n TooltipContent,\n TooltipArrow,\n} from '../primitives'\n\n// ── OpenPen-owned types (MUST NOT reference reka-ui types) ────────────────────\n\n/** Preferred side for tooltip content relative to the trigger. */\nexport type TooltipSide = 'top' | 'bottom' | 'left' | 'right'\n\nexport interface AppTooltipProps {\n /** Tooltip text content. */\n content: string\n /** Preferred placement side relative to the trigger. Default 'top'. */\n side?: TooltipSide\n /** Delay before tooltip appears on hover, in ms. Default 200. */\n delay?: number\n}\n\nexport interface AppTooltipSlots {\n /** Trigger element. Any element that receives hover events. */\n default: () => unknown\n}\n\nconst props = withDefaults(defineProps<AppTooltipProps>(), {\n side: 'top',\n delay: 200,\n})\n\ndefineSlots<AppTooltipSlots>()\n</script>\n\n<template>\n <TooltipProvider :delay-duration=\"props.delay\">\n <TooltipRoot>\n <TooltipTrigger as-child>\n <slot />\n </TooltipTrigger>\n <TooltipPortal>\n <TooltipContent\n :side=\"props.side\"\n :side-offset=\"6\"\n :avoid-collisions=\"true\"\n :collision-padding=\"8\"\n class=\"openpen-tooltip-content\"\n >\n {{ props.content }}\n <TooltipArrow class=\"openpen-tooltip-arrow\" :width=\"10\" :height=\"5\" />\n </TooltipContent>\n </TooltipPortal>\n </TooltipRoot>\n </TooltipProvider>\n</template>\n\n<!-- Styles for teleported TooltipContent MUST be unscoped — Vue scoped CSS\n does not follow Teleport targets (same rationale as AppPopover). -->\n<style>\n.openpen-tooltip-content {\n background: var(--openpen-color-tooltip-bg);\n border: 1px solid var(--openpen-color-border-hi);\n border-radius: var(--openpen-radius-sm);\n padding: 5px 9px;\n color: var(--openpen-color-tooltip-text);\n font-size: 12px;\n line-height: 1.4;\n /* Prevent text wrap for short tooltip labels */\n white-space: nowrap;\n box-shadow: var(--openpen-shadow-sm);\n z-index: 200;\n pointer-events: none;\n}\n\n.openpen-tooltip-arrow {\n fill: var(--openpen-color-tooltip-bg);\n}\n\n/* ── Open / close animations ────────────────────────────────────────────────\n Reka UI Presence keeps the element in the DOM until the CSS animation ends,\n so data-state=\"delayed-open\" / \"instant-open\" / \"closed\" all animate. */\n.openpen-tooltip-content[data-state='delayed-open'],\n.openpen-tooltip-content[data-state='instant-open'] {\n animation: openpen-tooltip-in var(--openpen-duration-fast) var(--openpen-easing-standard);\n}\n.openpen-tooltip-content[data-state='closed'] {\n animation: openpen-tooltip-out var(--openpen-duration-fast) ease forwards;\n}\n\n@keyframes openpen-tooltip-in {\n from { opacity: 0; transform: scale(0.95); }\n to { opacity: 1; transform: scale(1); }\n}\n@keyframes openpen-tooltip-out {\n from { opacity: 1; transform: scale(1); }\n to { opacity: 0; transform: scale(0.95); }\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * AppTabs — High-level wrapper (Layer 1).\n *\n * Wraps Reka UI Tabs (TabsRoot / TabsList / TabsTrigger / TabsContent).\n * Intended for settings UI tabbed navigation and any plugin tab panel.\n *\n * Plugin authors get a controlled tab switcher with OpenPen styling without\n * knowing about Reka UI internals.\n *\n * Styles use --openpen-* design tokens.\n * No Reka UI types are leaked in the public API.\n *\n * Usage:\n * <AppTabs\n * v-model=\"activeTab\"\n * :tabs=\"[{ id: 'general', label: 'General' }, { id: 'advanced', label: 'Advanced' }]\"\n * >\n * <template #default=\"{ activeTabId }\">\n * <div v-if=\"activeTabId === 'general'\">...</div>\n * <div v-if=\"activeTabId === 'advanced'\">...</div>\n * </template>\n * </AppTabs>\n */\nimport {\n TabsRoot,\n TabsList,\n TabsTrigger,\n TabsContent,\n} from '../primitives'\n\n// ── OpenPen-owned types (MUST NOT reference reka-ui types) ────────────────────\n\nexport interface AppTabItem {\n /** Unique identifier for this tab. Used as the v-model value. */\n id: string\n /** Human-readable label rendered in the tab trigger button. */\n label: string\n}\n\nexport interface AppTabsProps {\n /** Currently active tab id (controlled mode). */\n modelValue: string\n /** Ordered list of tab descriptors. */\n tabs: AppTabItem[]\n}\n\nexport interface AppTabsSlots {\n /**\n * Default slot for tab content area.\n * Receives the currently active tab id via scoped slot.\n */\n default: (scope: { activeTabId: string }) => unknown\n}\n\nconst props = defineProps<AppTabsProps>()\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: string]\n}>()\n\ndefineSlots<AppTabsSlots>()\n\nfunction onTabChange(val: string | number | undefined) {\n if (typeof val === 'string') {\n emit('update:modelValue', val)\n }\n}\n</script>\n\n<template>\n <TabsRoot\n :model-value=\"props.modelValue\"\n class=\"openpen-tabs\"\n @update:model-value=\"onTabChange\"\n >\n <TabsList class=\"openpen-tabs-list\">\n <TabsTrigger\n v-for=\"tab in props.tabs\"\n :key=\"tab.id\"\n :value=\"tab.id\"\n class=\"openpen-tabs-trigger\"\n >\n {{ tab.label }}\n </TabsTrigger>\n </TabsList>\n\n <!-- Single content outlet: plugin author renders tab content via scoped slot. -->\n <TabsContent\n v-for=\"tab in props.tabs\"\n :key=\"tab.id\"\n :value=\"tab.id\"\n class=\"openpen-tabs-content\"\n >\n <slot :active-tab-id=\"tab.id\" />\n </TabsContent>\n </TabsRoot>\n</template>\n\n<style scoped>\n/* ── Tab root ───────────────────────────────────────────────────────────────── */\n.openpen-tabs {\n display: flex;\n flex-direction: column;\n gap: var(--openpen-space-sm);\n}\n\n/* ── Tab list (trigger bar) ─────────────────────────────────────────────────── */\n.openpen-tabs-list {\n display: flex;\n flex-direction: row;\n gap: 2px;\n padding: 3px;\n background: var(--openpen-color-control-group);\n border: 1px solid var(--openpen-color-border);\n border-radius: var(--openpen-radius-md);\n}\n\n/* ── Individual trigger ─────────────────────────────────────────────────────── */\n.openpen-tabs-trigger {\n flex: 1;\n padding: var(--openpen-space-xs) var(--openpen-space-sm);\n border: none;\n border-radius: calc(var(--openpen-radius-md) - 3px);\n background: transparent;\n color: var(--openpen-color-text-dim);\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n transition:\n background var(--openpen-duration-fast) var(--openpen-easing-standard),\n color var(--openpen-duration-fast) var(--openpen-easing-standard);\n white-space: nowrap;\n /* Remove default button outline; focus-visible ring below handles a11y. */\n outline: none;\n}\n\n.openpen-tabs-trigger:hover {\n background: var(--openpen-color-control-hover);\n color: var(--openpen-color-text-primary);\n}\n\n.openpen-tabs-trigger[data-state='active'] {\n background: var(--openpen-color-accent-bg);\n color: var(--openpen-color-accent);\n box-shadow: 0 0 0 1px var(--openpen-color-accent-glow);\n}\n\n.openpen-tabs-trigger:focus-visible {\n box-shadow: 0 0 0 2px var(--openpen-color-accent);\n}\n\n/* ── Tab content ────────────────────────────────────────────────────────────── */\n.openpen-tabs-content {\n /* Content area has no chrome by default; plugin authors add their own. */\n outline: none;\n}\n\n.openpen-tabs-content:focus-visible {\n box-shadow: 0 0 0 2px var(--openpen-color-accent);\n border-radius: var(--openpen-radius-sm);\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * AppBanner — High-level wrapper (Layer 1).\n *\n * Presents info / warning / success / error status messages using the\n * --openpen-color-state-* design tokens. The icon is auto-rendered based on\n * the variant; message content goes in the default slot; optional inline action\n * buttons (e.g. restart / dismiss) go in the `#actions` slot.\n *\n * Inline mode (compact single-line) is enabled via the `inline` prop and is\n * suited for form validation feedback and status lines inside dialogs.\n */\n\nexport type BannerVariant = 'info' | 'warning' | 'success' | 'error'\n\nwithDefaults(defineProps<{\n variant: BannerVariant\n /** Compact single-line layout: smaller padding, vertically centered. */\n inline?: boolean\n}>(), { inline: false })\n</script>\n\n<template>\n <div\n :class=\"['app-banner', `app-banner-${variant}`, { 'app-banner-inline': inline }]\"\n :role=\"variant === 'error' ? 'alert' : 'status'\"\n >\n <span class=\"app-banner-icon\" aria-hidden=\"true\">\n <!-- info -->\n <svg v-if=\"variant === 'info'\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"12\" cy=\"12\" r=\"10\"/><line x1=\"12\" y1=\"8\" x2=\"12\" y2=\"12\"/><line x1=\"12\" y1=\"16\" x2=\"12.01\" y2=\"16\"/>\n </svg>\n <!-- warning -->\n <svg v-else-if=\"variant === 'warning'\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/><line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"/><line x1=\"12\" y1=\"17\" x2=\"12.01\" y2=\"17\"/>\n </svg>\n <!-- success -->\n <svg v-else-if=\"variant === 'success'\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M22 11.08V12a10 10 0 1 1-5.93-9.14\"/><polyline points=\"22 4 12 14.01 9 11.01\"/>\n </svg>\n <!-- error -->\n <svg v-else width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"12\" cy=\"12\" r=\"10\"/><line x1=\"15\" y1=\"9\" x2=\"9\" y2=\"15\"/><line x1=\"9\" y1=\"9\" x2=\"15\" y2=\"15\"/>\n </svg>\n </span>\n\n <span class=\"app-banner-body\"><slot /></span>\n\n <span v-if=\"$slots.actions\" class=\"app-banner-actions\">\n <slot name=\"actions\" />\n </span>\n </div>\n</template>\n\n<style scoped>\n.app-banner {\n display: flex;\n align-items: flex-start;\n gap: 10px;\n padding: 10px 14px;\n border-radius: var(--openpen-radius-md, 10px);\n border: 1px solid;\n font-size: 13px;\n line-height: 1.55;\n}\n\n.app-banner-inline {\n padding: 7px 12px;\n align-items: center;\n font-size: 12px;\n}\n\n.app-banner-icon {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n margin-top: 1px;\n}\n\n.app-banner-inline .app-banner-icon { margin-top: 0; }\n\n.app-banner-body {\n flex: 1;\n min-width: 0;\n}\n\n.app-banner-actions {\n display: flex;\n align-items: center;\n gap: 6px;\n flex-shrink: 0;\n margin-left: auto;\n}\n\n/* Variants */\n.app-banner-info {\n background: var(--openpen-color-state-info-bg);\n border-color: var(--openpen-color-state-info-border);\n color: var(--openpen-color-state-info-text);\n}\n.app-banner-info .app-banner-icon {\n color: var(--openpen-color-state-info-icon);\n}\n\n.app-banner-warning {\n background: var(--openpen-color-state-warning-bg);\n border-color: var(--openpen-color-state-warning-border);\n color: var(--openpen-color-state-warning-text);\n}\n.app-banner-warning .app-banner-icon {\n color: var(--openpen-color-state-warning-icon);\n}\n\n.app-banner-success {\n background: var(--openpen-color-state-success-bg);\n border-color: var(--openpen-color-state-success-border);\n color: var(--openpen-color-state-success-text);\n}\n.app-banner-success .app-banner-icon {\n color: var(--openpen-color-state-success-icon);\n}\n\n.app-banner-error {\n background: var(--openpen-color-state-error-bg);\n border-color: var(--openpen-color-state-error-border);\n color: var(--openpen-color-state-error-text);\n}\n.app-banner-error .app-banner-icon {\n color: var(--openpen-color-state-error-icon);\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * AppButton — control-bar button wrapper (Layer 1).\n *\n * Provides the standard 36×36 rounded button appearance that matches the\n * host's control-bar buttons, so plugins do not need to replicate the\n * visual design manually.\n *\n * Disabled state uses aria-disabled + a CSS class rather than the HTML\n * `disabled` attribute, because `disabled` removes pointer events and would\n * swallow the tooltip hover that this component is expected to keep alive.\n *\n * In a vertical control bar the button shrinks to 34×34 and the tooltip\n * points away from the snapped edge (snap-left → right side; snap-right →\n * left side; vbar-free → right side). In a horizontal bar the tooltip sits\n * above the button by default, but flips below when the bar is near the\n * workArea top edge (the host raises TOOLTIP_FLIP_DOWN_KEY in that case).\n *\n * All orientation signals come from the host via injection\n * (IS_VERTICAL_KEY / SNAP_EDGE_KEY / TOOLTIP_FLIP_DOWN_KEY) so plugin\n * buttons render identically to the host's own control-bar buttons.\n */\n\nimport { computed, inject } from 'vue'\nimport { SNAP_EDGE_KEY, IS_VERTICAL_KEY, TOOLTIP_FLIP_DOWN_KEY } from '../../inject-keys'\n\n// ── Public API ────────────────────────────────────────────────────────────────\n\nconst props = withDefaults(defineProps<{\n variant?: 'default' | 'danger'\n active?: boolean\n disabled?: boolean\n tooltip?: string\n ariaLabel?: string\n}>(), {\n variant: 'default',\n active: false,\n disabled: false,\n tooltip: undefined,\n ariaLabel: undefined,\n})\n\nconst emit = defineEmits<{\n click: []\n}>()\n\nfunction handleClick() {\n if (!props.disabled) {\n emit('click')\n }\n}\n\n// ── Orientation context (host-injected) ───────────────────────────────────────\n\nconst isVertical = inject(IS_VERTICAL_KEY, undefined)\nconst snapEdge = inject(SNAP_EDGE_KEY, undefined)\nconst tooltipFlipDown = inject(TOOLTIP_FLIP_DOWN_KEY, undefined)\n\nconst tooltipSide = computed<'top' | 'top-flip' | 'right' | 'left'>(() => {\n if (isVertical?.value) {\n return snapEdge?.value === 'right' ? 'left' : 'right'\n }\n return tooltipFlipDown?.value ? 'top-flip' : 'top'\n})\n</script>\n\n<template>\n <button\n class=\"app-btn\"\n :class=\"[\n `app-btn--tooltip-${tooltipSide}`,\n {\n 'app-btn--vertical': isVertical,\n active,\n danger: variant === 'danger',\n 'app-btn-disabled': disabled,\n },\n ]\"\n :data-tip=\"tooltip\"\n :aria-label=\"ariaLabel\"\n :aria-disabled=\"disabled || undefined\"\n @click=\"handleClick\"\n >\n <slot />\n </button>\n</template>\n\n<style scoped>\n.app-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 36px;\n height: 36px;\n border: none;\n border-radius: var(--openpen-radius-md);\n background: transparent;\n color: var(--openpen-color-text-dim);\n cursor: pointer;\n transition: background var(--openpen-duration-fast), color var(--openpen-duration-fast), transform var(--openpen-duration-fast);\n position: relative;\n flex-shrink: 0;\n padding: 0;\n}\n\n/* Vertical bars shrink the button to 34×34 to match the host's\n `.control-bar.is-vertical .cb-btn` rule. */\n.app-btn.app-btn--vertical {\n width: 34px;\n height: 34px;\n}\n\n.app-btn:hover {\n background: var(--openpen-color-control-hover);\n color: var(--openpen-color-text-primary);\n}\n\n.app-btn:active {\n transform: scale(0.88);\n}\n\n.app-btn.active {\n background: var(--openpen-color-accent-bg);\n color: var(--openpen-color-accent);\n box-shadow: 0 0 0 1px var(--openpen-color-accent-glow);\n}\n\n.app-btn.danger {\n color: var(--openpen-color-danger);\n}\n\n.app-btn.danger:hover {\n background: var(--openpen-color-danger-hover-bg);\n color: var(--openpen-color-danger-hover);\n}\n\n/* Disabled: keeps pointer events alive so the tooltip ::after hover still fires. */\n.app-btn.app-btn-disabled {\n cursor: not-allowed;\n}\n\n/* Dim only the button content — tooltip pseudo-element stays at full opacity. */\n.app-btn.app-btn-disabled > * {\n opacity: 0.30;\n}\n\n/* Tooltip — horizontal mode default (above the button). */\n.app-btn[data-tip]::after {\n content: attr(data-tip);\n position: absolute;\n bottom: calc(100% + 8px);\n left: 50%;\n transform: translateX(-50%);\n background: var(--openpen-color-tooltip-bg);\n border: 1px solid var(--openpen-color-tooltip-border);\n color: var(--openpen-color-tooltip-text);\n font-size: 11.5px;\n font-weight: 500;\n white-space: nowrap;\n padding: 5px 10px;\n border-radius: var(--openpen-radius-sm);\n pointer-events: none;\n opacity: 0;\n transition: opacity var(--openpen-duration-fast);\n box-shadow: var(--openpen-shadow-sm);\n z-index: 41;\n}\n\n/* Horizontal mode near workArea top: tooltip flips below the button so\n it does not extend off-screen. The host sets TOOLTIP_FLIP_DOWN_KEY when\n the ball is within ~60 px of the top edge. */\n.app-btn.app-btn--tooltip-top-flip[data-tip]::after {\n bottom: auto;\n top: calc(100% + 8px);\n}\n\n/* Vertical bar, snap-left or vbar-free: tooltip points right (inward). */\n.app-btn.app-btn--tooltip-right[data-tip]::after {\n top: 50%;\n bottom: auto;\n left: calc(100% + 8px);\n right: auto;\n transform: translateY(-50%);\n}\n\n/* Vertical bar, snap-right: tooltip points left (inward). */\n.app-btn.app-btn--tooltip-left[data-tip]::after {\n top: 50%;\n bottom: auto;\n left: auto;\n right: calc(100% + 8px);\n transform: translateY(-50%);\n}\n\n.app-btn[data-tip]:hover::after {\n opacity: 1;\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * AppButtonDropdown — split-mode dropdown button wrapper (Layer 1).\n *\n * Pairs an AppButton (main action) with a narrow caret button that toggles\n * an AppPopover. Modelled after Quasar's QBtnDropdown split-mode and shadcn\n * Button + DropdownMenu composition: main click and caret click are\n * independent events so the host can activate a tool on the main click\n * and open the dropdown on the caret click (or activate AND open from\n * the caret when the tool is currently inactive).\n *\n * Caret icon rotation tracks the host's snap edge and vertical layout via\n * the same injection keys AppButton uses — plugin authors get the right\n * rotation behaviour without configuring anything.\n *\n * Visual identity:\n * - Main button: identical to AppButton (36×36 horizontal, 34×34 vertical)\n * - Caret: 16 wide × 36 tall in horizontal mode (14 × 30 in vertical),\n * hover background + accent on active, chevron rotates toward the\n * popover side\n * - Wrap: flex row in horizontal mode, flex column in vertical\n *\n * The structural classes (`app-btn-dropdown-wrap`, `app-btn-dropdown-caret`,\n * `app-btn-dropdown-caret-icon`) are deliberately stable so host CSS can\n * apply contextual overrides (e.g. `.cb-group--inset .app-btn-dropdown-caret`\n * shrinks the caret inside inset groups).\n */\nimport { computed, inject } from 'vue'\nimport { SNAP_EDGE_KEY, IS_VERTICAL_KEY } from '../../inject-keys'\nimport AppButton from './AppButton.vue'\nimport AppPopover from './AppPopover.vue'\n\n// ── Public API ────────────────────────────────────────────────────────────────\n\nconst props = withDefaults(defineProps<{\n /** Globally unique popover id (passed through to the inner AppPopover). */\n popoverId: string\n /** Preferred popover placement; defaults to 'auto'. */\n popoverPlacement?: 'auto' | 'top' | 'bottom' | 'left' | 'right'\n /** Main button active state (drives accent highlight). */\n active?: boolean\n /** Disables both main and caret buttons; tooltip remains active. */\n disabled?: boolean\n /** Main button variant (passed through to AppButton). */\n variant?: 'default' | 'danger'\n /** Tooltip shown above the main button. */\n mainTooltip?: string\n /** Main button aria-label. */\n mainAriaLabel?: string\n /** Caret button aria-label. Required for screen-reader users. */\n caretAriaLabel?: string\n /** Optional test ids forwarded to the rendered buttons. */\n mainTestid?: string\n caretTestid?: string\n}>(), {\n popoverPlacement: 'auto',\n active: false,\n disabled: false,\n variant: 'default',\n mainTooltip: undefined,\n mainAriaLabel: undefined,\n caretAriaLabel: undefined,\n mainTestid: undefined,\n caretTestid: undefined,\n})\n\nconst emit = defineEmits<{\n /** Fires when the main button is clicked (unless `disabled`). */\n mainClick: []\n /**\n * Fires when the caret button is clicked (unless `disabled`).\n * AppPopover handles open/close via its own trigger logic; this event\n * lets the host run side-effects (e.g. activate the tool if it isn't\n * already active before opening the dropdown).\n */\n caretClick: []\n}>()\n\n// ── Orientation context (host-injected) ───────────────────────────────────────\n\nconst isVertical = inject(IS_VERTICAL_KEY, undefined)\nconst snapEdge = inject(SNAP_EDGE_KEY, undefined)\n\n/**\n * Caret chevron rotation when the popover is open.\n *\n * - inactive → down (resting state, points at the popover that will appear)\n * - horizontal + active → up (popover is above, chevron points to it)\n * - vertical + snap-left or vbar-free + active → right (popover opens right)\n * - vertical + snap-right + active → left (popover opens left)\n */\nfunction caretRotation(open: boolean): 'down' | 'up' | 'left' | 'right' {\n if (!open) return 'down'\n if (!isVertical?.value) return 'up'\n return snapEdge?.value === 'right' ? 'left' : 'right'\n}\n\nfunction onMainClick(): void {\n if (!props.disabled) emit('mainClick')\n}\n\nfunction onCaretClick(): void {\n if (!props.disabled) emit('caretClick')\n}\n</script>\n\n<template>\n <div\n class=\"app-btn-dropdown-wrap\"\n :class=\"{ 'app-btn-dropdown-wrap--vertical': isVertical }\"\n >\n <AppButton\n :variant=\"variant\"\n :active=\"active\"\n :disabled=\"disabled\"\n :tooltip=\"mainTooltip\"\n :aria-label=\"mainAriaLabel\"\n :data-testid=\"mainTestid\"\n @click=\"onMainClick\"\n >\n <slot name=\"main-content\" />\n </AppButton>\n\n <AppPopover :popover-id=\"popoverId\" :placement=\"popoverPlacement\">\n <template #trigger=\"{ active: open }\">\n <button\n type=\"button\"\n class=\"app-btn-dropdown-caret\"\n :class=\"{\n active: open,\n 'app-btn-dropdown-caret--vertical': isVertical,\n 'app-btn-dropdown-caret--disabled': disabled,\n }\"\n :aria-label=\"caretAriaLabel\"\n :aria-disabled=\"disabled || undefined\"\n :data-testid=\"caretTestid\"\n @click=\"onCaretClick\"\n >\n <svg\n class=\"app-btn-dropdown-caret-icon\"\n :class=\"`app-btn-dropdown-caret-icon--${caretRotation(open)}`\"\n width=\"10\"\n height=\"10\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2.5\"\n >\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </button>\n </template>\n <template #content>\n <slot name=\"popover-content\" />\n </template>\n </AppPopover>\n </div>\n</template>\n\n<style>\n/* Structural classes ship UNSCOPED on purpose so host CSS can apply\n contextual overrides (e.g. `.cb-group--inset .app-btn-dropdown-caret`\n shrinks the caret inside inset groups in the host control bar). The\n classes are deliberately prefixed `app-btn-dropdown-*` to avoid\n collisions in plugin code. */\n.app-btn-dropdown-wrap {\n display: flex;\n align-items: center;\n gap: 0;\n -webkit-app-region: no-drag;\n}\n\n.app-btn-dropdown-wrap--vertical {\n flex-direction: column;\n gap: 2px;\n}\n\n.app-btn-dropdown-caret {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 16px;\n height: 36px;\n border: none;\n background: transparent;\n color: var(--openpen-color-text-dim);\n cursor: pointer;\n padding: 0;\n border-radius: 8px;\n transition: color var(--openpen-duration-fast), background var(--openpen-duration-fast);\n}\n\n.app-btn-dropdown-caret--vertical {\n width: 30px;\n height: 14px;\n}\n\n.app-btn-dropdown-caret:hover {\n color: var(--openpen-color-text-primary);\n background: var(--openpen-color-control-hover);\n}\n\n.app-btn-dropdown-caret.active {\n color: var(--openpen-color-accent);\n background: var(--openpen-color-accent-bg);\n}\n\n.app-btn-dropdown-caret--disabled {\n cursor: not-allowed;\n}\n\n.app-btn-dropdown-caret--disabled > * {\n opacity: 0.30;\n}\n\n.app-btn-dropdown-caret-icon {\n transition: transform 180ms cubic-bezier(0.34, 1.56, 0.64, 1);\n}\n\n.app-btn-dropdown-caret-icon--down { transform: rotate(0deg); }\n.app-btn-dropdown-caret-icon--up { transform: rotate(180deg); }\n.app-btn-dropdown-caret-icon--right { transform: rotate(-90deg); }\n.app-btn-dropdown-caret-icon--left { transform: rotate(90deg); }\n</style>\n"],"names":["_createBlock","_unref","_createVNode","_renderSlot","_normalizeClass","_createElementBlock","_Fragment","_renderList","_createElementVNode","_toDisplayString","_createTextVNode","_openBlock","$slots","AppPopover","_withCtx"],"mappings":"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"}
|
package/dist/uikit/internal.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineComponent, inject, ref, watch, nextTick, computed, provide, openBlock, createBlock, createSlots, withCtx, createElementBlock, toDisplayString, Fragment, createElementVNode, withDirectives, withKeys, vModelText, resolveDynamicComponent, normalizeProps, mergeProps, createCommentVNode, normalizeClass } from "vue";
|
|
2
|
-
import { a as _activeRequest, D as DIALOG_PLUGIN_HANDLE_KEY, _ as _sfc_main$1, b as _resolveActive } from "../_chunks/AppButtonDropdown.vue_vue_type_style_index_0_lang-
|
|
2
|
+
import { a as _activeRequest, D as DIALOG_PLUGIN_HANDLE_KEY, _ as _sfc_main$1, b as _resolveActive } from "../_chunks/AppButtonDropdown.vue_vue_type_style_index_0_lang-B9dHU-LE.js";
|
|
3
3
|
import "reka-ui";
|
|
4
4
|
import { H as HOST_DIALOG_OPEN_COUNT_KEY } from "../_chunks/inject-keys-CRXCpN-M.js";
|
|
5
5
|
import "dompurify";
|
|
@@ -52,7 +52,7 @@ to { opacity: 0; transform: scale(0.92);
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/* ── Root container ────────────────────────────────────────────────────────── */
|
|
55
|
-
.app-slider-root[data-v-
|
|
55
|
+
.app-slider-root[data-v-ae09a023] {
|
|
56
56
|
position: relative;
|
|
57
57
|
display: flex;
|
|
58
58
|
align-items: center;
|
|
@@ -61,47 +61,47 @@ to { opacity: 0; transform: scale(0.92);
|
|
|
61
61
|
flex-shrink: 0;
|
|
62
62
|
cursor: pointer;
|
|
63
63
|
}
|
|
64
|
-
.app-slider-root[data-orientation='horizontal'][data-v-
|
|
64
|
+
.app-slider-root[data-orientation='horizontal'][data-v-ae09a023] {
|
|
65
65
|
height: var(--track-height, 4px);
|
|
66
66
|
}
|
|
67
|
-
.app-slider-root[data-orientation='vertical'][data-v-
|
|
67
|
+
.app-slider-root[data-orientation='vertical'][data-v-ae09a023] {
|
|
68
68
|
flex-direction: column;
|
|
69
69
|
width: var(--track-height, 4px);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/* ── Track ─────────────────────────────────────────────────────────────────── */
|
|
73
|
-
.app-slider-track[data-v-
|
|
73
|
+
.app-slider-track[data-v-ae09a023] {
|
|
74
74
|
position: relative;
|
|
75
75
|
flex-grow: 1;
|
|
76
76
|
border-radius: var(--track-radius, 2px);
|
|
77
77
|
background: var(--openpen-color-border-hi);
|
|
78
78
|
}
|
|
79
|
-
.app-slider-root[data-orientation='horizontal'] .app-slider-track[data-v-
|
|
79
|
+
.app-slider-root[data-orientation='horizontal'] .app-slider-track[data-v-ae09a023] {
|
|
80
80
|
height: var(--track-height, 4px);
|
|
81
81
|
}
|
|
82
|
-
.app-slider-root[data-orientation='vertical'] .app-slider-track[data-v-
|
|
82
|
+
.app-slider-root[data-orientation='vertical'] .app-slider-track[data-v-ae09a023] {
|
|
83
83
|
width: var(--track-height, 4px);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
/* ── Range (filled portion) ─────────────────────────────────────────────────── */
|
|
87
|
-
.app-slider-range[data-v-
|
|
87
|
+
.app-slider-range[data-v-ae09a023] {
|
|
88
88
|
position: absolute;
|
|
89
89
|
border-radius: var(--track-radius, 2px);
|
|
90
90
|
background: var(--openpen-color-accent);
|
|
91
91
|
}
|
|
92
|
-
.app-slider-root[data-orientation='horizontal'] .app-slider-range[data-v-
|
|
92
|
+
.app-slider-root[data-orientation='horizontal'] .app-slider-range[data-v-ae09a023] {
|
|
93
93
|
height: 100%;
|
|
94
94
|
top: 0;
|
|
95
95
|
left: 0;
|
|
96
96
|
}
|
|
97
|
-
.app-slider-root[data-orientation='vertical'] .app-slider-range[data-v-
|
|
97
|
+
.app-slider-root[data-orientation='vertical'] .app-slider-range[data-v-ae09a023] {
|
|
98
98
|
width: 100%;
|
|
99
99
|
bottom: 0;
|
|
100
100
|
left: 0;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
/* ── Thumb ─────────────────────────────────────────────────────────────────── */
|
|
104
|
-
.app-slider-thumb[data-v-
|
|
104
|
+
.app-slider-thumb[data-v-ae09a023] {
|
|
105
105
|
display: block;
|
|
106
106
|
width: var(--thumb-w, 14px);
|
|
107
107
|
height: var(--thumb-h, 14px);
|
|
@@ -112,15 +112,15 @@ to { opacity: 0; transform: scale(0.92);
|
|
|
112
112
|
outline: none;
|
|
113
113
|
transition: transform var(--openpen-duration-fast) var(--openpen-easing-standard);
|
|
114
114
|
}
|
|
115
|
-
.app-slider-thumb[data-v-
|
|
115
|
+
.app-slider-thumb[data-v-ae09a023]:hover {
|
|
116
116
|
transform: scale(1.2);
|
|
117
117
|
}
|
|
118
|
-
.app-slider-thumb[data-v-
|
|
118
|
+
.app-slider-thumb[data-v-ae09a023]:focus-visible {
|
|
119
119
|
box-shadow: 0 0 0 3px var(--openpen-color-accent-glow);
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
/* Root — the toggle track */
|
|
123
|
-
.app-toggle[data-v-
|
|
123
|
+
.app-toggle[data-v-d23dd615] {
|
|
124
124
|
width: 40px;
|
|
125
125
|
height: 24px;
|
|
126
126
|
border-radius: 12px;
|
|
@@ -137,12 +137,12 @@ to { opacity: 0; transform: scale(0.92);
|
|
|
137
137
|
display: inline-flex;
|
|
138
138
|
align-items: center;
|
|
139
139
|
}
|
|
140
|
-
.app-toggle.on[data-v-
|
|
140
|
+
.app-toggle.on[data-v-d23dd615] {
|
|
141
141
|
background: var(--openpen-color-accent);
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
/* Thumb — the white circular knob */
|
|
145
|
-
.app-toggle-thumb[data-v-
|
|
145
|
+
.app-toggle-thumb[data-v-d23dd615] {
|
|
146
146
|
position: absolute;
|
|
147
147
|
top: 3px;
|
|
148
148
|
left: 3px;
|
|
@@ -155,16 +155,16 @@ to { opacity: 0; transform: scale(0.92);
|
|
|
155
155
|
display: block;
|
|
156
156
|
pointer-events: none;
|
|
157
157
|
}
|
|
158
|
-
.app-toggle.on .app-toggle-thumb[data-v-
|
|
159
|
-
.app-toggle[data-state='checked'] .app-toggle-thumb[data-v-
|
|
158
|
+
.app-toggle.on .app-toggle-thumb[data-v-d23dd615],
|
|
159
|
+
.app-toggle[data-state='checked'] .app-toggle-thumb[data-v-d23dd615] {
|
|
160
160
|
left: 19px;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
.app-seg[data-v-
|
|
163
|
+
.app-seg[data-v-eaacab1f] {
|
|
164
164
|
display: flex;
|
|
165
165
|
gap: var(--openpen-space-xs);
|
|
166
166
|
}
|
|
167
|
-
.app-seg-btn[data-v-
|
|
167
|
+
.app-seg-btn[data-v-eaacab1f] {
|
|
168
168
|
flex: 1;
|
|
169
169
|
display: flex;
|
|
170
170
|
align-items: center;
|
|
@@ -184,22 +184,22 @@ to { opacity: 0; transform: scale(0.92);
|
|
|
184
184
|
color var(--openpen-duration-fast) var(--openpen-easing-standard),
|
|
185
185
|
border-color var(--openpen-duration-fast) var(--openpen-easing-standard);
|
|
186
186
|
}
|
|
187
|
-
.app-seg-icon[data-v-
|
|
187
|
+
.app-seg-icon[data-v-eaacab1f] {
|
|
188
188
|
display: inline-flex;
|
|
189
189
|
align-items: center;
|
|
190
190
|
justify-content: center;
|
|
191
191
|
line-height: 0;
|
|
192
192
|
}
|
|
193
|
-
.app-seg-btn[data-v-
|
|
193
|
+
.app-seg-btn[data-v-eaacab1f]:hover {
|
|
194
194
|
color: var(--openpen-color-text-primary);
|
|
195
195
|
background: var(--openpen-color-control-hover);
|
|
196
196
|
}
|
|
197
|
-
.app-seg-btn.active[data-v-
|
|
197
|
+
.app-seg-btn.active[data-v-eaacab1f] {
|
|
198
198
|
background: var(--openpen-color-accent-bg);
|
|
199
199
|
color: var(--openpen-color-accent);
|
|
200
200
|
border-color: color-mix(in srgb, var(--openpen-color-accent) 40%, transparent);
|
|
201
201
|
}
|
|
202
|
-
.app-seg--disabled[data-v-
|
|
202
|
+
.app-seg--disabled[data-v-eaacab1f] {
|
|
203
203
|
opacity: 0.45;
|
|
204
204
|
pointer-events: none;
|
|
205
205
|
cursor: not-allowed;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openpen/module-api",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "OpenPen Module API — public contract for built-in modules and third-party plugins",
|
|
5
5
|
"author": "navishachiku <navishachiku@gmail.com>",
|
|
6
6
|
"homepage": "https://github.com/openpen-platform/openpen#readme",
|
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
"sdk"
|
|
64
64
|
],
|
|
65
65
|
"dependencies": {
|
|
66
|
-
"dompurify": "^3.4.
|
|
67
|
-
"reka-ui": "~2.9.
|
|
66
|
+
"dompurify": "^3.4.8",
|
|
67
|
+
"reka-ui": "~2.9.9"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
70
|
"vue": "^3.5.0",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"AppButtonDropdown.vue_vue_type_style_index_0_lang-DBVFUpxc.js","sources":["../../src/uikit/components/AppDialog.vue","../../src/uikit/composables/useDialog.ts","../../src/uikit/composables/dialog-internal-keys.ts"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * AppDialog — High-level wrapper (Layer 1).\n *\n * Plugin authors use this component without needing to know about inject\n * keys, Reka UI, modal manager mutual exclusion, or the animating guard.\n *\n * Public API is stable: switching the underlying headless library in the\n * future does not break the plugin author's code.\n *\n * Implementation notes:\n * - `open` prop is the single source of truth (controlled mode). The\n * component emits `update:open` for two-way binding. Internally it also\n * registers with the modal manager for mutual exclusion (same pattern as\n * AppPopover): when `open` turns true we call manager.open(); when\n * another panel causes the manager to close us we emit update:open(false).\n * - `persistent`: when true, ESC and backdrop clicks are suppressed via\n * @interact-outside.prevent and @escape-key-down.prevent on DialogContent.\n * - `danger`: adds class `openpen-modal-danger` on the content element as a\n * styling hook for destructive-action dialogs (no default style yet).\n * - animating guard: auto-close during ControlBar scaleY animation.\n * - usePassthroughGuard: registered on the dialog content element so the\n * transparent Electron window remains clickable when dialog is open.\n * - Styles are unscoped because DialogContent is teleported outside the\n * component's DOM subtree (same reason as AppPopover).\n *\n * CSS class names (.openpen-modal-*) are intentionally kept as the carried-over\n * internal styling tokens — renaming carries no DX benefit and risks accidental\n * style breakage in any code path still referencing them.\n */\nimport { computed, inject, onBeforeUnmount, ref, useTemplateRef, watch } from 'vue'\nimport {\n DialogRoot,\n DialogTrigger,\n DialogPortal,\n DialogContent,\n DialogOverlay,\n DialogClose,\n DialogTitle,\n DialogDescription,\n} from '../primitives'\n// Inject keys MUST come from the package barrel — see AppPopover.vue\n// header comment for why relative imports break Symbol identity in prod.\nimport {\n MODAL_MANAGER_KEY,\n CONTROL_BAR_ANIMATING_KEY,\n HOST_DIALOG_OPEN_COUNT_KEY,\n} from '@openpen/module-api'\nimport { usePassthroughGuard } from '../../host/passthrough'\n\n// ── OpenPen-owned types (MUST NOT reference reka-ui types) ───────────────────\n\nexport interface AppDialogProps {\n /** Globally unique modal id; used for modal-stack mutual exclusion. */\n modalId: string\n /** Dialog title displayed in the dialog header. */\n title: string\n /** Controlled open state. The parent must handle update:open to toggle. */\n open: boolean\n /**\n * When true, ESC key and backdrop clicks do not close the dialog.\n * Use for confirmations that require an explicit button press.\n */\n persistent?: boolean\n /**\n * When true, adds the `openpen-modal-danger` CSS class on the content\n * element — a hook for styling destructive-action dialogs.\n */\n danger?: boolean\n}\n\nexport interface AppDialogSlots {\n /**\n * Trigger slot; scope exposes active state + toggle / open / close actions.\n * MUST NOT write @click=\"toggle\" on the trigger — DialogTrigger's as-child\n * handles activation automatically; toggle/open/close are escape hatches for\n * programmatic control only.\n */\n trigger: (scope: {\n active: boolean\n toggle: () => void\n open: () => void\n close: () => void\n }) => unknown\n /** Dialog body content slot. */\n default: () => unknown\n /** Optional footer slot (action buttons etc.). */\n footer?: () => unknown\n}\n\nconst props = withDefaults(defineProps<AppDialogProps>(), {\n persistent: false,\n danger: false,\n})\n\nconst emit = defineEmits<{\n 'update:open': [value: boolean]\n}>()\n\ndefineSlots<AppDialogSlots>()\n\n// ── 1. Inject host context (plugin author never sees these) ───────────────────\n\nconst modalManager = inject(MODAL_MANAGER_KEY)\nconst animating = inject(CONTROL_BAR_ANIMATING_KEY)\nconst dialogOpenCount = inject(HOST_DIALOG_OPEN_COUNT_KEY, null)\n\n// Track whether THIS instance currently holds a count, so we never double\n// increment / decrement (e.g. on rapid open / close cycles or unmount-while-open).\nlet counted = false\n\nfunction acquireCount() {\n if (counted || !dialogOpenCount) return\n dialogOpenCount.value += 1\n counted = true\n}\n\nfunction releaseCount() {\n if (!counted || !dialogOpenCount) return\n dialogOpenCount.value -= 1\n counted = false\n}\n\nonBeforeUnmount(releaseCount)\n\n// ── 2. Modal manager mutual exclusion + controlled open prop ─────────────────\n//\n// The `open` prop is the canonical state. We watch it and push changes into\n// the modal manager. When the manager closes us (e.g. another panel opens),\n// we detect that via the computed isManagerOpen and emit update:open(false).\n\nfunction openDialog() {\n modalManager?.open(props.modalId)\n emit('update:open', true)\n}\n\nfunction closeDialog() {\n modalManager?.close(props.modalId)\n emit('update:open', false)\n}\n\nfunction toggleDialog() {\n if (props.open) {\n closeDialog()\n } else {\n openDialog()\n }\n}\n\n// When the prop turns true: register with the manager + bump host dialog count.\n// When it turns false: unregister + release count. Keeps everything in sync.\nwatch(\n () => props.open,\n (newOpen) => {\n if (newOpen) {\n modalManager?.open(props.modalId)\n acquireCount()\n } else {\n modalManager?.close(props.modalId)\n releaseCount()\n }\n },\n { immediate: true }\n)\n\n// When the manager closes us (another panel opened), emit update:open(false)\n// so the parent's v-model updates.\nconst isManagerOpen = computed(() => modalManager?.isOpen(props.modalId) ?? props.open)\n\nwatch(isManagerOpen, (managerOpen) => {\n if (!managerOpen && props.open) {\n emit('update:open', false)\n }\n})\n\n// Effective open state passed to Reka UI's DialogRoot.\nconst isOpen = computed(() => props.open && isManagerOpen.value)\n\n// Sync Reka UI's internal close events (ESC, overlay click) back to caller\n// unless `persistent` suppresses them.\nfunction onUpdateOpen(v: boolean) {\n if (!v && props.persistent) {\n // Persistent mode: ignore Reka UI close attempts; stay open.\n return\n }\n if (v) {\n openDialog()\n } else {\n closeDialog()\n }\n}\n\n// ── 3. ControlBar animation guard — auto-close during transitions ─────────────\n\nwatch(\n () => animating?.value,\n (isAnimating) => {\n if (isAnimating && props.open) {\n closeDialog()\n }\n }\n)\n\n// ── 4. Passthrough guard integration ─────────────────────────────────────────\n//\n// DialogContent is a Vue component; normalize to HTMLElement via watcher.\n\nconst contentInstance = useTemplateRef<HTMLElement | { $el?: HTMLElement } | null>('content')\nconst contentEl = ref<HTMLElement | null>(null)\n\nwatch(contentInstance, (instance) => {\n if (!instance) {\n contentEl.value = null\n } else if (instance instanceof HTMLElement) {\n contentEl.value = instance\n } else if (typeof instance === 'object' && '$el' in instance && instance.$el instanceof HTMLElement) {\n contentEl.value = instance.$el\n } else {\n contentEl.value = null\n }\n})\n\nusePassthroughGuard(contentEl)\n</script>\n\n<template>\n <DialogRoot :open=\"isOpen\" @update:open=\"onUpdateOpen\">\n <DialogTrigger as-child>\n <slot\n name=\"trigger\"\n :active=\"isOpen\"\n :toggle=\"toggleDialog\"\n :open=\"openDialog\"\n :close=\"closeDialog\"\n />\n </DialogTrigger>\n <DialogPortal>\n <DialogOverlay class=\"openpen-modal-overlay\" />\n <DialogContent\n ref=\"content\"\n class=\"openpen-modal-content openpen-interactive\"\n :class=\"{ 'openpen-modal-danger': danger }\"\n @interact-outside=\"(e: Event) => persistent && e.preventDefault()\"\n @escape-key-down=\"(e: KeyboardEvent) => persistent && e.preventDefault()\"\n >\n <DialogTitle class=\"openpen-modal-title\">{{ title }}</DialogTitle>\n <DialogDescription class=\"openpen-modal-description sr-only\" />\n <div class=\"openpen-modal-body\">\n <slot />\n </div>\n <div v-if=\"$slots.footer\" class=\"openpen-modal-footer\">\n <slot name=\"footer\" />\n </div>\n <DialogClose class=\"openpen-modal-close\" aria-label=\"Close\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M1 1L13 13M13 1L1 13\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"/>\n </svg>\n </DialogClose>\n </DialogContent>\n </DialogPortal>\n </DialogRoot>\n</template>\n\n<!-- Styles MUST be unscoped — Vue scoped CSS does not follow Teleport targets,\n so .openpen-modal-* classes would not receive the scoped data-v-* attribute\n when rendered inside the portal. -->\n<style>\n/* ── Overlay (transparent click absorber, NOT a dim backdrop) ─────────────────\n OpenPen's overlay window is transparent + frameless. A traditional dim/blur\n backdrop renders as a small dark patch only over the visible chrome — visually\n jarring. The overlay is therefore kept fully transparent here; its only job\n is to absorb outside clicks (so Reka can dispatch interact-outside → close\n when not persistent) and to protect the canvas from stray clicks while the\n dialog is open. Host containers (e.g. ControlBar) render their own \"locked\"\n visual state via HOST_DIALOG_OPEN_COUNT_KEY. */\n.openpen-modal-overlay {\n position: fixed;\n inset: 0;\n background: transparent;\n z-index: 200;\n pointer-events: auto;\n}\n\n/* ── Dialog content panel ────────────────────────────────────────────────── */\n.openpen-modal-content {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n z-index: 201;\n min-width: 320px;\n max-width: min(480px, calc(100vw - 48px));\n max-height: calc(100vh - 96px);\n overflow-y: auto;\n\n background: var(--openpen-color-surface-popup);\n border: 1px solid var(--openpen-color-border-hi);\n border-radius: var(--openpen-radius-lg);\n box-shadow: var(--openpen-shadow);\n backdrop-filter: var(--openpen-blur);\n -webkit-backdrop-filter: var(--openpen-blur);\n\n color: var(--openpen-color-text-primary);\n font-size: 13px;\n padding: var(--openpen-space-lg);\n pointer-events: auto;\n}\n\n/* ── Title ───────────────────────────────────────────────────────────────── */\n.openpen-modal-title {\n font-size: 15px;\n font-weight: 600;\n color: var(--openpen-color-text-primary);\n margin: 0 0 var(--openpen-space-md) 0;\n padding-right: 28px; /* leave room for close button */\n line-height: 1.4;\n}\n\n/* ── Visually hidden description (a11y) ──────────────────────────────────── */\n.openpen-modal-description.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border-width: 0;\n}\n\n/* ── Body ────────────────────────────────────────────────────────────────── */\n.openpen-modal-body {\n color: var(--openpen-color-text-primary);\n}\n\n/* ── Footer ──────────────────────────────────────────────────────────────── */\n.openpen-modal-footer {\n display: flex;\n justify-content: flex-end;\n gap: var(--openpen-space-sm);\n margin-top: var(--openpen-space-lg);\n padding-top: var(--openpen-space-md);\n border-top: 1px solid var(--openpen-color-border);\n}\n\n/* ── Close button ────────────────────────────────────────────────────────── */\n.openpen-modal-close {\n position: absolute;\n top: var(--openpen-space-md);\n right: var(--openpen-space-md);\n width: 24px;\n height: 24px;\n display: flex;\n align-items: center;\n justify-content: center;\n border: none;\n background: transparent;\n color: var(--openpen-color-text-dim);\n border-radius: var(--openpen-radius-sm);\n cursor: pointer;\n transition: background var(--openpen-duration-fast), color var(--openpen-duration-fast);\n}\n\n.openpen-modal-close:hover {\n background: var(--openpen-color-control-hover);\n color: var(--openpen-color-text-primary);\n}\n\n/* ── Open / close animations ─────────────────────────────────────────────────\n Reka UI's Presence keeps the element in the DOM until the CSS animation\n ends, so [data-state=\"closed\"] animations DO play on close.\n Pattern matches AppPopover keyframes (200ms bounce enter, 150ms ease leave). */\n\n/* Overlay is always transparent — no fade-in/out animation needed.\n Only the dialog content panel animates. */\n\n.openpen-modal-content[data-state='open'] {\n animation: openpen-modal-in 200ms cubic-bezier(0.34, 1.56, 0.64, 1);\n}\n.openpen-modal-content[data-state='closed'] {\n animation: openpen-modal-out 150ms ease forwards;\n}\n\n@keyframes openpen-modal-in {\n from { opacity: 0; transform: translate(-50%, -50%) scale(0.92); }\n to { opacity: 1; transform: translate(-50%, -50%) scale(1); }\n}\n@keyframes openpen-modal-out {\n from { opacity: 1; transform: translate(-50%, -50%) scale(1); }\n to { opacity: 0; transform: translate(-50%, -50%) scale(0.92); }\n}\n</style>\n","/**\n * useDialog — imperative dialog API for OpenPen modules.\n *\n * Returns a DialogApi with confirm/alert/prompt/custom methods that each\n * return a Promise. At most one imperative dialog is open at a time; extra\n * calls are queued and executed sequentially.\n *\n * The active request is stored in a module-scoped reactive store. DialogHost\n * (mounted by the host at bootstrap) reads this store and renders the dialog.\n *\n * Usage:\n * const dialog = useDialog()\n * const ok = await dialog.confirm({ title: 'Delete?', message: '...' })\n */\nimport type { Component } from 'vue'\nimport { ref } from 'vue'\n\n// ── Public option types ───────────────────────────────────────────────────────\n\ninterface BaseDialogOptions {\n title: string\n persistent?: boolean\n danger?: boolean\n}\n\nexport interface DialogConfirmOptions extends BaseDialogOptions {\n message: string\n okLabel?: string\n cancelLabel?: string\n}\n\nexport interface DialogAlertOptions extends BaseDialogOptions {\n message: string\n okLabel?: string\n}\n\nexport interface DialogPromptOptions extends BaseDialogOptions {\n message: string\n defaultValue?: string\n placeholder?: string\n okLabel?: string\n cancelLabel?: string\n}\n\nexport interface DialogCustomOptions<T = unknown> extends BaseDialogOptions {\n component: Component\n componentProps?: Record<string, unknown>\n /** Phantom type field for TypeScript to infer the resolution payload type. */\n __payloadType?: T\n}\n\n// ── Internal request types (tagged union) ─────────────────────────────────────\n\ntype DialogRequest =\n | { kind: 'confirm'; opts: DialogConfirmOptions; resolve: (v: boolean) => void }\n | { kind: 'alert'; opts: DialogAlertOptions; resolve: () => void }\n | { kind: 'prompt'; opts: DialogPromptOptions; resolve: (v: string | null) => void }\n | { kind: 'custom'; opts: DialogCustomOptions<unknown>; resolve: (v: unknown) => void }\n\n// ── Module-scoped store ───────────────────────────────────────────────────────\n//\n// Single reactive ref holds the currently rendered request. DialogHost watches\n// this and renders the appropriate dialog. When DialogHost resolves a request,\n// it calls resolveActive() which drains the queue.\n\nexport const _activeRequest = ref<DialogRequest | null>(null)\n\nconst _queue: (() => void)[] = []\nlet _processing = false\n\nfunction _enqueue(run: () => void): void {\n if (_processing) {\n _queue.push(run)\n } else {\n _processing = true\n run()\n }\n}\n\n/**\n * Called by DialogHost when the dialog is resolved (ok/cancel/dismiss).\n * Advances the queue if there are pending requests.\n */\nexport function _resolveActive(): void {\n _activeRequest.value = null\n const next = _queue.shift()\n if (next) {\n next()\n } else {\n _processing = false\n }\n}\n\n// ── Public API ────────────────────────────────────────────────────────────────\n\nexport interface DialogApi {\n confirm(opts: DialogConfirmOptions): Promise<boolean>\n alert(opts: DialogAlertOptions): Promise<void>\n prompt(opts: DialogPromptOptions): Promise<string | null>\n custom<T = unknown>(opts: DialogCustomOptions<T>): Promise<T | null>\n}\n\nexport function useDialog(): DialogApi {\n return {\n confirm(opts: DialogConfirmOptions): Promise<boolean> {\n return new Promise<boolean>((resolve) => {\n _enqueue(() => {\n _activeRequest.value = { kind: 'confirm', opts, resolve }\n })\n })\n },\n\n alert(opts: DialogAlertOptions): Promise<void> {\n return new Promise<void>((resolve) => {\n _enqueue(() => {\n _activeRequest.value = { kind: 'alert', opts, resolve }\n })\n })\n },\n\n prompt(opts: DialogPromptOptions): Promise<string | null> {\n return new Promise<string | null>((resolve) => {\n _enqueue(() => {\n _activeRequest.value = { kind: 'prompt', opts, resolve }\n })\n })\n },\n\n custom<T = unknown>(opts: DialogCustomOptions<T>): Promise<T | null> {\n return new Promise<T | null>((resolve) => {\n _enqueue(() => {\n _activeRequest.value = {\n kind: 'custom',\n opts: opts as DialogCustomOptions<unknown>,\n resolve: resolve as (v: unknown) => void,\n }\n })\n })\n },\n }\n}\n","/**\n * dialog-internal-keys.ts — Internal injection keys for the imperative dialog system.\n *\n * HOST-INTERNAL: Do not export from any public barrel. Plugin authors must\n * never see or depend on these keys; they exist only so DialogHost can\n * provide the handle that useDialogPluginComponent() reads.\n */\nimport type { InjectionKey } from 'vue'\nimport type { DialogPluginComponentHandle } from './useDialogPluginComponent'\n\n/**\n * Injection key that DialogHost provides to custom dialog components.\n * Components rendered via useDialog().custom() use useDialogPluginComponent()\n * which injects this key to get ok/cancel/dismiss callbacks.\n */\nexport const DIALOG_PLUGIN_HANDLE_KEY: InjectionKey<DialogPluginComponentHandle<unknown>> =\n Symbol('openpen:dialog-plugin-handle')\n"],"names":["_createBlock","_unref","_createVNode","_renderSlot","_normalizeClass","_createElementVNode","$slots","_openBlock","_createElementBlock"],"mappings":"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"}
|