@openpen/module-api 1.1.0 → 1.2.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.
Files changed (98) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/_chunks/AppButtonDropdown.vue_vue_type_style_index_0_lang-DBVFUpxc.js +245 -0
  3. package/dist/_chunks/AppButtonDropdown.vue_vue_type_style_index_0_lang-DBVFUpxc.js.map +1 -0
  4. package/dist/_chunks/inject-keys-CRXCpN-M.js +196 -0
  5. package/dist/_chunks/inject-keys-CRXCpN-M.js.map +1 -0
  6. package/dist/_chunks/module-context-registry-Cn1IWYu9.js +16 -0
  7. package/dist/_chunks/module-context-registry-Cn1IWYu9.js.map +1 -0
  8. package/dist/_chunks/passthrough-DKU7JYuQ.js +6 -0
  9. package/dist/_chunks/passthrough-DKU7JYuQ.js.map +1 -0
  10. package/dist/cursors.d.ts +83 -0
  11. package/dist/define-module.d.ts +17 -0
  12. package/dist/host/color-utils.d.ts +11 -0
  13. package/{src/host/event-bus.ts → dist/host/event-bus.d.ts} +7 -5
  14. package/dist/host/host-commands.d.ts +1 -0
  15. package/dist/host/index.d.ts +24 -0
  16. package/dist/host/index.js +49 -0
  17. package/dist/host/index.js.map +1 -0
  18. package/{src/host/module-context-registry.ts → dist/host/module-context-registry.d.ts} +5 -19
  19. package/dist/host/passthrough.d.ts +2 -0
  20. package/dist/host/popup-anchor.d.ts +3 -0
  21. package/dist/host/registry.d.ts +39 -0
  22. package/dist/host/registry.js +37 -0
  23. package/dist/host/registry.js.map +1 -0
  24. package/dist/host/slot-entries.d.ts +2 -0
  25. package/dist/host/stroke-store.d.ts +5 -0
  26. package/dist/host/stroke-style.d.ts +3 -0
  27. package/dist/host/types.d.ts +164 -0
  28. package/dist/index.d.ts +28 -0
  29. package/dist/index.js +349 -0
  30. package/dist/index.js.map +1 -0
  31. package/{src/inject-keys.ts → dist/inject-keys.d.ts} +37 -66
  32. package/{src/locale.ts → dist/locale.d.ts} +3 -22
  33. package/dist/slots.d.ts +57 -0
  34. package/dist/types/contributions.d.ts +288 -0
  35. package/dist/types/control-bar-layout.d.ts +227 -0
  36. package/dist/types/module.d.ts +208 -0
  37. package/{src/types/notification.ts → dist/types/notification.d.ts} +12 -14
  38. package/dist/types/tool.d.ts +77 -0
  39. package/dist/uikit/components/AppBanner.vue.d.ts +34 -0
  40. package/dist/uikit/components/AppButton.vue.d.ts +30 -0
  41. package/dist/uikit/components/AppButtonDropdown.vue.d.ts +52 -0
  42. package/dist/uikit/components/AppDialog.vue.d.ts +53 -0
  43. package/dist/uikit/components/AppPopover.vue.d.ts +43 -0
  44. package/dist/uikit/components/AppSegmented.vue.d.ts +19 -0
  45. package/dist/uikit/components/AppSelect.vue.d.ts +26 -0
  46. package/dist/uikit/components/AppSlider.vue.d.ts +43 -0
  47. package/dist/uikit/components/AppTabs.vue.d.ts +35 -0
  48. package/dist/uikit/components/AppToggle.vue.d.ts +13 -0
  49. package/dist/uikit/components/AppTooltip.vue.d.ts +27 -0
  50. package/dist/uikit/components/DialogHost.vue.d.ts +3 -0
  51. package/{src/uikit/composables/dialog-internal-keys.ts → dist/uikit/composables/dialog-internal-keys.d.ts} +3 -5
  52. package/dist/uikit/composables/useDialog.d.ts +399 -0
  53. package/dist/uikit/composables/useDialogPluginComponent.d.ts +9 -0
  54. package/dist/uikit/index.d.ts +42 -0
  55. package/dist/uikit/index.js +884 -0
  56. package/dist/uikit/index.js.map +1 -0
  57. package/{src/uikit/internal.ts → dist/uikit/internal.d.ts} +1 -1
  58. package/dist/uikit/internal.js +157 -0
  59. package/dist/uikit/internal.js.map +1 -0
  60. package/dist/uikit/module-api.css +851 -0
  61. package/dist/uikit/primitives.d.ts +42 -0
  62. package/{src/use-module-context.ts → dist/use-module-context.d.ts} +2 -15
  63. package/{src/validation.ts → dist/validation.d.ts} +2 -6
  64. package/package.json +26 -12
  65. package/packages/module-api/CHANGELOG.md +0 -27
  66. package/src/cursors.ts +0 -369
  67. package/src/define-module.ts +0 -55
  68. package/src/host/color-utils.ts +0 -18
  69. package/src/host/host-commands.ts +0 -9
  70. package/src/host/index.ts +0 -53
  71. package/src/host/passthrough.ts +0 -10
  72. package/src/host/popup-anchor.ts +0 -14
  73. package/src/host/slot-entries.ts +0 -9
  74. package/src/host/stroke-store.ts +0 -20
  75. package/src/host/stroke-style.ts +0 -15
  76. package/src/index.ts +0 -131
  77. package/src/slots.ts +0 -135
  78. package/src/types/contributions.ts +0 -333
  79. package/src/types/control-bar-layout.ts +0 -174
  80. package/src/types/module.ts +0 -225
  81. package/src/types/tool.ts +0 -82
  82. package/src/uikit/components/.gitkeep +0 -0
  83. package/src/uikit/components/AppBanner.vue +0 -131
  84. package/src/uikit/components/AppButton.vue +0 -135
  85. package/src/uikit/components/AppDialog.vue +0 -393
  86. package/src/uikit/components/AppPopover.vue +0 -268
  87. package/src/uikit/components/AppSegmented.vue +0 -128
  88. package/src/uikit/components/AppSelect.vue +0 -235
  89. package/src/uikit/components/AppSlider.vue +0 -204
  90. package/src/uikit/components/AppTabs.vue +0 -163
  91. package/src/uikit/components/AppToggle.vue +0 -96
  92. package/src/uikit/components/AppTooltip.vue +0 -118
  93. package/src/uikit/components/DialogHost.vue +0 -272
  94. package/src/uikit/composables/useDialog.ts +0 -141
  95. package/src/uikit/composables/useDialogPluginComponent.ts +0 -27
  96. package/src/uikit/index.ts +0 -79
  97. package/src/uikit/primitives.ts +0 -150
  98. /package/{src → dist}/uikit/tokens.css +0 -0
@@ -1,163 +0,0 @@
1
- <script setup lang="ts">
2
- /**
3
- * AppTabs — High-level wrapper (Layer 1).
4
- *
5
- * Wraps Reka UI Tabs (TabsRoot / TabsList / TabsTrigger / TabsContent).
6
- * Intended for settings UI tabbed navigation and any plugin tab panel.
7
- *
8
- * Plugin authors get a controlled tab switcher with OpenPen styling without
9
- * knowing about Reka UI internals.
10
- *
11
- * Styles use --openpen-* design tokens.
12
- * No Reka UI types are leaked in the public API.
13
- *
14
- * Usage:
15
- * <AppTabs
16
- * v-model="activeTab"
17
- * :tabs="[{ id: 'general', label: 'General' }, { id: 'advanced', label: 'Advanced' }]"
18
- * >
19
- * <template #default="{ activeTabId }">
20
- * <div v-if="activeTabId === 'general'">...</div>
21
- * <div v-if="activeTabId === 'advanced'">...</div>
22
- * </template>
23
- * </AppTabs>
24
- */
25
- import {
26
- TabsRoot,
27
- TabsList,
28
- TabsTrigger,
29
- TabsContent,
30
- } from '../primitives'
31
-
32
- // ── OpenPen-owned types (MUST NOT reference reka-ui types) ────────────────────
33
-
34
- export interface AppTabItem {
35
- /** Unique identifier for this tab. Used as the v-model value. */
36
- id: string
37
- /** Human-readable label rendered in the tab trigger button. */
38
- label: string
39
- }
40
-
41
- export interface AppTabsProps {
42
- /** Currently active tab id (controlled mode). */
43
- modelValue: string
44
- /** Ordered list of tab descriptors. */
45
- tabs: AppTabItem[]
46
- }
47
-
48
- export interface AppTabsSlots {
49
- /**
50
- * Default slot for tab content area.
51
- * Receives the currently active tab id via scoped slot.
52
- */
53
- default: (scope: { activeTabId: string }) => unknown
54
- }
55
-
56
- const props = defineProps<AppTabsProps>()
57
-
58
- const emit = defineEmits<{
59
- 'update:modelValue': [value: string]
60
- }>()
61
-
62
- defineSlots<AppTabsSlots>()
63
-
64
- function onTabChange(val: string | number | undefined) {
65
- if (typeof val === 'string') {
66
- emit('update:modelValue', val)
67
- }
68
- }
69
- </script>
70
-
71
- <template>
72
- <TabsRoot
73
- :model-value="props.modelValue"
74
- class="openpen-tabs"
75
- @update:model-value="onTabChange"
76
- >
77
- <TabsList class="openpen-tabs-list">
78
- <TabsTrigger
79
- v-for="tab in props.tabs"
80
- :key="tab.id"
81
- :value="tab.id"
82
- class="openpen-tabs-trigger"
83
- >
84
- {{ tab.label }}
85
- </TabsTrigger>
86
- </TabsList>
87
-
88
- <!-- Single content outlet: plugin author renders tab content via scoped slot. -->
89
- <TabsContent
90
- v-for="tab in props.tabs"
91
- :key="tab.id"
92
- :value="tab.id"
93
- class="openpen-tabs-content"
94
- >
95
- <slot :active-tab-id="tab.id" />
96
- </TabsContent>
97
- </TabsRoot>
98
- </template>
99
-
100
- <style scoped>
101
- /* ── Tab root ───────────────────────────────────────────────────────────────── */
102
- .openpen-tabs {
103
- display: flex;
104
- flex-direction: column;
105
- gap: var(--openpen-space-sm);
106
- }
107
-
108
- /* ── Tab list (trigger bar) ─────────────────────────────────────────────────── */
109
- .openpen-tabs-list {
110
- display: flex;
111
- flex-direction: row;
112
- gap: 2px;
113
- padding: 3px;
114
- background: var(--openpen-color-control-group);
115
- border: 1px solid var(--openpen-color-border);
116
- border-radius: var(--openpen-radius-md);
117
- }
118
-
119
- /* ── Individual trigger ─────────────────────────────────────────────────────── */
120
- .openpen-tabs-trigger {
121
- flex: 1;
122
- padding: var(--openpen-space-xs) var(--openpen-space-sm);
123
- border: none;
124
- border-radius: calc(var(--openpen-radius-md) - 3px);
125
- background: transparent;
126
- color: var(--openpen-color-text-dim);
127
- font-size: 13px;
128
- font-weight: 500;
129
- cursor: pointer;
130
- transition:
131
- background var(--openpen-duration-fast) var(--openpen-easing-standard),
132
- color var(--openpen-duration-fast) var(--openpen-easing-standard);
133
- white-space: nowrap;
134
- /* Remove default button outline; focus-visible ring below handles a11y. */
135
- outline: none;
136
- }
137
-
138
- .openpen-tabs-trigger:hover {
139
- background: var(--openpen-color-control-hover);
140
- color: var(--openpen-color-text-primary);
141
- }
142
-
143
- .openpen-tabs-trigger[data-state='active'] {
144
- background: var(--openpen-color-accent-bg);
145
- color: var(--openpen-color-accent);
146
- box-shadow: 0 0 0 1px var(--openpen-color-accent-glow);
147
- }
148
-
149
- .openpen-tabs-trigger:focus-visible {
150
- box-shadow: 0 0 0 2px var(--openpen-color-accent);
151
- }
152
-
153
- /* ── Tab content ────────────────────────────────────────────────────────────── */
154
- .openpen-tabs-content {
155
- /* Content area has no chrome by default; plugin authors add their own. */
156
- outline: none;
157
- }
158
-
159
- .openpen-tabs-content:focus-visible {
160
- box-shadow: 0 0 0 2px var(--openpen-color-accent);
161
- border-radius: var(--openpen-radius-sm);
162
- }
163
- </style>
@@ -1,96 +0,0 @@
1
- <script setup lang="ts">
2
- /**
3
- * AppToggle — High-level wrapper (Layer 1).
4
- *
5
- * Wraps Reka UI Switch (SwitchRoot / SwitchThumb).
6
- *
7
- * Visual: 40×24px, off=toggle-off-bg, on=accent,
8
- * 18×18 white thumb, bounce transition.
9
- *
10
- * Styles use --openpen-* design tokens.
11
- * No Reka UI types are leaked in the public API.
12
- *
13
- * DOM notes:
14
- * - SwitchRoot renders a <button role="switch"> with class "app-toggle"
15
- * - "on" class is added when modelValue is true
16
- * - aria-pressed is added explicitly because Reka UI's SwitchRoot uses
17
- * aria-checked; both attributes co-exist for maximum a11y surface.
18
- */
19
- import {
20
- SwitchRoot,
21
- SwitchThumb,
22
- } from '../primitives'
23
-
24
- // ── Public API (MUST NOT reference reka-ui types) ─────────────────────────────
25
-
26
- withDefaults(defineProps<{
27
- modelValue: boolean
28
- ariaLabel?: string
29
- }>(), { ariaLabel: '' })
30
-
31
- const emit = defineEmits<{
32
- 'update:modelValue': [value: boolean]
33
- }>()
34
-
35
- function onCheckedChange(val: boolean) {
36
- emit('update:modelValue', val)
37
- }
38
- </script>
39
-
40
- <template>
41
- <SwitchRoot
42
- class="app-toggle"
43
- :class="{ on: modelValue }"
44
- :model-value="modelValue"
45
- :aria-label="ariaLabel"
46
- :aria-pressed="modelValue"
47
- @update:model-value="onCheckedChange"
48
- >
49
- <SwitchThumb class="app-toggle-thumb" />
50
- </SwitchRoot>
51
- </template>
52
-
53
- <style scoped>
54
- /* Root — the toggle track */
55
- .app-toggle {
56
- width: 40px;
57
- height: 24px;
58
- border-radius: 12px;
59
- border: none;
60
- cursor: pointer;
61
- position: relative;
62
- flex-shrink: 0;
63
- background: var(--openpen-color-toggle-off);
64
- transition: background var(--openpen-duration-base) var(--openpen-easing-standard);
65
- /* Remove default button styles */
66
- padding: 0;
67
- appearance: none;
68
- -webkit-appearance: none;
69
- display: inline-flex;
70
- align-items: center;
71
- }
72
-
73
- .app-toggle.on {
74
- background: var(--openpen-color-accent);
75
- }
76
-
77
- /* Thumb — the white circular knob */
78
- .app-toggle-thumb {
79
- position: absolute;
80
- top: 3px;
81
- left: 3px;
82
- width: 18px;
83
- height: 18px;
84
- border-radius: 50%;
85
- background: #fff;
86
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
87
- transition: left var(--openpen-duration-bounce) var(--openpen-easing-bounce);
88
- display: block;
89
- pointer-events: none;
90
- }
91
-
92
- .app-toggle.on .app-toggle-thumb,
93
- .app-toggle[data-state='checked'] .app-toggle-thumb {
94
- left: 19px;
95
- }
96
- </style>
@@ -1,118 +0,0 @@
1
- <script setup lang="ts">
2
- /**
3
- * AppTooltip — High-level wrapper (Layer 1).
4
- *
5
- * Wraps Reka UI Tooltip (TooltipProvider / TooltipRoot / TooltipTrigger /
6
- * TooltipPortal / TooltipContent / TooltipArrow).
7
- *
8
- * Plugin authors get hover-triggered tooltips with OpenPen styling without
9
- * knowing about Reka UI internals or inject keys.
10
- *
11
- * Styles use --openpen-* design tokens.
12
- * No Reka UI types are leaked in the public API.
13
- *
14
- * Usage:
15
- * <AppTooltip content="Save file" side="top">
16
- * <button>Save</button>
17
- * </AppTooltip>
18
- */
19
- import {
20
- TooltipProvider,
21
- TooltipRoot,
22
- TooltipTrigger,
23
- TooltipPortal,
24
- TooltipContent,
25
- TooltipArrow,
26
- } from '../primitives'
27
-
28
- // ── OpenPen-owned types (MUST NOT reference reka-ui types) ────────────────────
29
-
30
- /** Preferred side for tooltip content relative to the trigger. */
31
- export type TooltipSide = 'top' | 'bottom' | 'left' | 'right'
32
-
33
- export interface AppTooltipProps {
34
- /** Tooltip text content. */
35
- content: string
36
- /** Preferred placement side relative to the trigger. Default 'top'. */
37
- side?: TooltipSide
38
- /** Delay before tooltip appears on hover, in ms. Default 200. */
39
- delay?: number
40
- }
41
-
42
- export interface AppTooltipSlots {
43
- /** Trigger element. Any element that receives hover events. */
44
- default: () => unknown
45
- }
46
-
47
- const props = withDefaults(defineProps<AppTooltipProps>(), {
48
- side: 'top',
49
- delay: 200,
50
- })
51
-
52
- defineSlots<AppTooltipSlots>()
53
- </script>
54
-
55
- <template>
56
- <TooltipProvider :delay-duration="props.delay">
57
- <TooltipRoot>
58
- <TooltipTrigger as-child>
59
- <slot />
60
- </TooltipTrigger>
61
- <TooltipPortal>
62
- <TooltipContent
63
- :side="props.side"
64
- :side-offset="6"
65
- :avoid-collisions="true"
66
- :collision-padding="8"
67
- class="openpen-tooltip-content"
68
- >
69
- {{ props.content }}
70
- <TooltipArrow class="openpen-tooltip-arrow" :width="10" :height="5" />
71
- </TooltipContent>
72
- </TooltipPortal>
73
- </TooltipRoot>
74
- </TooltipProvider>
75
- </template>
76
-
77
- <!-- Styles for teleported TooltipContent MUST be unscoped — Vue scoped CSS
78
- does not follow Teleport targets (same rationale as AppPopover). -->
79
- <style>
80
- .openpen-tooltip-content {
81
- background: var(--openpen-color-tooltip-bg);
82
- border: 1px solid var(--openpen-color-border-hi);
83
- border-radius: var(--openpen-radius-sm);
84
- padding: 5px 9px;
85
- color: var(--openpen-color-tooltip-text);
86
- font-size: 12px;
87
- line-height: 1.4;
88
- /* Prevent text wrap for short tooltip labels */
89
- white-space: nowrap;
90
- box-shadow: var(--openpen-shadow-sm);
91
- z-index: 200;
92
- pointer-events: none;
93
- }
94
-
95
- .openpen-tooltip-arrow {
96
- fill: var(--openpen-color-tooltip-bg);
97
- }
98
-
99
- /* ── Open / close animations ────────────────────────────────────────────────
100
- Reka UI Presence keeps the element in the DOM until the CSS animation ends,
101
- so data-state="delayed-open" / "instant-open" / "closed" all animate. */
102
- .openpen-tooltip-content[data-state='delayed-open'],
103
- .openpen-tooltip-content[data-state='instant-open'] {
104
- animation: openpen-tooltip-in var(--openpen-duration-fast) var(--openpen-easing-standard);
105
- }
106
- .openpen-tooltip-content[data-state='closed'] {
107
- animation: openpen-tooltip-out var(--openpen-duration-fast) ease forwards;
108
- }
109
-
110
- @keyframes openpen-tooltip-in {
111
- from { opacity: 0; transform: scale(0.95); }
112
- to { opacity: 1; transform: scale(1); }
113
- }
114
- @keyframes openpen-tooltip-out {
115
- from { opacity: 1; transform: scale(1); }
116
- to { opacity: 0; transform: scale(0.95); }
117
- }
118
- </style>
@@ -1,272 +0,0 @@
1
- <script setup lang="ts">
2
- /**
3
- * DialogHost — private host-internal component.
4
- *
5
- * HOST-INTERNAL: Do not export from any public barrel. Plugin authors never
6
- * interact with this component directly. The host mounts exactly one instance
7
- * at bootstrap; it renders the currently active imperative dialog request from
8
- * the useDialog() store.
9
- *
10
- * Rendering strategy:
11
- * - Reads the active request from _activeRequest (module-scoped reactive ref).
12
- * - Renders an <AppDialog> wired to that request's options.
13
- * - On ok / cancel / dismiss: resolves the request's Promise and advances queue.
14
- *
15
- * Settings window constraint: AppDialog internally injects MODAL_MANAGER_KEY
16
- * and CONTROL_BAR_ANIMATING_KEY. In the settings window those keys are not
17
- * provided (ControlBar only exists in the overlay/main window), so the dialog
18
- * falls back gracefully (no mutual exclusion, no animating-guard) — it still
19
- * renders and resolves correctly, just without those host integrations.
20
- */
21
- import { computed, inject, nextTick, provide, ref, watch } from 'vue'
22
- import AppDialog from './AppDialog.vue'
23
- // _activeRequest MUST come from the package barrel — see AppPopover.vue header
24
- // comment. The runtime build externalises '@openpen/module-api/uikit' so host
25
- // + uikit share the same module instance. A relative '../composables/useDialog'
26
- // import bundles a duplicate ref into the host bundle, causing useDialog()
27
- // callers to write to a different ref than DialogHost watches (dialog never
28
- // opens in production).
29
- import { _activeRequest, _resolveActive } from '@openpen/module-api/uikit'
30
- import { HOST_DIALOG_OPEN_COUNT_KEY } from '@openpen/module-api'
31
- import { DIALOG_PLUGIN_HANDLE_KEY } from '../composables/dialog-internal-keys'
32
- import type { DialogPluginComponentHandle } from '../composables/useDialogPluginComponent'
33
-
34
- const MODAL_ID = '__openpen_imperative_dialog__'
35
-
36
- // Increment/decrement the host-level dialog-open counter so ControlBar can
37
- // pause the auto-collapse timer while any imperative dialog is visible.
38
- // Falls back to a local ref when not provided (settings window has no host bar).
39
- const dialogOpenCount = inject(HOST_DIALOG_OPEN_COUNT_KEY, ref(0))
40
-
41
- // Whether the dialog is open: true when there is an active request.
42
- const isOpen = ref(false)
43
-
44
- // Prompt value — bound to the input in prompt dialogs.
45
- const promptValue = ref('')
46
-
47
- // Track the prompt input element so we can focus it on open.
48
- const promptInputRef = ref<HTMLInputElement | null>(null)
49
-
50
- // Watch the active request: open when a request arrives, close when cleared.
51
- watch(_activeRequest, (req) => {
52
- if (req) {
53
- if (req.kind === 'prompt') {
54
- promptValue.value = req.opts.defaultValue ?? ''
55
- }
56
- if (!isOpen.value) {
57
- dialogOpenCount.value++
58
- }
59
- isOpen.value = true
60
- if (req.kind === 'prompt') {
61
- nextTick(() => {
62
- promptInputRef.value?.focus()
63
- })
64
- }
65
- } else {
66
- if (isOpen.value) {
67
- dialogOpenCount.value = Math.max(0, dialogOpenCount.value - 1)
68
- }
69
- isOpen.value = false
70
- }
71
- })
72
-
73
- // Derived from the active request for convenience.
74
- const req = computed(() => _activeRequest.value)
75
- const opts = computed(() => req.value?.opts)
76
-
77
- function handleOk() {
78
- const r = _activeRequest.value
79
- if (!r) return
80
- if (r.kind === 'confirm') {
81
- r.resolve(true)
82
- } else if (r.kind === 'alert') {
83
- r.resolve()
84
- } else if (r.kind === 'prompt') {
85
- r.resolve(promptValue.value)
86
- }
87
- // custom: custom component handles its own resolution via ok/cancel/dismiss
88
- _resolveActive()
89
- }
90
-
91
- function handleCancel() {
92
- const r = _activeRequest.value
93
- if (!r) return
94
- if (r.kind === 'confirm') {
95
- r.resolve(false)
96
- } else if (r.kind === 'prompt') {
97
- r.resolve(null)
98
- }
99
- _resolveActive()
100
- }
101
-
102
- function handleUpdateOpen(v: boolean) {
103
- if (!v) {
104
- // Dialog closed via ESC / close button (not persistent).
105
- const r = _activeRequest.value
106
- if (!r) return
107
- if (r.kind === 'confirm') {
108
- r.resolve(false)
109
- } else if (r.kind === 'alert') {
110
- r.resolve()
111
- } else if (r.kind === 'prompt') {
112
- r.resolve(null)
113
- } else if (r.kind === 'custom') {
114
- r.resolve(null)
115
- }
116
- _resolveActive()
117
- }
118
- }
119
-
120
- // ── Custom component plugin handle ───────────────────────────────────────────
121
- //
122
- // Provide the handle so custom components can call ok/cancel/dismiss via
123
- // useDialogPluginComponent(). We provide at this component level so only the
124
- // custom component subtree gets it.
125
-
126
- function customOk(payload?: unknown) {
127
- const r = _activeRequest.value
128
- if (!r || r.kind !== 'custom') return
129
- r.resolve(payload ?? null)
130
- _resolveActive()
131
- }
132
-
133
- function customCancel() {
134
- const r = _activeRequest.value
135
- if (!r || r.kind !== 'custom') return
136
- r.resolve(null)
137
- _resolveActive()
138
- }
139
-
140
- const pluginHandle: DialogPluginComponentHandle<unknown> = {
141
- ok: customOk,
142
- cancel: customCancel,
143
- dismiss: customCancel,
144
- }
145
-
146
- provide(DIALOG_PLUGIN_HANDLE_KEY, pluginHandle)
147
- </script>
148
-
149
- <template>
150
- <!-- DialogHost renders nothing when there is no active request. -->
151
- <AppDialog
152
- v-if="req"
153
- :modal-id="MODAL_ID"
154
- :title="opts!.title"
155
- :open="isOpen"
156
- :persistent="opts!.persistent"
157
- :danger="opts!.danger"
158
- @update:open="handleUpdateOpen"
159
- >
160
- <!-- Body slot: varies by kind -->
161
- <template v-if="req.kind === 'confirm' || req.kind === 'alert'">
162
- <p class="openpen-dialog-host-message">{{ (opts as { message: string }).message }}</p>
163
- </template>
164
-
165
- <template v-else-if="req.kind === 'prompt'">
166
- <p class="openpen-dialog-host-message">{{ (opts as { message: string }).message }}</p>
167
- <input
168
- ref="promptInputRef"
169
- v-model="promptValue"
170
- class="openpen-dialog-host-input"
171
- :placeholder="(opts as { placeholder?: string }).placeholder"
172
- type="text"
173
- @keydown.enter="handleOk"
174
- @keydown.esc="handleCancel"
175
- />
176
- </template>
177
-
178
- <template v-else-if="req.kind === 'custom'">
179
- <!-- Render the custom component; it gets ok/cancel/dismiss via injection. -->
180
- <component
181
- :is="(opts as { component: unknown }).component"
182
- v-bind="(opts as { componentProps?: Record<string, unknown> }).componentProps ?? {}"
183
- />
184
- </template>
185
-
186
- <!-- Footer slot: only for confirm/alert/prompt; custom owns its own buttons. -->
187
- <template v-if="req.kind !== 'custom'" #footer>
188
- <button
189
- v-if="req.kind === 'confirm' || req.kind === 'prompt'"
190
- class="openpen-btn openpen-btn-secondary"
191
- @click="handleCancel"
192
- >
193
- {{ (opts as { cancelLabel?: string }).cancelLabel ?? 'Cancel' }}
194
- </button>
195
- <button
196
- class="openpen-btn openpen-btn-primary"
197
- :class="{ 'openpen-btn-danger': opts!.danger }"
198
- @click="handleOk"
199
- >
200
- {{ (opts as { okLabel?: string }).okLabel ?? 'OK' }}
201
- </button>
202
- </template>
203
- </AppDialog>
204
- </template>
205
-
206
- <style>
207
- /* ── DialogHost utility styles ───────────────────────────────────────────── */
208
- .openpen-dialog-host-message {
209
- margin: 0 0 var(--openpen-space-md, 12px) 0;
210
- color: var(--openpen-color-text-primary, #fff);
211
- line-height: 1.5;
212
- }
213
-
214
- .openpen-dialog-host-input {
215
- display: block;
216
- width: 100%;
217
- box-sizing: border-box;
218
- padding: 6px 10px;
219
- border: 1px solid var(--openpen-color-border-hi, rgba(255,255,255,0.2));
220
- border-radius: var(--openpen-radius-sm, 6px);
221
- background: var(--openpen-color-control, rgba(255,255,255,0.08));
222
- color: var(--openpen-color-text-primary, #fff);
223
- font-size: 13px;
224
- outline: none;
225
- }
226
-
227
- .openpen-dialog-host-input:focus {
228
- border-color: var(--accent, #7c7cff);
229
- box-shadow: 0 0 0 2px var(--accent-bg, rgba(124,124,255,0.18));
230
- }
231
-
232
- /* ── DialogHost button styles ─────────────────────────────────────────────── */
233
- .openpen-btn {
234
- display: inline-flex;
235
- align-items: center;
236
- justify-content: center;
237
- padding: 6px 16px;
238
- border-radius: var(--openpen-radius-sm, 6px);
239
- font-size: 13px;
240
- font-weight: 500;
241
- cursor: pointer;
242
- border: none;
243
- transition: background var(--openpen-duration-fast, 120ms), color var(--openpen-duration-fast, 120ms);
244
- }
245
-
246
- .openpen-btn-secondary {
247
- background: var(--openpen-color-control, rgba(255,255,255,0.08));
248
- color: var(--openpen-color-text-primary, #fff);
249
- }
250
-
251
- .openpen-btn-secondary:hover {
252
- background: var(--openpen-color-control-hover, rgba(255,255,255,0.14));
253
- }
254
-
255
- .openpen-btn-primary {
256
- background: var(--accent, #7c7cff);
257
- color: #fff;
258
- }
259
-
260
- .openpen-btn-primary:hover {
261
- opacity: 0.9;
262
- }
263
-
264
- .openpen-btn-danger {
265
- background: var(--openpen-color-danger, #e05252);
266
- color: #fff;
267
- }
268
-
269
- .openpen-btn-danger:hover {
270
- opacity: 0.9;
271
- }
272
- </style>