frappe-ui 1.0.0-beta.62 → 1.0.0-beta.63

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 (111) hide show
  1. package/experimental/CodeEditor/CodeEditor.api.md +1 -1
  2. package/experimental/CodeEditor/CodeEditor.md +1 -1
  3. package/experimental/CodeEditor/CodeEditor.playground.vue +1 -1
  4. package/experimental/CodeEditor/CodeEditor.vue +2 -2
  5. package/experimental/CodeEditor/stories/Sizes.vue +1 -1
  6. package/experimental/CodeEditor/types.ts +1 -1
  7. package/experimental/MultiEmailInput/MultiEmailInput.cy.ts +27 -0
  8. package/experimental/MultiEmailInput/MultiEmailInput.vue +9 -1
  9. package/package.json +1 -1
  10. package/src/components/Checkbox/Checkbox.cy.ts +3 -3
  11. package/src/components/Combobox/Combobox.api.md +2 -2
  12. package/src/components/Combobox/Combobox.playground.vue +1 -1
  13. package/src/components/Combobox/Combobox.vue +5 -0
  14. package/src/components/Combobox/stories/Clearable.vue +5 -5
  15. package/src/components/Combobox/types.ts +9 -8
  16. package/src/components/ContextMenu/index.ts +1 -0
  17. package/src/components/ContextMenu/types.ts +16 -0
  18. package/src/components/DesktopShell/DesktopShell.api.md +1 -1
  19. package/src/components/DesktopShell/DesktopShell.md +6 -5
  20. package/src/components/DesktopShell/DesktopShell.vue +1 -1
  21. package/src/components/DesktopShell/stories/Default.vue +8 -8
  22. package/src/components/Divider/Divider.api.md +4 -4
  23. package/src/components/Divider/index.ts +1 -1
  24. package/src/components/Divider/types.ts +24 -1
  25. package/src/components/Duration/Duration.playground.vue +1 -1
  26. package/src/components/Duration/stories/Sizes.vue +1 -1
  27. package/src/components/FormControl/FormControl.api.md +2 -2
  28. package/src/components/FormControl/FormControl.cy.ts +40 -0
  29. package/src/components/FormControl/FormControl.playground.vue +2 -0
  30. package/src/components/FormControl/FormControl.vue +6 -1
  31. package/src/components/FormControl/types.ts +7 -2
  32. package/src/components/FormLabel/FormLabel.api.md +0 -7
  33. package/src/components/FormLabel/FormLabel.cy.ts +24 -5
  34. package/src/components/FormLabel/FormLabel.playground.vue +1 -11
  35. package/src/components/FormLabel/FormLabel.vue +7 -12
  36. package/src/components/FormLabel/types.ts +0 -3
  37. package/src/components/HoverCard/HoverCard.api.md +1 -1
  38. package/src/components/HoverCard/types.ts +3 -0
  39. package/src/components/InputLabeling/InputDescription.vue +1 -1
  40. package/src/components/InputLabeling/InputLabel.vue +9 -4
  41. package/src/components/ItemListRow/ItemListRow.api.md +1 -1
  42. package/src/components/ItemListRow/ItemListRow.vue +13 -7
  43. package/src/components/ItemListRow/types.ts +7 -1
  44. package/src/components/KeyboardShortcutsDialog/KeyboardShortcutsDialog.api.md +1 -1
  45. package/src/components/KeyboardShortcutsDialog/types.ts +3 -0
  46. package/src/components/MultiSelect/MultiSelect.api.md +3 -3
  47. package/src/components/MultiSelect/MultiSelect.playground.vue +1 -1
  48. package/src/components/MultiSelect/MultiSelect.vue +5 -2
  49. package/src/components/MultiSelect/types.ts +4 -8
  50. package/src/components/Password/Password.playground.vue +1 -1
  51. package/src/components/Password/stories/Sizes.vue +1 -1
  52. package/src/components/Rating/Rating.cy.ts +1 -1
  53. package/src/components/Rating/Rating.playground.vue +1 -1
  54. package/src/components/Rating/Rating.vue +14 -8
  55. package/src/components/Rating/stories/Sizes.vue +1 -1
  56. package/src/components/Select/Select.api.md +2 -2
  57. package/src/components/Select/Select.cy.ts +19 -1
  58. package/src/components/Select/Select.playground.vue +1 -1
  59. package/src/components/Select/types.ts +3 -2
  60. package/src/components/Select/utils.ts +12 -6
  61. package/src/components/{Rail/Rail.api.md → SidebarRail/SidebarRail.api.md} +11 -11
  62. package/src/components/{Rail/Rail.cy.ts → SidebarRail/SidebarRail.cy.ts} +31 -31
  63. package/src/components/SidebarRail/SidebarRail.md +36 -0
  64. package/src/components/{Rail/Rail.vue → SidebarRail/SidebarRail.vue} +2 -2
  65. package/src/components/{Rail/RailItem.vue → SidebarRail/SidebarRailItem.vue} +12 -12
  66. package/src/components/{Rail/RailItemBadge.vue → SidebarRail/SidebarRailItemBadge.vue} +1 -1
  67. package/src/components/SidebarRail/index.ts +4 -0
  68. package/src/components/{Rail → SidebarRail}/stories/Default.vue +10 -10
  69. package/src/components/{Rail → SidebarRail}/types.ts +1 -1
  70. package/src/components/Switch/Switch.cy.ts +2 -2
  71. package/src/components/TabButtons/TabButtons.api.md +4 -4
  72. package/src/components/TabButtons/TabButtons.vue +3 -0
  73. package/src/components/TabButtons/index.ts +1 -0
  74. package/src/components/TabButtons/types.ts +7 -0
  75. package/src/components/TextInput/TextInput.cy.ts +108 -4
  76. package/src/components/TextInput/TextInput.playground.vue +1 -1
  77. package/src/components/TextInput/TextInput.vue +60 -26
  78. package/src/components/TextInput/stories/Sizes.vue +1 -1
  79. package/src/components/Textarea/Textarea.api.md +1 -1
  80. package/src/components/Textarea/Textarea.cy.ts +65 -34
  81. package/src/components/Textarea/Textarea.playground.vue +1 -1
  82. package/src/components/Textarea/Textarea.vue +39 -14
  83. package/src/components/Textarea/stories/Sizes.vue +1 -1
  84. package/src/components/Textarea/types.ts +5 -1
  85. package/src/components/TimePicker/stories/SizesAndVariants.vue +1 -1
  86. package/src/components/Tree/Tree.vue +2 -6
  87. package/src/components/Tree/types.ts +8 -0
  88. package/src/components/shared/picker/PickerShell.vue +34 -37
  89. package/src/components/shared/selection/utils.test.ts +54 -2
  90. package/src/components/shared/selection/utils.ts +64 -21
  91. package/src/components/shared/tabs/styles.ts +1 -1
  92. package/src/composables/inputTypes.ts +14 -2
  93. package/src/composables/usePortalTarget.spec.ts +6 -5
  94. package/src/index.ts +12 -1
  95. package/src/molecules/list/List.cy.ts +426 -0
  96. package/src/molecules/list/List.ssr.test.ts +59 -0
  97. package/src/molecules/list/List.vue +92 -7
  98. package/src/molecules/list/index.ts +2 -0
  99. package/src/molecules/list/list-context.ts +2 -1
  100. package/src/molecules/list/list.api.md +3 -3
  101. package/src/molecules/list/list.md +152 -33
  102. package/src/molecules/list/stories/Columns.vue +15 -9
  103. package/src/molecules/list/stories/Responsive.vue +56 -0
  104. package/src/molecules/list/stories/RowActions.vue +123 -0
  105. package/src/molecules/list/style.css +67 -26
  106. package/src/molecules/list/types.ts +51 -9
  107. package/tailwind/listColumns.js +191 -0
  108. package/tailwind/listColumns.test.js +358 -0
  109. package/tailwind/plugin.js +5 -6
  110. package/src/components/Rail/Rail.md +0 -30
  111. package/src/components/Rail/index.ts +0 -2
@@ -41,7 +41,7 @@
41
41
  },
42
42
  {
43
43
  name: 'size',
44
- description: 'Size token, mirroring frappe-ui inputs. Scales the editor\'s font size and\nminimum height (`sm | md | lg | xl`). Defaults to `md`.',
44
+ description: 'Size token, mirroring frappe-ui inputs. Scales the editor\'s font size and\nminimum height (`xs | sm | md | lg`). Defaults to `md`.',
45
45
  required: false,
46
46
  type: 'InputSize',
47
47
  default: '"md"'
@@ -40,7 +40,7 @@ and loses the affordance that it's an editable field.
40
40
 
41
41
  ## Sizes
42
42
 
43
- `size` mirrors the frappe-ui input sizes (`sm | md | lg | xl`, default `md`),
43
+ `size` mirrors the frappe-ui input sizes (`xs | sm | md | lg`, default `md`),
44
44
  scaling the editor's font size and minimum height.
45
45
 
46
46
  <ComponentPreview name="CodeEditor-Sizes" />
@@ -43,10 +43,10 @@ const knobs: Knob[] = [
43
43
  type: 'tabs',
44
44
  default: 'md',
45
45
  options: [
46
+ { label: 'xs', value: 'xs' },
46
47
  { label: 'sm', value: 'sm' },
47
48
  { label: 'md', value: 'md' },
48
49
  { label: 'lg', value: 'lg' },
49
- { label: 'xl', value: 'xl' },
50
50
  ],
51
51
  },
52
52
  { name: 'placeholder', type: 'text', default: '', width: '12rem' },
@@ -288,17 +288,17 @@ function buildVariant(v?: string, disabled?: boolean): Extension {
288
288
  return cmView.EditorView.theme(fragments[v ?? 'subtle'] ?? fragments.subtle)
289
289
  }
290
290
 
291
- // Per-size font scale + content min-height, mirroring frappe-ui's `sm|md|lg|xl`
291
+ // Per-size font scale + content min-height, mirroring frappe-ui's `xs|sm|md|lg`
292
292
  // input sizes. This compartment is the SOLE source of `&` font size and
293
293
  // `.cm-content` min-height — the base `theme` deliberately omits both so there's
294
294
  // no equal-specificity fight whose winner depends on StyleModule order.
295
295
  function buildSize(s?: string): Extension {
296
296
  if (!cmView) return []
297
297
  const sizes: Record<string, { fontSize: string; minHeight: string }> = {
298
+ xs: { fontSize: '11px', minHeight: '2.5rem' },
298
299
  sm: { fontSize: '12px', minHeight: '3.5rem' },
299
300
  md: { fontSize: '13px', minHeight: '4.5rem' },
300
301
  lg: { fontSize: '14px', minHeight: '6rem' },
301
- xl: { fontSize: '16px', minHeight: '7.5rem' },
302
302
  }
303
303
  const cfg = sizes[s ?? 'md'] ?? sizes.md
304
304
  return cmView.EditorView.theme({
@@ -7,9 +7,9 @@ const code = ref(`const sum = (a, b) => a + b`)
7
7
 
8
8
  <template>
9
9
  <div class="w-full max-w-xl space-y-4">
10
+ <CodeEditor v-model="code" language="javascript" size="xs" />
10
11
  <CodeEditor v-model="code" language="javascript" size="sm" />
11
12
  <CodeEditor v-model="code" language="javascript" size="md" />
12
13
  <CodeEditor v-model="code" language="javascript" size="lg" />
13
- <CodeEditor v-model="code" language="javascript" size="xl" />
14
14
  </div>
15
15
  </template>
@@ -42,7 +42,7 @@ export interface CodeEditorProps extends InputLabelingProps {
42
42
  variant?: Exclude<InputVariant, 'ghost'>
43
43
  /**
44
44
  * Size token, mirroring frappe-ui inputs. Scales the editor's font size and
45
- * minimum height (`sm | md | lg | xl`). Defaults to `md`.
45
+ * minimum height (`xs | sm | md | lg`). Defaults to `md`.
46
46
  */
47
47
  size?: InputSize
48
48
  }
@@ -17,6 +17,7 @@ function host(
17
17
  required?: boolean
18
18
  error?: string
19
19
  disabled?: boolean
20
+ size?: 'xs' | 'sm' | 'md' | 'lg'
20
21
  } = {},
21
22
  ) {
22
23
  return defineComponent({
@@ -46,6 +47,7 @@ function host(
46
47
  required: options.required,
47
48
  error: options.error,
48
49
  disabled: options.disabled,
50
+ size: options.size,
49
51
  placeholder: 'Add email…',
50
52
  }),
51
53
  ]
@@ -152,4 +154,29 @@ describe('MultiEmailInput', () => {
152
154
  cy.mount(host({ disabled: true }))
153
155
  cy.get('[data-slot="input"]').should('have.attr', 'disabled')
154
156
  })
157
+
158
+ // The option-row avatar has to shrink with the row. `md` (24px) is the whole
159
+ // height of an `xs` row before its padding, so a size-invariant avatar makes
160
+ // the smallest control draw the biggest picture.
161
+ const rowAvatarHeights: Array<[size: 'xs' | 'sm' | 'md' | 'lg', px: number]> =
162
+ [
163
+ ['xs', 16],
164
+ ['sm', 20],
165
+ ['md', 24],
166
+ ['lg', 24],
167
+ ]
168
+
169
+ for (const [size, px] of rowAvatarHeights) {
170
+ it(`draws a ${px}px option-row avatar at size="${size}"`, () => {
171
+ cy.mount(host({ size }))
172
+ cy.get('[data-slot="input"]').click()
173
+ cy.get('[data-slot="item"]')
174
+ .first()
175
+ .find('[data-slot="item-prefix"] > div')
176
+ .first()
177
+ .then(($avatar) => {
178
+ expect($avatar[0].getBoundingClientRect().height).to.equal(px)
179
+ })
180
+ })
181
+ }
155
182
  })
@@ -166,7 +166,15 @@ const showEmpty = computed(
166
166
  () => !props.loading && !suggestions.value.length && !showCreateOption.value,
167
167
  )
168
168
 
169
- const rowAvatarSize = computed(() => (props.size === 'sm' ? 'sm' : 'md'))
169
+ // Avatar has its own scale (`xs` 16px, `sm` 20px, `md` 24px), and the option
170
+ // row is `min-h-6`/`min-h-7`/`min-h-8`/`min-h-10` plus `ItemListRow`'s own
171
+ // padding. Step the avatar down with the row for the two small sizes rather
172
+ // than forwarding a size the row cannot draw — a `md` avatar is 24px, the
173
+ // whole height of an `xs` row before its `py-1`. `md` and `lg` rows both fit
174
+ // `md`; a 28px `lg` avatar would leave no room in a 32px `md` row.
175
+ const rowAvatarSize = computed(() =>
176
+ props.size === 'xs' ? 'xs' : props.size === 'sm' ? 'sm' : 'md',
177
+ )
170
178
 
171
179
  const boxClasses = computed(() => [
172
180
  'flex flex-wrap items-center gap-1.5 rounded-4 bg-surface-gray-2 px-1.5 py-1 transition-colors focus-within:ring-2 focus-within:ring-outline-gray-3',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frappe-ui",
3
- "version": "1.0.0-beta.62",
3
+ "version": "1.0.0-beta.63",
4
4
  "description": "A set of components and utilities for rapid UI development",
5
5
  "engines": {
6
6
  "node": ">=20.19.0"
@@ -17,7 +17,7 @@ describe('Checkbox', () => {
17
17
  cy.get('input[type="checkbox"]').should('be.disabled')
18
18
  // A disabled control dims its label — `disabled` outranks `color="gray-7"`.
19
19
  cy.get('label')
20
- .should('have.class', 'text-base')
20
+ .should('have.class', 'text-sm')
21
21
  .and('have.class', 'text-ink-gray-4')
22
22
  .and('not.have.class', 'text-ink-gray-7')
23
23
  })
@@ -28,8 +28,8 @@ describe('Checkbox', () => {
28
28
  })
29
29
 
30
30
  cy.get('[data-slot="description"]')
31
- .should('have.class', 'text-ink-gray-3')
32
- .and('not.have.class', 'text-ink-gray-5')
31
+ .should('have.class', 'text-ink-gray-4')
32
+ .and('not.have.class', 'text-ink-gray-6')
33
33
  })
34
34
 
35
35
  it('toggles with the space key', () => {
@@ -30,14 +30,14 @@
30
30
  name: 'variant',
31
31
  description: 'Visual style of the combobox.',
32
32
  required: false,
33
- type: 'ComboboxVariant',
33
+ type: 'InputVariant',
34
34
  default: '"subtle"'
35
35
  },
36
36
  {
37
37
  name: 'size',
38
38
  description: 'Size of the trigger and option rows.',
39
39
  required: false,
40
- type: 'ComboboxSize',
40
+ type: 'InputSize',
41
41
  default: '"sm"'
42
42
  },
43
43
  {
@@ -35,10 +35,10 @@ const knobs: Knob[] = [
35
35
  type: 'tabs',
36
36
  default: 'sm',
37
37
  options: [
38
+ { label: 'xs', value: 'xs' },
38
39
  { label: 'sm', value: 'sm' },
39
40
  { label: 'md', value: 'md' },
40
41
  { label: 'lg', value: 'lg' },
41
- { label: 'xl', value: 'xl' },
42
42
  ],
43
43
  },
44
44
  {
@@ -88,7 +88,12 @@ const slots = useReactiveSlots<ComboboxSlots>()
88
88
  const slotFns = slots as Record<string, ((props?: any) => any) | undefined>
89
89
 
90
90
  const model = defineModel<ComboboxOptionValue | null>({ default: null })
91
+ // Documented on `open` in `./types.ts`. A JSDoc block here would be appended
92
+ // to that description by `propsgen`, not replace it.
91
93
  const open = defineModel<boolean>('open', { default: false })
94
+ // Optional outside-in control of the search box. Unbound, `defineModel` keeps
95
+ // the value local, so `v-model:query` is never required. Documented on `query`
96
+ // in `./types.ts`.
92
97
  const query = defineModel<string>('query', { default: '' })
93
98
  // Bound, the query is the consumer's — the component never resets it on its
94
99
  // own (see `skipInitialDisplaySync` and the open watcher below).
@@ -11,7 +11,7 @@ const fields: Field[] = [
11
11
  label: 'colour',
12
12
  options: ['gray', 'blue', 'green', 'red'],
13
13
  },
14
- { key: 'size', label: 'size', options: ['sm', 'md', 'lg', 'xl'] },
14
+ { key: 'size', label: 'size', options: ['xs', 'sm', 'md', 'lg'] },
15
15
  { key: 'style', label: 'style', options: ['subtle', 'outline', 'ghost'] },
16
16
  {
17
17
  key: 'fontSize',
@@ -41,10 +41,10 @@ const colourSwatch: Record<string, string> = {
41
41
  }
42
42
 
43
43
  const sizeDot: Record<string, string> = {
44
- sm: 'size-1.5',
45
- md: 'size-2',
46
- lg: 'size-2.5',
47
- xl: 'size-3',
44
+ xs: 'size-1.5',
45
+ sm: 'size-2',
46
+ md: 'size-2.5',
47
+ lg: 'size-3',
48
48
  }
49
49
 
50
50
  const fontSizePx: Record<string, number> = {
@@ -1,9 +1,16 @@
1
1
  import type { Component, VNodeChild } from 'vue'
2
+ import type { InputSize, InputVariant } from '../../composables/inputTypes'
2
3
  import type { InputLabelingProps } from '../../composables/useInputLabeling'
3
4
  import type { PortalTarget } from '../../composables/usePortalTarget'
4
5
 
5
- export type ComboboxVariant = 'subtle' | 'outline' | 'ghost'
6
- export type ComboboxSize = 'sm' | 'md' | 'lg' | 'xl'
6
+ /**
7
+ * Combobox renders the same trigger geometry and surfaces as every other text
8
+ * input, so these are the input scales under the component's own names. They
9
+ * stay aliases, not copies — a Combobox-only scale would have to come with a
10
+ * Combobox-only renderer.
11
+ */
12
+ export type ComboboxVariant = InputVariant
13
+ export type ComboboxSize = InputSize
7
14
 
8
15
  import type { PopoverSide, PopoverAlign } from '../shared/selection/types'
9
16
  export type { PopoverSide, PopoverAlign }
@@ -312,12 +319,6 @@ export interface ComboboxEmits {
312
319
  /** Fired when the committed value changes. */
313
320
  'update:modelValue': [value: ComboboxOptionValue | null]
314
321
 
315
- /** Fired when the open state changes. */
316
- 'update:open': [value: boolean]
317
-
318
- /** Fired when the query changes. */
319
- 'update:query': [value: string]
320
-
321
322
  /** Fired when the resolved selected option changes. */
322
323
  'update:selectedOption': [
323
324
  option: ComboboxSelectableOption | ComboboxCustomOption | null,
@@ -2,6 +2,7 @@ export { default as ContextMenu } from './ContextMenu.vue'
2
2
  export type {
3
3
  ContextMenuActionOption,
4
4
  ContextMenuBaseOption,
5
+ ContextMenuEmits,
5
6
  ContextMenuGroupOption,
6
7
  ContextMenuGroupSlotProps,
7
8
  ContextMenuItem,
@@ -30,6 +30,22 @@ export interface ContextMenuProps {
30
30
  open?: boolean
31
31
  }
32
32
 
33
+ // Exported for consumers to import, and deliberately **not** passed to
34
+ // `defineEmits`. `update:open` is declared by `defineModel('open')` in the SFC;
35
+ // declaring it a second time through `defineEmits` makes Vue's generated
36
+ // `__VLS_ModelEmit & __VLS_Emit` an intersection of two tuples, which collapses
37
+ // `$emit` and every listener to `(...args: unknown[]) => any` — the defect
38
+ // #1098 removed from `Combobox` and `MultiSelect`.
39
+ //
40
+ // Those two are wired because they carry events no model declares (`focus`,
41
+ // `blur`, `update:selectedOption`); this component has none, so `defineEmits`
42
+ // would add nothing and cost the typed listener. `DropdownEmits`, `SelectEmits`,
43
+ // `SettingsDialogEmits`, `RatingEmits`, `DurationEmits` and `RadioGroupEmits`
44
+ // are unwired for the same reason.
45
+ export interface ContextMenuEmits {
46
+ 'update:open': [open: boolean]
47
+ }
48
+
33
49
  export type ContextMenuSlots = Omit<MenuSlots, 'default' | 'trigger'> & {
34
50
  /** The right-clickable region that opens the menu. */
35
51
  default?: (props: ContextMenuTriggerSlotProps) => any
@@ -17,7 +17,7 @@
17
17
  const slotsData = [
18
18
  {
19
19
  name: 'rail',
20
- description: 'The icon column — usually a `Rail`. Omit it on shells that don\'t use one.',
20
+ description: 'The icon column — usually a `SidebarRail`. Omit it on shells that don\'t use one.',
21
21
  type: 'any'
22
22
  },
23
23
  {
@@ -1,10 +1,11 @@
1
1
  # DesktopShell
2
2
 
3
- The desktop app frame. `DesktopShell` arranges an app's `Rail` and `Sidebar`
4
- alongside the main content, and owns the skeleton the content needs: a pinned
5
- `PageHeaderTarget` and a registered scroll region. Its mobile counterpart is a
6
- separate family — [`MobileShell`](/docs/components/mobileshell) — because the
7
- two are different navigation models, not one responsive component.
3
+ The desktop app frame. `DesktopShell` arranges an app's `SidebarRail` and
4
+ `Sidebar` alongside the main content, and owns the skeleton the content needs:
5
+ a pinned `PageHeaderTarget` and a registered scroll region. Its mobile
6
+ counterpart is a separate family —
7
+ [`MobileShell`](/docs/components/mobileshell) — because the two are different
8
+ navigation models, not one responsive component.
8
9
 
9
10
  <ComponentPreview name="DesktopShell-Default" />
10
11
 
@@ -55,7 +55,7 @@ withDefaults(
55
55
  )
56
56
 
57
57
  defineSlots<{
58
- /** The icon column — usually a `Rail`. Omit it on shells that don't use one. */
58
+ /** The icon column — usually a `SidebarRail`. Omit it on shells that don't use one. */
59
59
  rail?: () => any
60
60
  /** The navigation panel — usually a `Sidebar`. Render it conditionally to hide it on routes that don't need it. */
61
61
  sidebar?: () => any
@@ -2,8 +2,8 @@
2
2
  import { ref } from 'vue'
3
3
  import {
4
4
  DesktopShell,
5
- Rail,
6
- RailItem,
5
+ SidebarRail,
6
+ SidebarRailItem,
7
7
  Sidebar,
8
8
  SidebarItem,
9
9
  SidebarLabel,
@@ -65,8 +65,8 @@ const posts = (
65
65
  >
66
66
  <DesktopShell>
67
67
  <template #rail>
68
- <Rail class="border-r">
69
- <RailItem
68
+ <SidebarRail class="border-r">
69
+ <SidebarRailItem
70
70
  label="Home"
71
71
  variant="ghost"
72
72
  icon="lucide-house"
@@ -74,7 +74,7 @@ const posts = (
74
74
  @click="community = ''"
75
75
  />
76
76
  <div class="flex w-full flex-1 flex-col items-center gap-3 pt-3">
77
- <RailItem
77
+ <SidebarRailItem
78
78
  v-for="c in communities"
79
79
  :key="c.id"
80
80
  :label="c.id"
@@ -84,10 +84,10 @@ const posts = (
84
84
  <span class="text-2xs-medium uppercase text-ink-gray-5">{{
85
85
  c.initials
86
86
  }}</span>
87
- </RailItem>
87
+ </SidebarRailItem>
88
88
  </div>
89
- <RailItem label="Search" variant="ghost" icon="lucide-search" />
90
- </Rail>
89
+ <SidebarRailItem label="Search" variant="ghost" icon="lucide-search" />
90
+ </SidebarRail>
91
91
  </template>
92
92
 
93
93
  <template #sidebar>
@@ -7,27 +7,27 @@
7
7
  const propsData = [
8
8
  {
9
9
  name: 'orientation',
10
- description: '',
10
+ description: 'Which way the rule runs.',
11
11
  required: false,
12
12
  type: '"horizontal" | "vertical"',
13
13
  default: '"horizontal"'
14
14
  },
15
15
  {
16
16
  name: 'position',
17
- description: '',
17
+ description: 'Where the action sits along the rule. Ignored without an `action`, since a\nplain rule has nothing to place.',
18
18
  required: false,
19
19
  type: '"start" | "center" | "end"',
20
20
  default: '"center"'
21
21
  },
22
22
  {
23
23
  name: 'flexItem',
24
- description: '',
24
+ description: 'Stretches the divider to the cross-axis size of its flex parent\n(`align-self: stretch`) instead of taking the full height of its own box.\nUse it for a vertical divider between flex items of differing heights.',
25
25
  required: false,
26
26
  type: 'boolean'
27
27
  },
28
28
  {
29
29
  name: 'action',
30
- description: '',
30
+ description: 'Renders a button on the rule instead of a bare `<hr>`. The rule is drawn\nbehind the button and runs the full length of the divider.',
31
31
  required: false,
32
32
  type: 'DividerAction'
33
33
  }
@@ -1,2 +1,2 @@
1
1
  export { default as Divider } from './Divider.vue'
2
- export type { DividerProps } from './types'
2
+ export type { DividerAction, DividerProps } from './types'
@@ -1,12 +1,35 @@
1
- interface DividerAction {
1
+ /** The inline action a `Divider` can host on its rule. */
2
+ export interface DividerAction {
3
+ /** Text of the action button. */
2
4
  label: string
5
+
6
+ /** Called when the action button is clicked. */
3
7
  onClick?: () => any
8
+
9
+ /** Shows a spinner in the action button and blocks further clicks. */
4
10
  loading?: boolean
5
11
  }
6
12
 
7
13
  export interface DividerProps {
14
+ /** Which way the rule runs. */
8
15
  orientation?: 'horizontal' | 'vertical'
16
+
17
+ /**
18
+ * Where the action sits along the rule. Ignored without an `action`, since a
19
+ * plain rule has nothing to place.
20
+ */
9
21
  position?: 'start' | 'center' | 'end'
22
+
23
+ /**
24
+ * Stretches the divider to the cross-axis size of its flex parent
25
+ * (`align-self: stretch`) instead of taking the full height of its own box.
26
+ * Use it for a vertical divider between flex items of differing heights.
27
+ */
10
28
  flexItem?: boolean
29
+
30
+ /**
31
+ * Renders a button on the rule instead of a bare `<hr>`. The rule is drawn
32
+ * behind the button and runs the full length of the divider.
33
+ */
11
34
  action?: DividerAction
12
35
  }
@@ -32,10 +32,10 @@ const knobs: Knob[] = [
32
32
  type: 'tabs',
33
33
  default: 'sm',
34
34
  options: [
35
+ { label: 'xs', value: 'xs' },
35
36
  { label: 'sm', value: 'sm' },
36
37
  { label: 'md', value: 'md' },
37
38
  { label: 'lg', value: 'lg' },
38
- { label: 'xl', value: 'xl' },
39
39
  ],
40
40
  },
41
41
  {
@@ -7,9 +7,9 @@ const value = ref<number | null>(5445)
7
7
 
8
8
  <template>
9
9
  <div class="flex flex-col gap-4 items-start">
10
+ <Duration v-model="value" size="xs" />
10
11
  <Duration v-model="value" size="sm" />
11
12
  <Duration v-model="value" size="md" />
12
13
  <Duration v-model="value" size="lg" />
13
- <Duration v-model="value" size="xl" />
14
14
  </div>
15
15
  </template>
@@ -32,9 +32,9 @@
32
32
  },
33
33
  {
34
34
  name: 'size',
35
- description: 'Size of the input',
35
+ description: 'Size of the input. The full input scale — `xs` 24px, `sm` 28px, `md` 32px,\n`lg` 40px. `type="checkbox"` renders on the narrower toggle scale, so `lg`\nis clamped to that scale\'s largest value there.',
36
36
  required: false,
37
- type: '"sm" | "md"',
37
+ type: 'InputSize',
38
38
  default: '"sm"'
39
39
  },
40
40
  {
@@ -247,4 +247,44 @@ describe('FormControl', () => {
247
247
  })
248
248
  })
249
249
  })
250
+ describe('size scale', () => {
251
+ const heights = { xs: 24, sm: 28, md: 32, lg: 40 } as const
252
+
253
+ for (const [size, height] of Object.entries(heights)) {
254
+ it(`forwards size="${size}" to a text input as ${height}px`, () => {
255
+ cy.mount(FormControl, { props: { type: 'text', size, label: 'Email' } })
256
+ cy.get('input')
257
+ .should('have.attr', 'data-size', size)
258
+ .and('have.css', 'height', `${height}px`)
259
+ cy.get('label').should('have.css', 'font-size', '13px')
260
+ })
261
+
262
+ it(`forwards size="${size}" to a textarea`, () => {
263
+ cy.mount(FormControl, {
264
+ props: { type: 'textarea', size, label: 'Notes' },
265
+ })
266
+ cy.get('textarea')
267
+ .should('have.attr', 'data-size', size)
268
+ .and('have.css', 'font-size', '13px')
269
+ })
270
+ }
271
+
272
+ it('clamps lg to md for a checkbox, which has no lg', () => {
273
+ // Checkbox renders on `ToggleSize` (`xs | sm | md`). Forwarding `lg`
274
+ // would fall off the end of its size chain and draw an `xs` control.
275
+ cy.mount(FormControl, {
276
+ props: { type: 'checkbox', size: 'lg', label: 'Subscribe' },
277
+ })
278
+ cy.get('input[type="checkbox"]').should('have.attr', 'data-size', 'md')
279
+ })
280
+
281
+ it('forwards the narrower sizes to a checkbox unchanged', () => {
282
+ for (const size of ['xs', 'sm', 'md'] as const) {
283
+ cy.mount(FormControl, {
284
+ props: { type: 'checkbox', size, label: 'Subscribe' },
285
+ })
286
+ cy.get('input[type="checkbox"]').should('have.attr', 'data-size', size)
287
+ }
288
+ })
289
+ })
250
290
  })
@@ -32,8 +32,10 @@ const knobs: Knob[] = [
32
32
  type: 'tabs',
33
33
  default: 'sm',
34
34
  options: [
35
+ { label: 'xs', value: 'xs' },
35
36
  { label: 'sm', value: 'sm' },
36
37
  { label: 'md', value: 'md' },
38
+ { label: 'lg', value: 'lg' },
37
39
  ],
38
40
  },
39
41
  {
@@ -105,7 +105,12 @@ const forwardedAttrs = computed(() => {
105
105
  if (key !== 'class' && key !== 'style') out[key] = attrs[key]
106
106
  }
107
107
 
108
- out.size = props.size
108
+ // Checkbox is a toggle, not a text input: it renders on `ToggleSize`
109
+ // (`xs | sm | md`), which has no `lg`. Clamp rather than forward a value the
110
+ // control cannot draw — `FormControl` advertises the whole input scale, so
111
+ // `size="lg"` has to mean something for every type it dispatches to.
112
+ out.size =
113
+ props.type === 'checkbox' && props.size === 'lg' ? 'md' : props.size
109
114
  out.variant = props.variant
110
115
 
111
116
  if (props.label !== undefined) out.label = props.label
@@ -1,4 +1,5 @@
1
1
  import type { TextInputTypes } from '../types/TextInput'
2
+ import type { InputSize } from '../../composables/inputTypes'
2
3
  import type { FrappeUIError } from '../../composables/useInputLabeling'
3
4
 
4
5
  export interface FormControlProps {
@@ -29,8 +30,12 @@ export interface FormControlProps {
29
30
  | 'daterange'
30
31
  | 'datetime'
31
32
  | 'time'
32
- /** Size of the input */
33
- size?: 'sm' | 'md'
33
+ /**
34
+ * Size of the input. The full input scale — `xs` 24px, `sm` 28px, `md` 32px,
35
+ * `lg` 40px. `type="checkbox"` renders on the narrower toggle scale, so `lg`
36
+ * is clamped to that scale's largest value there.
37
+ */
38
+ size?: InputSize
34
39
  /** Visual variant of the input */
35
40
  variant?: 'subtle' | 'outline'
36
41
  /** Whether the input is required */
@@ -11,13 +11,6 @@
11
11
  required: true,
12
12
  type: 'string'
13
13
  },
14
- {
15
- name: 'size',
16
- description: 'Size of the label text.',
17
- required: false,
18
- type: '"sm" | "md"',
19
- default: '"sm"'
20
- },
21
14
  {
22
15
  name: 'id',
23
16
  description: 'id of the form control this label is for; wired to the `for` attribute.',