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.
Files changed (83) hide show
  1. package/package.json +30 -4
  2. package/src/components/Button/Button.api.md +37 -32
  3. package/src/components/Button/Button.cy.ts +1 -1
  4. package/src/components/Button/Button.vue +42 -31
  5. package/src/components/Button/stories/Icons.vue +8 -1
  6. package/src/components/Button/stories/Sizes.vue +1 -2
  7. package/src/components/Button/stories/Themes.vue +0 -2
  8. package/src/components/Button/types.ts +1 -1
  9. package/src/components/KeyboardShortcut.vue +3 -7
  10. package/src/components/LoadingIndicator.vue +13 -22
  11. package/src/components/LoadingText.vue +3 -13
  12. package/src/components/Spinner/Spinner.api.md +32 -0
  13. package/src/components/Spinner/Spinner.cy.ts +87 -0
  14. package/src/components/Spinner/Spinner.md +21 -0
  15. package/src/components/Spinner/Spinner.vue +168 -0
  16. package/src/components/Spinner/index.ts +2 -0
  17. package/src/components/Spinner/stories/InContext.vue +49 -0
  18. package/src/components/Spinner/stories/Sizes.vue +12 -0
  19. package/src/components/Spinner/stories/Themes.vue +10 -0
  20. package/src/components/Spinner/stories/Track.vue +12 -0
  21. package/src/components/Spinner/types.ts +14 -0
  22. package/src/components/TextEditor/extensions/iframe/iframe-extension.ts +62 -19
  23. package/src/index.ts +3 -1
  24. package/src/molecules/editor/commands.ts +3 -2
  25. package/src/molecules/editor/components/EditorDropZone.vue +25 -9
  26. package/src/molecules/editor/components/MediaNodeView.vue +156 -49
  27. package/src/molecules/editor/components/MediaResizeHandles.vue +36 -0
  28. package/src/molecules/editor/components/MediaToolbar.vue +107 -53
  29. package/src/molecules/editor/components/UploadProgressIndicator.vue +33 -0
  30. package/src/molecules/editor/components/VideoControls.vue +208 -0
  31. package/src/molecules/editor/components/media-node-view-controller.ts +4 -4
  32. package/src/molecules/editor/components/media-node-view-utils.test.ts +0 -13
  33. package/src/molecules/editor/components/media-node-view-utils.ts +0 -10
  34. package/src/molecules/editor/composables/useEditorFileDrop.ts +21 -3
  35. package/src/molecules/editor/composables/useFloatingPopup.ts +1 -0
  36. package/src/molecules/editor/composables/useNodeViewResize.test.ts +60 -11
  37. package/src/molecules/editor/composables/useNodeViewResize.ts +80 -21
  38. package/src/molecules/editor/extensions/content-paste/content-paste-extension.ts +5 -3
  39. package/src/molecules/editor/extensions/iframe/IframeInsertDialog.vue +31 -4
  40. package/src/molecules/editor/extensions/iframe/IframeNodeView.vue +54 -48
  41. package/src/molecules/editor/extensions/iframe/iframe-allowlist.ts +33 -4
  42. package/src/molecules/editor/extensions/iframe/iframe-commands.ts +49 -5
  43. package/src/molecules/editor/extensions/iframe/iframe-embed-utils.ts +30 -0
  44. package/src/molecules/editor/extensions/iframe/iframe-extension.ts +4 -4
  45. package/src/molecules/editor/extensions/iframe/iframeInsertDialogController.ts +12 -1
  46. package/src/molecules/editor/extensions/iframe/index.ts +4 -4
  47. package/src/molecules/editor/extensions/iframe/useIframeDialog.ts +25 -13
  48. package/src/molecules/editor/extensions/image/image-extension.ts +29 -11
  49. package/src/molecules/editor/extensions/image-group/ImageGroupGrid.vue +27 -11
  50. package/src/molecules/editor/extensions/image-group/ImageGroupGridCell.vue +55 -11
  51. package/src/molecules/editor/extensions/image-group/ImageGroupNodeView.vue +64 -31
  52. package/src/molecules/editor/extensions/image-group/ImageGroupUploadDialog.vue +81 -37
  53. package/src/molecules/editor/extensions/image-group/image-group-extension.ts +4 -3
  54. package/src/molecules/editor/extensions/image-group/useImageGroupDialog.ts +116 -17
  55. package/src/molecules/editor/extensions/shared/media-dimensions.ts +36 -3
  56. package/src/molecules/editor/extensions/shared/media-node-ops.ts +17 -0
  57. package/src/molecules/editor/extensions/shared/media-plugin.test.ts +60 -0
  58. package/src/molecules/editor/extensions/shared/media-plugin.ts +1 -0
  59. package/src/molecules/editor/extensions/shared/media-upload-engine.test.ts +39 -2
  60. package/src/molecules/editor/extensions/shared/media-upload-engine.ts +104 -10
  61. package/src/molecules/editor/extensions/shared/media-upload-state.ts +55 -0
  62. package/src/molecules/editor/extensions/shared/media-upload-types.ts +33 -1
  63. package/src/molecules/editor/extensions/shared/suggestion-types.ts +7 -0
  64. package/src/molecules/editor/extensions/shared/upload-types.ts +1 -0
  65. package/src/molecules/editor/extensions/slash-commands/slash-commands-extension.test.ts +38 -0
  66. package/src/molecules/editor/extensions/slash-commands/slash-commands-extension.ts +109 -55
  67. package/src/molecules/editor/extensions/suggestion/SuggestionList.vue +49 -17
  68. package/src/molecules/editor/extensions/table/table-navigation.ts +23 -2
  69. package/src/molecules/editor/extensions/video/video-extension.ts +64 -5
  70. package/src/molecules/editor/extensions.ts +116 -4
  71. package/src/molecules/editor/kits.test.ts +12 -1
  72. package/src/molecules/editor/kits.ts +20 -24
  73. package/src/molecules/editor/menu.test.ts +3 -1
  74. package/src/molecules/editor/style.css +6 -0
  75. package/src/molecules/editor/useEditor.test.ts +4 -0
  76. package/src/molecules/editor/useEditor.ts +7 -3
  77. package/src/utils/config.ts +4 -1
  78. package/src/utils/dialog.cy.ts +7 -7
  79. package/src/utils/fileSize.ts +36 -0
  80. package/src/utils/fileUploadHandler.ts +49 -5
  81. package/src/utils/plugin.js +19 -0
  82. package/src/utils/useFileUpload.ts +68 -22
  83. 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",
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": "^3.11.0",
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: '"gray"'
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: '"subtle"'
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: 'any'
109
+ type: '[void]'
105
110
  },
106
111
  {
107
112
  name: 'icon',
108
113
  description: 'Icon-only content for icon buttons',
109
- type: 'any'
114
+ type: '[void]'
110
115
  },
111
116
  {
112
117
  name: 'default',
113
118
  description: 'Main button content (overrides `label`)',
114
- type: 'any'
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: 'any'
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('svg').should('exist') // Loading Spinner
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 LoadingIndicator from '../LoadingIndicator.vue'
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
- isDisabled.value ? disabledClasses : variantClasses,
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<{ 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
- }
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
- return { is: 'button', props: { type: props.type, disabled: isDisabled.value } }
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
- return h(LoadingIndicator, {
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
- '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',
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('div', { class: slotClasses.value }, slots.default?.() ?? props.label)
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>
@@ -4,7 +4,5 @@ import { Button } from 'frappe-ui'
4
4
 
5
5
  <template>
6
6
  <Button theme="gray">Gray</Button>
7
- <Button theme="blue">Blue</Button>
8
- <Button theme="green">Green</Button>
9
7
  <Button theme="red">Red</Button>
10
8
  </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' | 'xl' | '2xl'
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-1"
4
- :class="!bg ? 'text-ink-gray-4 text-sm' : ''"
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
- <svg
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
- defineProps({ scale: { required: false, default: 100 } })
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
- <script>
6
+
7
+ <script setup lang="ts">
7
8
  import LoadingIndicator from './LoadingIndicator.vue'
8
9
 
9
- export default {
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 -->