frappe-ui 1.0.0-beta.69 → 1.0.0-beta.70
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/experimental/CodeEditor/CodeEditor.api.md +2 -2
- package/experimental.ts +0 -1
- package/package.json +5 -2
- package/scripts/migrate-data-v1.js +424 -0
- package/src/charts/tokens.ts +2 -2
- package/src/components/Alert/Alert.api.md +1 -1
- package/src/components/Alert/Alert.md +36 -0
- package/src/components/Alert/types.ts +1 -1
- package/src/components/BottomSheet/BottomSheet.md +9 -2
- package/src/components/BottomSheet/BottomSheet.test.ts +31 -0
- package/src/components/BottomSheet/BottomSheet.vue +5 -1
- package/src/components/Breadcrumbs/Breadcrumbs.md +16 -0
- package/src/components/Breadcrumbs/types.ts +2 -2
- package/src/components/Button/Button.api.md +1 -1
- package/src/components/Button/types.ts +2 -2
- package/src/components/Checkbox/Checkbox.api.md +2 -2
- package/src/components/Checkbox/Checkbox.cy.ts +50 -0
- package/src/components/Checkbox/Checkbox.md +23 -0
- package/src/components/Checkbox/Checkbox.vue +24 -2
- package/src/components/Combobox/Combobox.api.md +5 -6
- package/src/components/Combobox/Combobox.cy.ts +37 -0
- package/src/components/Combobox/Combobox.vue +21 -11
- package/src/components/Combobox/types.ts +6 -3
- package/src/components/DatePicker/DatePicker.api.md +8 -8
- package/src/components/DatePicker/DatePicker.cy.ts +74 -0
- package/src/components/DatePicker/DatePicker.md +27 -0
- package/src/components/DatePicker/DatePicker.vue +10 -2
- package/src/components/DatePicker/DateRangePicker.cy.ts +19 -0
- package/src/components/DatePicker/DateRangePicker.vue +10 -2
- package/src/components/DatePicker/DateTimePicker.cy.ts +19 -0
- package/src/components/DatePicker/DateTimePicker.vue +10 -2
- package/src/components/DatePicker/index.ts +26 -1
- package/src/components/DatePicker/stories/Range.vue +6 -6
- package/src/components/DatePicker/types.ts +10 -5
- package/src/components/DesktopShell/DesktopShell.api.md +1 -1
- package/src/components/DesktopShell/DesktopShell.cy.ts +73 -2
- package/src/components/DesktopShell/DesktopShell.md +41 -6
- package/src/components/DesktopShell/DesktopShell.vue +18 -13
- package/src/components/DesktopShell/types.ts +15 -2
- package/src/components/Dialog/Dialog.api.md +8 -2
- package/src/components/Dialog/Dialog.cy.ts +106 -2
- package/src/components/Dialog/Dialog.md +48 -0
- package/src/components/Dialog/Dialog.vue +44 -24
- package/src/components/Dialog/index.ts +0 -1
- package/src/components/Dialog/types.ts +12 -8
- package/src/components/Duration/Duration.api.md +17 -2
- package/src/components/Duration/Duration.cy.ts +39 -0
- package/src/components/Duration/Duration.md +14 -1
- package/src/components/Duration/Duration.vue +17 -1
- package/src/components/Duration/types.ts +6 -5
- package/src/components/ErrorMessage/ErrorMessage.api.md +2 -2
- package/src/components/ErrorMessage/ErrorMessage.md +8 -0
- package/src/components/ErrorMessage/ErrorMessage.test.ts +57 -1
- package/src/components/ErrorMessage/ErrorMessage.vue +22 -21
- package/src/components/ErrorMessage/index.ts +1 -1
- package/src/components/ErrorMessage/stories/SeveralMessages.vue +12 -0
- package/src/components/ErrorMessage/types.ts +7 -2
- package/src/components/FormControl/FormControl.api.md +1 -1
- package/src/components/FormControl/FormControl.cy.ts +34 -0
- package/src/components/FormControl/FormControl.md +7 -0
- package/src/components/FormControl/FormControl.vue +17 -2
- package/src/components/FormControl/types.ts +2 -2
- package/src/components/FormLabel/FormLabel.cy.ts +9 -0
- package/src/components/FormLabel/FormLabel.vue +1 -1
- package/src/components/Menu/types.ts +2 -2
- package/src/components/MobileNav/MobileNav.api.md +1 -1
- package/src/components/MobileNav/types.ts +2 -2
- package/src/components/MobileShell/MobileShell.cy.ts +64 -2
- package/src/components/MobileShell/MobileShell.md +10 -4
- package/src/components/MobileShell/MobileShell.vue +23 -1
- package/src/components/MultiSelect/MultiSelect.api.md +3 -3
- package/src/components/MultiSelect/types.ts +6 -3
- package/src/components/PageHeader/PageHeader.api.md +1 -1
- package/src/components/PageHeader/PageHeader.cy.ts +40 -0
- package/src/components/PageHeader/PageHeader.md +14 -4
- package/src/components/PageHeader/PageHeaderBase.vue +8 -3
- package/src/components/PageHeader/PageHeaderMobile.vue +3 -3
- package/src/components/PageHeader/PageHeaderTarget.vue +5 -0
- package/src/components/PageHeader/PageHeaderTitle.vue +3 -4
- package/src/components/PageHeader/index.ts +10 -0
- package/src/components/PageHeader/target.ts +12 -1
- package/src/components/PageHeader/types.ts +7 -2
- package/src/components/PageHeader/useHeaderBack.ts +5 -4
- package/src/components/Password/Password.api.md +2 -2
- package/src/components/Radio/Radio.api.md +2 -2
- package/src/components/Radio/Radio.cy.ts +52 -0
- package/src/components/Radio/Radio.md +23 -0
- package/src/components/Radio/RadioGroup.vue +36 -3
- package/src/components/Radio/types.ts +5 -2
- package/src/components/Rating/Rating.api.md +3 -3
- package/src/components/Rating/Rating.cy.ts +53 -0
- package/src/components/Rating/Rating.md +10 -0
- package/src/components/Rating/Rating.vue +36 -2
- package/src/components/ScrollArea/ScrollArea.api.md +4 -20
- package/src/components/ScrollArea/ScrollArea.md +14 -4
- package/src/components/ScrollArea/index.ts +4 -6
- package/src/components/Select/Select.api.md +5 -5
- package/src/components/Select/Select.cy.ts +87 -0
- package/src/components/Select/Select.md +4 -1
- package/src/components/Select/Select.vue +25 -7
- package/src/components/Select/types.test.ts +25 -0
- package/src/components/Select/types.ts +12 -6
- package/src/components/Sidebar/Sidebar.api.md +1 -1
- package/src/components/Sidebar/Sidebar.md +4 -0
- package/src/components/Sidebar/types.ts +2 -2
- package/src/components/SidebarRail/SidebarRail.api.md +1 -1
- package/src/components/SidebarRail/types.ts +2 -2
- package/src/components/Slider/Slider.api.md +2 -2
- package/src/components/Slider/Slider.cy.ts +25 -0
- package/src/components/Slider/Slider.md +18 -0
- package/src/components/Slider/Slider.vue +16 -1
- package/src/components/Switch/Switch.api.md +2 -2
- package/src/components/Switch/Switch.cy.ts +36 -0
- package/src/components/Switch/Switch.md +23 -0
- package/src/components/Switch/Switch.vue +25 -3
- package/src/components/TabButtons/types.ts +2 -2
- package/src/components/Tabs/Tabs.api.md +1 -1
- package/src/components/Tabs/types.ts +3 -3
- package/src/components/TextInput/TextInput.api.md +2 -2
- package/src/components/TextInput/TextInput.cy.ts +34 -0
- package/src/components/TextInput/TextInput.md +16 -0
- package/src/components/TextInput/types.ts +6 -5
- package/src/components/Textarea/Textarea.api.md +2 -2
- package/src/components/TimePicker/TimePicker.api.md +4 -24
- package/src/components/TimePicker/TimePicker.cy.ts +127 -8
- package/src/components/TimePicker/TimePicker.md +27 -0
- package/src/components/TimePicker/TimePicker.vue +22 -20
- package/src/components/TimePicker/index.ts +1 -1
- package/src/components/TimePicker/types.ts +12 -6
- package/src/components/Toast/Toast.md +28 -0
- package/src/components/Toast/Toast.test.ts +10 -0
- package/src/components/Toast/types.ts +27 -0
- package/src/components/shared/picker/PickerShell.vue +30 -1
- package/src/components/shared/picker/types.ts +23 -6
- package/src/components/shared/route.ts +47 -0
- package/src/components/shared/selection/types.ts +57 -4
- package/src/composables/inputTypes.ts +16 -0
- package/src/composables/useColorScheme.spec.ts +227 -1
- package/src/composables/useColorScheme.ts +116 -6
- package/src/composables/useInputLabeling.ts +12 -26
- package/src/composables/useShellScrolled.spec.ts +53 -8
- package/src/composables/useShellScrolled.ts +52 -9
- package/src/data-fetching/staleWrites.test.ts +2 -1
- package/src/data-fetching/useAction.ts +10 -1
- package/src/data-fetching/useCall/useCall.test.ts +382 -2
- package/src/data-fetching/useCall/useCall.ts +73 -3
- package/src/data-fetching/useDoc/useDoc.test.ts +60 -4
- package/src/data-fetching/useDoc/useDoc.ts +36 -3
- package/src/data-fetching/useFrappeFetch.ts +28 -0
- package/src/data-fetching/useIsolatedCall.ts +20 -6
- package/src/data-fetching/useNewDoc/useNewDoc.test.ts +24 -0
- package/src/data-fetching/useNewDoc/useNewDoc.ts +22 -4
- package/src/exports.test.ts +191 -0
- package/src/index.ts +42 -11
- package/src/mocks/handlers.ts +8 -1
- package/src/molecules/editor/exports.test.ts +43 -0
- package/src/molecules/editor/index.ts +121 -2
- package/src/resources/index.ts +2 -2
- package/src/utils/call.ts +5 -9
- package/src/utils/dayjs.ts +7 -0
- package/src/utils/dialog.ts +15 -19
- package/src/utils/errorLines.test.ts +50 -0
- package/src/utils/errorLines.ts +36 -0
- package/src/utils/fileUploadHandler.test.ts +97 -0
- package/src/utils/fileUploadHandler.ts +20 -4
- package/src/utils/frappeRequest.ts +14 -5
- package/src/utils/iconString.test.ts +71 -0
- package/src/utils/iconString.ts +62 -0
- package/src/utils/plugin.test.ts +20 -0
- package/src/utils/plugin.ts +2 -2
- package/src/utils/useFileUpload.test.ts +157 -18
- package/src/utils/useFileUpload.ts +58 -23
|
@@ -60,9 +60,9 @@
|
|
|
60
60
|
},
|
|
61
61
|
{
|
|
62
62
|
name: 'error',
|
|
63
|
-
description: 'Error message rendered below the input. When set, the control receives\n`aria-invalid="true"` and `data-state="invalid"`.
|
|
63
|
+
description: 'Error message rendered below the input. When set, the control receives\n`aria-invalid="true"` and `data-state="invalid"`. Takes a string, an\narray of strings, or an `Error` whose `messages` are rendered as stacked\nlines (with `Error.message` as the fallback). This is the same value\n`ErrorMessage.message` takes. An empty array and an empty string both\nmean no error.',
|
|
64
64
|
required: false,
|
|
65
|
-
type: '
|
|
65
|
+
type: 'ErrorMessageValue'
|
|
66
66
|
},
|
|
67
67
|
{
|
|
68
68
|
name: 'required',
|
package/experimental.ts
CHANGED
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.70",
|
|
4
4
|
"description": "A set of components and utilities for rapid UI development",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20.19.0"
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"tokens-v2": "./tailwind/migrate-tokens-v2.js",
|
|
27
27
|
"shortcuts-v1": "./scripts/migrate-shortcuts-v1.js",
|
|
28
28
|
"editor-v1": "./scripts/migrate-editor-v1.js",
|
|
29
|
-
"destinations-v1": "./scripts/migrate-destinations-v1.js"
|
|
29
|
+
"destinations-v1": "./scripts/migrate-destinations-v1.js",
|
|
30
|
+
"data-v1": "./scripts/migrate-data-v1.js"
|
|
30
31
|
},
|
|
31
32
|
"scripts": {
|
|
32
33
|
"test": "vitest --run",
|
|
@@ -113,6 +114,8 @@
|
|
|
113
114
|
"scripts/migrate-list-v1.js",
|
|
114
115
|
"scripts/migrate-navigation-v1.js",
|
|
115
116
|
"scripts/migrate-overlays-v1.js",
|
|
117
|
+
"scripts/migrate-destinations-v1.js",
|
|
118
|
+
"scripts/migrate-data-v1.js",
|
|
116
119
|
"experimental",
|
|
117
120
|
"experimental.ts",
|
|
118
121
|
"tsconfig.base.json"
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Data v1 migration codemod.
|
|
4
|
+
*
|
|
5
|
+
* The FrappeUI plugin's `resources` option is a boolean in v1. The object
|
|
6
|
+
* form was never read — the plugin only checked whether the value was set —
|
|
7
|
+
* so every object value becomes `true`:
|
|
8
|
+
*
|
|
9
|
+
* app.use(FrappeUI, { resources: { todos } }) -> app.use(FrappeUI, { resources: true })
|
|
10
|
+
*
|
|
11
|
+
* A value the codemod cannot read (a variable, a call, a spread) is reported
|
|
12
|
+
* and the file is left unchanged.
|
|
13
|
+
*
|
|
14
|
+
* Usage: data-v1 [--dry-run] <dir-or-file...>
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from 'node:fs'
|
|
18
|
+
import path from 'node:path'
|
|
19
|
+
import { fileURLToPath } from 'node:url'
|
|
20
|
+
import ts from 'typescript'
|
|
21
|
+
import { parse as parseSfc } from '@vue/compiler-sfc'
|
|
22
|
+
|
|
23
|
+
const USAGE = `Usage: data-v1 [--dry-run] <dir-or-file...>
|
|
24
|
+
|
|
25
|
+
Narrows the FrappeUI plugin's resources option to a boolean:
|
|
26
|
+
app.use(FrappeUI, { resources: { … } }) -> app.use(FrappeUI, { resources: true })`
|
|
27
|
+
|
|
28
|
+
const SCRIPT_EXTENSIONS = new Set([
|
|
29
|
+
'.js',
|
|
30
|
+
'.mjs',
|
|
31
|
+
'.cjs',
|
|
32
|
+
'.jsx',
|
|
33
|
+
'.ts',
|
|
34
|
+
'.mts',
|
|
35
|
+
'.cts',
|
|
36
|
+
'.tsx',
|
|
37
|
+
])
|
|
38
|
+
const EXTENSIONS = new Set([...SCRIPT_EXTENSIONS, '.vue'])
|
|
39
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'coverage'])
|
|
40
|
+
|
|
41
|
+
function walk(target, visited = new Set()) {
|
|
42
|
+
if (fs.lstatSync(target).isSymbolicLink()) return []
|
|
43
|
+
const resolved = fs.realpathSync(target)
|
|
44
|
+
if (visited.has(resolved)) return []
|
|
45
|
+
visited.add(resolved)
|
|
46
|
+
if (fs.statSync(resolved).isFile()) {
|
|
47
|
+
return EXTENSIONS.has(path.extname(target)) ? [target] : []
|
|
48
|
+
}
|
|
49
|
+
return fs.readdirSync(resolved, { withFileTypes: true }).flatMap((entry) => {
|
|
50
|
+
if (SKIP_DIRS.has(entry.name)) return []
|
|
51
|
+
return walk(path.join(target, entry.name), visited)
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function lineAt(source, offset) {
|
|
56
|
+
return source.slice(0, offset).split('\n').length
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function scriptKindFor(filename) {
|
|
60
|
+
const extension = path.extname(filename)
|
|
61
|
+
if (extension === '.tsx' || extension === '.jsx') return ts.ScriptKind.TSX
|
|
62
|
+
return ts.ScriptKind.TS
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Names the frappe-ui plugin is bound to in this chunk.
|
|
67
|
+
*
|
|
68
|
+
* `FrappeUI` is a named export, so `import { FrappeUI }` and
|
|
69
|
+
* `import { FrappeUI as Plugin }` are the two direct forms. A namespace
|
|
70
|
+
* import is matched through its `.FrappeUI` property access instead.
|
|
71
|
+
*/
|
|
72
|
+
function pluginBindings(sourceFile) {
|
|
73
|
+
const direct = new Set()
|
|
74
|
+
const namespaces = new Set()
|
|
75
|
+
for (const statement of sourceFile.statements) {
|
|
76
|
+
if (!ts.isImportDeclaration(statement)) continue
|
|
77
|
+
if (!ts.isStringLiteral(statement.moduleSpecifier)) continue
|
|
78
|
+
if (statement.moduleSpecifier.text !== 'frappe-ui') continue
|
|
79
|
+
const bindings = statement.importClause?.namedBindings
|
|
80
|
+
if (!bindings) continue
|
|
81
|
+
if (ts.isNamespaceImport(bindings)) {
|
|
82
|
+
namespaces.add(bindings.name.text)
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
for (const element of bindings.elements) {
|
|
86
|
+
const imported = element.propertyName?.text ?? element.name.text
|
|
87
|
+
if (imported === 'FrappeUI') direct.add(element.name.text)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { direct, namespaces }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Names a node declares in its own scope. Parameters included. */
|
|
94
|
+
function declaredNames(node) {
|
|
95
|
+
const names = new Set()
|
|
96
|
+
const addBindingName = (name) => {
|
|
97
|
+
if (ts.isIdentifier(name)) {
|
|
98
|
+
names.add(name.text)
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
for (const element of name.elements ?? []) {
|
|
102
|
+
if (ts.isOmittedExpression(element)) continue
|
|
103
|
+
addBindingName(element.name)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const addStatement = (statement) => {
|
|
108
|
+
if (ts.isVariableStatement(statement)) {
|
|
109
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
110
|
+
addBindingName(declaration.name)
|
|
111
|
+
}
|
|
112
|
+
} else if (
|
|
113
|
+
(ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement)) &&
|
|
114
|
+
statement.name
|
|
115
|
+
) {
|
|
116
|
+
names.add(statement.name.text)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
for (const parameter of node.parameters ?? []) addBindingName(parameter.name)
|
|
121
|
+
// `catch (error)` binds its parameter through `variableDeclaration`, not
|
|
122
|
+
// `parameters`.
|
|
123
|
+
if (ts.isCatchClause(node) && node.variableDeclaration) {
|
|
124
|
+
addBindingName(node.variableDeclaration.name)
|
|
125
|
+
}
|
|
126
|
+
// A named function expression binds its own name inside its body, and
|
|
127
|
+
// nowhere else. `const f = function FrappeUI() { … }` shadows the import.
|
|
128
|
+
if (
|
|
129
|
+
(ts.isFunctionExpression(node) || ts.isClassExpression(node)) &&
|
|
130
|
+
node.name
|
|
131
|
+
) {
|
|
132
|
+
names.add(node.name.text)
|
|
133
|
+
}
|
|
134
|
+
// A source file, a bare block and a namespace body hold their own
|
|
135
|
+
// statements. Everything else that opens a scope keeps them under `body`,
|
|
136
|
+
// which is undefined for those, so read the node itself in those cases.
|
|
137
|
+
const body =
|
|
138
|
+
ts.isSourceFile(node) || ts.isBlock(node) || ts.isModuleBlock(node)
|
|
139
|
+
? node
|
|
140
|
+
: node.body
|
|
141
|
+
if (body && 'statements' in body) {
|
|
142
|
+
for (const statement of body.statements) addStatement(statement)
|
|
143
|
+
}
|
|
144
|
+
// `switch` bodies share one scope across every clause, and a clause without
|
|
145
|
+
// braces keeps its statements on the clause, not on a block.
|
|
146
|
+
if (ts.isCaseBlock(node)) {
|
|
147
|
+
for (const clause of node.clauses) {
|
|
148
|
+
for (const statement of clause.statements) addStatement(statement)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// `for (…;;)`, `for…of` and `for…in` all bind their loop variable in the
|
|
152
|
+
// loop's own scope, through `initializer`.
|
|
153
|
+
if (
|
|
154
|
+
(ts.isForStatement(node) ||
|
|
155
|
+
ts.isForOfStatement(node) ||
|
|
156
|
+
ts.isForInStatement(node)) &&
|
|
157
|
+
node.initializer &&
|
|
158
|
+
ts.isVariableDeclarationList(node.initializer)
|
|
159
|
+
) {
|
|
160
|
+
for (const declaration of node.initializer.declarations) {
|
|
161
|
+
addBindingName(declaration.name)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return names
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function opensScope(node) {
|
|
168
|
+
return (
|
|
169
|
+
ts.isSourceFile(node) ||
|
|
170
|
+
ts.isFunctionDeclaration(node) ||
|
|
171
|
+
ts.isFunctionExpression(node) ||
|
|
172
|
+
ts.isArrowFunction(node) ||
|
|
173
|
+
ts.isMethodDeclaration(node) ||
|
|
174
|
+
ts.isConstructorDeclaration(node) ||
|
|
175
|
+
ts.isGetAccessorDeclaration(node) ||
|
|
176
|
+
ts.isSetAccessorDeclaration(node) ||
|
|
177
|
+
ts.isBlock(node) ||
|
|
178
|
+
ts.isCaseBlock(node) ||
|
|
179
|
+
ts.isModuleBlock(node) ||
|
|
180
|
+
ts.isClassExpression(node) ||
|
|
181
|
+
ts.isForStatement(node) ||
|
|
182
|
+
ts.isForOfStatement(node) ||
|
|
183
|
+
ts.isForInStatement(node) ||
|
|
184
|
+
ts.isCatchClause(node)
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function propertyName(property) {
|
|
189
|
+
const name = property.name
|
|
190
|
+
if (!name) return undefined
|
|
191
|
+
if (ts.isIdentifier(name)) return name.text
|
|
192
|
+
if (ts.isStringLiteral(name)) return name.text
|
|
193
|
+
return undefined
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Rewrites one script chunk. `offset` maps chunk positions back onto the
|
|
198
|
+
* original file so refusal line numbers and edits line up in `.vue` files.
|
|
199
|
+
*/
|
|
200
|
+
function migrateChunk(text, offset, filename, source) {
|
|
201
|
+
const sourceFile = ts.createSourceFile(
|
|
202
|
+
filename,
|
|
203
|
+
text,
|
|
204
|
+
ts.ScriptTarget.Latest,
|
|
205
|
+
true,
|
|
206
|
+
scriptKindFor(filename),
|
|
207
|
+
)
|
|
208
|
+
const { direct, namespaces } = pluginBindings(sourceFile)
|
|
209
|
+
if (!direct.size && !namespaces.size) return { edits: [], refusals: [] }
|
|
210
|
+
|
|
211
|
+
const edits = []
|
|
212
|
+
const refusals = []
|
|
213
|
+
const refuse = (node, message) => {
|
|
214
|
+
refusals.push({ line: lineAt(source, offset + node.getStart(sourceFile)), message })
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** True when `name` resolves to something other than the frappe-ui import. */
|
|
218
|
+
const isShadowed = (name, scopes) => {
|
|
219
|
+
for (let index = scopes.length - 1; index > 0; index--) {
|
|
220
|
+
if (scopes[index].has(name)) return true
|
|
221
|
+
}
|
|
222
|
+
return false
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const isPluginArgument = (argument, scopes) => {
|
|
226
|
+
if (ts.isIdentifier(argument)) {
|
|
227
|
+
return direct.has(argument.text) && !isShadowed(argument.text, scopes)
|
|
228
|
+
}
|
|
229
|
+
if (
|
|
230
|
+
ts.isPropertyAccessExpression(argument) &&
|
|
231
|
+
ts.isIdentifier(argument.expression) &&
|
|
232
|
+
argument.name.text === 'FrappeUI'
|
|
233
|
+
) {
|
|
234
|
+
return (
|
|
235
|
+
namespaces.has(argument.expression.text) &&
|
|
236
|
+
!isShadowed(argument.expression.text, scopes)
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
return false
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const visitCall = (node, scopes) => {
|
|
243
|
+
if (!ts.isPropertyAccessExpression(node.expression)) return
|
|
244
|
+
if (node.expression.name.text !== 'use') return
|
|
245
|
+
const [plugin, options] = node.arguments
|
|
246
|
+
if (!plugin || !isPluginArgument(plugin, scopes)) return
|
|
247
|
+
if (!options) return
|
|
248
|
+
|
|
249
|
+
if (!ts.isObjectLiteralExpression(options)) {
|
|
250
|
+
refuse(
|
|
251
|
+
options,
|
|
252
|
+
'app.use(FrappeUI, …) receives options this codemod cannot read. Set `resources` to true or false by hand if the value is an object.',
|
|
253
|
+
)
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
for (const property of options.properties) {
|
|
258
|
+
if (ts.isSpreadAssignment(property)) {
|
|
259
|
+
refuse(
|
|
260
|
+
property,
|
|
261
|
+
'the FrappeUI options object spreads another value that may carry `resources`. Narrow it to a boolean by hand.',
|
|
262
|
+
)
|
|
263
|
+
continue
|
|
264
|
+
}
|
|
265
|
+
if (
|
|
266
|
+
ts.isPropertyAssignment(property) &&
|
|
267
|
+
property.name &&
|
|
268
|
+
ts.isComputedPropertyName(property.name)
|
|
269
|
+
) {
|
|
270
|
+
refuse(
|
|
271
|
+
property,
|
|
272
|
+
'the FrappeUI options object uses a computed key that may be `resources`. Narrow it to a boolean by hand.',
|
|
273
|
+
)
|
|
274
|
+
continue
|
|
275
|
+
}
|
|
276
|
+
if (propertyName(property) !== 'resources') continue
|
|
277
|
+
|
|
278
|
+
if (ts.isShorthandPropertyAssignment(property)) {
|
|
279
|
+
refuse(
|
|
280
|
+
property,
|
|
281
|
+
'`resources` is bound to a variable. Pass true or false instead.',
|
|
282
|
+
)
|
|
283
|
+
continue
|
|
284
|
+
}
|
|
285
|
+
if (!ts.isPropertyAssignment(property)) continue
|
|
286
|
+
|
|
287
|
+
const value = property.initializer
|
|
288
|
+
if (
|
|
289
|
+
value.kind === ts.SyntaxKind.TrueKeyword ||
|
|
290
|
+
value.kind === ts.SyntaxKind.FalseKeyword
|
|
291
|
+
) {
|
|
292
|
+
continue
|
|
293
|
+
}
|
|
294
|
+
if (!ts.isObjectLiteralExpression(value)) {
|
|
295
|
+
refuse(
|
|
296
|
+
value,
|
|
297
|
+
'`resources` is set to a value this codemod cannot read. Pass true or false instead.',
|
|
298
|
+
)
|
|
299
|
+
continue
|
|
300
|
+
}
|
|
301
|
+
edits.push({
|
|
302
|
+
start: offset + value.getStart(sourceFile),
|
|
303
|
+
end: offset + value.getEnd(),
|
|
304
|
+
text: 'true',
|
|
305
|
+
})
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const visit = (node, scopes) => {
|
|
310
|
+
const nextScopes = opensScope(node)
|
|
311
|
+
? [...scopes, declaredNames(node)]
|
|
312
|
+
: scopes
|
|
313
|
+
if (ts.isCallExpression(node)) visitCall(node, nextScopes)
|
|
314
|
+
ts.forEachChild(node, (child) => visit(child, nextScopes))
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
visit(sourceFile, [declaredNames(sourceFile)])
|
|
318
|
+
return { edits, refusals }
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function migrateData(source, filename = 'source.ts') {
|
|
322
|
+
const chunks = []
|
|
323
|
+
if (path.extname(filename) === '.vue') {
|
|
324
|
+
const parsed = parseSfc(source, { filename })
|
|
325
|
+
if (parsed.errors.length) {
|
|
326
|
+
return {
|
|
327
|
+
migrated: source,
|
|
328
|
+
changes: [],
|
|
329
|
+
refusals: [
|
|
330
|
+
{
|
|
331
|
+
line: 1,
|
|
332
|
+
message: `could not be parsed as a Vue single-file component (${parsed.errors[0].message}). This file is unchanged.`,
|
|
333
|
+
},
|
|
334
|
+
],
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const { script, scriptSetup } = parsed.descriptor
|
|
338
|
+
for (const block of [script, scriptSetup]) {
|
|
339
|
+
if (!block) continue
|
|
340
|
+
const lang = block.lang === 'tsx' || block.lang === 'jsx' ? 'tsx' : 'ts'
|
|
341
|
+
chunks.push({
|
|
342
|
+
text: block.content,
|
|
343
|
+
offset: block.loc.start.offset,
|
|
344
|
+
name: `${filename}.${lang}`,
|
|
345
|
+
})
|
|
346
|
+
}
|
|
347
|
+
} else {
|
|
348
|
+
chunks.push({ text: source, offset: 0, name: filename })
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const edits = []
|
|
352
|
+
const refusals = []
|
|
353
|
+
for (const chunk of chunks) {
|
|
354
|
+
const result = migrateChunk(chunk.text, chunk.offset, chunk.name, source)
|
|
355
|
+
edits.push(...result.edits)
|
|
356
|
+
refusals.push(...result.refusals)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (refusals.length) return { migrated: source, changes: [], refusals }
|
|
360
|
+
const migrated = [...edits]
|
|
361
|
+
.sort((a, b) => b.start - a.start)
|
|
362
|
+
.reduce(
|
|
363
|
+
(text, edit) => text.slice(0, edit.start) + edit.text + text.slice(edit.end),
|
|
364
|
+
source,
|
|
365
|
+
)
|
|
366
|
+
return { migrated, changes: edits, refusals }
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function main() {
|
|
370
|
+
const args = process.argv.slice(2)
|
|
371
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
372
|
+
console.log(USAGE)
|
|
373
|
+
return
|
|
374
|
+
}
|
|
375
|
+
const unknown = args.filter(
|
|
376
|
+
(arg) => arg.startsWith('-') && arg !== '--dry-run',
|
|
377
|
+
)
|
|
378
|
+
const targets = args.filter((arg) => !arg.startsWith('-'))
|
|
379
|
+
if (unknown.length || !targets.length) {
|
|
380
|
+
if (unknown.length) console.error(`Unknown option: ${unknown.join(' ')}`)
|
|
381
|
+
console.error(USAGE)
|
|
382
|
+
process.exit(1)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
for (const target of targets) {
|
|
386
|
+
if (!fs.existsSync(target)) {
|
|
387
|
+
console.error(`Invalid path: ${target}`)
|
|
388
|
+
process.exit(1)
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const dryRun = args.includes('--dry-run')
|
|
393
|
+
const files = [...new Set(targets.flatMap((target) => walk(target)))]
|
|
394
|
+
let changed = 0
|
|
395
|
+
const refusals = []
|
|
396
|
+
for (const file of files) {
|
|
397
|
+
const source = fs.readFileSync(file, 'utf8')
|
|
398
|
+
const result = migrateData(source, file)
|
|
399
|
+
if (result.refusals.length) {
|
|
400
|
+
for (const refusal of result.refusals) refusals.push({ file, ...refusal })
|
|
401
|
+
continue
|
|
402
|
+
}
|
|
403
|
+
if (!result.changes.length) continue
|
|
404
|
+
changed++
|
|
405
|
+
if (!dryRun) fs.writeFileSync(file, result.migrated)
|
|
406
|
+
console.log(`${file} (${result.changes.length})`)
|
|
407
|
+
}
|
|
408
|
+
console.log(
|
|
409
|
+
`\n${dryRun ? '[dry-run] would update' : 'Updated'} ${changed} files`,
|
|
410
|
+
)
|
|
411
|
+
if (refusals.length) {
|
|
412
|
+
console.error(
|
|
413
|
+
`\nNot converted — ${refusals.length} sites need a decision; affected files were left unchanged:`,
|
|
414
|
+
)
|
|
415
|
+
for (const refusal of refusals)
|
|
416
|
+
console.error(` ${refusal.file}:L${refusal.line} ${refusal.message}`)
|
|
417
|
+
process.exit(1)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const scriptPath = fileURLToPath(import.meta.url)
|
|
422
|
+
const invokedPath = process.argv[1]
|
|
423
|
+
if (invokedPath && fs.realpathSync(invokedPath) === fs.realpathSync(scriptPath))
|
|
424
|
+
main()
|
package/src/charts/tokens.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { computed, type ComputedRef, type Ref } from 'vue'
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
getResolvedColorScheme,
|
|
4
4
|
type ResolvedColorScheme,
|
|
5
5
|
} from '../composables/useColorScheme'
|
|
6
6
|
import { documentAttributes } from './utils'
|
|
@@ -207,7 +207,7 @@ function isTransparent(color: string) {
|
|
|
207
207
|
* lookup so a subtree that redefines `--chart-*` wins over the document root.
|
|
208
208
|
*/
|
|
209
209
|
export function resolveChartTokens(el?: HTMLElement | null): ChartTokens {
|
|
210
|
-
const scheme =
|
|
210
|
+
const scheme = getResolvedColorScheme()
|
|
211
211
|
const fallbacks = FALLBACK_TOKENS[scheme]
|
|
212
212
|
|
|
213
213
|
if (typeof window === 'undefined' || typeof getComputedStyle !== 'function') {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
28
|
name: 'icon',
|
|
29
|
-
description: 'Status icon: unset shows the theme\'s auto icon (gray shows the info glyph in black ink), `false` hides it, a `lucide-*` string or Component renders a custom theme-colored glyph',
|
|
29
|
+
description: 'Status icon: unset or `true` shows the theme\'s auto icon (gray shows the info glyph in black ink), `false` hides it, a `lucide-*` string or Component renders a custom theme-colored glyph',
|
|
30
30
|
required: false,
|
|
31
31
|
type: 'boolean | string | Component'
|
|
32
32
|
},
|
|
@@ -36,4 +36,40 @@ An info banner with one action and a × button in the corner.
|
|
|
36
36
|
|
|
37
37
|
<ComponentPreview name="Alert-ImportProgress" />
|
|
38
38
|
|
|
39
|
+
## The icon prop
|
|
40
|
+
|
|
41
|
+
`icon` has three states. Unset or `true` renders the theme's own status glyph
|
|
42
|
+
(gray shows the info glyph in black ink). `false` hides it. A `lucide-*` class
|
|
43
|
+
name or a Vue component renders that glyph instead, in the theme's color.
|
|
44
|
+
|
|
45
|
+
```vue
|
|
46
|
+
<Alert title="Saved" theme="green" />
|
|
47
|
+
<!-- the theme's auto icon -->
|
|
48
|
+
<Alert title="Saved" theme="green" :icon="true" />
|
|
49
|
+
<!-- the same auto icon, stated -->
|
|
50
|
+
<Alert title="Saved" :icon="false" />
|
|
51
|
+
<!-- no icon -->
|
|
52
|
+
<Alert title="Saved" icon="lucide-rocket" />
|
|
53
|
+
<!-- your own -->
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Styling
|
|
57
|
+
|
|
58
|
+
The tone is on the root as `data-color`, and the computed layout as
|
|
59
|
+
`data-layout`:
|
|
60
|
+
|
|
61
|
+
| Hook | Meaning |
|
|
62
|
+
| ----------------------------------------------- | ------------------------------------------- |
|
|
63
|
+
| `[data-color="gray\|blue\|green\|amber\|red"]` | the `theme` prop |
|
|
64
|
+
| `[data-layout="row\|banner"]` | one line, or the stacked layout |
|
|
65
|
+
|
|
66
|
+
`data-color`, not `data-theme`: `data-theme` is the light/dark attribute the
|
|
67
|
+
app sets on the document, and an alert must not look like a theme root.
|
|
68
|
+
|
|
69
|
+
```css
|
|
70
|
+
:where([data-color='red']) {
|
|
71
|
+
/* your overrides */
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
39
75
|
<!-- @include: ./Alert.api.md -->
|
|
@@ -30,7 +30,7 @@ export const alertProps = {
|
|
|
30
30
|
description: { type: String, default: undefined },
|
|
31
31
|
/** Color theme of the status icon and the row action label; the container never changes with theme */
|
|
32
32
|
theme: { type: String as PropType<StatusTheme>, default: 'gray' },
|
|
33
|
-
/** Status icon: unset shows the theme's auto icon (gray shows the info glyph in black ink), `false` hides it, a `lucide-*` string or Component renders a custom theme-colored glyph */
|
|
33
|
+
/** Status icon: unset or `true` shows the theme's auto icon (gray shows the info glyph in black ink), `false` hides it, a `lucide-*` string or Component renders a custom theme-colored glyph */
|
|
34
34
|
icon: {
|
|
35
35
|
type: [Boolean, String, Object, Function] as PropType<
|
|
36
36
|
boolean | string | Component
|
|
@@ -37,8 +37,15 @@ starts inside a scrolled list scrolls that list instead of moving the sheet; it
|
|
|
37
37
|
becomes a drag once the list is back at its top. A drag closes the sheet if it
|
|
38
38
|
passes a distance threshold or ends in a fast flick, and springs back otherwise.
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
Put `data-no-sheet-drag` on an element that owns its own horizontal or vertical
|
|
41
|
+
gesture — a carousel, a map, a slider — and a drag starting there never moves
|
|
42
|
+
the sheet:
|
|
43
|
+
|
|
44
|
+
```vue
|
|
45
|
+
<BottomSheet v-model:open="open">
|
|
46
|
+
<ImageCarousel data-no-sheet-drag />
|
|
47
|
+
</BottomSheet>
|
|
48
|
+
```
|
|
42
49
|
|
|
43
50
|
## Events
|
|
44
51
|
|
|
@@ -528,6 +528,37 @@ describe('BottomSheet drag to dismiss', () => {
|
|
|
528
528
|
app.unmount()
|
|
529
529
|
})
|
|
530
530
|
|
|
531
|
+
// SHELL-Q9: the opt-out attribute is public and keeps this name.
|
|
532
|
+
it('leaves a gesture that starts on data-no-sheet-drag alone', async () => {
|
|
533
|
+
const { sheet, content, app } = await openSheet()
|
|
534
|
+
const carousel = document.createElement('div')
|
|
535
|
+
carousel.setAttribute('data-no-sheet-drag', '')
|
|
536
|
+
content.appendChild(carousel)
|
|
537
|
+
|
|
538
|
+
touch('touchstart', carousel, 0, 300)
|
|
539
|
+
const move = touch('touchmove', carousel, 0, 320)
|
|
540
|
+
|
|
541
|
+
expect(move.defaultPrevented).toBe(false)
|
|
542
|
+
expect(isDragging(sheet)).toBe(false)
|
|
543
|
+
app.unmount()
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
it('also covers a child of a data-no-sheet-drag element', async () => {
|
|
547
|
+
const { sheet, content, app } = await openSheet()
|
|
548
|
+
const carousel = document.createElement('div')
|
|
549
|
+
carousel.setAttribute('data-no-sheet-drag', '')
|
|
550
|
+
const slide = document.createElement('div')
|
|
551
|
+
carousel.appendChild(slide)
|
|
552
|
+
content.appendChild(carousel)
|
|
553
|
+
|
|
554
|
+
touch('touchstart', slide, 0, 300)
|
|
555
|
+
const move = touch('touchmove', slide, 0, 320)
|
|
556
|
+
|
|
557
|
+
expect(move.defaultPrevented).toBe(false)
|
|
558
|
+
expect(isDragging(sheet)).toBe(false)
|
|
559
|
+
app.unmount()
|
|
560
|
+
})
|
|
561
|
+
|
|
531
562
|
it('drags a sheet that was mounted closed and opened afterwards', async () => {
|
|
532
563
|
const { sheet, content, openUpdates, app } = await openSheet(
|
|
533
564
|
{},
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
honest when a mobile URL bar collapses.
|
|
15
15
|
-->
|
|
16
16
|
<DialogContent
|
|
17
|
+
data-slot="content"
|
|
17
18
|
class="fixed inset-x-0 bottom-0 z-50 mx-auto flex max-h-[90dvh] w-full max-w-2xl flex-col rounded-t-[36px] bg-surface-base shadow-lg [corner-shape:squircle] bottom-sheet-content focus:outline-none after:pointer-events-none after:absolute after:inset-x-0 after:top-full after:h-screen after:bg-surface-base"
|
|
18
19
|
:aria-label="title || 'Bottom sheet'"
|
|
19
20
|
@escape-key-down="onDismissAttempt"
|
|
@@ -101,9 +102,12 @@ const handleRef = ref<HTMLElement | null>(null)
|
|
|
101
102
|
* fires exactly once, with the comment, and the gesture is bound to nothing.
|
|
102
103
|
* `handleRef` is a plain element ref, which Vue re-runs every time the content
|
|
103
104
|
* mounts and unmounts, so this re-resolves on every open.
|
|
105
|
+
*
|
|
106
|
+
* The query reads the sheet's own `data-slot` hook, the same attribute an app
|
|
107
|
+
* styles it with. `closest` stops at the nearest match, which is this sheet.
|
|
104
108
|
*/
|
|
105
109
|
const sheetEl = computed(
|
|
106
|
-
() => handleRef.value?.closest<HTMLElement>('
|
|
110
|
+
() => handleRef.value?.closest<HTMLElement>('[data-slot="content"]') ?? null,
|
|
107
111
|
)
|
|
108
112
|
|
|
109
113
|
/*
|
|
@@ -38,4 +38,20 @@ const items = [
|
|
|
38
38
|
]
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
An item also carries any extra field you put on it. `BreadcrumbItem` keeps an
|
|
42
|
+
open index signature, so a crumb can hold the record it came from and the
|
|
43
|
+
`#prefix` / `#suffix` slots can read it back:
|
|
44
|
+
|
|
45
|
+
```vue
|
|
46
|
+
<Breadcrumbs :items="items">
|
|
47
|
+
<template #prefix="{ item }">
|
|
48
|
+
<Avatar v-if="item.user" :image="item.user.avatar" size="sm" />
|
|
49
|
+
</template>
|
|
50
|
+
</Breadcrumbs>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Both slots are named for where they render, not for what goes in them:
|
|
54
|
+
`#prefix` renders before the crumb's label and `#suffix` after it. Each
|
|
55
|
+
receives `{ item }`, and they render for every crumb.
|
|
56
|
+
|
|
41
57
|
<!-- @include: ./Breadcrumbs.api.md -->
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { RouteDestination } from '../shared/route'
|
|
2
2
|
|
|
3
3
|
export interface BreadcrumbItem {
|
|
4
4
|
/** Text shown for the breadcrumb item */
|
|
5
5
|
label: string
|
|
6
6
|
|
|
7
7
|
/** Route location used when the item is a link */
|
|
8
|
-
route?:
|
|
8
|
+
route?: RouteDestination
|
|
9
9
|
|
|
10
10
|
/** URL used when the item is a plain link, ignored if `route` is set */
|
|
11
11
|
href?: string
|