@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.
- package/CHANGELOG.md +43 -0
- package/dist/_chunks/AppButtonDropdown.vue_vue_type_style_index_0_lang-DBVFUpxc.js +245 -0
- package/dist/_chunks/AppButtonDropdown.vue_vue_type_style_index_0_lang-DBVFUpxc.js.map +1 -0
- package/dist/_chunks/inject-keys-CRXCpN-M.js +196 -0
- package/dist/_chunks/inject-keys-CRXCpN-M.js.map +1 -0
- package/dist/_chunks/module-context-registry-Cn1IWYu9.js +16 -0
- package/dist/_chunks/module-context-registry-Cn1IWYu9.js.map +1 -0
- package/dist/_chunks/passthrough-DKU7JYuQ.js +6 -0
- package/dist/_chunks/passthrough-DKU7JYuQ.js.map +1 -0
- package/dist/cursors.d.ts +83 -0
- package/dist/define-module.d.ts +17 -0
- package/dist/host/color-utils.d.ts +11 -0
- package/{src/host/event-bus.ts → dist/host/event-bus.d.ts} +7 -5
- package/dist/host/host-commands.d.ts +1 -0
- package/dist/host/index.d.ts +24 -0
- package/dist/host/index.js +49 -0
- package/dist/host/index.js.map +1 -0
- package/{src/host/module-context-registry.ts → dist/host/module-context-registry.d.ts} +5 -19
- package/dist/host/passthrough.d.ts +2 -0
- package/dist/host/popup-anchor.d.ts +3 -0
- package/dist/host/registry.d.ts +39 -0
- package/dist/host/registry.js +37 -0
- package/dist/host/registry.js.map +1 -0
- package/dist/host/slot-entries.d.ts +2 -0
- package/dist/host/stroke-store.d.ts +5 -0
- package/dist/host/stroke-style.d.ts +3 -0
- package/dist/host/types.d.ts +164 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +349 -0
- package/dist/index.js.map +1 -0
- package/{src/inject-keys.ts → dist/inject-keys.d.ts} +37 -66
- package/{src/locale.ts → dist/locale.d.ts} +3 -22
- package/dist/slots.d.ts +57 -0
- package/dist/types/contributions.d.ts +288 -0
- package/dist/types/control-bar-layout.d.ts +227 -0
- package/dist/types/module.d.ts +208 -0
- package/{src/types/notification.ts → dist/types/notification.d.ts} +12 -14
- package/dist/types/tool.d.ts +77 -0
- package/dist/uikit/components/AppBanner.vue.d.ts +34 -0
- package/dist/uikit/components/AppButton.vue.d.ts +30 -0
- package/dist/uikit/components/AppButtonDropdown.vue.d.ts +52 -0
- package/dist/uikit/components/AppDialog.vue.d.ts +53 -0
- package/dist/uikit/components/AppPopover.vue.d.ts +43 -0
- package/dist/uikit/components/AppSegmented.vue.d.ts +19 -0
- package/dist/uikit/components/AppSelect.vue.d.ts +26 -0
- package/dist/uikit/components/AppSlider.vue.d.ts +43 -0
- package/dist/uikit/components/AppTabs.vue.d.ts +35 -0
- package/dist/uikit/components/AppToggle.vue.d.ts +13 -0
- package/dist/uikit/components/AppTooltip.vue.d.ts +27 -0
- package/dist/uikit/components/DialogHost.vue.d.ts +3 -0
- package/{src/uikit/composables/dialog-internal-keys.ts → dist/uikit/composables/dialog-internal-keys.d.ts} +3 -5
- package/dist/uikit/composables/useDialog.d.ts +399 -0
- package/dist/uikit/composables/useDialogPluginComponent.d.ts +9 -0
- package/dist/uikit/index.d.ts +42 -0
- package/dist/uikit/index.js +884 -0
- package/dist/uikit/index.js.map +1 -0
- package/{src/uikit/internal.ts → dist/uikit/internal.d.ts} +1 -1
- package/dist/uikit/internal.js +157 -0
- package/dist/uikit/internal.js.map +1 -0
- package/dist/uikit/module-api.css +851 -0
- package/dist/uikit/primitives.d.ts +42 -0
- package/{src/use-module-context.ts → dist/use-module-context.d.ts} +2 -15
- package/{src/validation.ts → dist/validation.d.ts} +2 -6
- package/package.json +26 -12
- package/packages/module-api/CHANGELOG.md +0 -27
- package/src/cursors.ts +0 -369
- package/src/define-module.ts +0 -55
- package/src/host/color-utils.ts +0 -18
- package/src/host/host-commands.ts +0 -9
- package/src/host/index.ts +0 -53
- package/src/host/passthrough.ts +0 -10
- package/src/host/popup-anchor.ts +0 -14
- package/src/host/slot-entries.ts +0 -9
- package/src/host/stroke-store.ts +0 -20
- package/src/host/stroke-style.ts +0 -15
- package/src/index.ts +0 -131
- package/src/slots.ts +0 -135
- package/src/types/contributions.ts +0 -333
- package/src/types/control-bar-layout.ts +0 -174
- package/src/types/module.ts +0 -225
- package/src/types/tool.ts +0 -82
- package/src/uikit/components/.gitkeep +0 -0
- package/src/uikit/components/AppBanner.vue +0 -131
- package/src/uikit/components/AppButton.vue +0 -135
- package/src/uikit/components/AppDialog.vue +0 -393
- package/src/uikit/components/AppPopover.vue +0 -268
- package/src/uikit/components/AppSegmented.vue +0 -128
- package/src/uikit/components/AppSelect.vue +0 -235
- package/src/uikit/components/AppSlider.vue +0 -204
- package/src/uikit/components/AppTabs.vue +0 -163
- package/src/uikit/components/AppToggle.vue +0 -96
- package/src/uikit/components/AppTooltip.vue +0 -118
- package/src/uikit/components/DialogHost.vue +0 -272
- package/src/uikit/composables/useDialog.ts +0 -141
- package/src/uikit/composables/useDialogPluginComponent.ts +0 -27
- package/src/uikit/index.ts +0 -79
- package/src/uikit/primitives.ts +0 -150
- /package/{src → dist}/uikit/tokens.css +0 -0
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* AppSegmented — High-level wrapper (Layer 1).
|
|
4
|
-
*
|
|
5
|
-
* Wraps Reka UI RadioGroup (RadioGroupRoot / RadioGroupItem).
|
|
6
|
-
*
|
|
7
|
-
* Visual: flex gap 4px, 1.5px border var(--border-hi),
|
|
8
|
-
* active = var(--accent-bg) background + var(--accent) color.
|
|
9
|
-
*
|
|
10
|
-
* Styles use --openpen-* design tokens.
|
|
11
|
-
* No Reka UI types are leaked in the public API.
|
|
12
|
-
*/
|
|
13
|
-
import {
|
|
14
|
-
RadioGroupRoot,
|
|
15
|
-
RadioGroupItem,
|
|
16
|
-
} from '../primitives'
|
|
17
|
-
|
|
18
|
-
// ── Public API (MUST NOT reference reka-ui types) ─────────────────────────────
|
|
19
|
-
|
|
20
|
-
interface Option {
|
|
21
|
-
value: string
|
|
22
|
-
label: string
|
|
23
|
-
icon?: string
|
|
24
|
-
testid?: string
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
defineProps<{
|
|
28
|
-
modelValue: string
|
|
29
|
-
options: Option[]
|
|
30
|
-
/** When true, all options are non-interactive and visually dimmed. */
|
|
31
|
-
disabled?: boolean
|
|
32
|
-
}>()
|
|
33
|
-
|
|
34
|
-
const emit = defineEmits<{
|
|
35
|
-
'update:modelValue': [value: string]
|
|
36
|
-
}>()
|
|
37
|
-
|
|
38
|
-
// AcceptableValue from Reka UI can be null; we narrow to string so the public API has no Reka UI type leakage.
|
|
39
|
-
function onValueChange(val: unknown) {
|
|
40
|
-
if (typeof val === 'string') {
|
|
41
|
-
emit('update:modelValue', val)
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
</script>
|
|
45
|
-
|
|
46
|
-
<template>
|
|
47
|
-
<RadioGroupRoot
|
|
48
|
-
class="app-seg"
|
|
49
|
-
:class="{ 'app-seg--disabled': disabled }"
|
|
50
|
-
:model-value="modelValue"
|
|
51
|
-
:disabled="disabled"
|
|
52
|
-
orientation="horizontal"
|
|
53
|
-
@update:model-value="onValueChange"
|
|
54
|
-
>
|
|
55
|
-
<RadioGroupItem
|
|
56
|
-
v-for="opt in options"
|
|
57
|
-
:key="opt.value"
|
|
58
|
-
:value="opt.value"
|
|
59
|
-
as-child
|
|
60
|
-
>
|
|
61
|
-
<button
|
|
62
|
-
class="app-seg-btn"
|
|
63
|
-
:class="{ active: modelValue === opt.value }"
|
|
64
|
-
:data-testid="opt.testid"
|
|
65
|
-
>
|
|
66
|
-
<span
|
|
67
|
-
v-if="opt.icon"
|
|
68
|
-
class="app-seg-icon"
|
|
69
|
-
aria-hidden="true"
|
|
70
|
-
v-html="opt.icon"
|
|
71
|
-
/>
|
|
72
|
-
<span>{{ opt.label }}</span>
|
|
73
|
-
</button>
|
|
74
|
-
</RadioGroupItem>
|
|
75
|
-
</RadioGroupRoot>
|
|
76
|
-
</template>
|
|
77
|
-
|
|
78
|
-
<style scoped>
|
|
79
|
-
.app-seg {
|
|
80
|
-
display: flex;
|
|
81
|
-
gap: var(--openpen-space-xs);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
.app-seg-btn {
|
|
85
|
-
flex: 1;
|
|
86
|
-
display: flex;
|
|
87
|
-
align-items: center;
|
|
88
|
-
justify-content: center;
|
|
89
|
-
gap: 6px;
|
|
90
|
-
padding: 6px var(--openpen-space-lg);
|
|
91
|
-
border: 1.5px solid var(--openpen-color-border-hi);
|
|
92
|
-
border-radius: var(--openpen-radius-sm);
|
|
93
|
-
background: var(--openpen-color-input-bg);
|
|
94
|
-
color: var(--openpen-color-text-dim);
|
|
95
|
-
font-size: 12px;
|
|
96
|
-
font-weight: 600;
|
|
97
|
-
cursor: pointer;
|
|
98
|
-
white-space: nowrap;
|
|
99
|
-
transition:
|
|
100
|
-
background var(--openpen-duration-fast) var(--openpen-easing-standard),
|
|
101
|
-
color var(--openpen-duration-fast) var(--openpen-easing-standard),
|
|
102
|
-
border-color var(--openpen-duration-fast) var(--openpen-easing-standard);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
.app-seg-icon {
|
|
106
|
-
display: inline-flex;
|
|
107
|
-
align-items: center;
|
|
108
|
-
justify-content: center;
|
|
109
|
-
line-height: 0;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
.app-seg-btn:hover {
|
|
113
|
-
color: var(--openpen-color-text-primary);
|
|
114
|
-
background: var(--openpen-color-control-hover);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
.app-seg-btn.active {
|
|
118
|
-
background: var(--openpen-color-accent-bg);
|
|
119
|
-
color: var(--openpen-color-accent);
|
|
120
|
-
border-color: color-mix(in srgb, var(--openpen-color-accent) 40%, transparent);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
.app-seg--disabled {
|
|
124
|
-
opacity: 0.45;
|
|
125
|
-
pointer-events: none;
|
|
126
|
-
cursor: not-allowed;
|
|
127
|
-
}
|
|
128
|
-
</style>
|
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* AppSelect — High-level wrapper (Layer 1).
|
|
4
|
-
*
|
|
5
|
-
* Plugin authors use this component without needing to know about Reka UI's
|
|
6
|
-
* Select primitives. The API surface is OpenPen-owned and stable.
|
|
7
|
-
*
|
|
8
|
-
* Public API is stable: switching the underlying headless library in the
|
|
9
|
-
* future does not break the plugin author's code.
|
|
10
|
-
*
|
|
11
|
-
* Implementation notes:
|
|
12
|
-
* - Controlled via modelValue / update:modelValue (standard Vue v-model pattern).
|
|
13
|
-
* - options[] is an OpenPen-owned type; no Reka UI types leak to the public API.
|
|
14
|
-
* - Popup styling mirrors surface-popup tokens (same as AppPopover content).
|
|
15
|
-
* - Styles are unscoped because SelectContent is teleported via SelectPortal.
|
|
16
|
-
*/
|
|
17
|
-
import {
|
|
18
|
-
SelectRoot,
|
|
19
|
-
SelectTrigger,
|
|
20
|
-
SelectPortal,
|
|
21
|
-
SelectContent,
|
|
22
|
-
SelectViewport,
|
|
23
|
-
SelectItem,
|
|
24
|
-
SelectItemText,
|
|
25
|
-
SelectItemIndicator,
|
|
26
|
-
SelectValue,
|
|
27
|
-
SelectIcon,
|
|
28
|
-
} from '../primitives'
|
|
29
|
-
|
|
30
|
-
// ── OpenPen-owned types (MUST NOT reference reka-ui types) ───────────────────
|
|
31
|
-
|
|
32
|
-
/** A single option in the dropdown list. */
|
|
33
|
-
export interface SelectOption {
|
|
34
|
-
/** The value emitted on selection. */
|
|
35
|
-
value: string
|
|
36
|
-
/** The label shown to the user. */
|
|
37
|
-
label: string
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface AppSelectProps {
|
|
41
|
-
/** Currently selected value (bind with v-model). */
|
|
42
|
-
modelValue: string
|
|
43
|
-
/** List of selectable options. */
|
|
44
|
-
options: SelectOption[]
|
|
45
|
-
/** Placeholder shown when no option is selected. */
|
|
46
|
-
placeholder: string
|
|
47
|
-
/** Whether the select is disabled. */
|
|
48
|
-
disabled?: boolean
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const props = withDefaults(defineProps<AppSelectProps>(), {
|
|
52
|
-
disabled: false,
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
const emit = defineEmits<{
|
|
56
|
-
'update:modelValue': [value: string]
|
|
57
|
-
}>()
|
|
58
|
-
|
|
59
|
-
function onValueChange(v: string) {
|
|
60
|
-
emit('update:modelValue', v)
|
|
61
|
-
}
|
|
62
|
-
</script>
|
|
63
|
-
|
|
64
|
-
<template>
|
|
65
|
-
<SelectRoot
|
|
66
|
-
:model-value="modelValue"
|
|
67
|
-
:disabled="disabled"
|
|
68
|
-
@update:model-value="onValueChange"
|
|
69
|
-
>
|
|
70
|
-
<SelectTrigger class="openpen-select-trigger">
|
|
71
|
-
<SelectValue :placeholder="placeholder" />
|
|
72
|
-
<SelectIcon class="openpen-select-icon" aria-hidden="true">
|
|
73
|
-
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
|
|
74
|
-
<path d="M2 4L6 8L10 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
75
|
-
</svg>
|
|
76
|
-
</SelectIcon>
|
|
77
|
-
</SelectTrigger>
|
|
78
|
-
<SelectPortal>
|
|
79
|
-
<SelectContent class="openpen-select-content" position="popper" :side-offset="4">
|
|
80
|
-
<SelectViewport class="openpen-select-viewport">
|
|
81
|
-
<SelectItem
|
|
82
|
-
v-for="option in options"
|
|
83
|
-
:key="option.value"
|
|
84
|
-
:value="option.value"
|
|
85
|
-
class="openpen-select-item"
|
|
86
|
-
>
|
|
87
|
-
<SelectItemIndicator class="openpen-select-item-indicator">
|
|
88
|
-
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
|
89
|
-
<path d="M2 6L5 9L10 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
90
|
-
</svg>
|
|
91
|
-
</SelectItemIndicator>
|
|
92
|
-
<SelectItemText>{{ option.label }}</SelectItemText>
|
|
93
|
-
</SelectItem>
|
|
94
|
-
</SelectViewport>
|
|
95
|
-
</SelectContent>
|
|
96
|
-
</SelectPortal>
|
|
97
|
-
</SelectRoot>
|
|
98
|
-
</template>
|
|
99
|
-
|
|
100
|
-
<!-- Styles MUST be unscoped — Vue scoped CSS does not follow Teleport targets,
|
|
101
|
-
so .openpen-select-* classes would not receive the scoped data-v-* attribute
|
|
102
|
-
when rendered inside the portal. -->
|
|
103
|
-
<style>
|
|
104
|
-
/* ── Trigger ─────────────────────────────────────────────────────────────── */
|
|
105
|
-
.openpen-select-trigger {
|
|
106
|
-
display: inline-flex;
|
|
107
|
-
align-items: center;
|
|
108
|
-
justify-content: space-between;
|
|
109
|
-
gap: var(--openpen-space-sm);
|
|
110
|
-
min-width: 120px;
|
|
111
|
-
height: 32px;
|
|
112
|
-
padding: 0 var(--openpen-space-md);
|
|
113
|
-
background: var(--openpen-color-input-bg);
|
|
114
|
-
border: 1px solid var(--openpen-color-border-hi);
|
|
115
|
-
border-radius: var(--openpen-radius-md);
|
|
116
|
-
color: var(--openpen-color-text-primary);
|
|
117
|
-
font-size: 13px;
|
|
118
|
-
cursor: pointer;
|
|
119
|
-
transition:
|
|
120
|
-
background var(--openpen-duration-fast),
|
|
121
|
-
border-color var(--openpen-duration-fast);
|
|
122
|
-
outline: none;
|
|
123
|
-
white-space: nowrap;
|
|
124
|
-
user-select: none;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
.openpen-select-trigger:hover {
|
|
128
|
-
background: var(--openpen-color-control-hover);
|
|
129
|
-
border-color: var(--openpen-color-border-hi);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
.openpen-select-trigger:focus-visible {
|
|
133
|
-
border-color: var(--openpen-color-accent);
|
|
134
|
-
box-shadow: 0 0 0 2px var(--openpen-color-accent-glow);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
.openpen-select-trigger[data-disabled] {
|
|
138
|
-
opacity: 0.45;
|
|
139
|
-
cursor: not-allowed;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/* ── Trigger icon (chevron) ──────────────────────────────────────────────── */
|
|
143
|
-
.openpen-select-icon {
|
|
144
|
-
color: var(--openpen-color-text-dim);
|
|
145
|
-
flex-shrink: 0;
|
|
146
|
-
display: flex;
|
|
147
|
-
align-items: center;
|
|
148
|
-
transition: transform var(--openpen-duration-fast);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
.openpen-select-trigger[data-state='open'] .openpen-select-icon {
|
|
152
|
-
transform: rotate(180deg);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/* ── Dropdown content ────────────────────────────────────────────────────── */
|
|
156
|
-
.openpen-select-content {
|
|
157
|
-
background: var(--openpen-color-surface-popup);
|
|
158
|
-
border: 1px solid var(--openpen-color-border-hi);
|
|
159
|
-
border-radius: var(--openpen-radius-md);
|
|
160
|
-
box-shadow: var(--openpen-shadow);
|
|
161
|
-
backdrop-filter: var(--openpen-blur);
|
|
162
|
-
-webkit-backdrop-filter: var(--openpen-blur);
|
|
163
|
-
z-index: 100;
|
|
164
|
-
pointer-events: auto;
|
|
165
|
-
/* Match trigger width */
|
|
166
|
-
min-width: var(--reka-select-trigger-width, 120px);
|
|
167
|
-
max-height: 240px;
|
|
168
|
-
overflow: hidden;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/* ── Viewport scrollable area ────────────────────────────────────────────── */
|
|
172
|
-
.openpen-select-viewport {
|
|
173
|
-
padding: var(--openpen-space-xs);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/* ── Individual items ────────────────────────────────────────────────────── */
|
|
177
|
-
.openpen-select-item {
|
|
178
|
-
display: flex;
|
|
179
|
-
align-items: center;
|
|
180
|
-
gap: var(--openpen-space-sm);
|
|
181
|
-
height: 30px;
|
|
182
|
-
padding: 0 var(--openpen-space-md);
|
|
183
|
-
padding-left: 28px; /* indent to align text past indicator */
|
|
184
|
-
border-radius: var(--openpen-radius-sm);
|
|
185
|
-
font-size: 13px;
|
|
186
|
-
color: var(--openpen-color-text-primary);
|
|
187
|
-
cursor: pointer;
|
|
188
|
-
position: relative;
|
|
189
|
-
user-select: none;
|
|
190
|
-
outline: none;
|
|
191
|
-
transition: background var(--openpen-duration-fast);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
.openpen-select-item[data-highlighted] {
|
|
195
|
-
background: var(--openpen-color-control-hover);
|
|
196
|
-
color: var(--openpen-color-text-primary);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
.openpen-select-item[data-state='checked'] {
|
|
200
|
-
color: var(--openpen-color-accent);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
.openpen-select-item[data-disabled] {
|
|
204
|
-
opacity: 0.45;
|
|
205
|
-
cursor: not-allowed;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/* ── Selected item indicator (checkmark) ─────────────────────────────────── */
|
|
209
|
-
.openpen-select-item-indicator {
|
|
210
|
-
position: absolute;
|
|
211
|
-
left: var(--openpen-space-sm);
|
|
212
|
-
display: flex;
|
|
213
|
-
align-items: center;
|
|
214
|
-
justify-content: center;
|
|
215
|
-
color: var(--openpen-color-accent);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/* ── Open / close animations ─────────────────────────────────────────────────
|
|
219
|
-
Pattern matches AppPopover keyframes (200ms bounce enter, 150ms ease leave). */
|
|
220
|
-
.openpen-select-content[data-state='open'] {
|
|
221
|
-
animation: openpen-select-in 200ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
222
|
-
}
|
|
223
|
-
.openpen-select-content[data-state='closed'] {
|
|
224
|
-
animation: openpen-select-out 150ms ease forwards;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
@keyframes openpen-select-in {
|
|
228
|
-
from { opacity: 0; transform: scale(0.95); }
|
|
229
|
-
to { opacity: 1; transform: scale(1); }
|
|
230
|
-
}
|
|
231
|
-
@keyframes openpen-select-out {
|
|
232
|
-
from { opacity: 1; transform: scale(1); }
|
|
233
|
-
to { opacity: 0; transform: scale(0.95); }
|
|
234
|
-
}
|
|
235
|
-
</style>
|
|
@@ -1,204 +0,0 @@
|
|
|
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>
|