@openpen/module-api 1.0.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/LICENSE +21 -0
- package/README.md +99 -0
- package/package.json +53 -0
- package/src/define-module.ts +55 -0
- package/src/host/color-utils.ts +18 -0
- package/src/host/event-bus.ts +37 -0
- package/src/host/host-commands.ts +9 -0
- package/src/host/index.ts +53 -0
- package/src/host/module-context-registry.ts +34 -0
- package/src/host/passthrough.ts +10 -0
- package/src/host/popup-anchor.ts +14 -0
- package/src/host/slot-entries.ts +9 -0
- package/src/host/stroke-store.ts +20 -0
- package/src/host/stroke-style.ts +15 -0
- package/src/index.ts +106 -0
- package/src/inject-keys.ts +143 -0
- package/src/locale.ts +52 -0
- package/src/slots.ts +135 -0
- package/src/types/contributions.ts +298 -0
- package/src/types/control-bar-layout.ts +174 -0
- package/src/types/module.ts +225 -0
- package/src/types/notification.ts +40 -0
- package/src/types/tool.ts +82 -0
- package/src/uikit/components/.gitkeep +0 -0
- package/src/uikit/components/AppBanner.vue +131 -0
- package/src/uikit/components/AppButton.vue +135 -0
- package/src/uikit/components/AppDialog.vue +393 -0
- package/src/uikit/components/AppPopover.vue +268 -0
- package/src/uikit/components/AppSegmented.vue +126 -0
- package/src/uikit/components/AppSelect.vue +235 -0
- package/src/uikit/components/AppSlider.vue +204 -0
- package/src/uikit/components/AppTabs.vue +163 -0
- package/src/uikit/components/AppToggle.vue +96 -0
- package/src/uikit/components/AppTooltip.vue +118 -0
- package/src/uikit/components/DialogHost.vue +260 -0
- package/src/uikit/composables/dialog-internal-keys.ts +17 -0
- package/src/uikit/composables/useDialog.ts +141 -0
- package/src/uikit/composables/useDialogPluginComponent.ts +27 -0
- package/src/uikit/index.ts +79 -0
- package/src/uikit/internal.ts +12 -0
- package/src/uikit/primitives.ts +150 -0
- package/src/uikit/tokens.css +205 -0
- package/src/use-module-context.ts +50 -0
- package/src/validation.ts +31 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* AppSlider — High-level wrapper (Layer 1).
|
|
4
|
+
*
|
|
5
|
+
* Wraps Reka UI Slider (SliderRoot / SliderTrack / SliderRange / SliderThumb).
|
|
6
|
+
*
|
|
7
|
+
* Visual: 4px track, 14×14 white thumb, accent fill,
|
|
8
|
+
* hover scale(1.2) at var(--openpen-duration-fast) easing.
|
|
9
|
+
*
|
|
10
|
+
* Styles use --openpen-* design tokens.
|
|
11
|
+
* No Reka UI types are leaked in the public API.
|
|
12
|
+
*/
|
|
13
|
+
import { computed } from 'vue'
|
|
14
|
+
import {
|
|
15
|
+
SliderRoot,
|
|
16
|
+
SliderTrack,
|
|
17
|
+
SliderRange,
|
|
18
|
+
SliderThumb,
|
|
19
|
+
} from '../primitives'
|
|
20
|
+
|
|
21
|
+
// ── Public API (MUST NOT reference reka-ui types) ─────────────────────────────
|
|
22
|
+
|
|
23
|
+
const props = withDefaults(defineProps<{
|
|
24
|
+
modelValue: number
|
|
25
|
+
min?: number
|
|
26
|
+
max?: number
|
|
27
|
+
step?: number
|
|
28
|
+
/** Container width (e.g. '100%', '72px'). In vertical mode this sets the height. */
|
|
29
|
+
width?: string
|
|
30
|
+
orientation?: 'horizontal' | 'vertical'
|
|
31
|
+
trackHeight?: string
|
|
32
|
+
trackRadius?: string
|
|
33
|
+
thumbWidth?: string
|
|
34
|
+
thumbHeight?: string
|
|
35
|
+
thumbRadius?: string
|
|
36
|
+
/**
|
|
37
|
+
* Reverse the value-to-position mapping. With orientation='vertical', sets
|
|
38
|
+
* top=min and bottom=max (drag down to increase). With orientation='horizontal',
|
|
39
|
+
* sets right=min and left=max (drag left to increase). Useful when an icon
|
|
40
|
+
* legend implies the opposite direction of the underlying control.
|
|
41
|
+
*
|
|
42
|
+
* Delegates to reka-ui SliderRoot's native `inverted` prop, which swaps
|
|
43
|
+
* startEdge/endEdge on SliderRange — no manual value transformation needed.
|
|
44
|
+
*/
|
|
45
|
+
inverted?: boolean
|
|
46
|
+
}>(), {
|
|
47
|
+
min: 0,
|
|
48
|
+
max: 100,
|
|
49
|
+
step: 1,
|
|
50
|
+
width: '100%',
|
|
51
|
+
orientation: 'horizontal',
|
|
52
|
+
trackHeight: '4px',
|
|
53
|
+
trackRadius: '2px',
|
|
54
|
+
thumbWidth: '14px',
|
|
55
|
+
thumbHeight: '14px',
|
|
56
|
+
thumbRadius: '50%',
|
|
57
|
+
inverted: false,
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const emit = defineEmits<{
|
|
61
|
+
'update:modelValue': [value: number]
|
|
62
|
+
}>()
|
|
63
|
+
|
|
64
|
+
// ── Internal state ────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
const isVertical = computed(() => props.orientation === 'vertical')
|
|
67
|
+
|
|
68
|
+
/** Fill percentage for the track range background. */
|
|
69
|
+
const fillPct = computed(() => {
|
|
70
|
+
const range = props.max - props.min
|
|
71
|
+
if (range === 0) return '0.00%'
|
|
72
|
+
const pct = ((props.modelValue - props.min) / range) * 100
|
|
73
|
+
return Math.max(0, Math.min(100, pct)).toFixed(2) + '%'
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
/** Root element inline style */
|
|
77
|
+
const rootStyle = computed(() => {
|
|
78
|
+
if (isVertical.value) {
|
|
79
|
+
return {
|
|
80
|
+
'--thumb-w': props.thumbWidth,
|
|
81
|
+
'--thumb-h': props.thumbHeight,
|
|
82
|
+
'--thumb-r': props.thumbRadius,
|
|
83
|
+
'--fill-pct': fillPct.value,
|
|
84
|
+
'--track-height': props.trackHeight,
|
|
85
|
+
'--track-radius': props.trackRadius,
|
|
86
|
+
height: props.width,
|
|
87
|
+
width: '16px',
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
'--thumb-w': props.thumbWidth,
|
|
92
|
+
'--thumb-h': props.thumbHeight,
|
|
93
|
+
'--thumb-r': props.thumbRadius,
|
|
94
|
+
'--fill-pct': fillPct.value,
|
|
95
|
+
'--track-height': props.trackHeight,
|
|
96
|
+
'--track-radius': props.trackRadius,
|
|
97
|
+
width: props.width,
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
function onValueChange(val: number[] | undefined) {
|
|
102
|
+
if (val && val.length > 0) {
|
|
103
|
+
emit('update:modelValue', val[0])
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
</script>
|
|
107
|
+
|
|
108
|
+
<template>
|
|
109
|
+
<SliderRoot
|
|
110
|
+
class="app-slider-root"
|
|
111
|
+
:class="{ 'app-slider-root--vertical': isVertical, 'app-slider-root--inverted': inverted }"
|
|
112
|
+
:style="rootStyle"
|
|
113
|
+
:model-value="[modelValue]"
|
|
114
|
+
:min="min"
|
|
115
|
+
:max="max"
|
|
116
|
+
:step="step"
|
|
117
|
+
:orientation="orientation"
|
|
118
|
+
:inverted="inverted"
|
|
119
|
+
@update:model-value="onValueChange"
|
|
120
|
+
>
|
|
121
|
+
<SliderTrack class="app-slider-track">
|
|
122
|
+
<SliderRange class="app-slider-range" />
|
|
123
|
+
</SliderTrack>
|
|
124
|
+
<SliderThumb class="app-slider-thumb" />
|
|
125
|
+
</SliderRoot>
|
|
126
|
+
</template>
|
|
127
|
+
|
|
128
|
+
<style scoped>
|
|
129
|
+
/* ── Root container ────────────────────────────────────────────────────────── */
|
|
130
|
+
.app-slider-root {
|
|
131
|
+
position: relative;
|
|
132
|
+
display: flex;
|
|
133
|
+
align-items: center;
|
|
134
|
+
user-select: none;
|
|
135
|
+
touch-action: none;
|
|
136
|
+
flex-shrink: 0;
|
|
137
|
+
cursor: pointer;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.app-slider-root[data-orientation='horizontal'] {
|
|
141
|
+
height: var(--track-height, 4px);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.app-slider-root[data-orientation='vertical'] {
|
|
145
|
+
flex-direction: column;
|
|
146
|
+
width: var(--track-height, 4px);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* ── Track ─────────────────────────────────────────────────────────────────── */
|
|
150
|
+
.app-slider-track {
|
|
151
|
+
position: relative;
|
|
152
|
+
flex-grow: 1;
|
|
153
|
+
border-radius: var(--track-radius, 2px);
|
|
154
|
+
background: var(--openpen-color-border-hi);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.app-slider-root[data-orientation='horizontal'] .app-slider-track {
|
|
158
|
+
height: var(--track-height, 4px);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.app-slider-root[data-orientation='vertical'] .app-slider-track {
|
|
162
|
+
width: var(--track-height, 4px);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* ── Range (filled portion) ─────────────────────────────────────────────────── */
|
|
166
|
+
.app-slider-range {
|
|
167
|
+
position: absolute;
|
|
168
|
+
border-radius: var(--track-radius, 2px);
|
|
169
|
+
background: var(--openpen-color-accent);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.app-slider-root[data-orientation='horizontal'] .app-slider-range {
|
|
173
|
+
height: 100%;
|
|
174
|
+
top: 0;
|
|
175
|
+
left: 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.app-slider-root[data-orientation='vertical'] .app-slider-range {
|
|
179
|
+
width: 100%;
|
|
180
|
+
bottom: 0;
|
|
181
|
+
left: 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/* ── Thumb ─────────────────────────────────────────────────────────────────── */
|
|
185
|
+
.app-slider-thumb {
|
|
186
|
+
display: block;
|
|
187
|
+
width: var(--thumb-w, 14px);
|
|
188
|
+
height: var(--thumb-h, 14px);
|
|
189
|
+
border-radius: var(--thumb-r, 50%);
|
|
190
|
+
background: #fff;
|
|
191
|
+
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.50);
|
|
192
|
+
cursor: pointer;
|
|
193
|
+
outline: none;
|
|
194
|
+
transition: transform var(--openpen-duration-fast) var(--openpen-easing-standard);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.app-slider-thumb:hover {
|
|
198
|
+
transform: scale(1.2);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.app-slider-thumb:focus-visible {
|
|
202
|
+
box-shadow: 0 0 0 3px var(--openpen-color-accent-glow);
|
|
203
|
+
}
|
|
204
|
+
</style>
|
|
@@ -0,0 +1,163 @@
|
|
|
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>
|
|
@@ -0,0 +1,96 @@
|
|
|
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>
|
|
@@ -0,0 +1,118 @@
|
|
|
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>
|