frappe-ui 1.0.0-beta.67 → 1.0.0-beta.68
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 +4 -1
- package/scripts/migrate-base-props-v1.js +379 -0
- package/src/components/Badge/Badge.api.md +1 -1
- package/src/components/Badge/types.ts +1 -5
- package/src/components/Divider/Divider.api.md +1 -1
- package/src/components/Divider/Divider.cy.ts +23 -0
- package/src/components/Divider/Divider.md +3 -0
- package/src/components/Divider/Divider.playground.vue +4 -4
- package/src/components/Divider/Divider.vue +11 -7
- package/src/components/Divider/stories/Action.vue +1 -1
- package/src/components/Divider/types.ts +4 -11
- package/src/components/Icon/Icon.api.md +7 -1
- package/src/components/Icon/Icon.cy.ts +43 -11
- package/src/components/Icon/Icon.md +11 -0
- package/src/components/Icon/Icon.vue +15 -8
- package/src/components/Icon/stories/ComponentIcon.vue +1 -1
- package/src/components/Icon/stories/Lucide.vue +4 -4
- package/src/components/Icon/types.ts +6 -0
- package/src/components/Progress/Progress.api.md +2 -10
- package/src/components/Progress/Progress.cy.ts +16 -6
- package/src/components/Progress/Progress.md +2 -2
- package/src/components/Progress/Progress.playground.vue +4 -9
- package/src/components/Progress/Progress.vue +3 -5
- package/src/components/Progress/stories/MultiStepForm.vue +1 -6
- package/src/components/Progress/types.ts +2 -5
- package/src/components/shared/action.ts +5 -5
- package/src/components/shared/tabs/Pill.vue +2 -2
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.68",
|
|
4
4
|
"description": "A set of components and utilities for rapid UI development",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20.19.0"
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"#utils/*": "./src/utils/*"
|
|
20
20
|
},
|
|
21
21
|
"bin": {
|
|
22
|
+
"base-props-v1": "./scripts/migrate-base-props-v1.js",
|
|
22
23
|
"tokens-v2": "./tailwind/migrate-tokens-v2.js",
|
|
23
24
|
"shortcuts-v1": "./scripts/migrate-shortcuts-v1.js",
|
|
24
25
|
"editor-v1": "./scripts/migrate-editor-v1.js",
|
|
@@ -104,6 +105,7 @@
|
|
|
104
105
|
"tailwind",
|
|
105
106
|
"vitepress",
|
|
106
107
|
"scripts/migrate-shortcuts-v1.js",
|
|
108
|
+
"scripts/migrate-base-props-v1.js",
|
|
107
109
|
"scripts/migrate-editor-v1.js",
|
|
108
110
|
"experimental",
|
|
109
111
|
"experimental.ts",
|
|
@@ -116,6 +118,7 @@
|
|
|
116
118
|
"author": "Frappe Technologies Pvt. Ltd.",
|
|
117
119
|
"license": "MIT",
|
|
118
120
|
"dependencies": {
|
|
121
|
+
"@vue/compiler-sfc": "^3.5.13",
|
|
119
122
|
"@codemirror/lang-css": "^6.3.0",
|
|
120
123
|
"@codemirror/lang-html": "^6.4.9",
|
|
121
124
|
"@codemirror/lang-javascript": "^6.2.2",
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import { parse as parseSfc } from '@vue/compiler-sfc'
|
|
7
|
+
|
|
8
|
+
const NodeTypes = {
|
|
9
|
+
ELEMENT: 1,
|
|
10
|
+
SIMPLE_EXPRESSION: 4,
|
|
11
|
+
ATTRIBUTE: 6,
|
|
12
|
+
DIRECTIVE: 7,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const USAGE = `Usage: base-props-v1 [--dry-run] <dir-or-file...>
|
|
16
|
+
|
|
17
|
+
Renames the v1 base-component props on statically named Vue component tags:
|
|
18
|
+
Icon name -> icon (optional canonical spelling)
|
|
19
|
+
Divider position -> align
|
|
20
|
+
Progress intervals + intervalCount -> numeric intervals`
|
|
21
|
+
|
|
22
|
+
const COMPONENTS = new Set(['Icon', 'Divider', 'Progress'])
|
|
23
|
+
|
|
24
|
+
function walk(target, visited = new Set()) {
|
|
25
|
+
if (fs.lstatSync(target).isSymbolicLink()) return []
|
|
26
|
+
const resolved = fs.realpathSync(target)
|
|
27
|
+
if (visited.has(resolved)) return []
|
|
28
|
+
visited.add(resolved)
|
|
29
|
+
const stat = fs.statSync(resolved)
|
|
30
|
+
if (stat.isFile()) return path.extname(target) === '.vue' ? [target] : []
|
|
31
|
+
return fs.readdirSync(resolved, { withFileTypes: true }).flatMap((entry) => {
|
|
32
|
+
if (entry.name === 'node_modules' || entry.name === '.git') return []
|
|
33
|
+
return walk(path.join(resolved, entry.name), visited)
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function kebabCase(name) {
|
|
38
|
+
return name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function setAlias(aliases, binding, component) {
|
|
42
|
+
aliases.set(binding, component)
|
|
43
|
+
aliases.set(kebabCase(binding), component)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function importedComponentAliases(source) {
|
|
47
|
+
const aliases = new Map()
|
|
48
|
+
for (const name of COMPONENTS) setAlias(aliases, name, name)
|
|
49
|
+
const importPattern = /import\s*\{([\s\S]*?)\}\s*from\s*['"]([^'"]+)['"]/g
|
|
50
|
+
for (const match of source.matchAll(importPattern)) {
|
|
51
|
+
for (const part of match[1].split(',')) {
|
|
52
|
+
const binding = part
|
|
53
|
+
.trim()
|
|
54
|
+
.match(/^(Icon|Divider|Progress)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/)
|
|
55
|
+
if (!binding) continue
|
|
56
|
+
const name = binding[2] || binding[1]
|
|
57
|
+
if (match[2] === 'frappe-ui') setAlias(aliases, name, binding[1])
|
|
58
|
+
else {
|
|
59
|
+
aliases.delete(name)
|
|
60
|
+
aliases.delete(kebabCase(name))
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (const match of source.matchAll(
|
|
65
|
+
/import\s+([A-Za-z_$][\w$]*)\s+from\s*['"]([^'"]+)['"]/g,
|
|
66
|
+
)) {
|
|
67
|
+
const [, binding, specifier] = match
|
|
68
|
+
const component = [...COMPONENTS].find((name) =>
|
|
69
|
+
specifier.endsWith(`/${name}/${name}.vue`),
|
|
70
|
+
)
|
|
71
|
+
if (component) setAlias(aliases, binding, component)
|
|
72
|
+
else {
|
|
73
|
+
aliases.delete(binding)
|
|
74
|
+
aliases.delete(kebabCase(binding))
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return aliases
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function propName(prop) {
|
|
81
|
+
if (prop.type === NodeTypes.ATTRIBUTE) return prop.name
|
|
82
|
+
if (
|
|
83
|
+
prop.type === NodeTypes.DIRECTIVE &&
|
|
84
|
+
prop.name === 'bind' &&
|
|
85
|
+
prop.arg?.type === NodeTypes.SIMPLE_EXPRESSION &&
|
|
86
|
+
prop.arg.isStatic
|
|
87
|
+
) {
|
|
88
|
+
return prop.arg.content
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function renamePropEdit(prop, from, to, offset) {
|
|
93
|
+
if (
|
|
94
|
+
prop.type === NodeTypes.DIRECTIVE &&
|
|
95
|
+
prop.name === 'bind' &&
|
|
96
|
+
prop.exp === undefined
|
|
97
|
+
) {
|
|
98
|
+
const renamed = prop.loc.source.replace(from, to)
|
|
99
|
+
return {
|
|
100
|
+
start: offset + prop.loc.start.offset,
|
|
101
|
+
end: offset + prop.loc.end.offset,
|
|
102
|
+
text: `${renamed}="${from}"`,
|
|
103
|
+
description: `${from} -> ${to}`,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const relative = prop.loc.source.indexOf(from)
|
|
107
|
+
return {
|
|
108
|
+
start: offset + prop.loc.start.offset + relative,
|
|
109
|
+
end: offset + prop.loc.start.offset + relative + from.length,
|
|
110
|
+
text: to,
|
|
111
|
+
description: `${from} -> ${to}`,
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function boundExpression(prop) {
|
|
116
|
+
if (prop?.type !== NodeTypes.DIRECTIVE || prop.name !== 'bind')
|
|
117
|
+
return undefined
|
|
118
|
+
const match = prop.loc.source.match(/^(?::|v-bind:)[^=]+=(['"])([\s\S]*)\1$/)
|
|
119
|
+
return match?.[2]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function countExpression(prop) {
|
|
123
|
+
if (prop.type === NodeTypes.ATTRIBUTE) return prop.value?.content
|
|
124
|
+
return boundExpression(prop)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function looksNumericExpression(expression) {
|
|
128
|
+
return (
|
|
129
|
+
/^\d+(?:\.\d+)?$/.test(expression) ||
|
|
130
|
+
/(?:^|\.)(?:length|size)$/.test(expression) ||
|
|
131
|
+
/^(?:Number|parseInt|parseFloat)\s*\(/.test(expression) ||
|
|
132
|
+
/[+*/%]|\?[^:]+:\s*(?:undefined|\d)/.test(expression)
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function progressEdit(element, offset, refusals) {
|
|
137
|
+
const intervals = element.props.find((prop) => propName(prop) === 'intervals')
|
|
138
|
+
const count = element.props.find((prop) =>
|
|
139
|
+
['intervalCount', 'interval-count'].includes(propName(prop)),
|
|
140
|
+
)
|
|
141
|
+
if (!count && !intervals) return []
|
|
142
|
+
|
|
143
|
+
if (count && !intervals) {
|
|
144
|
+
refusals.push({
|
|
145
|
+
line: count.loc.start.line,
|
|
146
|
+
message:
|
|
147
|
+
'Progress has intervalCount without intervals; choose continuous or interval mode',
|
|
148
|
+
})
|
|
149
|
+
return []
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const condition = boundExpression(intervals)
|
|
153
|
+
const staticValue =
|
|
154
|
+
intervals.type === NodeTypes.ATTRIBUTE
|
|
155
|
+
? intervals.value?.content
|
|
156
|
+
: undefined
|
|
157
|
+
const bareBoolean =
|
|
158
|
+
intervals.type === NodeTypes.ATTRIBUTE && intervals.value === undefined
|
|
159
|
+
const numericStatic =
|
|
160
|
+
staticValue !== undefined && /^\d+(?:\.\d+)?$/.test(staticValue)
|
|
161
|
+
const shorthandBinding =
|
|
162
|
+
intervals.type === NodeTypes.DIRECTIVE && condition === undefined
|
|
163
|
+
// A dynamic `:intervals` without `intervalCount` may already be the v1
|
|
164
|
+
// numeric API. Leaving it alone makes repeat runs idempotent.
|
|
165
|
+
if (!count && shorthandBinding) {
|
|
166
|
+
refusals.push({
|
|
167
|
+
line: intervals.loc.start.line,
|
|
168
|
+
message:
|
|
169
|
+
'Progress shorthand :intervals may still be the v0 boolean mode; replace it with a segment count',
|
|
170
|
+
})
|
|
171
|
+
return []
|
|
172
|
+
}
|
|
173
|
+
if (
|
|
174
|
+
!count &&
|
|
175
|
+
!bareBoolean &&
|
|
176
|
+
staticValue !== 'true' &&
|
|
177
|
+
!numericStatic &&
|
|
178
|
+
condition !== 'true' &&
|
|
179
|
+
condition !== 'false'
|
|
180
|
+
) {
|
|
181
|
+
if (!looksNumericExpression(condition)) {
|
|
182
|
+
refusals.push({
|
|
183
|
+
line: intervals.loc.start.line,
|
|
184
|
+
message: `Progress ${intervals.loc.source} may still be the v0 boolean mode; replace it with a segment count`,
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
return []
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (condition === 'false') {
|
|
191
|
+
const removeProp = (prop, description) => {
|
|
192
|
+
let start = prop.loc.start.offset
|
|
193
|
+
while (
|
|
194
|
+
start > element.loc.start.offset &&
|
|
195
|
+
/\s/.test(
|
|
196
|
+
element.loc.source[start - element.loc.start.offset - 1] || '',
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
start--
|
|
200
|
+
return {
|
|
201
|
+
start: offset + start,
|
|
202
|
+
end: offset + prop.loc.end.offset,
|
|
203
|
+
text: '',
|
|
204
|
+
description,
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return [
|
|
208
|
+
removeProp(intervals, 'remove disabled Progress interval mode'),
|
|
209
|
+
...(count
|
|
210
|
+
? [removeProp(count, 'remove unused Progress intervalCount')]
|
|
211
|
+
: []),
|
|
212
|
+
]
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let replacement
|
|
216
|
+
if (!count && numericStatic) {
|
|
217
|
+
replacement = `:intervals="${staticValue}"`
|
|
218
|
+
} else {
|
|
219
|
+
const countValue = count ? countExpression(count) : '6'
|
|
220
|
+
if (!countValue) {
|
|
221
|
+
refusals.push({
|
|
222
|
+
line: count.loc.start.line,
|
|
223
|
+
message: 'Progress intervalCount has no static value to migrate',
|
|
224
|
+
})
|
|
225
|
+
return []
|
|
226
|
+
}
|
|
227
|
+
if (condition && condition !== 'true') {
|
|
228
|
+
replacement = `:intervals="(${condition}) ? (${countValue}) : undefined"`
|
|
229
|
+
} else {
|
|
230
|
+
replacement = `:intervals="${countValue}"`
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const edits = [
|
|
235
|
+
{
|
|
236
|
+
start: offset + intervals.loc.start.offset,
|
|
237
|
+
end: offset + intervals.loc.end.offset,
|
|
238
|
+
text: replacement,
|
|
239
|
+
description: 'Progress intervals + intervalCount -> numeric intervals',
|
|
240
|
+
},
|
|
241
|
+
]
|
|
242
|
+
if (count) {
|
|
243
|
+
let countStart = count.loc.start.offset
|
|
244
|
+
let countEnd = count.loc.end.offset
|
|
245
|
+
if (countStart > intervals.loc.end.offset) {
|
|
246
|
+
while (
|
|
247
|
+
countStart > intervals.loc.end.offset &&
|
|
248
|
+
/\s/.test(
|
|
249
|
+
element.loc.source[countStart - element.loc.start.offset - 1] || '',
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
countStart--
|
|
253
|
+
} else {
|
|
254
|
+
while (
|
|
255
|
+
countEnd < intervals.loc.start.offset &&
|
|
256
|
+
/\s/.test(element.loc.source[countEnd - element.loc.start.offset] || '')
|
|
257
|
+
)
|
|
258
|
+
countEnd++
|
|
259
|
+
}
|
|
260
|
+
edits.push({
|
|
261
|
+
start: offset + countStart,
|
|
262
|
+
end: offset + countEnd,
|
|
263
|
+
text: '',
|
|
264
|
+
description: 'remove Progress intervalCount',
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
return edits
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function migrateBaseProps(source) {
|
|
271
|
+
const parsed = parseSfc(source, { filename: 'component.vue' })
|
|
272
|
+
if (parsed.errors.length) {
|
|
273
|
+
return {
|
|
274
|
+
migrated: source,
|
|
275
|
+
changes: [],
|
|
276
|
+
refusals: [
|
|
277
|
+
{ line: 1, message: `Vue parse error: ${String(parsed.errors[0])}` },
|
|
278
|
+
],
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const block = parsed.descriptor.template
|
|
282
|
+
if (!block) return { migrated: source, changes: [], refusals: [] }
|
|
283
|
+
|
|
284
|
+
const aliases = importedComponentAliases(source)
|
|
285
|
+
const ast = block.ast
|
|
286
|
+
const edits = []
|
|
287
|
+
const refusals = []
|
|
288
|
+
|
|
289
|
+
function visit(node) {
|
|
290
|
+
if (node.type === NodeTypes.ELEMENT) {
|
|
291
|
+
const component = aliases.get(node.tag)
|
|
292
|
+
if (component === 'Icon') {
|
|
293
|
+
const name = node.props.find((prop) => propName(prop) === 'name')
|
|
294
|
+
const icon = node.props.find((prop) => propName(prop) === 'icon')
|
|
295
|
+
// Both props are a supported contract. Leave the lower-precedence name
|
|
296
|
+
// intact instead of creating a duplicate icon binding.
|
|
297
|
+
if (name && !icon) {
|
|
298
|
+
edits.push(renamePropEdit(name, 'name', 'icon', 0))
|
|
299
|
+
}
|
|
300
|
+
} else if (component === 'Divider') {
|
|
301
|
+
for (const prop of node.props) {
|
|
302
|
+
if (propName(prop) === 'position')
|
|
303
|
+
edits.push(renamePropEdit(prop, 'position', 'align', 0))
|
|
304
|
+
}
|
|
305
|
+
} else if (component === 'Progress') {
|
|
306
|
+
edits.push(...progressEdit(node, 0, refusals))
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
for (const child of node.children || []) visit(child)
|
|
310
|
+
}
|
|
311
|
+
visit(ast)
|
|
312
|
+
|
|
313
|
+
if (refusals.length) return { migrated: source, changes: [], refusals }
|
|
314
|
+
const migrated = [...edits]
|
|
315
|
+
.sort((a, b) => b.start - a.start)
|
|
316
|
+
.reduce(
|
|
317
|
+
(text, edit) =>
|
|
318
|
+
text.slice(0, edit.start) + edit.text + text.slice(edit.end),
|
|
319
|
+
source,
|
|
320
|
+
)
|
|
321
|
+
return { migrated, changes: edits, refusals }
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function main() {
|
|
325
|
+
const args = process.argv.slice(2)
|
|
326
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
327
|
+
console.log(USAGE)
|
|
328
|
+
return
|
|
329
|
+
}
|
|
330
|
+
const unknown = args.filter(
|
|
331
|
+
(arg) => arg.startsWith('-') && arg !== '--dry-run',
|
|
332
|
+
)
|
|
333
|
+
const targets = args.filter((arg) => !arg.startsWith('-'))
|
|
334
|
+
if (unknown.length || !targets.length) {
|
|
335
|
+
if (unknown.length) console.error(`Unknown option: ${unknown.join(' ')}`)
|
|
336
|
+
console.error(USAGE)
|
|
337
|
+
process.exit(1)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
for (const target of targets) {
|
|
341
|
+
if (!fs.existsSync(target)) {
|
|
342
|
+
console.error(`Invalid path: ${target}`)
|
|
343
|
+
process.exit(1)
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const dryRun = args.includes('--dry-run')
|
|
348
|
+
const files = [...new Set(targets.flatMap((target) => walk(target)))]
|
|
349
|
+
let changed = 0
|
|
350
|
+
const refusals = []
|
|
351
|
+
for (const file of files) {
|
|
352
|
+
const source = fs.readFileSync(file, 'utf8')
|
|
353
|
+
const result = migrateBaseProps(source)
|
|
354
|
+
if (result.refusals.length) {
|
|
355
|
+
for (const refusal of result.refusals) refusals.push({ file, ...refusal })
|
|
356
|
+
continue
|
|
357
|
+
}
|
|
358
|
+
if (!result.changes.length) continue
|
|
359
|
+
changed++
|
|
360
|
+
if (!dryRun) fs.writeFileSync(file, result.migrated)
|
|
361
|
+
console.log(`${file} (${result.changes.length})`)
|
|
362
|
+
}
|
|
363
|
+
console.log(
|
|
364
|
+
`\n${dryRun ? '[dry-run] would update' : 'Updated'} ${changed} files`,
|
|
365
|
+
)
|
|
366
|
+
if (refusals.length) {
|
|
367
|
+
console.error(
|
|
368
|
+
`\nNot converted — ${refusals.length} sites need a decision; affected files were left unchanged:`,
|
|
369
|
+
)
|
|
370
|
+
for (const refusal of refusals)
|
|
371
|
+
console.error(` ${refusal.file}:L${refusal.line} ${refusal.message}`)
|
|
372
|
+
process.exit(1)
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const scriptPath = fileURLToPath(import.meta.url)
|
|
377
|
+
const invokedPath = process.argv[1]
|
|
378
|
+
if (invokedPath && fs.realpathSync(invokedPath) === fs.realpathSync(scriptPath))
|
|
379
|
+
main()
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
interface Label {
|
|
2
|
-
toString(): string
|
|
3
|
-
}
|
|
4
|
-
|
|
5
1
|
export interface BadgeProps {
|
|
6
2
|
/** Visual color theme of the badge */
|
|
7
3
|
theme?: 'gray' | 'blue' | 'green' | 'amber' | 'red' | 'violet'
|
|
@@ -13,5 +9,5 @@ export interface BadgeProps {
|
|
|
13
9
|
variant?: 'solid' | 'subtle' | 'outline' | 'ghost'
|
|
14
10
|
|
|
15
11
|
/** Content displayed inside the badge */
|
|
16
|
-
label?:
|
|
12
|
+
label?: string | number
|
|
17
13
|
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
default: '"horizontal"'
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
|
-
name: '
|
|
16
|
+
name: 'align',
|
|
17
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"',
|
|
@@ -41,6 +41,29 @@ describe('Divider', () => {
|
|
|
41
41
|
cy.get('div').should('exist')
|
|
42
42
|
})
|
|
43
43
|
|
|
44
|
+
it('accepts shared Button action fields', () => {
|
|
45
|
+
cy.mount(Divider, {
|
|
46
|
+
props: {
|
|
47
|
+
action: { label: 'Delete', theme: 'red', variant: 'solid', size: 'md' },
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
cy.contains('button', 'Delete')
|
|
52
|
+
.should('have.class', 'bg-surface-red-7')
|
|
53
|
+
.and('have.class', 'h-8')
|
|
54
|
+
.and('have.class', 'text-base-medium')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('aligns an action along the divider', () => {
|
|
58
|
+
cy.mount(Divider, {
|
|
59
|
+
props: { align: 'start', action: { label: 'Add row' } },
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
cy.contains('button', 'Add row')
|
|
63
|
+
.parent()
|
|
64
|
+
.should('have.class', 'justify-start')
|
|
65
|
+
})
|
|
66
|
+
|
|
44
67
|
it('does not overlap sibling content in vertical action mode', () => {
|
|
45
68
|
cy.mount({
|
|
46
69
|
render() {
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Separates content visually, either horizontally or vertically. It can also host a small inline action.
|
|
4
4
|
|
|
5
|
+
Use `align="start|center|end"` to place an action along the rule. The action
|
|
6
|
+
accepts the shared button fields such as `theme`, `variant`, `size`, and icons.
|
|
7
|
+
|
|
5
8
|
## Playground
|
|
6
9
|
|
|
7
10
|
<ComponentPlayground name="Divider" />
|
|
@@ -13,7 +13,7 @@ const knobs: Knob[] = [
|
|
|
13
13
|
],
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
|
-
name: '
|
|
16
|
+
name: 'align',
|
|
17
17
|
type: 'tabs',
|
|
18
18
|
default: 'center',
|
|
19
19
|
options: [
|
|
@@ -29,7 +29,7 @@ function buildCode(v: Record<string, any>) {
|
|
|
29
29
|
const attrs: string[] = []
|
|
30
30
|
if (v.orientation !== 'horizontal')
|
|
31
31
|
attrs.push(`orientation="${v.orientation}"`)
|
|
32
|
-
if (v.
|
|
32
|
+
if (v.align !== 'center') attrs.push(`align="${v.align}"`)
|
|
33
33
|
if (v.withAction) attrs.push(`:action="{ label: 'Add row' }"`)
|
|
34
34
|
if (!attrs.length) return '<Divider />'
|
|
35
35
|
return ['<Divider', ...attrs.map((a) => ' ' + a), '/>'].join('\n')
|
|
@@ -50,7 +50,7 @@ function buildCode(v: Record<string, any>) {
|
|
|
50
50
|
<span class="text-sm text-ink-gray-6 self-center">Before</span>
|
|
51
51
|
<Divider
|
|
52
52
|
orientation="vertical"
|
|
53
|
-
:
|
|
53
|
+
:align="values.align"
|
|
54
54
|
flex-item
|
|
55
55
|
:action="values.withAction ? { label: 'Add row' } : undefined"
|
|
56
56
|
/>
|
|
@@ -58,7 +58,7 @@ function buildCode(v: Record<string, any>) {
|
|
|
58
58
|
</template>
|
|
59
59
|
<Divider
|
|
60
60
|
v-else
|
|
61
|
-
:
|
|
61
|
+
:align="values.align"
|
|
62
62
|
:action="values.withAction ? { label: 'Add row' } : undefined"
|
|
63
63
|
/>
|
|
64
64
|
</div>
|
|
@@ -7,11 +7,8 @@
|
|
|
7
7
|
:class="actionLineClasses"
|
|
8
8
|
/>
|
|
9
9
|
<Button
|
|
10
|
-
|
|
11
|
-
:loading="props.action.loading"
|
|
10
|
+
v-bind="actionButtonProps"
|
|
12
11
|
class="relative z-10"
|
|
13
|
-
size="sm"
|
|
14
|
-
variant="outline"
|
|
15
12
|
@click="actionOnClick"
|
|
16
13
|
/>
|
|
17
14
|
</div>
|
|
@@ -22,13 +19,20 @@
|
|
|
22
19
|
<script lang="ts" setup>
|
|
23
20
|
import { computed } from 'vue'
|
|
24
21
|
import { Button } from '../../index'
|
|
22
|
+
import { mergeActionProps } from '../shared/action'
|
|
25
23
|
import type { DividerProps } from './types'
|
|
26
24
|
|
|
27
25
|
const props = withDefaults(defineProps<DividerProps>(), {
|
|
28
26
|
orientation: 'horizontal',
|
|
29
|
-
|
|
27
|
+
align: 'center',
|
|
30
28
|
})
|
|
31
29
|
|
|
30
|
+
const actionButtonProps = computed(() =>
|
|
31
|
+
props.action
|
|
32
|
+
? mergeActionProps({ size: 'sm', variant: 'outline' }, props.action)
|
|
33
|
+
: undefined,
|
|
34
|
+
)
|
|
35
|
+
|
|
32
36
|
const actionOnClick = computed(() => {
|
|
33
37
|
return props.action?.onClick
|
|
34
38
|
})
|
|
@@ -54,7 +58,7 @@ const actionContainerClasses = computed(() => {
|
|
|
54
58
|
center: 'justify-center',
|
|
55
59
|
start: 'justify-start pl-4',
|
|
56
60
|
end: 'justify-end pr-4',
|
|
57
|
-
}[props.
|
|
61
|
+
}[props.align]
|
|
58
62
|
|
|
59
63
|
return [...baseClasses, 'flex w-full min-h-7 items-center', positionClasses]
|
|
60
64
|
}
|
|
@@ -64,7 +68,7 @@ const actionContainerClasses = computed(() => {
|
|
|
64
68
|
center: 'items-center',
|
|
65
69
|
start: 'items-start pt-4',
|
|
66
70
|
end: 'items-end pb-4',
|
|
67
|
-
}[props.
|
|
71
|
+
}[props.align]
|
|
68
72
|
|
|
69
73
|
return [...baseClasses, 'flex justify-center', heightClasses, positionClasses]
|
|
70
74
|
})
|
|
@@ -1,14 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
export interface DividerAction {
|
|
3
|
-
/** Text of the action button. */
|
|
4
|
-
label: string
|
|
1
|
+
import type { Action } from '../shared/action'
|
|
5
2
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
/** Shows a spinner in the action button and blocks further clicks. */
|
|
10
|
-
loading?: boolean
|
|
11
|
-
}
|
|
3
|
+
/** A shared button action with no component-specific callback context. */
|
|
4
|
+
export type DividerAction = Action<void> & { label: string }
|
|
12
5
|
|
|
13
6
|
export interface DividerProps {
|
|
14
7
|
/** Which way the rule runs. */
|
|
@@ -18,7 +11,7 @@ export interface DividerProps {
|
|
|
18
11
|
* Where the action sits along the rule. Ignored without an `action`, since a
|
|
19
12
|
* plain rule has nothing to place.
|
|
20
13
|
*/
|
|
21
|
-
|
|
14
|
+
align?: 'start' | 'center' | 'end'
|
|
22
15
|
|
|
23
16
|
/**
|
|
24
17
|
* Stretches the divider to the cross-axis size of its flex parent
|
|
@@ -6,10 +6,16 @@
|
|
|
6
6
|
|
|
7
7
|
const propsData = [
|
|
8
8
|
{
|
|
9
|
-
name: '
|
|
9
|
+
name: 'icon',
|
|
10
10
|
description: 'Icon source. Supported forms:\n- `lucide-*` string → rendered via the Tailwind mask plugin.\n- emoji / symbol string → rendered as plain text.\n- Vue component → rendered via `<component :is>`.\nAny other string (e.g. a bare feather-style name) is unsupported —\nit renders nothing and warns once in dev. Falsy values render nothing.',
|
|
11
11
|
required: false,
|
|
12
12
|
type: 'string | Component | null'
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: 'name',
|
|
16
|
+
description: 'Supported alternate name for `icon`. `icon` takes precedence when both\nprops are supplied.',
|
|
17
|
+
required: false,
|
|
18
|
+
type: 'string | Component | null'
|
|
13
19
|
}
|
|
14
20
|
]
|
|
15
21
|
</script>
|
|
@@ -3,7 +3,7 @@ import Icon from './Icon.vue'
|
|
|
3
3
|
|
|
4
4
|
describe('<Icon />', () => {
|
|
5
5
|
it('renders a lucide string as a span with the class', () => {
|
|
6
|
-
cy.mount(Icon, { props: {
|
|
6
|
+
cy.mount(Icon, { props: { icon: 'lucide-star' } })
|
|
7
7
|
cy.get('span').should('have.class', 'lucide-star')
|
|
8
8
|
cy.get('span').should('have.attr', 'aria-hidden', 'true')
|
|
9
9
|
cy.get('span').should('be.empty')
|
|
@@ -11,7 +11,7 @@ describe('<Icon />', () => {
|
|
|
11
11
|
|
|
12
12
|
it('passes through fallthrough class on lucide spans', () => {
|
|
13
13
|
cy.mount(Icon, {
|
|
14
|
-
props: {
|
|
14
|
+
props: { icon: 'lucide-check' },
|
|
15
15
|
attrs: { class: 'size-4 text-ink-gray-6' },
|
|
16
16
|
})
|
|
17
17
|
cy.get('span')
|
|
@@ -21,18 +21,18 @@ describe('<Icon />', () => {
|
|
|
21
21
|
})
|
|
22
22
|
|
|
23
23
|
it('renders an emoji string as text content', () => {
|
|
24
|
-
cy.mount(Icon, { props: {
|
|
24
|
+
cy.mount(Icon, { props: { icon: '🚀' } })
|
|
25
25
|
cy.get('span').should('have.text', '🚀')
|
|
26
26
|
cy.get('span').should('have.attr', 'aria-hidden', 'true')
|
|
27
27
|
})
|
|
28
28
|
|
|
29
29
|
it('treats other glyphs (no ASCII letters/digits) as emoji', () => {
|
|
30
|
-
cy.mount(Icon, { props: {
|
|
30
|
+
cy.mount(Icon, { props: { icon: '✨' } })
|
|
31
31
|
cy.get('span').should('have.text', '✨')
|
|
32
32
|
})
|
|
33
33
|
|
|
34
34
|
it('renders nothing for an unsupported (non-lucide, non-emoji) string', () => {
|
|
35
|
-
cy.mount(Icon, { props: {
|
|
35
|
+
cy.mount(Icon, { props: { icon: 'check' } })
|
|
36
36
|
cy.get('span').should('not.exist')
|
|
37
37
|
cy.get('svg').should('not.exist')
|
|
38
38
|
})
|
|
@@ -44,10 +44,42 @@ describe('<Icon />', () => {
|
|
|
44
44
|
return h('svg', { 'data-cy': 'custom-icon' })
|
|
45
45
|
},
|
|
46
46
|
}
|
|
47
|
-
cy.mount(Icon, { props: {
|
|
47
|
+
cy.mount(Icon, { props: { icon: CustomIcon } })
|
|
48
48
|
cy.get('[data-cy="custom-icon"]').should('exist')
|
|
49
49
|
})
|
|
50
50
|
|
|
51
|
+
it('supports the name prop with string and Component values', () => {
|
|
52
|
+
const CustomIcon = {
|
|
53
|
+
render() {
|
|
54
|
+
return h('svg', { 'data-cy': 'name-component' })
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
cy.mount(Icon, { props: { name: 'lucide-star' } })
|
|
59
|
+
cy.get('span').should('have.class', 'lucide-star')
|
|
60
|
+
|
|
61
|
+
cy.mount(Icon, { props: { name: CustomIcon } })
|
|
62
|
+
cy.get('[data-cy="name-component"]').should('exist')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('prefers icon unless it is undefined', () => {
|
|
66
|
+
cy.mount(Icon, {
|
|
67
|
+
props: { icon: 'lucide-check', name: 'lucide-star' },
|
|
68
|
+
})
|
|
69
|
+
cy.get('span')
|
|
70
|
+
.should('have.class', 'lucide-check')
|
|
71
|
+
.and('not.have.class', 'lucide-star')
|
|
72
|
+
|
|
73
|
+
cy.mount(Icon, { props: { icon: undefined, name: 'lucide-star' } })
|
|
74
|
+
cy.get('span').should('have.class', 'lucide-star')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('treats an explicit null icon as the selected empty value', () => {
|
|
78
|
+
cy.mount(Icon, { props: { icon: null, name: 'lucide-star' } })
|
|
79
|
+
cy.get('span').should('not.exist')
|
|
80
|
+
cy.get('svg').should('not.exist')
|
|
81
|
+
})
|
|
82
|
+
|
|
51
83
|
it('passes fallthrough attrs to the rendered Component', () => {
|
|
52
84
|
const CustomIcon = {
|
|
53
85
|
name: 'custom-icon',
|
|
@@ -56,22 +88,22 @@ describe('<Icon />', () => {
|
|
|
56
88
|
},
|
|
57
89
|
}
|
|
58
90
|
cy.mount(Icon, {
|
|
59
|
-
props: {
|
|
91
|
+
props: { icon: CustomIcon },
|
|
60
92
|
attrs: { class: 'size-5' },
|
|
61
93
|
})
|
|
62
94
|
cy.get('[data-cy="custom-icon"]').should('have.class', 'size-5')
|
|
63
95
|
})
|
|
64
96
|
|
|
65
|
-
it('renders nothing when
|
|
66
|
-
cy.mount(Icon, { props: {
|
|
97
|
+
it('renders nothing when the resolved value is empty or omitted', () => {
|
|
98
|
+
cy.mount(Icon, { props: { icon: '' } })
|
|
67
99
|
cy.get('span').should('not.exist')
|
|
68
100
|
cy.get('svg').should('not.exist')
|
|
69
101
|
|
|
70
|
-
cy.mount(Icon, { props: {
|
|
102
|
+
cy.mount(Icon, { props: {} })
|
|
71
103
|
cy.get('span').should('not.exist')
|
|
72
104
|
cy.get('svg').should('not.exist')
|
|
73
105
|
|
|
74
|
-
cy.mount(Icon, { props: {} })
|
|
106
|
+
cy.mount(Icon, { props: { name: null } })
|
|
75
107
|
cy.get('span').should('not.exist')
|
|
76
108
|
cy.get('svg').should('not.exist')
|
|
77
109
|
})
|
|
@@ -6,6 +6,17 @@ components take an icon prop directly and render it through `Icon`
|
|
|
6
6
|
internally — reach for `Icon` yourself only when you need an icon outside
|
|
7
7
|
of one of those props.
|
|
8
8
|
|
|
9
|
+
Pass the source through the canonical `icon` prop:
|
|
10
|
+
|
|
11
|
+
```vue
|
|
12
|
+
<Icon icon="lucide-circle-check" class="size-4" />
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The existing `name` prop remains fully supported with the same value types.
|
|
16
|
+
When both props are present, `icon` takes precedence; an explicit `icon=""` or
|
|
17
|
+
`:icon="null"` intentionally renders nothing, while `:icon="undefined"` falls
|
|
18
|
+
back to `name`.
|
|
19
|
+
|
|
9
20
|
## Lucide string
|
|
10
21
|
|
|
11
22
|
<ComponentPreview name="Icon-Lucide" />
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { watchEffect } from 'vue'
|
|
2
|
+
import { computed, watchEffect } from 'vue'
|
|
3
3
|
import {
|
|
4
4
|
isEmojiIconString,
|
|
5
5
|
isLucideIconString,
|
|
@@ -11,28 +11,35 @@ defineOptions({ inheritAttrs: false })
|
|
|
11
11
|
|
|
12
12
|
const props = defineProps<IconProps>()
|
|
13
13
|
|
|
14
|
+
const resolvedIcon = computed(() =>
|
|
15
|
+
props.icon !== undefined ? props.icon : props.name,
|
|
16
|
+
)
|
|
17
|
+
|
|
14
18
|
watchEffect(() => {
|
|
15
|
-
|
|
19
|
+
const sourceProp = props.icon !== undefined ? 'icon' : 'name'
|
|
20
|
+
warnUnsupportedIconString('Icon', sourceProp, resolvedIcon.value)
|
|
16
21
|
})
|
|
17
22
|
</script>
|
|
18
23
|
|
|
19
24
|
<template>
|
|
20
25
|
<span
|
|
21
|
-
v-if="typeof
|
|
22
|
-
:class="[
|
|
26
|
+
v-if="typeof resolvedIcon === 'string' && isLucideIconString(resolvedIcon)"
|
|
27
|
+
:class="[resolvedIcon]"
|
|
23
28
|
v-bind="$attrs"
|
|
24
29
|
aria-hidden="true"
|
|
25
30
|
/>
|
|
26
31
|
<span
|
|
27
|
-
v-else-if="
|
|
32
|
+
v-else-if="
|
|
33
|
+
typeof resolvedIcon === 'string' && isEmojiIconString(resolvedIcon)
|
|
34
|
+
"
|
|
28
35
|
class="inline-flex items-center justify-center leading-none"
|
|
29
36
|
v-bind="$attrs"
|
|
30
37
|
aria-hidden="true"
|
|
31
|
-
>{{
|
|
38
|
+
>{{ resolvedIcon }}</span
|
|
32
39
|
>
|
|
33
40
|
<component
|
|
34
|
-
v-else-if="
|
|
35
|
-
:is="
|
|
41
|
+
v-else-if="resolvedIcon && typeof resolvedIcon !== 'string'"
|
|
42
|
+
:is="resolvedIcon"
|
|
36
43
|
v-bind="$attrs"
|
|
37
44
|
aria-hidden="true"
|
|
38
45
|
/>
|
|
@@ -4,9 +4,9 @@ import { Icon } from 'frappe-ui'
|
|
|
4
4
|
|
|
5
5
|
<template>
|
|
6
6
|
<div class="flex items-center gap-4 text-ink-gray-8">
|
|
7
|
-
<Icon
|
|
8
|
-
<Icon
|
|
9
|
-
<Icon
|
|
10
|
-
<Icon
|
|
7
|
+
<Icon icon="lucide-plus" class="size-5" />
|
|
8
|
+
<Icon icon="lucide-trash-2" class="size-5" />
|
|
9
|
+
<Icon icon="lucide-check" class="size-5" />
|
|
10
|
+
<Icon icon="🎉" class="text-xl" />
|
|
11
11
|
</div>
|
|
12
12
|
</template>
|
|
@@ -9,5 +9,11 @@ export interface IconProps {
|
|
|
9
9
|
* Any other string (e.g. a bare feather-style name) is unsupported —
|
|
10
10
|
* it renders nothing and warns once in dev. Falsy values render nothing.
|
|
11
11
|
*/
|
|
12
|
+
icon?: string | Component | null
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Supported alternate name for `icon`. `icon` takes precedence when both
|
|
16
|
+
* props are supplied.
|
|
17
|
+
*/
|
|
12
18
|
name?: string | Component | null
|
|
13
19
|
}
|
|
@@ -34,17 +34,9 @@
|
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
name: 'intervals',
|
|
37
|
-
description: '
|
|
37
|
+
description: 'Number of interval markers to show; omit for a continuous progress bar',
|
|
38
38
|
required: false,
|
|
39
|
-
type: '
|
|
40
|
-
default: 'false'
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
name: 'intervalCount',
|
|
44
|
-
description: 'Number of intervals to display if `intervals` is true',
|
|
45
|
-
required: false,
|
|
46
|
-
type: 'number',
|
|
47
|
-
default: '6'
|
|
39
|
+
type: 'number'
|
|
48
40
|
}
|
|
49
41
|
]
|
|
50
42
|
|
|
@@ -42,12 +42,23 @@ describe('Progress', () => {
|
|
|
42
42
|
}
|
|
43
43
|
})
|
|
44
44
|
|
|
45
|
-
it('
|
|
45
|
+
it('renders the label without a hint', () => {
|
|
46
|
+
cy.mount(Progress, { props: { label: 'Uploading', value: 20 } })
|
|
47
|
+
|
|
48
|
+
cy.contains('span', 'Uploading').should('be.visible')
|
|
49
|
+
cy.contains('20%').should('not.exist')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('renders the hint prop without a label', () => {
|
|
53
|
+
cy.mount(Progress, { props: { value: 20, hint: true } })
|
|
54
|
+
|
|
55
|
+
cy.contains('span', '20%').should('be.visible')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('renders the hint slot without a label or hint prop', () => {
|
|
46
59
|
cy.mount(Progress, {
|
|
47
60
|
props: {
|
|
48
|
-
label: 'label',
|
|
49
61
|
value: 20,
|
|
50
|
-
hint: true,
|
|
51
62
|
},
|
|
52
63
|
slots: {
|
|
53
64
|
hint: () => 'hint 20%',
|
|
@@ -65,8 +76,7 @@ describe('Progress', () => {
|
|
|
65
76
|
props: {
|
|
66
77
|
label: 'label',
|
|
67
78
|
value,
|
|
68
|
-
intervals:
|
|
69
|
-
intervalCount,
|
|
79
|
+
intervals: intervalCount,
|
|
70
80
|
},
|
|
71
81
|
})
|
|
72
82
|
|
|
@@ -109,7 +119,7 @@ describe('Progress', () => {
|
|
|
109
119
|
props: {
|
|
110
120
|
label: 'label',
|
|
111
121
|
value: 60,
|
|
112
|
-
intervals:
|
|
122
|
+
intervals: 6,
|
|
113
123
|
},
|
|
114
124
|
})
|
|
115
125
|
|
|
@@ -31,8 +31,8 @@ bar. Completing items is the most common source of a progress value.
|
|
|
31
31
|
|
|
32
32
|
## Multi-step form
|
|
33
33
|
|
|
34
|
-
`intervals`
|
|
35
|
-
up to the current
|
|
34
|
+
Pass the number of segments to `intervals` to turn the bar into a step
|
|
35
|
+
indicator, filled up to the current step. Omit it for a continuous bar.
|
|
36
36
|
|
|
37
37
|
<ComponentPreview name="Progress-MultiStepForm" />
|
|
38
38
|
|
|
@@ -17,8 +17,7 @@ const knobs: Knob[] = [
|
|
|
17
17
|
],
|
|
18
18
|
},
|
|
19
19
|
{ name: 'hint', type: 'switch', default: true },
|
|
20
|
-
{ name: 'intervals', type: '
|
|
21
|
-
{ name: 'intervalCount', type: 'text', default: '6', width: '4rem' },
|
|
20
|
+
{ name: 'intervals', type: 'text', default: '', width: '4rem' },
|
|
22
21
|
]
|
|
23
22
|
|
|
24
23
|
function buildCode(v: Record<string, any>) {
|
|
@@ -26,11 +25,8 @@ function buildCode(v: Record<string, any>) {
|
|
|
26
25
|
if (v.label) attrs.push(`label="${v.label}"`)
|
|
27
26
|
if (v.size !== 'sm') attrs.push(`size="${v.size}"`)
|
|
28
27
|
if (v.hint) attrs.push('hint')
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const ic = Number(v.intervalCount)
|
|
32
|
-
if (ic && ic !== 6) attrs.push(`:interval-count="${ic}"`)
|
|
33
|
-
}
|
|
28
|
+
const intervals = Number(v.intervals)
|
|
29
|
+
if (intervals > 0) attrs.push(`:intervals="${intervals}"`)
|
|
34
30
|
return ['<Progress', ...attrs.map((a) => ' ' + a), '/>'].join('\n')
|
|
35
31
|
}
|
|
36
32
|
</script>
|
|
@@ -44,8 +40,7 @@ function buildCode(v: Record<string, any>) {
|
|
|
44
40
|
:label="values.label || undefined"
|
|
45
41
|
:size="values.size"
|
|
46
42
|
:hint="values.hint"
|
|
47
|
-
:intervals="values.intervals"
|
|
48
|
-
:interval-count="Number(values.intervalCount) || 6"
|
|
43
|
+
:intervals="Number(values.intervals) || undefined"
|
|
49
44
|
/>
|
|
50
45
|
</div>
|
|
51
46
|
</template>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="w-full space-y-[10px]">
|
|
3
3
|
<div
|
|
4
|
-
v-if="props.label || props.hint"
|
|
4
|
+
v-if="props.label || props.hint || $slots.hint"
|
|
5
5
|
class="flex items-baseline justify-between"
|
|
6
6
|
>
|
|
7
7
|
<span v-if="props.label" class="text-base-medium text-ink-gray-8">
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
<!-- Interval Progress Bar -->
|
|
43
43
|
<div
|
|
44
44
|
v-else
|
|
45
|
-
v-for="index in
|
|
45
|
+
v-for="index in props.intervals"
|
|
46
46
|
class="h-full w-full"
|
|
47
47
|
:class="
|
|
48
48
|
index <= filledIntervalCount
|
|
@@ -66,8 +66,6 @@ const props = withDefaults(defineProps<ProgressProps>(), {
|
|
|
66
66
|
size: 'sm',
|
|
67
67
|
hint: false,
|
|
68
68
|
label: '',
|
|
69
|
-
intervals: false,
|
|
70
|
-
intervalCount: 6,
|
|
71
69
|
})
|
|
72
70
|
|
|
73
71
|
const indicatorContainerClasses = computed(() => {
|
|
@@ -104,7 +102,7 @@ const valueLabel = computed(
|
|
|
104
102
|
)
|
|
105
103
|
|
|
106
104
|
const filledIntervalCount = computed(() =>
|
|
107
|
-
Math.round((clampedValue.value / MAX_VALUE) * props.
|
|
105
|
+
Math.round((clampedValue.value / MAX_VALUE) * (props.intervals ?? 0)),
|
|
108
106
|
)
|
|
109
107
|
|
|
110
108
|
defineSlots<{
|
|
@@ -21,12 +21,7 @@ function continueForm() {
|
|
|
21
21
|
|
|
22
22
|
<template>
|
|
23
23
|
<div class="w-full max-w-[400px] rounded-7 border border-outline-gray-1 p-4">
|
|
24
|
-
<Progress
|
|
25
|
-
:value="percent"
|
|
26
|
-
:label="steps[step]"
|
|
27
|
-
:intervals="true"
|
|
28
|
-
:interval-count="steps.length"
|
|
29
|
-
>
|
|
24
|
+
<Progress :value="percent" :label="steps[step]" :intervals="steps.length">
|
|
30
25
|
<template #hint>
|
|
31
26
|
<span class="text-base text-ink-gray-5">
|
|
32
27
|
Step {{ step + 1 }} of {{ steps.length }}
|
|
@@ -11,9 +11,6 @@ export interface ProgressProps {
|
|
|
11
11
|
/** Whether to show a hint/tooltip for the progress value */
|
|
12
12
|
hint?: boolean
|
|
13
13
|
|
|
14
|
-
/**
|
|
15
|
-
intervals?:
|
|
16
|
-
|
|
17
|
-
/** Number of intervals to display if `intervals` is true */
|
|
18
|
-
intervalCount?: number
|
|
14
|
+
/** Number of interval markers to show; omit for a continuous progress bar */
|
|
15
|
+
intervals?: number
|
|
19
16
|
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared action-button config for components that render caller-provided
|
|
3
|
-
* actions (`Dialog`, `Alert`, `SidebarCard`): `ButtonProps` plus
|
|
4
|
-
*
|
|
3
|
+
* actions (`Dialog`, `Alert`, `SidebarCard`, `Divider`): `ButtonProps` plus
|
|
4
|
+
* `onClick`.
|
|
5
5
|
*/
|
|
6
6
|
import type { ButtonProps } from '../Button'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Internal generic behind the public action types (`AlertAction`,
|
|
10
|
-
* `DialogAction`): `ButtonProps` plus
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* `DialogAction`, `DividerAction`): `ButtonProps` plus `onClick`. Dialog
|
|
11
|
+
* receives `{ close }`, Alert and SidebarCard receive `{ dismiss }`, and
|
|
12
|
+
* Divider has no callback context. Not exported from the package root.
|
|
13
13
|
*/
|
|
14
14
|
export type Action<Ctx> = ButtonProps & {
|
|
15
15
|
/** Called on click with the component's context (e.g. `{ dismiss }` or `{ close }`). */
|
|
@@ -108,8 +108,8 @@ function hasLabel(label: PillProps['label']) {
|
|
|
108
108
|
|
|
109
109
|
<template>
|
|
110
110
|
<span :class="rootClasses" :data-state="active ? 'active' : 'inactive'">
|
|
111
|
-
<Icon v-if="icon" :
|
|
112
|
-
<Icon v-else-if="iconLeft" :
|
|
111
|
+
<Icon v-if="icon" :icon="icon" :class="iconClass" />
|
|
112
|
+
<Icon v-else-if="iconLeft" :icon="iconLeft" :class="iconClass" />
|
|
113
113
|
|
|
114
114
|
<span
|
|
115
115
|
v-if="$slots.prefix"
|