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

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 (126) 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/charts/areaChartOptions.test.ts +2 -3
  11. package/src/charts/barChartOptions.test.ts +8 -8
  12. package/src/charts/chartColors.test.ts +35 -15
  13. package/src/charts/comboChartOptions.test.ts +1 -1
  14. package/src/charts/donutChartOptions.test.ts +1 -1
  15. package/src/charts/heatmapOptions.test.ts +7 -13
  16. package/src/charts/lineChartOptions.test.ts +1 -1
  17. package/src/charts/style.css +8 -5
  18. package/src/charts/tokens.ts +51 -31
  19. package/src/components/Checkbox/Checkbox.cy.ts +3 -3
  20. package/src/components/Combobox/Combobox.api.md +2 -2
  21. package/src/components/Combobox/Combobox.playground.vue +1 -1
  22. package/src/components/Combobox/Combobox.vue +5 -0
  23. package/src/components/Combobox/stories/Clearable.vue +5 -5
  24. package/src/components/Combobox/types.ts +9 -8
  25. package/src/components/ContextMenu/index.ts +1 -0
  26. package/src/components/ContextMenu/types.ts +16 -0
  27. package/src/components/DesktopShell/DesktopShell.api.md +1 -1
  28. package/src/components/DesktopShell/DesktopShell.md +6 -5
  29. package/src/components/DesktopShell/DesktopShell.vue +1 -1
  30. package/src/components/DesktopShell/stories/Default.vue +8 -8
  31. package/src/components/Divider/Divider.api.md +4 -4
  32. package/src/components/Divider/index.ts +1 -1
  33. package/src/components/Divider/types.ts +24 -1
  34. package/src/components/Duration/Duration.playground.vue +1 -1
  35. package/src/components/Duration/stories/Sizes.vue +1 -1
  36. package/src/components/FormControl/FormControl.api.md +2 -2
  37. package/src/components/FormControl/FormControl.cy.ts +40 -0
  38. package/src/components/FormControl/FormControl.playground.vue +2 -0
  39. package/src/components/FormControl/FormControl.vue +6 -1
  40. package/src/components/FormControl/types.ts +7 -2
  41. package/src/components/FormLabel/FormLabel.api.md +0 -7
  42. package/src/components/FormLabel/FormLabel.cy.ts +24 -5
  43. package/src/components/FormLabel/FormLabel.playground.vue +1 -11
  44. package/src/components/FormLabel/FormLabel.vue +7 -12
  45. package/src/components/FormLabel/types.ts +0 -3
  46. package/src/components/HoverCard/HoverCard.api.md +1 -1
  47. package/src/components/HoverCard/types.ts +3 -0
  48. package/src/components/InputLabeling/InputDescription.vue +1 -1
  49. package/src/components/InputLabeling/InputLabel.vue +9 -4
  50. package/src/components/ItemListRow/ItemListRow.api.md +1 -1
  51. package/src/components/ItemListRow/ItemListRow.vue +13 -7
  52. package/src/components/ItemListRow/types.ts +7 -1
  53. package/src/components/KeyboardShortcutsDialog/KeyboardShortcutsDialog.api.md +1 -1
  54. package/src/components/KeyboardShortcutsDialog/types.ts +3 -0
  55. package/src/components/MultiSelect/MultiSelect.api.md +3 -3
  56. package/src/components/MultiSelect/MultiSelect.playground.vue +1 -1
  57. package/src/components/MultiSelect/MultiSelect.vue +5 -2
  58. package/src/components/MultiSelect/types.ts +4 -8
  59. package/src/components/Password/Password.playground.vue +1 -1
  60. package/src/components/Password/stories/Sizes.vue +1 -1
  61. package/src/components/Rating/Rating.cy.ts +1 -1
  62. package/src/components/Rating/Rating.playground.vue +1 -1
  63. package/src/components/Rating/Rating.vue +14 -8
  64. package/src/components/Rating/stories/Sizes.vue +1 -1
  65. package/src/components/Select/Select.api.md +2 -2
  66. package/src/components/Select/Select.cy.ts +19 -1
  67. package/src/components/Select/Select.playground.vue +1 -1
  68. package/src/components/Select/types.ts +3 -2
  69. package/src/components/Select/utils.ts +12 -6
  70. package/src/components/{Rail/Rail.api.md → SidebarRail/SidebarRail.api.md} +11 -11
  71. package/src/components/{Rail/Rail.cy.ts → SidebarRail/SidebarRail.cy.ts} +31 -31
  72. package/src/components/SidebarRail/SidebarRail.md +36 -0
  73. package/src/components/{Rail/Rail.vue → SidebarRail/SidebarRail.vue} +2 -2
  74. package/src/components/{Rail/RailItem.vue → SidebarRail/SidebarRailItem.vue} +12 -12
  75. package/src/components/{Rail/RailItemBadge.vue → SidebarRail/SidebarRailItemBadge.vue} +1 -1
  76. package/src/components/SidebarRail/index.ts +4 -0
  77. package/src/components/{Rail → SidebarRail}/stories/Default.vue +10 -10
  78. package/src/components/{Rail → SidebarRail}/types.ts +1 -1
  79. package/src/components/Switch/Switch.cy.ts +2 -2
  80. package/src/components/TabButtons/TabButtons.api.md +4 -4
  81. package/src/components/TabButtons/TabButtons.vue +3 -0
  82. package/src/components/TabButtons/index.ts +1 -0
  83. package/src/components/TabButtons/types.ts +7 -0
  84. package/src/components/TextInput/TextInput.cy.ts +108 -4
  85. package/src/components/TextInput/TextInput.playground.vue +1 -1
  86. package/src/components/TextInput/TextInput.vue +60 -26
  87. package/src/components/TextInput/stories/Sizes.vue +1 -1
  88. package/src/components/Textarea/Textarea.api.md +1 -1
  89. package/src/components/Textarea/Textarea.cy.ts +65 -34
  90. package/src/components/Textarea/Textarea.playground.vue +1 -1
  91. package/src/components/Textarea/Textarea.vue +39 -14
  92. package/src/components/Textarea/stories/Sizes.vue +1 -1
  93. package/src/components/Textarea/types.ts +5 -1
  94. package/src/components/TimePicker/stories/SizesAndVariants.vue +1 -1
  95. package/src/components/Tree/Tree.vue +2 -6
  96. package/src/components/Tree/types.ts +8 -0
  97. package/src/components/shared/picker/PickerShell.vue +34 -37
  98. package/src/components/shared/selection/utils.test.ts +54 -2
  99. package/src/components/shared/selection/utils.ts +64 -21
  100. package/src/components/shared/slotReactivity.cy.ts +36 -0
  101. package/src/components/shared/tabs/Pill.vue +2 -3
  102. package/src/components/shared/tabs/styles.ts +1 -1
  103. package/src/composables/inputTypes.ts +14 -2
  104. package/src/composables/usePortalTarget.spec.ts +6 -5
  105. package/src/index.ts +12 -1
  106. package/src/molecules/editor/components/MediaNodeView.test.ts +30 -0
  107. package/src/molecules/editor/components/MediaNodeView.vue +13 -8
  108. package/src/molecules/editor/components/VideoControls.test.ts +118 -2
  109. package/src/molecules/editor/components/VideoControls.vue +89 -11
  110. package/src/molecules/list/List.cy.ts +426 -0
  111. package/src/molecules/list/List.ssr.test.ts +59 -0
  112. package/src/molecules/list/List.vue +92 -7
  113. package/src/molecules/list/index.ts +2 -0
  114. package/src/molecules/list/list-context.ts +2 -1
  115. package/src/molecules/list/list.api.md +3 -3
  116. package/src/molecules/list/list.md +152 -33
  117. package/src/molecules/list/stories/Columns.vue +15 -9
  118. package/src/molecules/list/stories/Responsive.vue +56 -0
  119. package/src/molecules/list/stories/RowActions.vue +123 -0
  120. package/src/molecules/list/style.css +67 -26
  121. package/src/molecules/list/types.ts +51 -9
  122. package/tailwind/listColumns.js +191 -0
  123. package/tailwind/listColumns.test.js +358 -0
  124. package/tailwind/plugin.js +5 -6
  125. package/src/components/Rail/Rail.md +0 -30
  126. 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.64",
4
4
  "description": "A set of components and utilities for rapid UI development",
5
5
  "engines": {
6
6
  "node": ">=20.19.0"
@@ -9,7 +9,7 @@ import type { AxisChartConfig } from './types'
9
9
 
10
10
  const tokens: ChartTokens = {
11
11
  categorical: ['#111111', '#222222', '#333333'],
12
- sequential: ['#000011', '#000022', '#000033', '#000044', '#000055'],
12
+ sequential: ['#000011', '#000022', '#000033'],
13
13
  diverging: ['#001100', '#002200', '#003300'],
14
14
  axisLabel: 'ink-5',
15
15
  axisTitle: 'ink-7',
@@ -169,9 +169,8 @@ describe('area chart option', () => {
169
169
  build({
170
170
  fillOpacity: 0.5,
171
171
  series: [{ name: 'sales', fillOpacity: 0.25 }],
172
- // A lone series takes the ramp's mid stop, hence the shifted blue.
173
172
  }).series[0].areaStyle.color.colorStops[0].color,
174
- ).toBe('rgba(0, 0, 34, 0.25)')
173
+ ).toBe('rgba(0, 0, 17, 0.25)')
175
174
  })
176
175
 
177
176
  it('falls back to a flat fill for a color it cannot add alpha to', () => {
@@ -7,8 +7,8 @@ import type { AxisChartConfig, AxisChartSeriesConfig } from './types'
7
7
 
8
8
  const tokens: ChartTokens = {
9
9
  categorical: ['#111111', '#222222', '#333333'],
10
- // Five stops so the sequential pale-tail trim and even spacing are visible.
11
- sequential: ['#000011', '#000022', '#000033', '#000044', '#000055'],
10
+ // Three stops so even spacing across the ramp is visible.
11
+ sequential: ['#000011', '#000022', '#000033'],
12
12
  diverging: ['#001100', '#002200', '#003300'],
13
13
  axisLabel: 'ink-5',
14
14
  axisTitle: 'ink-7',
@@ -70,13 +70,13 @@ describe('resolveSeriesColors', () => {
70
70
  ).toEqual({ sales: '#000011', refunds: 'red' })
71
71
  })
72
72
 
73
- it('gives a lone sequential series a mid stop, not the darkest', () => {
73
+ it('gives a lone sequential series the darkest stop', () => {
74
74
  expect(colorsFor({ series: [{ name: 'sales' }] })).toEqual({
75
- sales: '#000022',
75
+ sales: '#000011',
76
76
  })
77
77
  })
78
78
 
79
- it('spaces sequential series out dark to light, skipping the palest stops', () => {
79
+ it('spaces sequential series out dark to light', () => {
80
80
  expect(Object.values(colorsFor({ series: named(3) }))).toEqual([
81
81
  '#000011',
82
82
  '#000022',
@@ -84,10 +84,10 @@ describe('resolveSeriesColors', () => {
84
84
  ])
85
85
  })
86
86
 
87
- it('cycles the sequential ramp once there are more series than usable stops', () => {
88
- const colors = colorsFor({ series: named(6) })
87
+ it('cycles the sequential ramp once there are more series than stops', () => {
88
+ const colors = colorsFor({ series: named(4) })
89
89
  expect(colors.a).toBe('#000011')
90
- expect(colors.f).toBe(colors.a)
90
+ expect(colors.d).toBe(colors.a)
91
91
  })
92
92
 
93
93
  it('cycles the categorical ramp in series order', () => {
@@ -25,9 +25,6 @@ const tokens: ChartTokens = {
25
25
  backdrop: '#ffffff',
26
26
  }
27
27
 
28
- /** The sequential ramp minus the two palest stops, as every chart reads it. */
29
- const USABLE_SEQUENTIAL = tokens.sequential.slice(0, 7)
30
-
31
28
  describe('chartColors: precedence', () => {
32
29
  it("draws in the caller's own colors when they passed a list", () => {
33
30
  expect(
@@ -56,7 +53,7 @@ describe('chartColors: precedence', () => {
56
53
  ).toEqual(tokens.categorical.slice(0, 2))
57
54
  expect(
58
55
  chartColors([], tokens, { fallback: 'sequential', count: 'ramp' }),
59
- ).toEqual(USABLE_SEQUENTIAL)
56
+ ).toEqual(tokens.sequential)
60
57
  })
61
58
  })
62
59
 
@@ -90,6 +87,38 @@ describe('chartColors: one color per thing drawn', () => {
90
87
  })
91
88
  })
92
89
 
90
+ describe('chartColors: how a sequential ramp is spent', () => {
91
+ const stops = (count: number) =>
92
+ chartColors('sequential', tokens, { fallback: 'sequential', count }).map(
93
+ (color) => tokens.sequential.indexOf(color) + 1,
94
+ )
95
+
96
+ it('gives a lone series the deep end', () => {
97
+ expect(stops(1)).toEqual([1])
98
+ })
99
+
100
+ it('keeps a small chart a prefix of the next size up', () => {
101
+ expect(stops(2)).toEqual([1, 4])
102
+ expect(stops(3)).toEqual([1, 4, 7])
103
+ })
104
+
105
+ it('stays inside the first seven stops up to seven series', () => {
106
+ expect(stops(4)).toEqual([1, 3, 5, 7])
107
+ expect(stops(5)).toEqual([1, 3, 4, 6, 7])
108
+ expect(stops(6)).toEqual([1, 2, 3, 5, 6, 7])
109
+ expect(stops(7)).toEqual([1, 2, 3, 4, 5, 6, 7])
110
+ })
111
+
112
+ it('reaches the pale stops only once the series outnumber the span', () => {
113
+ expect(stops(8)).toEqual([1, 2, 3, 4, 5, 6, 7, 8])
114
+ expect(stops(9)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9])
115
+ })
116
+
117
+ it('cycles once there are more series than stops', () => {
118
+ expect(stops(10)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 1])
119
+ })
120
+ })
121
+
93
122
  describe('chartColors: the ramp itself', () => {
94
123
  it('gives the stops rather than a slot each', () => {
95
124
  expect(
@@ -97,16 +126,7 @@ describe('chartColors: the ramp itself', () => {
97
126
  fallback: 'sequential',
98
127
  count: 'ramp',
99
128
  }),
100
- ).toEqual(USABLE_SEQUENTIAL)
101
- })
102
-
103
- it('trims the sequential stops that vanish against a card', () => {
104
- const ramp = chartColors(undefined, tokens, {
105
- fallback: 'sequential',
106
- count: 'ramp',
107
- })
108
- expect(ramp).not.toContain('#f5f5f5')
109
- expect(ramp).not.toContain('#e0e0e0')
129
+ ).toEqual(tokens.sequential)
110
130
  })
111
131
 
112
132
  it('takes a diverging ramp end to end', () => {
@@ -151,7 +171,7 @@ describe('chartColors: which end of the ramp leads', () => {
151
171
  count: 'ramp',
152
172
  deepEnd: 'last',
153
173
  }),
154
- ).toEqual([...USABLE_SEQUENTIAL].reverse())
174
+ ).toEqual([...tokens.sequential].reverse())
155
175
  })
156
176
 
157
177
  it('leaves a categorical set alone, having no order to reverse', () => {
@@ -9,7 +9,7 @@ import type { AxisChartConfig, ChartMark } from './types'
9
9
 
10
10
  const tokens: ChartTokens = {
11
11
  categorical: ['#111111', '#222222', '#333333'],
12
- sequential: ['#000011', '#000022', '#000033', '#000044', '#000055'],
12
+ sequential: ['#000011', '#000022', '#000033'],
13
13
  diverging: ['#001100', '#002200', '#003300'],
14
14
  axisLabel: 'ink-5',
15
15
  axisTitle: 'ink-7',
@@ -6,7 +6,7 @@ import type { DonutChartConfig } from './types'
6
6
 
7
7
  const tokens: ChartTokens = {
8
8
  categorical: ['#111111', '#222222', '#333333'],
9
- sequential: ['#000011', '#000022', '#000033', '#000044', '#000055'],
9
+ sequential: ['#000011', '#000022', '#000033'],
10
10
  diverging: ['#001100', '#002200', '#003300'],
11
11
  axisLabel: 'ink-5',
12
12
  axisTitle: 'ink-7',
@@ -203,18 +203,10 @@ describe('buildHeatmapMatrix', () => {
203
203
  })
204
204
 
205
205
  describe('heatmapRampStops', () => {
206
- it('reverses the sequential ramp and trims its palest stops', () => {
207
- const stops = heatmapRampStops(config(), tokens)
208
-
209
- expect(stops).toEqual([
210
- '#cccccc',
211
- '#aaaaaa',
212
- '#8a8a8a',
213
- '#5a5a5a',
214
- '#3a3a3a',
215
- '#1a1a1a',
216
- '#0a0a0a',
217
- ])
206
+ it('reverses the sequential ramp, palest first', () => {
207
+ expect(heatmapRampStops(config(), tokens)).toEqual(
208
+ [...tokens.sequential].reverse(),
209
+ )
218
210
  })
219
211
 
220
212
  it('takes the diverging ramp as authored, cool end first', () => {
@@ -347,7 +339,9 @@ describe('buildHeatmapOption', () => {
347
339
  const resting = cells[index].color
348
340
  const hovered = item.emphasis.itemStyle.color
349
341
  expect(hovered).toBe(hoverCellColor(resting))
350
- expect(hexToOklch(hovered).l).toBeGreaterThan(hexToOklch(resting).l)
342
+ expect(hexToOklch(hovered).l).toBeGreaterThanOrEqual(
343
+ hexToOklch(resting).l,
344
+ )
351
345
  })
352
346
  })
353
347
  })
@@ -5,7 +5,7 @@ import type { AxisChartConfig } from './types'
5
5
 
6
6
  const tokens: ChartTokens = {
7
7
  categorical: ['#111111', '#222222', '#333333'],
8
- sequential: ['#000011', '#000022', '#000033', '#000044', '#000055'],
8
+ sequential: ['#000011', '#000022', '#000033'],
9
9
  diverging: ['#001100', '#002200', '#003300'],
10
10
  axisLabel: 'ink-5',
11
11
  axisTitle: 'ink-7',
@@ -25,7 +25,10 @@
25
25
  The red family sits at hue 17. It was authored at 5, a crimson, whose light
26
26
  partner lightens into a pink rather than into a warning — so anything
27
27
  reading the ramp for a status had to leave it. 17 keeps the family clear of
28
- the amber at 75 while its light partner still reads as red. */
28
+ the amber at 75 while its light partner still reads as red. Its chroma
29
+ sits at 0.155, under the cap, because the eye weights red above every
30
+ other hue and at the cap a slice of it reads as an alert. Lower still and
31
+ it turns brown. */
29
32
  --chart-categorical-1: #2283c3;
30
33
  --chart-categorical-2: #84c5f9;
31
34
  --chart-categorical-3: #289e60;
@@ -34,8 +37,8 @@
34
37
  --chart-categorical-6: #bb9df1;
35
38
  --chart-categorical-7: #c98c28;
36
39
  --chart-categorical-8: #f5ca8e;
37
- --chart-categorical-9: #bd2040;
38
- --chart-categorical-10: #fc8e94;
40
+ --chart-categorical-9: #c54b58;
41
+ --chart-categorical-10: #fca0a4;
39
42
 
40
43
  --chart-sequential-1: #095895;
41
44
  --chart-sequential-2: #2283c3;
@@ -95,8 +98,8 @@
95
98
  --chart-categorical-6: #b294e7;
96
99
  --chart-categorical-7: #bf8319;
97
100
  --chart-categorical-8: #ebc085;
98
- --chart-categorical-9: #b20e37;
99
- --chart-categorical-10: #f2858b;
101
+ --chart-categorical-9: #ba4250;
102
+ --chart-categorical-10: #f2979b;
100
103
 
101
104
  --chart-sequential-1: #074677;
102
105
  --chart-sequential-2: #1b699c;
@@ -56,8 +56,8 @@ const LIGHT_CATEGORICAL = [
56
56
  '#bb9df1',
57
57
  '#c98c28',
58
58
  '#f5ca8e',
59
- '#bd2040',
60
- '#fc8e94',
59
+ '#c54b58',
60
+ '#fca0a4',
61
61
  ]
62
62
 
63
63
  const LIGHT_SEQUENTIAL = [
@@ -95,8 +95,8 @@ const DARK_CATEGORICAL = [
95
95
  '#b294e7',
96
96
  '#bf8319',
97
97
  '#ebc085',
98
- '#b20e37',
99
- '#f2858b',
98
+ '#ba4250',
99
+ '#f2979b',
100
100
  ]
101
101
 
102
102
  const DARK_SEQUENTIAL = [
@@ -277,10 +277,6 @@ export function pickSeriesColor(ramp: string[], index: number) {
277
277
  return ramp[index % ramp.length]
278
278
  }
279
279
 
280
- /** The two palest sequential stops vanish against a white card. */
281
- const SEQUENTIAL_TAIL_TRIM = 2
282
- /** A lone series reads best as one confident mid-blue, not the ramp's dark end. */
283
- const SEQUENTIAL_SOLO_INDEX = 1
284
280
  /**
285
281
  * Below this relative luminance a fill needs white text on top of it. It is the
286
282
  * crossover, not a taste call: white and the near-black inside-label ink
@@ -290,23 +286,57 @@ const SEQUENTIAL_SOLO_INDEX = 1
290
286
  const DARK_FILL_LUMINANCE = 0.22
291
287
 
292
288
  /**
293
- * `count` evenly spaced stops from a continuous ramp, so a stack reads as one
294
- * progression rather than n unrelated hues. Cycles instead once there are more
295
- * series than usable stops, where even spacing would hand out duplicates.
289
+ * How much of the sequential ramp a chart spends before it must widen. The
290
+ * last two stops of a nine-stop ramp are pale enough that a series in them is
291
+ * hard to read against a card, so a chart stays inside the first seven until
292
+ * it has more series than that.
296
293
  */
297
- function rampStops(ramp: string[], count: number, lastIndex: number): string[] {
298
- if (count > lastIndex + 1) {
294
+ const SEQUENTIAL_SPAN = 7
295
+ /**
296
+ * The most stops apart two neighbouring series sit. Without it two series
297
+ * take the two ends of the span, and a third then recolors the second. With
298
+ * it a chart of one, two or three series is a prefix of the next.
299
+ */
300
+ const SEQUENTIAL_MAX_STEP = 3
301
+
302
+ /**
303
+ * `count` stops from the deep end of a sequential ramp: spread evenly over the
304
+ * first `SEQUENTIAL_SPAN` stops, never more than `SEQUENTIAL_MAX_STEP` apart,
305
+ * widening past the span only for a chart with more series than it holds.
306
+ * Cycles once there are more series than stops.
307
+ */
308
+ function sequentialStops(ramp: string[], count: number): string[] {
309
+ if (count === 1) return [ramp[0]]
310
+ if (count > ramp.length) {
299
311
  return Array.from({ length: count }, (_, i) => pickSeriesColor(ramp, i))
300
312
  }
313
+ const lastIndex = Math.min(Math.max(SEQUENTIAL_SPAN, count), ramp.length) - 1
301
314
  return Array.from(
302
315
  { length: count },
303
- (_, i) => ramp[Math.round((i * lastIndex) / (count - 1))],
316
+ (_, i) =>
317
+ ramp[
318
+ Math.min(
319
+ Math.round((i * lastIndex) / (count - 1)),
320
+ i * SEQUENTIAL_MAX_STEP,
321
+ )
322
+ ],
304
323
  )
305
324
  }
306
325
 
307
- /** The sequential ramp without the stops that vanish against a card. */
308
- function usableSequential(ramp: string[]) {
309
- return ramp.slice(0, Math.max(1, ramp.length - SEQUENTIAL_TAIL_TRIM))
326
+ /**
327
+ * `count` evenly spaced stops across the whole of a diverging ramp, which is
328
+ * read by its extremes. Cycles once there are more series than stops.
329
+ */
330
+ function divergingStops(ramp: string[], count: number): string[] {
331
+ if (count === 1) return [ramp[0]]
332
+ if (count > ramp.length) {
333
+ return Array.from({ length: count }, (_, i) => pickSeriesColor(ramp, i))
334
+ }
335
+ const lastIndex = ramp.length - 1
336
+ return Array.from(
337
+ { length: count },
338
+ (_, i) => ramp[Math.round((i * lastIndex) / (count - 1))],
339
+ )
310
340
  }
311
341
 
312
342
  /**
@@ -328,16 +358,9 @@ export function paletteColors(
328
358
 
329
359
  const ramp = name === 'diverging' ? tokens.diverging : tokens.sequential
330
360
  if (!ramp.length) return cycle(tokens.categorical)
331
-
332
- if (count === 1) {
333
- if (name === 'diverging') return [ramp[0]]
334
- return [ramp[Math.min(SEQUENTIAL_SOLO_INDEX, ramp.length - 1)]]
335
- }
336
-
337
- // A diverging ramp is read by its extremes, so it always spans end to end.
338
- const lastIndex =
339
- name === 'diverging' ? ramp.length - 1 : usableSequential(ramp).length - 1
340
- return rampStops(ramp, count, lastIndex)
361
+ return name === 'diverging'
362
+ ? divergingStops(ramp, count)
363
+ : sequentialStops(ramp, count)
341
364
  }
342
365
 
343
366
  /**
@@ -347,10 +370,7 @@ export function paletteColors(
347
370
  */
348
371
  function namedRamp(name: ChartPaletteName, tokens: ChartTokens): string[] {
349
372
  if (name === 'categorical') return tokens.categorical
350
- const ramp =
351
- name === 'diverging'
352
- ? tokens.diverging
353
- : usableSequential(tokens.sequential)
373
+ const ramp = name === 'diverging' ? tokens.diverging : tokens.sequential
354
374
  return ramp.length ? ramp : tokens.categorical
355
375
  }
356
376
 
@@ -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,