frappe-ui 1.0.0-beta.1 → 1.0.0-beta.3
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/package.json +7 -1
- package/src/components/Button/Button.vue +292 -284
- package/src/components/Button/types.ts +38 -32
- package/src/components/Tooltip/Tooltip.cy.ts +48 -0
- package/src/components/Tooltip/Tooltip.md +6 -0
- package/src/components/Tooltip/Tooltip.vue +19 -4
- package/src/components/Tooltip/TooltipProvider.vue +53 -0
- package/src/components/Tooltip/index.ts +1 -0
- package/src/components/Tooltip/stories/Group.vue +22 -0
- package/src/molecules/editor/EditorTableMenu.vue +216 -0
- package/src/molecules/editor/MenuItems.vue +64 -52
- package/src/molecules/editor/components/EditorDropZone.vue +89 -0
- package/src/molecules/editor/components/EditorPopover.vue +89 -0
- package/src/molecules/editor/components/ImageViewerModal.vue +6 -6
- package/src/molecules/editor/components/MediaNodeView.vue +6 -6
- package/src/molecules/editor/components/MediaToolbar.vue +1 -1
- package/src/molecules/editor/components/TableContextMenu.vue +87 -0
- package/src/molecules/editor/components/font-color/ColorSwatchGrid.vue +4 -6
- package/src/molecules/editor/components/font-color/fontColorController.ts +30 -25
- package/src/molecules/editor/components/font-color/swatches.ts +1 -1
- package/src/molecules/editor/components/image-viewer/ImageViewerControlsBar.vue +1 -1
- package/src/molecules/editor/components/media-node-view-controller.ts +1 -1
- package/src/molecules/editor/components/table-color/tableCellColorController.ts +80 -0
- package/src/molecules/editor/composables/useEditorFileDrop.ts +99 -0
- package/src/molecules/editor/composables/useFloatingPopup.ts +45 -2
- package/src/molecules/editor/composables/useNamedColorState.ts +1 -1
- package/src/molecules/editor/composables/useNodeViewResize.ts +1 -1
- package/src/molecules/editor/composables/useSuggestionList.ts +2 -2
- package/src/molecules/editor/composables/useTableCellColorState.ts +78 -0
- package/src/molecules/editor/composables/useTocActiveHeading.ts +1 -1
- package/src/molecules/editor/composables/useTocAnchors.ts +2 -2
- package/src/molecules/editor/composables/useWindowFileDragging.ts +68 -0
- package/src/molecules/editor/extensions/code-block/CodeBlockComponent.css +6 -26
- package/src/molecules/editor/extensions/code-block/CodeBlockComponent.vue +109 -24
- package/src/molecules/editor/extensions/code-block/code-block.ts +1 -1
- package/src/molecules/editor/extensions/content-paste/content-paste-extension.ts +3 -3
- package/src/molecules/editor/extensions/content-paste/paste-image-controller.test.ts +1 -1
- package/src/molecules/editor/extensions/content-paste/paste-image-controller.ts +1 -1
- package/src/molecules/editor/extensions/content-paste/paste-markdown-utils.ts +2 -2
- package/src/molecules/editor/extensions/emoji/EmojiList.vue +2 -2
- package/src/molecules/editor/extensions/emoji/emoji-extension.ts +1 -1
- package/src/molecules/editor/extensions/iframe/IframeInsertDialog.vue +3 -3
- package/src/molecules/editor/extensions/iframe/IframeNodeView.vue +3 -3
- package/src/molecules/editor/extensions/iframe/iframe-allowlist.ts +1 -1
- package/src/molecules/editor/extensions/iframe/iframe-embed-utils.ts +1 -1
- package/src/molecules/editor/extensions/iframe/iframe-paste-handler.ts +1 -1
- package/src/molecules/editor/extensions/image/image-engine.ts +3 -3
- package/src/molecules/editor/extensions/image/image-extension.ts +7 -7
- package/src/molecules/editor/extensions/image-group/ImageGroupGridCell.vue +1 -1
- package/src/molecules/editor/extensions/image-group/ImageGroupNodeView.vue +5 -5
- package/src/molecules/editor/extensions/image-group/ImageGroupUploadDialog.vue +5 -5
- package/src/molecules/editor/extensions/image-group/image-group-commands.ts +2 -2
- package/src/molecules/editor/extensions/image-group/image-group-extension.ts +1 -1
- package/src/molecules/editor/extensions/image-group/image-group-utils.ts +1 -1
- package/src/molecules/editor/extensions/image-group/useImageGroupDialog.ts +2 -2
- package/src/molecules/editor/extensions/link/LinkEditorPopup.vue +217 -74
- package/src/molecules/editor/extensions/link/link-commands.ts +26 -5
- package/src/molecules/editor/extensions/link/link-extension.ts +2 -2
- package/src/molecules/editor/extensions/link/link-paste-plugin.ts +1 -1
- package/src/molecules/editor/extensions/link/link-popup-controller.ts +47 -3
- package/src/molecules/editor/extensions/link/link-shortcut-plugin.ts +7 -2
- package/src/molecules/editor/extensions/media-drop/media-drop-extension.ts +136 -0
- package/src/molecules/editor/extensions/mention/mention-extension.ts +1 -1
- package/src/molecules/editor/extensions/shared/media-node-ops.ts +3 -3
- package/src/molecules/editor/extensions/shared/media-plugin.ts +9 -40
- package/src/molecules/editor/extensions/shared/media-upload-engine.test.ts +10 -10
- package/src/molecules/editor/extensions/shared/media-upload-engine.ts +8 -8
- package/src/molecules/editor/extensions/shared/media-upload-types.ts +22 -3
- package/src/molecules/editor/extensions/shared/suggestion-helpers.ts +21 -1
- package/src/molecules/editor/extensions/shared/suggestion-renderer.ts +1 -1
- package/src/molecules/editor/extensions/shared/suggestion-types.ts +1 -1
- package/src/molecules/editor/extensions/shared/upload-types.ts +1 -1
- package/src/molecules/editor/extensions/slash-commands/SlashCommandsList.vue +2 -2
- package/src/molecules/editor/extensions/slash-commands/slash-commands-extension.ts +2 -2
- package/src/molecules/editor/extensions/suggestion/SuggestionList.vue +18 -10
- package/src/molecules/editor/extensions/suggestion/SuggestionListItem.vue +2 -2
- package/src/molecules/editor/extensions/suggestion/createSuggestionExtension.ts +6 -2
- package/src/molecules/editor/extensions/suggestion/index.ts +1 -1
- package/src/molecules/editor/extensions/table/table-cell-color.ts +113 -0
- package/src/molecules/editor/extensions/table/table-navigation.ts +331 -0
- package/src/molecules/editor/extensions/table/table-selection-overlay.ts +114 -0
- package/src/molecules/editor/extensions/tag/tag-extension.ts +1 -1
- package/src/molecules/editor/extensions/toc-node/TocItem.vue +2 -2
- package/src/molecules/editor/extensions/toc-node/TocNodeView.vue +6 -6
- package/src/molecules/editor/extensions/toc-node/toc-render.ts +2 -2
- package/src/molecules/editor/extensions/toc-node/toc-scroll-controller.ts +1 -1
- package/src/molecules/editor/extensions/video/video-config.ts +2 -2
- package/src/molecules/editor/extensions/video/video-extension.ts +14 -6
- package/src/molecules/editor/extensions.ts +29 -3
- package/src/molecules/editor/index.ts +7 -0
- package/src/molecules/editor/kits.ts +26 -9
- package/src/molecules/editor/menu.ts +112 -0
- package/src/molecules/editor/style.css +71 -2
- package/tailwind/preset.js +4 -0
package/package.json
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frappe-ui",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.3",
|
|
4
4
|
"description": "A set of components and utilities for rapid UI development",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
|
+
"imports": {
|
|
8
|
+
"#components/*": "./src/components/*",
|
|
9
|
+
"#molecules/*": "./src/molecules/*",
|
|
10
|
+
"#composables/*": "./src/composables/*",
|
|
11
|
+
"#utils/*": "./src/utils/*"
|
|
12
|
+
},
|
|
7
13
|
"scripts": {
|
|
8
14
|
"test": "vitest --run",
|
|
9
15
|
"test:coverage": "vitest --run --coverage",
|
|
@@ -1,296 +1,304 @@
|
|
|
1
|
-
<
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}"
|
|
20
|
-
/>
|
|
21
|
-
<slot name="prefix" v-else-if="$slots['prefix'] || iconLeft">
|
|
22
|
-
<span
|
|
23
|
-
v-if="iconLeft && typeof iconLeft === 'string' && iconLeft.startsWith('lucide-')"
|
|
24
|
-
:class="[iconLeft, lucideSlotClasses]"
|
|
25
|
-
aria-hidden="true"
|
|
26
|
-
/>
|
|
27
|
-
<FeatherIcon
|
|
28
|
-
v-else-if="iconLeft && typeof iconLeft === 'string'"
|
|
29
|
-
:name="iconLeft"
|
|
30
|
-
:class="slotClasses"
|
|
31
|
-
aria-hidden="true"
|
|
32
|
-
/>
|
|
33
|
-
<component v-else-if="iconLeft" :is="iconLeft" :class="slotClasses" />
|
|
34
|
-
</slot>
|
|
35
|
-
|
|
36
|
-
<template v-if="loading && loadingText">{{ loadingText }}</template>
|
|
37
|
-
<template v-else-if="isIconButton && !loading">
|
|
38
|
-
<span
|
|
39
|
-
v-if="icon && typeof icon === 'string' && icon.startsWith('lucide-')"
|
|
40
|
-
:class="[icon, lucideSlotClasses]"
|
|
41
|
-
aria-hidden="true"
|
|
42
|
-
/>
|
|
43
|
-
<FeatherIcon
|
|
44
|
-
v-else-if="icon && typeof icon === 'string'"
|
|
45
|
-
:name="icon"
|
|
46
|
-
:class="slotClasses"
|
|
47
|
-
/>
|
|
48
|
-
<component v-else-if="icon" :is="icon" :class="slotClasses" />
|
|
49
|
-
<slot name="icon" v-else-if="$slots.icon" />
|
|
50
|
-
<div v-else-if="hasLucideIconInDefaultSlot" :class="slotClasses">
|
|
51
|
-
<slot>{{ label }}</slot>
|
|
52
|
-
</div>
|
|
53
|
-
</template>
|
|
54
|
-
<span v-else :class="{ 'sr-only': isIconButton }" class="truncate">
|
|
55
|
-
<slot>{{ label }}</slot>
|
|
56
|
-
</span>
|
|
57
|
-
|
|
58
|
-
<slot name="suffix">
|
|
59
|
-
<span
|
|
60
|
-
v-if="iconRight && typeof iconRight === 'string' && iconRight.startsWith('lucide-')"
|
|
61
|
-
:class="[iconRight, lucideSlotClasses]"
|
|
62
|
-
aria-hidden="true"
|
|
63
|
-
/>
|
|
64
|
-
<FeatherIcon
|
|
65
|
-
v-else-if="iconRight && typeof iconRight === 'string'"
|
|
66
|
-
:name="iconRight"
|
|
67
|
-
:class="slotClasses"
|
|
68
|
-
aria-hidden="true"
|
|
69
|
-
/>
|
|
70
|
-
<component
|
|
71
|
-
v-else-if="iconRight"
|
|
72
|
-
:is="iconRight"
|
|
73
|
-
:class="slotClasses"
|
|
74
|
-
/>
|
|
75
|
-
</slot>
|
|
76
|
-
</component>
|
|
77
|
-
</TooltipTrigger>
|
|
78
|
-
<TooltipBubble v-if="tooltip?.length" :text="tooltip" />
|
|
79
|
-
</TooltipRoot>
|
|
80
|
-
</TooltipProvider>
|
|
81
|
-
</template>
|
|
82
|
-
<script lang="ts" setup>
|
|
83
|
-
import { computed, h, ref, useSlots, watchEffect } from 'vue'
|
|
84
|
-
import { TooltipProvider, TooltipRoot, TooltipTrigger } from 'reka-ui'
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
computed,
|
|
4
|
+
defineComponent,
|
|
5
|
+
h,
|
|
6
|
+
ref,
|
|
7
|
+
watchEffect,
|
|
8
|
+
type Component,
|
|
9
|
+
type SlotsType,
|
|
10
|
+
type VNode,
|
|
11
|
+
} from 'vue'
|
|
12
|
+
import {
|
|
13
|
+
TooltipProvider,
|
|
14
|
+
TooltipRoot,
|
|
15
|
+
TooltipTrigger,
|
|
16
|
+
injectTooltipProviderContext,
|
|
17
|
+
} from 'reka-ui'
|
|
18
|
+
import { RouterLink } from 'vue-router'
|
|
85
19
|
import FeatherIcon from '../FeatherIcon.vue'
|
|
86
20
|
import LoadingIndicator from '../LoadingIndicator.vue'
|
|
87
21
|
import TooltipBubble from '../Tooltip/TooltipBubble.vue'
|
|
88
|
-
import { RouterLink } from 'vue-router'
|
|
89
22
|
import { warnFeatherIconUsage } from '../../utils/iconString'
|
|
90
|
-
import
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
let solidClasses = {
|
|
113
|
-
gray: 'text-ink-white bg-surface-gray-7 hover:bg-surface-gray-6 active:bg-surface-gray-5',
|
|
114
|
-
blue: 'text-ink-white bg-blue-500 hover:bg-surface-blue-3 active:bg-blue-700',
|
|
115
|
-
green:
|
|
116
|
-
'text-ink-white bg-surface-green-3 hover:bg-green-700 active:bg-green-800',
|
|
117
|
-
red: 'text-ink-white bg-surface-red-5 hover:bg-surface-red-6 active:bg-surface-red-7',
|
|
118
|
-
}[props.theme]
|
|
119
|
-
|
|
120
|
-
let subtleClasses = {
|
|
121
|
-
gray: 'text-ink-gray-8 bg-surface-gray-2 hover:bg-surface-gray-3 active:bg-surface-gray-4',
|
|
122
|
-
blue: 'text-ink-blue-3 bg-surface-blue-2 hover:bg-blue-200 active:bg-blue-300',
|
|
123
|
-
green:
|
|
124
|
-
'text-green-800 bg-surface-green-2 hover:bg-green-200 active:bg-green-300',
|
|
125
|
-
red: 'text-red-700 bg-surface-red-2 hover:bg-surface-red-3 active:bg-surface-red-4',
|
|
126
|
-
}[props.theme]
|
|
127
|
-
|
|
128
|
-
let outlineClasses = {
|
|
129
|
-
gray: 'text-ink-gray-8 bg-surface-white bg-surface-white border border-outline-gray-2 hover:border-outline-gray-3 active:border-outline-gray-3 active:bg-surface-gray-4',
|
|
130
|
-
blue: 'text-ink-blue-3 bg-surface-white border border-outline-blue-1 hover:border-blue-400 active:border-blue-400 active:bg-blue-300',
|
|
131
|
-
green:
|
|
132
|
-
'text-green-800 bg-surface-white border border-outline-green-2 hover:border-green-500 active:border-green-500 active:bg-green-300',
|
|
133
|
-
red: 'text-red-700 bg-surface-white border border-outline-red-1 hover:border-outline-red-2 active:border-outline-red-2 active:bg-surface-red-3',
|
|
134
|
-
}[props.theme]
|
|
135
|
-
|
|
136
|
-
let ghostClasses = {
|
|
137
|
-
gray: 'text-ink-gray-8 bg-transparent hover:bg-surface-gray-3 active:bg-surface-gray-4',
|
|
138
|
-
blue: 'text-ink-blue-3 bg-transparent hover:bg-blue-200 active:bg-blue-300',
|
|
139
|
-
green:
|
|
140
|
-
'text-green-800 bg-transparent hover:bg-green-200 active:bg-green-300',
|
|
141
|
-
red: 'text-red-700 bg-transparent hover:bg-surface-red-3 active:bg-surface-red-4',
|
|
142
|
-
}[props.theme]
|
|
143
|
-
|
|
144
|
-
let focusClasses = {
|
|
145
|
-
gray: 'focus-visible:ring focus-visible:ring-outline-gray-3',
|
|
146
|
-
blue: 'focus-visible:ring focus-visible:ring-blue-400',
|
|
147
|
-
green: 'focus-visible:ring focus-visible:ring-outline-green-2',
|
|
148
|
-
red: 'focus-visible:ring focus-visible:ring-outline-red-2',
|
|
149
|
-
}[props.theme]
|
|
150
|
-
|
|
151
|
-
let variantClasses = {
|
|
152
|
-
subtle: subtleClasses,
|
|
153
|
-
solid: solidClasses,
|
|
154
|
-
outline: outlineClasses,
|
|
155
|
-
ghost: ghostClasses,
|
|
156
|
-
}[props.variant]
|
|
157
|
-
|
|
158
|
-
let themeVariant: ThemeVariant = `${props.theme}-${props.variant}`
|
|
159
|
-
|
|
160
|
-
let disabledClassesMap: Record<ThemeVariant, string> = {
|
|
161
|
-
'gray-solid': 'bg-surface-gray-2 text-ink-gray-4',
|
|
162
|
-
'gray-subtle': 'bg-surface-gray-2 text-ink-gray-4',
|
|
163
|
-
'gray-outline':
|
|
164
|
-
'bg-surface-gray-2 text-ink-gray-4 border border-outline-gray-2',
|
|
165
|
-
'gray-ghost': 'text-ink-gray-4',
|
|
166
|
-
|
|
167
|
-
'blue-solid': 'bg-blue-300 text-ink-white',
|
|
168
|
-
'blue-subtle': 'bg-surface-blue-2 text-ink-blue-link',
|
|
169
|
-
'blue-outline':
|
|
170
|
-
'bg-surface-blue-2 text-ink-blue-link border border-outline-blue-1',
|
|
171
|
-
'blue-ghost': 'text-ink-blue-link',
|
|
172
|
-
|
|
173
|
-
'green-solid': 'bg-surface-green-2 text-ink-green-2',
|
|
174
|
-
'green-subtle': 'bg-surface-green-2 text-ink-green-2',
|
|
175
|
-
'green-outline':
|
|
176
|
-
'bg-surface-green-2 text-ink-green-2 border border-outline-green-2',
|
|
177
|
-
'green-ghost': 'text-ink-green-2',
|
|
178
|
-
|
|
179
|
-
'red-solid': 'bg-surface-red-2 text-ink-red-2',
|
|
180
|
-
'red-subtle': 'bg-surface-red-2 text-ink-red-2',
|
|
181
|
-
'red-outline':
|
|
182
|
-
'bg-surface-red-2 text-ink-red-2 border border-outline-red-1',
|
|
183
|
-
'red-ghost': 'text-ink-red-2',
|
|
184
|
-
}
|
|
185
|
-
let disabledClasses = disabledClassesMap[themeVariant]
|
|
186
|
-
|
|
187
|
-
let sizeClasses = {
|
|
188
|
-
sm: 'h-7 text-base px-2 rounded',
|
|
189
|
-
md: 'h-8 text-base font-medium px-2.5 rounded',
|
|
190
|
-
lg: 'h-10 text-lg font-medium px-3 rounded-md',
|
|
191
|
-
xl: 'h-11.5 text-xl font-medium px-3.5 rounded-lg',
|
|
192
|
-
'2xl': 'h-13 text-2xl font-medium px-3.5 rounded-xl',
|
|
193
|
-
}[props.size]
|
|
194
|
-
|
|
195
|
-
if (isIconButton.value) {
|
|
196
|
-
sizeClasses = {
|
|
197
|
-
sm: 'h-7 w-7 rounded',
|
|
198
|
-
md: 'h-8 w-8 rounded',
|
|
199
|
-
lg: 'h-10 w-10 rounded-md',
|
|
200
|
-
xl: 'h-11.5 w-11.5 rounded-lg',
|
|
201
|
-
'2xl': 'h-13 w-13 rounded-xl',
|
|
202
|
-
}[props.size]
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return [
|
|
206
|
-
'inline-flex items-center justify-center gap-2 transition-colors focus:outline-none shrink-0',
|
|
207
|
-
isDisabled.value ? disabledClasses : variantClasses,
|
|
208
|
-
focusClasses,
|
|
209
|
-
sizeClasses,
|
|
210
|
-
]
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
const slotClasses = computed(() => {
|
|
214
|
-
let classes = {
|
|
215
|
-
sm: 'h-4',
|
|
216
|
-
md: 'h-4.5',
|
|
217
|
-
lg: 'h-5',
|
|
218
|
-
xl: 'h-6',
|
|
219
|
-
'2xl': 'h-6',
|
|
220
|
-
}[props.size]
|
|
221
|
-
|
|
222
|
-
return classes
|
|
223
|
-
})
|
|
224
|
-
|
|
225
|
-
const lucideSlotClasses = computed(() => {
|
|
226
|
-
return {
|
|
227
|
-
sm: 'size-4',
|
|
228
|
-
md: 'size-4.5',
|
|
229
|
-
lg: 'size-5',
|
|
230
|
-
xl: 'size-6',
|
|
231
|
-
'2xl': 'size-6',
|
|
232
|
-
}[props.size]
|
|
233
|
-
})
|
|
23
|
+
import { buttonProps, type ThemeVariant } from './types'
|
|
24
|
+
|
|
25
|
+
export default defineComponent({
|
|
26
|
+
name: 'Button',
|
|
27
|
+
inheritAttrs: false,
|
|
28
|
+
props: buttonProps,
|
|
29
|
+
slots: Object as SlotsType<{
|
|
30
|
+
/** Content shown before the button label (left icon / custom content) */
|
|
31
|
+
prefix: void
|
|
32
|
+
/** Icon-only content for icon buttons */
|
|
33
|
+
icon: void
|
|
34
|
+
/** Main button content (overrides `label`) */
|
|
35
|
+
default: void
|
|
36
|
+
/** Content shown after the button label (right icon / custom content) */
|
|
37
|
+
suffix: void
|
|
38
|
+
}>,
|
|
39
|
+
setup(props, { attrs, slots, expose }) {
|
|
40
|
+
watchEffect(() => {
|
|
41
|
+
warnFeatherIconUsage('Button', 'icon', props.icon)
|
|
42
|
+
warnFeatherIconUsage('Button', 'iconLeft', props.iconLeft)
|
|
43
|
+
warnFeatherIconUsage('Button', 'iconRight', props.iconRight)
|
|
44
|
+
})
|
|
234
45
|
|
|
235
|
-
const isDisabled = computed(() =>
|
|
236
|
-
|
|
237
|
-
})
|
|
46
|
+
const isDisabled = computed(() => props.disabled || props.loading)
|
|
47
|
+
const hasTooltip = computed(() => Boolean(props.tooltip?.length))
|
|
238
48
|
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
-
// globally registered "Button" component in consumer apps
|
|
243
|
-
const Root = computed(() => {
|
|
244
|
-
if (!isDisabled.value && props.route) {
|
|
245
|
-
return h(RouterLink, { to: props.route })
|
|
246
|
-
}
|
|
49
|
+
// Reuse a surrounding <TooltipProvider> (button group) when present so the
|
|
50
|
+
// group's skip-delay applies to this button instead of a private provider.
|
|
51
|
+
const parentTooltipProvider = injectTooltipProviderContext(null)
|
|
247
52
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
53
|
+
// Render as an icon button when the default slot is exactly one lucide-* icon.
|
|
54
|
+
const hasLucideIconInDefaultSlot = computed(() => {
|
|
55
|
+
const content = slots.default?.()
|
|
56
|
+
if (!Array.isArray(content)) return false
|
|
57
|
+
const name = (content[0]?.type as { name?: string })?.name
|
|
58
|
+
return typeof name === 'string' && name.startsWith('lucide-')
|
|
253
59
|
})
|
|
254
|
-
}
|
|
255
60
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
|
|
61
|
+
const isIconButton = computed(
|
|
62
|
+
() =>
|
|
63
|
+
Boolean(props.icon) ||
|
|
64
|
+
Boolean(slots.icon) ||
|
|
65
|
+
hasLucideIconInDefaultSlot.value,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
const slotClasses = computed(
|
|
69
|
+
() =>
|
|
70
|
+
({ xs: 'h-4', sm: 'h-4', md: 'h-4.5', lg: 'h-5', xl: 'h-6', '2xl': 'h-6' })[
|
|
71
|
+
props.size
|
|
72
|
+
],
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
const lucideSlotClasses = computed(
|
|
76
|
+
() =>
|
|
77
|
+
({
|
|
78
|
+
xs: 'size-4',
|
|
79
|
+
sm: 'size-4',
|
|
80
|
+
md: 'size-4.5',
|
|
81
|
+
lg: 'size-5',
|
|
82
|
+
xl: 'size-6',
|
|
83
|
+
'2xl': 'size-6',
|
|
84
|
+
})[props.size],
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
const buttonClasses = computed(() => {
|
|
88
|
+
const solidClasses = {
|
|
89
|
+
gray: 'text-ink-white bg-surface-gray-7 hover:bg-surface-gray-6 active:bg-surface-gray-5',
|
|
90
|
+
blue: 'text-ink-white bg-blue-500 hover:bg-surface-blue-3 active:bg-blue-700',
|
|
91
|
+
green:
|
|
92
|
+
'text-ink-white bg-surface-green-3 hover:bg-green-700 active:bg-green-800',
|
|
93
|
+
red: 'text-ink-white bg-surface-red-5 hover:bg-surface-red-6 active:bg-surface-red-7',
|
|
94
|
+
}[props.theme]
|
|
95
|
+
|
|
96
|
+
const subtleClasses = {
|
|
97
|
+
gray: 'text-ink-gray-8 bg-surface-gray-2 hover:bg-surface-gray-3 active:bg-surface-gray-4',
|
|
98
|
+
blue: 'text-ink-blue-3 bg-surface-blue-2 hover:bg-blue-200 active:bg-blue-300',
|
|
99
|
+
green:
|
|
100
|
+
'text-green-800 bg-surface-green-2 hover:bg-green-200 active:bg-green-300',
|
|
101
|
+
red: 'text-red-700 bg-surface-red-2 hover:bg-surface-red-3 active:bg-surface-red-4',
|
|
102
|
+
}[props.theme]
|
|
103
|
+
|
|
104
|
+
const outlineClasses = {
|
|
105
|
+
gray: 'text-ink-gray-8 bg-surface-white bg-surface-white border border-outline-gray-2 hover:border-outline-gray-3 active:border-outline-gray-3 active:bg-surface-gray-4',
|
|
106
|
+
blue: 'text-ink-blue-3 bg-surface-white border border-outline-blue-1 hover:border-blue-400 active:border-blue-400 active:bg-blue-300',
|
|
107
|
+
green:
|
|
108
|
+
'text-green-800 bg-surface-white border border-outline-green-2 hover:border-green-500 active:border-green-500 active:bg-green-300',
|
|
109
|
+
red: 'text-red-700 bg-surface-white border border-outline-red-1 hover:border-outline-red-2 active:border-outline-red-2 active:bg-surface-red-3',
|
|
110
|
+
}[props.theme]
|
|
111
|
+
|
|
112
|
+
const ghostClasses = {
|
|
113
|
+
gray: 'text-ink-gray-8 bg-transparent hover:bg-surface-gray-3 active:bg-surface-gray-4',
|
|
114
|
+
blue: 'text-ink-blue-3 bg-transparent hover:bg-blue-200 active:bg-blue-300',
|
|
115
|
+
green:
|
|
116
|
+
'text-green-800 bg-transparent hover:bg-green-200 active:bg-green-300',
|
|
117
|
+
red: 'text-red-700 bg-transparent hover:bg-surface-red-3 active:bg-surface-red-4',
|
|
118
|
+
}[props.theme]
|
|
119
|
+
|
|
120
|
+
const focusClasses = {
|
|
121
|
+
gray: 'focus-visible:ring focus-visible:ring-outline-gray-3',
|
|
122
|
+
blue: 'focus-visible:ring focus-visible:ring-blue-400',
|
|
123
|
+
green: 'focus-visible:ring focus-visible:ring-outline-green-2',
|
|
124
|
+
red: 'focus-visible:ring focus-visible:ring-outline-red-2',
|
|
125
|
+
}[props.theme]
|
|
126
|
+
|
|
127
|
+
const variantClasses = {
|
|
128
|
+
subtle: subtleClasses,
|
|
129
|
+
solid: solidClasses,
|
|
130
|
+
outline: outlineClasses,
|
|
131
|
+
ghost: ghostClasses,
|
|
132
|
+
}[props.variant]
|
|
133
|
+
|
|
134
|
+
const themeVariant: ThemeVariant = `${props.theme}-${props.variant}`
|
|
135
|
+
|
|
136
|
+
const disabledClassesMap: Record<ThemeVariant, string> = {
|
|
137
|
+
'gray-solid': 'bg-surface-gray-2 text-ink-gray-4',
|
|
138
|
+
'gray-subtle': 'bg-surface-gray-2 text-ink-gray-4',
|
|
139
|
+
'gray-outline':
|
|
140
|
+
'bg-surface-gray-2 text-ink-gray-4 border border-outline-gray-2',
|
|
141
|
+
'gray-ghost': 'text-ink-gray-4',
|
|
142
|
+
|
|
143
|
+
'blue-solid': 'bg-blue-300 text-ink-white',
|
|
144
|
+
'blue-subtle': 'bg-surface-blue-2 text-ink-blue-link',
|
|
145
|
+
'blue-outline':
|
|
146
|
+
'bg-surface-blue-2 text-ink-blue-link border border-outline-blue-1',
|
|
147
|
+
'blue-ghost': 'text-ink-blue-link',
|
|
148
|
+
|
|
149
|
+
'green-solid': 'bg-surface-green-2 text-ink-green-2',
|
|
150
|
+
'green-subtle': 'bg-surface-green-2 text-ink-green-2',
|
|
151
|
+
'green-outline':
|
|
152
|
+
'bg-surface-green-2 text-ink-green-2 border border-outline-green-2',
|
|
153
|
+
'green-ghost': 'text-ink-green-2',
|
|
154
|
+
|
|
155
|
+
'red-solid': 'bg-surface-red-2 text-ink-red-2',
|
|
156
|
+
'red-subtle': 'bg-surface-red-2 text-ink-red-2',
|
|
157
|
+
'red-outline':
|
|
158
|
+
'bg-surface-red-2 text-ink-red-2 border border-outline-red-1',
|
|
159
|
+
'red-ghost': 'text-ink-red-2',
|
|
160
|
+
}
|
|
161
|
+
const disabledClasses = disabledClassesMap[themeVariant]
|
|
162
|
+
|
|
163
|
+
const sizeClasses = isIconButton.value
|
|
164
|
+
? {
|
|
165
|
+
xs: 'h-6 w-6 rounded',
|
|
166
|
+
sm: 'h-7 w-7 rounded',
|
|
167
|
+
md: 'h-8 w-8 rounded',
|
|
168
|
+
lg: 'h-10 w-10 rounded-md',
|
|
169
|
+
xl: 'h-11.5 w-11.5 rounded-lg',
|
|
170
|
+
'2xl': 'h-13 w-13 rounded-xl',
|
|
171
|
+
}[props.size]
|
|
172
|
+
: {
|
|
173
|
+
xs: 'h-6 text-sm px-1.5 rounded',
|
|
174
|
+
sm: 'h-7 text-base px-2 rounded',
|
|
175
|
+
md: 'h-8 text-base font-medium px-2.5 rounded',
|
|
176
|
+
lg: 'h-10 text-lg font-medium px-3 rounded-md',
|
|
177
|
+
xl: 'h-11.5 text-xl font-medium px-3.5 rounded-lg',
|
|
178
|
+
'2xl': 'h-13 text-2xl font-medium px-3.5 rounded-xl',
|
|
179
|
+
}[props.size]
|
|
180
|
+
|
|
181
|
+
return [
|
|
182
|
+
'inline-flex items-center justify-center gap-2 transition-colors focus:outline-none shrink-0',
|
|
183
|
+
isDisabled.value ? disabledClasses : variantClasses,
|
|
184
|
+
focusClasses,
|
|
185
|
+
sizeClasses,
|
|
186
|
+
]
|
|
187
|
+
})
|
|
265
188
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
189
|
+
const rootRef = ref()
|
|
190
|
+
expose({ rootRef })
|
|
191
|
+
|
|
192
|
+
// The dynamic root: router link, external anchor, or native button. Using the
|
|
193
|
+
// raw 'button' string (not <component :is>) sidesteps the historic recursion
|
|
194
|
+
// with a globally-registered <Button> in consumer apps.
|
|
195
|
+
const root = computed<{ is: Component | string; props: Record<string, unknown> }>(
|
|
196
|
+
() => {
|
|
197
|
+
if (!isDisabled.value && props.route) {
|
|
198
|
+
return { is: RouterLink, props: { to: props.route } }
|
|
199
|
+
}
|
|
200
|
+
if (!isDisabled.value && props.link) {
|
|
201
|
+
return {
|
|
202
|
+
is: 'a',
|
|
203
|
+
props: { href: props.link, target: '_blank', rel: 'noreferrer noopener' },
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return { is: 'button', props: { type: props.type, disabled: isDisabled.value } }
|
|
207
|
+
},
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
/** Resolve an icon prop to a vnode: lucide class-span, FeatherIcon, or component. */
|
|
211
|
+
function renderIcon(
|
|
212
|
+
icon: string | Component | undefined,
|
|
213
|
+
featherHidden: boolean,
|
|
214
|
+
): VNode | null {
|
|
215
|
+
if (!icon) return null
|
|
216
|
+
if (typeof icon === 'string') {
|
|
217
|
+
if (icon.startsWith('lucide-')) {
|
|
218
|
+
return h('span', {
|
|
219
|
+
class: [icon, lucideSlotClasses.value],
|
|
220
|
+
'aria-hidden': 'true',
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
return h(FeatherIcon, {
|
|
224
|
+
name: icon,
|
|
225
|
+
class: slotClasses.value,
|
|
226
|
+
...(featherHidden ? { 'aria-hidden': 'true' } : {}),
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
return h(icon, { class: slotClasses.value })
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function renderPrefix() {
|
|
233
|
+
if (props.loading) {
|
|
234
|
+
return h(LoadingIndicator, {
|
|
235
|
+
class: {
|
|
236
|
+
'h-3 w-3': props.size === 'xs' || props.size === 'sm',
|
|
237
|
+
'h-[13.5px] w-[13.5px]': props.size === 'md',
|
|
238
|
+
'h-[15px] w-[15px]': props.size === 'lg',
|
|
239
|
+
'h-4.5 w-4.5': props.size === 'xl' || props.size === '2xl',
|
|
240
|
+
},
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
if (slots.prefix) return slots.prefix()
|
|
244
|
+
return renderIcon(props.iconLeft, true)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function renderMain() {
|
|
248
|
+
if (props.loading && props.loadingText) return props.loadingText
|
|
249
|
+
if (isIconButton.value && !props.loading) {
|
|
250
|
+
if (props.icon) return renderIcon(props.icon, false)
|
|
251
|
+
if (slots.icon) return slots.icon()
|
|
252
|
+
if (hasLucideIconInDefaultSlot.value) {
|
|
253
|
+
return h('div', { class: slotClasses.value }, slots.default?.() ?? props.label)
|
|
254
|
+
}
|
|
255
|
+
return null
|
|
256
|
+
}
|
|
257
|
+
return h(
|
|
258
|
+
'span',
|
|
259
|
+
{ class: ['truncate', { 'sr-only': isIconButton.value }] },
|
|
260
|
+
slots.default?.() ?? props.label,
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function renderSuffix() {
|
|
265
|
+
if (slots.suffix) return slots.suffix()
|
|
266
|
+
return renderIcon(props.iconRight, true)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return () => {
|
|
270
|
+
const { class: attrClass, ...restAttrs } = attrs
|
|
271
|
+
const { is, props: rootProps } = root.value
|
|
272
|
+
const children = [renderPrefix(), renderMain(), renderSuffix()]
|
|
273
|
+
const mergedProps = {
|
|
274
|
+
...rootProps,
|
|
275
|
+
...restAttrs,
|
|
276
|
+
class: [attrClass, buttonClasses.value],
|
|
277
|
+
'aria-label': props.label,
|
|
278
|
+
ref: rootRef,
|
|
279
|
+
}
|
|
280
|
+
const button =
|
|
281
|
+
typeof is === 'string'
|
|
282
|
+
? h(is, mergedProps, children)
|
|
283
|
+
: h(is, mergedProps, { default: () => children })
|
|
284
|
+
|
|
285
|
+
if (!hasTooltip.value) return button
|
|
286
|
+
|
|
287
|
+
// Tooltip scaffolding renders only when a tooltip is set, so a bare button
|
|
288
|
+
// ships without any tooltip context, listeners, or pointerdown-to-close.
|
|
289
|
+
const tooltipRoot = h(TooltipRoot, null, {
|
|
290
|
+
default: () => [
|
|
291
|
+
h(TooltipTrigger, { asChild: true }, { default: () => button }),
|
|
292
|
+
h(TooltipBubble, { text: props.tooltip }),
|
|
293
|
+
],
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
// Inside a button group, the provider already exists upstream — mounting
|
|
297
|
+
// our own here would isolate this button from the shared skip-delay.
|
|
298
|
+
return parentTooltipProvider
|
|
299
|
+
? tooltipRoot
|
|
300
|
+
: h(TooltipProvider, null, { default: () => tooltipRoot })
|
|
301
|
+
}
|
|
302
|
+
},
|
|
278
303
|
})
|
|
279
|
-
|
|
280
|
-
const rootRef = ref()
|
|
281
|
-
defineExpose({ rootRef })
|
|
282
|
-
|
|
283
|
-
defineSlots<{
|
|
284
|
-
/** Content shown before the button label (left icon / custom content) */
|
|
285
|
-
prefix?: () => any
|
|
286
|
-
|
|
287
|
-
/** Icon-only content for icon buttons */
|
|
288
|
-
icon?: () => any
|
|
289
|
-
|
|
290
|
-
/** Main button content (overrides `label`) */
|
|
291
|
-
default?: () => any
|
|
292
|
-
|
|
293
|
-
/** Content shown after the button label (right icon / custom content) */
|
|
294
|
-
suffix?: () => any
|
|
295
|
-
}>()
|
|
296
304
|
</script>
|