frappe-ui 1.0.0-beta.3 → 1.0.0-beta.4
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 +30 -4
- package/src/components/Button/Button.api.md +37 -32
- package/src/components/Button/Button.cy.ts +1 -1
- package/src/components/Button/Button.vue +42 -31
- package/src/components/Button/stories/Icons.vue +8 -1
- package/src/components/Button/stories/Sizes.vue +1 -2
- package/src/components/Button/stories/Themes.vue +0 -2
- package/src/components/Button/types.ts +1 -1
- package/src/components/KeyboardShortcut.vue +3 -7
- package/src/components/LoadingIndicator.vue +13 -22
- package/src/components/LoadingText.vue +3 -13
- package/src/components/Spinner/Spinner.api.md +32 -0
- package/src/components/Spinner/Spinner.cy.ts +87 -0
- package/src/components/Spinner/Spinner.md +21 -0
- package/src/components/Spinner/Spinner.vue +168 -0
- package/src/components/Spinner/index.ts +2 -0
- package/src/components/Spinner/stories/InContext.vue +49 -0
- package/src/components/Spinner/stories/Sizes.vue +12 -0
- package/src/components/Spinner/stories/Themes.vue +10 -0
- package/src/components/Spinner/stories/Track.vue +12 -0
- package/src/components/Spinner/types.ts +14 -0
- package/src/components/TextEditor/extensions/iframe/iframe-extension.ts +62 -19
- package/src/index.ts +3 -1
- package/src/molecules/editor/commands.ts +3 -2
- package/src/molecules/editor/components/EditorDropZone.vue +25 -9
- package/src/molecules/editor/components/MediaNodeView.vue +156 -49
- package/src/molecules/editor/components/MediaResizeHandles.vue +36 -0
- package/src/molecules/editor/components/MediaToolbar.vue +107 -53
- package/src/molecules/editor/components/UploadProgressIndicator.vue +33 -0
- package/src/molecules/editor/components/VideoControls.vue +208 -0
- package/src/molecules/editor/components/media-node-view-controller.ts +4 -4
- package/src/molecules/editor/components/media-node-view-utils.test.ts +0 -13
- package/src/molecules/editor/components/media-node-view-utils.ts +0 -10
- package/src/molecules/editor/composables/useEditorFileDrop.ts +21 -3
- package/src/molecules/editor/composables/useFloatingPopup.ts +1 -0
- package/src/molecules/editor/composables/useNodeViewResize.test.ts +60 -11
- package/src/molecules/editor/composables/useNodeViewResize.ts +80 -21
- package/src/molecules/editor/extensions/content-paste/content-paste-extension.ts +5 -3
- package/src/molecules/editor/extensions/iframe/IframeInsertDialog.vue +31 -4
- package/src/molecules/editor/extensions/iframe/IframeNodeView.vue +54 -48
- package/src/molecules/editor/extensions/iframe/iframe-allowlist.ts +33 -4
- package/src/molecules/editor/extensions/iframe/iframe-commands.ts +49 -5
- package/src/molecules/editor/extensions/iframe/iframe-embed-utils.ts +30 -0
- package/src/molecules/editor/extensions/iframe/iframe-extension.ts +4 -4
- package/src/molecules/editor/extensions/iframe/iframeInsertDialogController.ts +12 -1
- package/src/molecules/editor/extensions/iframe/index.ts +4 -4
- package/src/molecules/editor/extensions/iframe/useIframeDialog.ts +25 -13
- package/src/molecules/editor/extensions/image/image-extension.ts +29 -11
- package/src/molecules/editor/extensions/image-group/ImageGroupGrid.vue +27 -11
- package/src/molecules/editor/extensions/image-group/ImageGroupGridCell.vue +55 -11
- package/src/molecules/editor/extensions/image-group/ImageGroupNodeView.vue +64 -31
- package/src/molecules/editor/extensions/image-group/ImageGroupUploadDialog.vue +81 -37
- package/src/molecules/editor/extensions/image-group/image-group-extension.ts +4 -3
- package/src/molecules/editor/extensions/image-group/useImageGroupDialog.ts +116 -17
- package/src/molecules/editor/extensions/shared/media-dimensions.ts +36 -3
- package/src/molecules/editor/extensions/shared/media-node-ops.ts +17 -0
- package/src/molecules/editor/extensions/shared/media-plugin.test.ts +60 -0
- package/src/molecules/editor/extensions/shared/media-plugin.ts +1 -0
- package/src/molecules/editor/extensions/shared/media-upload-engine.test.ts +39 -2
- package/src/molecules/editor/extensions/shared/media-upload-engine.ts +104 -10
- package/src/molecules/editor/extensions/shared/media-upload-state.ts +55 -0
- package/src/molecules/editor/extensions/shared/media-upload-types.ts +33 -1
- package/src/molecules/editor/extensions/shared/suggestion-types.ts +7 -0
- package/src/molecules/editor/extensions/shared/upload-types.ts +1 -0
- package/src/molecules/editor/extensions/slash-commands/slash-commands-extension.test.ts +38 -0
- package/src/molecules/editor/extensions/slash-commands/slash-commands-extension.ts +109 -55
- package/src/molecules/editor/extensions/suggestion/SuggestionList.vue +49 -17
- package/src/molecules/editor/extensions/table/table-navigation.ts +23 -2
- package/src/molecules/editor/extensions/video/video-extension.ts +64 -5
- package/src/molecules/editor/extensions.ts +116 -4
- package/src/molecules/editor/kits.test.ts +12 -1
- package/src/molecules/editor/kits.ts +20 -24
- package/src/molecules/editor/menu.test.ts +3 -1
- package/src/molecules/editor/style.css +6 -0
- package/src/molecules/editor/useEditor.test.ts +4 -0
- package/src/molecules/editor/useEditor.ts +7 -3
- package/src/utils/config.ts +4 -1
- package/src/utils/dialog.cy.ts +7 -7
- package/src/utils/fileSize.ts +36 -0
- package/src/utils/fileUploadHandler.ts +49 -5
- package/src/utils/plugin.js +19 -0
- package/src/utils/useFileUpload.ts +68 -22
- package/src/components/Spinner.vue +0 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frappe-ui",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.4",
|
|
4
4
|
"description": "A set of components and utilities for rapid UI development",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
"test:cypress:coverage": "cross-env COVERAGE=true cypress run --component",
|
|
18
18
|
"coverage": "yarn test:coverage && yarn test:cypress:coverage && yarn coverage:merge",
|
|
19
19
|
"coverage:merge": "tsx .github/scripts/merge-coverage.ts",
|
|
20
|
-
"type-check": "vue-tsc --noEmit",
|
|
21
|
-
"type-check:tsc": "tsc --noEmit",
|
|
20
|
+
"type-check": "vue-tsc --noEmit -p tsconfig.app.json",
|
|
21
|
+
"type-check:tsc": "tsc --noEmit -p tsconfig.app.json",
|
|
22
22
|
"format": "prettier -w ./src",
|
|
23
23
|
"bump-and-release": "yarn test && git pull --rebase origin main && yarn run release-patch",
|
|
24
24
|
"release-patch": "yarn version --patch && git push && git push --tags",
|
|
@@ -99,28 +99,38 @@
|
|
|
99
99
|
"@tailwindcss/line-clamp": "^0.4.4",
|
|
100
100
|
"@tailwindcss/typography": "^0.5.16",
|
|
101
101
|
"@tiptap/core": "^3.11.0",
|
|
102
|
+
"@tiptap/extension-blockquote": "3.11.0",
|
|
103
|
+
"@tiptap/extension-bold": "3.11.0",
|
|
102
104
|
"@tiptap/extension-bubble-menu": "^3.11.0",
|
|
103
105
|
"@tiptap/extension-code": "^3.11.0",
|
|
104
106
|
"@tiptap/extension-code-block": "^3.11.0",
|
|
105
107
|
"@tiptap/extension-code-block-lowlight": "^3.11.0",
|
|
106
108
|
"@tiptap/extension-color": "^3.11.0",
|
|
109
|
+
"@tiptap/extension-document": "3.11.0",
|
|
110
|
+
"@tiptap/extension-hard-break": "3.11.0",
|
|
107
111
|
"@tiptap/extension-heading": "^3.11.0",
|
|
108
112
|
"@tiptap/extension-highlight": "^3.11.0",
|
|
113
|
+
"@tiptap/extension-horizontal-rule": "3.11.0",
|
|
109
114
|
"@tiptap/extension-image": "^3.11.0",
|
|
115
|
+
"@tiptap/extension-italic": "3.11.0",
|
|
110
116
|
"@tiptap/extension-link": "^3.11.0",
|
|
111
117
|
"@tiptap/extension-list": "^3.11.0",
|
|
112
118
|
"@tiptap/extension-mention": "^3.11.0",
|
|
113
119
|
"@tiptap/extension-node-range": "^3.11.0",
|
|
114
120
|
"@tiptap/extension-placeholder": "^3.11.0",
|
|
121
|
+
"@tiptap/extension-paragraph": "3.11.0",
|
|
122
|
+
"@tiptap/extension-strike": "3.11.0",
|
|
115
123
|
"@tiptap/extension-table": "^3.11.0",
|
|
116
124
|
"@tiptap/extension-task-item": "^3.11.0",
|
|
117
125
|
"@tiptap/extension-task-list": "^3.11.0",
|
|
126
|
+
"@tiptap/extension-text": "3.11.0",
|
|
118
127
|
"@tiptap/extension-text-align": "^3.11.0",
|
|
119
128
|
"@tiptap/extension-text-style": "^3.11.0",
|
|
120
129
|
"@tiptap/extension-typography": "^3.11.0",
|
|
130
|
+
"@tiptap/extension-underline": "3.11.0",
|
|
121
131
|
"@tiptap/extensions": "^3.11.0",
|
|
122
132
|
"@tiptap/pm": "^3.11.0",
|
|
123
|
-
"@tiptap/starter-kit": "
|
|
133
|
+
"@tiptap/starter-kit": "3.11.0",
|
|
124
134
|
"@tiptap/suggestion": "^3.11.0",
|
|
125
135
|
"@tiptap/vue-3": "^3.11.0",
|
|
126
136
|
"@vueuse/core": "^10.4.1",
|
|
@@ -182,6 +192,22 @@
|
|
|
182
192
|
"prosemirror-state": "1.4.3",
|
|
183
193
|
"prosemirror-view": "1.40.0",
|
|
184
194
|
"prosemirror-transform": "1.10.4",
|
|
195
|
+
"@tiptap/extension-blockquote": "3.11.0",
|
|
196
|
+
"@tiptap/extension-bold": "3.11.0",
|
|
197
|
+
"@tiptap/extension-bullet-list": "3.11.0",
|
|
198
|
+
"@tiptap/extension-document": "3.11.0",
|
|
199
|
+
"@tiptap/extension-dropcursor": "3.11.0",
|
|
200
|
+
"@tiptap/extension-gapcursor": "3.11.0",
|
|
201
|
+
"@tiptap/extension-hard-break": "3.11.0",
|
|
202
|
+
"@tiptap/extension-horizontal-rule": "3.11.0",
|
|
203
|
+
"@tiptap/extension-italic": "3.11.0",
|
|
204
|
+
"@tiptap/extension-list-item": "3.11.0",
|
|
205
|
+
"@tiptap/extension-list-keymap": "3.11.0",
|
|
206
|
+
"@tiptap/extension-ordered-list": "3.11.0",
|
|
207
|
+
"@tiptap/extension-paragraph": "3.11.0",
|
|
208
|
+
"@tiptap/extension-strike": "3.11.0",
|
|
209
|
+
"@tiptap/extension-text": "3.11.0",
|
|
210
|
+
"@tiptap/extension-underline": "3.11.0",
|
|
185
211
|
"defu": "^6.1.5",
|
|
186
212
|
"esbuild": "^0.25.0",
|
|
187
213
|
"lodash": "^4.18.0",
|
|
@@ -10,27 +10,42 @@
|
|
|
10
10
|
description: 'Visual color theme of the button',
|
|
11
11
|
required: false,
|
|
12
12
|
type: 'Theme',
|
|
13
|
-
default: '
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
name: 'size',
|
|
17
|
-
description: 'Controls the button size',
|
|
18
|
-
required: false,
|
|
19
|
-
type: 'Size',
|
|
20
|
-
default: '"sm"'
|
|
13
|
+
default: '\'gray\''
|
|
21
14
|
},
|
|
22
15
|
{
|
|
23
16
|
name: 'variant',
|
|
24
17
|
description: 'Visual style of the button',
|
|
25
18
|
required: false,
|
|
26
19
|
type: 'Variant',
|
|
27
|
-
default: '
|
|
20
|
+
default: '\'subtle\''
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'type',
|
|
24
|
+
description: 'Native button type',
|
|
25
|
+
required: false,
|
|
26
|
+
type: '"button" | "submit" | "reset"',
|
|
27
|
+
default: '\'button\''
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
name: 'label',
|
|
31
31
|
description: 'Text label displayed inside the button',
|
|
32
32
|
required: false,
|
|
33
|
-
type: 'string'
|
|
33
|
+
type: 'string',
|
|
34
|
+
default: 'undefined'
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'loading',
|
|
38
|
+
description: 'Shows a loading state and disables interaction',
|
|
39
|
+
required: false,
|
|
40
|
+
type: 'boolean',
|
|
41
|
+
default: 'false'
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'size',
|
|
45
|
+
description: 'Controls the button size',
|
|
46
|
+
required: false,
|
|
47
|
+
type: 'Size',
|
|
48
|
+
default: '\'sm\''
|
|
34
49
|
},
|
|
35
50
|
{
|
|
36
51
|
name: 'icon',
|
|
@@ -54,20 +69,15 @@
|
|
|
54
69
|
name: 'tooltip',
|
|
55
70
|
description: 'Tooltip text shown on hover',
|
|
56
71
|
required: false,
|
|
57
|
-
type: 'string'
|
|
58
|
-
|
|
59
|
-
{
|
|
60
|
-
name: 'loading',
|
|
61
|
-
description: 'Shows a loading state and disables interaction',
|
|
62
|
-
required: false,
|
|
63
|
-
type: 'boolean',
|
|
64
|
-
default: 'false'
|
|
72
|
+
type: 'string',
|
|
73
|
+
default: 'undefined'
|
|
65
74
|
},
|
|
66
75
|
{
|
|
67
76
|
name: 'loadingText',
|
|
68
77
|
description: 'Text shown while the button is loading',
|
|
69
78
|
required: false,
|
|
70
|
-
type: 'string'
|
|
79
|
+
type: 'string',
|
|
80
|
+
default: 'undefined'
|
|
71
81
|
},
|
|
72
82
|
{
|
|
73
83
|
name: 'disabled',
|
|
@@ -80,20 +90,15 @@
|
|
|
80
90
|
name: 'route',
|
|
81
91
|
description: 'Router destination when used as a link',
|
|
82
92
|
required: false,
|
|
83
|
-
type: 'string | kt | Tt'
|
|
93
|
+
type: 'string | kt | Tt',
|
|
94
|
+
default: 'undefined'
|
|
84
95
|
},
|
|
85
96
|
{
|
|
86
97
|
name: 'link',
|
|
87
98
|
description: 'External link URL',
|
|
88
99
|
required: false,
|
|
89
|
-
type: 'string'
|
|
90
|
-
|
|
91
|
-
{
|
|
92
|
-
name: 'type',
|
|
93
|
-
description: 'Native button type',
|
|
94
|
-
required: false,
|
|
95
|
-
type: '"button" | "submit" | "reset"',
|
|
96
|
-
default: '"button"'
|
|
100
|
+
type: 'string',
|
|
101
|
+
default: 'undefined'
|
|
97
102
|
}
|
|
98
103
|
]
|
|
99
104
|
|
|
@@ -101,22 +106,22 @@
|
|
|
101
106
|
{
|
|
102
107
|
name: 'prefix',
|
|
103
108
|
description: 'Content shown before the button label (left icon / custom content)',
|
|
104
|
-
type: '
|
|
109
|
+
type: '[void]'
|
|
105
110
|
},
|
|
106
111
|
{
|
|
107
112
|
name: 'icon',
|
|
108
113
|
description: 'Icon-only content for icon buttons',
|
|
109
|
-
type: '
|
|
114
|
+
type: '[void]'
|
|
110
115
|
},
|
|
111
116
|
{
|
|
112
117
|
name: 'default',
|
|
113
118
|
description: 'Main button content (overrides `label`)',
|
|
114
|
-
type: '
|
|
119
|
+
type: '[void]'
|
|
115
120
|
},
|
|
116
121
|
{
|
|
117
122
|
name: 'suffix',
|
|
118
123
|
description: 'Content shown after the button label (right icon / custom content)',
|
|
119
|
-
type: '
|
|
124
|
+
type: '[void]'
|
|
120
125
|
}
|
|
121
126
|
]
|
|
122
127
|
</script>
|
|
@@ -57,7 +57,7 @@ describe('<Button />', () => {
|
|
|
57
57
|
})
|
|
58
58
|
cy.get('button').should('be.disabled')
|
|
59
59
|
cy.get('button').should('contain.text', 'Processing...')
|
|
60
|
-
cy.get('
|
|
60
|
+
cy.get('[role="status"]').should('exist') // Loading Spinner
|
|
61
61
|
})
|
|
62
62
|
|
|
63
63
|
it('handles prefix and suffix slots (replacing deprecated icon props)', () => {
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
} from 'reka-ui'
|
|
18
18
|
import { RouterLink } from 'vue-router'
|
|
19
19
|
import FeatherIcon from '../FeatherIcon.vue'
|
|
20
|
-
import
|
|
20
|
+
import Spinner from '../Spinner/Spinner.vue'
|
|
21
21
|
import TooltipBubble from '../Tooltip/TooltipBubble.vue'
|
|
22
22
|
import { warnFeatherIconUsage } from '../../utils/iconString'
|
|
23
23
|
import { buttonProps, type ThemeVariant } from './types'
|
|
@@ -66,10 +66,7 @@ export default defineComponent({
|
|
|
66
66
|
)
|
|
67
67
|
|
|
68
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
|
-
],
|
|
69
|
+
() => ({ xs: 'h-4', sm: 'h-4', md: 'h-4.5', lg: 'h-5' })[props.size],
|
|
73
70
|
)
|
|
74
71
|
|
|
75
72
|
const lucideSlotClasses = computed(
|
|
@@ -79,8 +76,6 @@ export default defineComponent({
|
|
|
79
76
|
sm: 'size-4',
|
|
80
77
|
md: 'size-4.5',
|
|
81
78
|
lg: 'size-5',
|
|
82
|
-
xl: 'size-6',
|
|
83
|
-
'2xl': 'size-6',
|
|
84
79
|
})[props.size],
|
|
85
80
|
)
|
|
86
81
|
|
|
@@ -166,21 +161,22 @@ export default defineComponent({
|
|
|
166
161
|
sm: 'h-7 w-7 rounded',
|
|
167
162
|
md: 'h-8 w-8 rounded',
|
|
168
163
|
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
164
|
}[props.size]
|
|
172
165
|
: {
|
|
173
166
|
xs: 'h-6 text-sm px-1.5 rounded',
|
|
174
167
|
sm: 'h-7 text-base px-2 rounded',
|
|
175
168
|
md: 'h-8 text-base font-medium px-2.5 rounded',
|
|
176
169
|
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
170
|
}[props.size]
|
|
180
171
|
|
|
181
172
|
return [
|
|
182
173
|
'inline-flex items-center justify-center gap-2 transition-colors focus:outline-none shrink-0',
|
|
183
|
-
|
|
174
|
+
// Only an explicit `disabled` dims the button. A `loading` button keeps
|
|
175
|
+
// its normal look (it's still non-interactive via the native `disabled`
|
|
176
|
+
// attr below); `pointer-events-none` suppresses hover/active visuals so
|
|
177
|
+
// it doesn't appear clickable while busy.
|
|
178
|
+
props.disabled ? disabledClasses : variantClasses,
|
|
179
|
+
props.loading && !props.disabled ? 'pointer-events-none' : '',
|
|
184
180
|
focusClasses,
|
|
185
181
|
sizeClasses,
|
|
186
182
|
]
|
|
@@ -192,20 +188,28 @@ export default defineComponent({
|
|
|
192
188
|
// The dynamic root: router link, external anchor, or native button. Using the
|
|
193
189
|
// raw 'button' string (not <component :is>) sidesteps the historic recursion
|
|
194
190
|
// with a globally-registered <Button> in consumer apps.
|
|
195
|
-
const root = computed<{
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
191
|
+
const root = computed<{
|
|
192
|
+
is: Component | string
|
|
193
|
+
props: Record<string, unknown>
|
|
194
|
+
}>(() => {
|
|
195
|
+
if (!isDisabled.value && props.route) {
|
|
196
|
+
return { is: RouterLink, props: { to: props.route } }
|
|
197
|
+
}
|
|
198
|
+
if (!isDisabled.value && props.link) {
|
|
199
|
+
return {
|
|
200
|
+
is: 'a',
|
|
201
|
+
props: {
|
|
202
|
+
href: props.link,
|
|
203
|
+
target: '_blank',
|
|
204
|
+
rel: 'noreferrer noopener',
|
|
205
|
+
},
|
|
205
206
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
is: 'button',
|
|
210
|
+
props: { type: props.type, disabled: isDisabled.value },
|
|
211
|
+
}
|
|
212
|
+
})
|
|
209
213
|
|
|
210
214
|
/** Resolve an icon prop to a vnode: lucide class-span, FeatherIcon, or component. */
|
|
211
215
|
function renderIcon(
|
|
@@ -231,12 +235,14 @@ export default defineComponent({
|
|
|
231
235
|
|
|
232
236
|
function renderPrefix() {
|
|
233
237
|
if (props.loading) {
|
|
234
|
-
|
|
238
|
+
// No `size`/`theme` props: button spinner diameters are tuned per
|
|
239
|
+
// button size and don't line up with Spinner's fixed sizes, and the
|
|
240
|
+
// spinner inherits the button's text color.
|
|
241
|
+
return h(Spinner, {
|
|
235
242
|
class: {
|
|
236
|
-
'
|
|
237
|
-
'
|
|
238
|
-
'
|
|
239
|
-
'h-4.5 w-4.5': props.size === 'xl' || props.size === '2xl',
|
|
243
|
+
'size-4': props.size === 'xs' || props.size === 'sm',
|
|
244
|
+
'size-4.5': props.size === 'md',
|
|
245
|
+
'size-5': props.size === 'lg',
|
|
240
246
|
},
|
|
241
247
|
})
|
|
242
248
|
}
|
|
@@ -250,7 +256,11 @@ export default defineComponent({
|
|
|
250
256
|
if (props.icon) return renderIcon(props.icon, false)
|
|
251
257
|
if (slots.icon) return slots.icon()
|
|
252
258
|
if (hasLucideIconInDefaultSlot.value) {
|
|
253
|
-
return h(
|
|
259
|
+
return h(
|
|
260
|
+
'div',
|
|
261
|
+
{ class: slotClasses.value },
|
|
262
|
+
slots.default?.() ?? props.label,
|
|
263
|
+
)
|
|
254
264
|
}
|
|
255
265
|
return null
|
|
256
266
|
}
|
|
@@ -275,6 +285,7 @@ export default defineComponent({
|
|
|
275
285
|
...restAttrs,
|
|
276
286
|
class: [attrClass, buttonClasses.value],
|
|
277
287
|
'aria-label': props.label,
|
|
288
|
+
'aria-busy': props.loading || undefined,
|
|
278
289
|
ref: rootRef,
|
|
279
290
|
}
|
|
280
291
|
const button =
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { Button } from 'frappe-ui'
|
|
2
|
+
import { Button, KeyboardShortcut } from 'frappe-ui'
|
|
3
3
|
</script>
|
|
4
4
|
|
|
5
5
|
<template>
|
|
@@ -23,5 +23,12 @@ import { Button } from 'frappe-ui'
|
|
|
23
23
|
Get Started
|
|
24
24
|
</Button>
|
|
25
25
|
|
|
26
|
+
<Button>
|
|
27
|
+
Discover
|
|
28
|
+
<template #suffix>
|
|
29
|
+
<KeyboardShortcut combo="Mod+K" :show-plus="false" />
|
|
30
|
+
</template>
|
|
31
|
+
</Button>
|
|
32
|
+
|
|
26
33
|
<Button :loading="true"> Fetching </Button>
|
|
27
34
|
</template>
|
|
@@ -3,9 +3,8 @@ import { Button } from 'frappe-ui'
|
|
|
3
3
|
</script>
|
|
4
4
|
|
|
5
5
|
<template>
|
|
6
|
+
<Button size="xs">Button</Button>
|
|
6
7
|
<Button size="sm">Button</Button>
|
|
7
8
|
<Button size="md">Button</Button>
|
|
8
9
|
<Button size="lg">Button</Button>
|
|
9
|
-
<Button size="xl">Button</Button>
|
|
10
|
-
<Button size="2xl">Button</Button>
|
|
11
10
|
</template>
|
|
@@ -2,7 +2,7 @@ import { type RouterLinkProps } from 'vue-router'
|
|
|
2
2
|
import { type Component, type ExtractPublicPropTypes, type PropType } from 'vue'
|
|
3
3
|
|
|
4
4
|
export type Theme = 'gray' | 'blue' | 'green' | 'red'
|
|
5
|
-
export type Size = 'xs' | 'sm' | 'md' | 'lg'
|
|
5
|
+
export type Size = 'xs' | 'sm' | 'md' | 'lg'
|
|
6
6
|
export type Variant = 'solid' | 'subtle' | 'outline' | 'ghost'
|
|
7
7
|
|
|
8
8
|
const iconProp = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<span
|
|
3
|
-
class="inline-flex items-center gap-
|
|
4
|
-
:class="!bg ? 'text-ink-gray-
|
|
3
|
+
class="inline-flex items-center gap-0.5"
|
|
4
|
+
:class="!bg ? 'text-ink-gray-5 text-sm' : ''"
|
|
5
5
|
:aria-label="ariaLabel"
|
|
6
6
|
role="note"
|
|
7
7
|
v-bind="$attrs"
|
|
@@ -45,11 +45,7 @@
|
|
|
45
45
|
role="img"
|
|
46
46
|
:aria-label="part.display"
|
|
47
47
|
/>
|
|
48
|
-
<span
|
|
49
|
-
v-else
|
|
50
|
-
class="font-mono leading-none tracking-wide uppercase text-[10px]"
|
|
51
|
-
>{{ part.display }}</span
|
|
52
|
-
>
|
|
48
|
+
<span v-else class="leading-none uppercase">{{ part.display }}</span>
|
|
53
49
|
</span>
|
|
54
50
|
<span
|
|
55
51
|
v-if="idx < parsedParts.length - 1 && showPlus"
|
|
@@ -1,26 +1,17 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
class="max-w-xs animate-spin"
|
|
4
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
-
fill="none"
|
|
6
|
-
:style="`scale: ${scale}%;`"
|
|
7
|
-
viewBox="0 0 24 24"
|
|
8
|
-
>
|
|
9
|
-
<circle
|
|
10
|
-
class="opacity-25"
|
|
11
|
-
cx="12"
|
|
12
|
-
cy="12"
|
|
13
|
-
r="10"
|
|
14
|
-
stroke="currentColor"
|
|
15
|
-
stroke-width="4"
|
|
16
|
-
></circle>
|
|
17
|
-
<path
|
|
18
|
-
class="opacity-75"
|
|
19
|
-
fill="currentColor"
|
|
20
|
-
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
21
|
-
></path>
|
|
22
|
-
</svg>
|
|
2
|
+
<Spinner :style="scaleStyle" />
|
|
23
3
|
</template>
|
|
4
|
+
|
|
24
5
|
<script setup lang="ts">
|
|
25
|
-
|
|
6
|
+
import { computed } from 'vue'
|
|
7
|
+
import Spinner from './Spinner/Spinner.vue'
|
|
8
|
+
|
|
9
|
+
// Thin wrapper kept for backward compatibility: it forwards sizing/color
|
|
10
|
+
// classes through to the new Spinner. Size and color come from the
|
|
11
|
+
// width/height and text-color classes the caller passes.
|
|
12
|
+
const props = withDefaults(defineProps<{ scale?: number }>(), { scale: 100 })
|
|
13
|
+
|
|
14
|
+
const scaleStyle = computed(() =>
|
|
15
|
+
props.scale === 100 ? undefined : { scale: `${props.scale}%` },
|
|
16
|
+
)
|
|
26
17
|
</script>
|
|
@@ -3,19 +3,9 @@
|
|
|
3
3
|
<LoadingIndicator class="-ml-1 mr-2 h-3 w-3" /> {{ text }}
|
|
4
4
|
</div>
|
|
5
5
|
</template>
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
<script setup lang="ts">
|
|
7
8
|
import LoadingIndicator from './LoadingIndicator.vue'
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
name: 'Loading',
|
|
11
|
-
props: {
|
|
12
|
-
text: {
|
|
13
|
-
type: String,
|
|
14
|
-
default: 'Loading...',
|
|
15
|
-
},
|
|
16
|
-
},
|
|
17
|
-
components: {
|
|
18
|
-
LoadingIndicator,
|
|
19
|
-
},
|
|
20
|
-
}
|
|
10
|
+
withDefaults(defineProps<{ text?: string }>(), { text: 'Loading...' })
|
|
21
11
|
</script>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<!-- Auto Generated by scripts/propsgen.ts -->
|
|
2
|
+
<script setup>
|
|
3
|
+
import PropsTable from '@/components/Docs/PropsTable.vue'
|
|
4
|
+
import SlotsTable from '@/components/Docs/SlotsTable.vue'
|
|
5
|
+
import EmitsTable from '@/components/Docs/EmitsTable.vue'
|
|
6
|
+
|
|
7
|
+
const propsData = [
|
|
8
|
+
{
|
|
9
|
+
name: 'size',
|
|
10
|
+
description: 'Diameter — xs=12px, sm=14px, md=16px, lg=20px. Omit to size with classes (default 16px).',
|
|
11
|
+
required: false,
|
|
12
|
+
type: 'SpinnerSize'
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: 'theme',
|
|
16
|
+
description: 'Spinner color. Omit to inherit the text color.',
|
|
17
|
+
required: false,
|
|
18
|
+
type: 'SpinnerTheme'
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'track',
|
|
22
|
+
description: 'Show a faint track behind the arc',
|
|
23
|
+
required: false,
|
|
24
|
+
type: 'boolean',
|
|
25
|
+
default: 'false'
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
</script>
|
|
29
|
+
## API Reference
|
|
30
|
+
|
|
31
|
+
<PropsTable name="Spinner" :data="propsData"/>
|
|
32
|
+
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import Spinner from './Spinner.vue'
|
|
2
|
+
|
|
3
|
+
describe('Spinner', () => {
|
|
4
|
+
it('renders with role="status" and default aria-label', () => {
|
|
5
|
+
cy.mount(Spinner)
|
|
6
|
+
|
|
7
|
+
cy.get('[role="status"]')
|
|
8
|
+
.should('exist')
|
|
9
|
+
.and('have.attr', 'aria-label', 'Loading')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
describe('sizes', () => {
|
|
13
|
+
const sizes = [
|
|
14
|
+
{ size: 'xs', px: 12 },
|
|
15
|
+
{ size: 'sm', px: 14 },
|
|
16
|
+
{ size: 'md', px: 16 },
|
|
17
|
+
{ size: 'lg', px: 20 },
|
|
18
|
+
] as const
|
|
19
|
+
|
|
20
|
+
sizes.forEach(({ size, px }) => {
|
|
21
|
+
it(`size="${size}" sets ${px}px width and height`, () => {
|
|
22
|
+
cy.mount(Spinner, { props: { size } })
|
|
23
|
+
|
|
24
|
+
cy.get('[role="status"]').should(($el) => {
|
|
25
|
+
expect($el[0].style.width).to.equal(`${px}px`)
|
|
26
|
+
expect($el[0].style.height).to.equal(`${px}px`)
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('no size prop applies no inline width/height and defaults to 16px via svg attributes', () => {
|
|
32
|
+
cy.mount(Spinner)
|
|
33
|
+
|
|
34
|
+
cy.get('[role="status"]').should(($el) => {
|
|
35
|
+
expect($el[0].style.width).to.equal('')
|
|
36
|
+
expect($el[0].style.height).to.equal('')
|
|
37
|
+
expect(getComputedStyle($el[0]).width).to.equal('16px')
|
|
38
|
+
expect(getComputedStyle($el[0]).height).to.equal('16px')
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('without size prop, width/height classes win over the CSS default', () => {
|
|
43
|
+
cy.mount(Spinner, { attrs: { class: 'h-3 w-3' } })
|
|
44
|
+
|
|
45
|
+
cy.get('[role="status"]').should(($el) => {
|
|
46
|
+
expect(getComputedStyle($el[0]).width).to.equal('12px')
|
|
47
|
+
expect(getComputedStyle($el[0]).height).to.equal('12px')
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('theme', () => {
|
|
53
|
+
it('theme="gray" applies text-ink-gray-8 class', () => {
|
|
54
|
+
cy.mount(Spinner, { props: { theme: 'gray' } })
|
|
55
|
+
|
|
56
|
+
cy.get('[role="status"]').should('have.class', 'text-ink-gray-8')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('theme="red" applies text-ink-red-4 class', () => {
|
|
60
|
+
cy.mount(Spinner, { props: { theme: 'red' } })
|
|
61
|
+
|
|
62
|
+
cy.get('[role="status"]').should('have.class', 'text-ink-red-4')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('no theme prop applies no color class (inherits currentColor)', () => {
|
|
66
|
+
cy.mount(Spinner)
|
|
67
|
+
|
|
68
|
+
cy.get('[role="status"]')
|
|
69
|
+
.should('not.have.class', 'text-ink-gray-8')
|
|
70
|
+
.and('not.have.class', 'text-ink-red-4')
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe('track', () => {
|
|
75
|
+
it('track=false (default) does not apply track class', () => {
|
|
76
|
+
cy.mount(Spinner)
|
|
77
|
+
|
|
78
|
+
cy.get('[role="status"]').should('not.have.class', 'fui-spinner--track')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('track=true applies fui-spinner--track class', () => {
|
|
82
|
+
cy.mount(Spinner, { props: { track: true } })
|
|
83
|
+
|
|
84
|
+
cy.get('[role="status"]').should('have.class', 'fui-spinner--track')
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Spinner
|
|
2
|
+
|
|
3
|
+
Communicates an ongoing, indeterminate loading state.
|
|
4
|
+
|
|
5
|
+
## Sizes
|
|
6
|
+
|
|
7
|
+
<ComponentPreview name="Spinner-Sizes" />
|
|
8
|
+
|
|
9
|
+
## Themes
|
|
10
|
+
|
|
11
|
+
<ComponentPreview name="Spinner-Themes" />
|
|
12
|
+
|
|
13
|
+
## Track
|
|
14
|
+
|
|
15
|
+
<ComponentPreview name="Spinner-Track" />
|
|
16
|
+
|
|
17
|
+
## In context
|
|
18
|
+
|
|
19
|
+
<ComponentPreview name="Spinner-InContext" />
|
|
20
|
+
|
|
21
|
+
<!-- @include: ./Spinner.api.md -->
|