frappe-ui 1.0.0-beta.65 → 1.0.0-beta.67

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 (50) hide show
  1. package/package.json +5 -2
  2. package/scripts/migrate-destinations-v1.js +229 -0
  3. package/scripts/migrate-editor-v1.js +477 -0
  4. package/src/components/Button/Button.api.md +2 -2
  5. package/src/components/Button/Button.cy.ts +4 -4
  6. package/src/components/Button/Button.vue +2 -2
  7. package/src/components/Button/types.ts +2 -2
  8. package/src/components/MobileNav/MobileNav.api.md +8 -2
  9. package/src/components/MobileNav/MobileNav.cy.ts +18 -5
  10. package/src/components/MobileNav/MobileNav.md +6 -5
  11. package/src/components/MobileNav/MobileNavItem.vue +10 -6
  12. package/src/components/MobileNav/types.ts +5 -2
  13. package/src/components/PageHeader/PageHeader.api.md +1 -1
  14. package/src/components/PageHeader/PageHeader.cy.ts +24 -0
  15. package/src/components/PageHeader/PageHeader.md +3 -2
  16. package/src/components/PageHeader/PageHeaderBackButton.vue +1 -1
  17. package/src/components/PageHeader/types.ts +1 -1
  18. package/src/components/PageHeader/useHeaderBack.ts +8 -8
  19. package/src/components/Sidebar/Sidebar.api.md +8 -2
  20. package/src/components/Sidebar/Sidebar.cy.ts +13 -4
  21. package/src/components/Sidebar/Sidebar.md +5 -3
  22. package/src/components/Sidebar/SidebarItem.vue +19 -8
  23. package/src/components/Sidebar/types.ts +5 -2
  24. package/src/components/SidebarRail/SidebarRail.api.md +7 -1
  25. package/src/components/SidebarRail/SidebarRail.cy.ts +14 -3
  26. package/src/components/SidebarRail/SidebarRail.md +2 -1
  27. package/src/components/SidebarRail/SidebarRailItem.vue +9 -9
  28. package/src/components/SidebarRail/types.ts +4 -1
  29. package/src/components/TabButtons/TabButtons.md +4 -5
  30. package/src/components/TabButtons/TabButtons.vue +3 -3
  31. package/src/components/TabButtons/types.ts +11 -4
  32. package/src/molecules/editor/EditorFixedMenu.vue +2 -2
  33. package/src/molecules/editor/MenuItems.vue +2 -2
  34. package/src/molecules/editor/SuggestionExtension.ts +10 -3
  35. package/src/molecules/editor/extensions/emoji/emoji-extension.ts +1 -1
  36. package/src/molecules/editor/extensions/mention/mention-extension.test.ts +17 -1
  37. package/src/molecules/editor/extensions/mention/mention-extension.ts +11 -14
  38. package/src/molecules/editor/extensions/shared/suggestion-open.test.ts +1 -1
  39. package/src/molecules/editor/extensions/slash-commands/slash-commands-extension.ts +1 -1
  40. package/src/molecules/editor/extensions/suggestion/createSuggestionExtension.ts +2 -2
  41. package/src/molecules/editor/extensions/tag/tag-extension.ts +1 -1
  42. package/src/molecules/editor/kits.ts +1 -1
  43. package/src/molecules/editor/menu.test.ts +11 -0
  44. package/src/molecules/editor/useEditor.test.ts +30 -2
  45. package/src/molecules/list/List.cy.ts +6 -4
  46. package/src/molecules/list/ListRow.vue +4 -1
  47. package/src/molecules/list/list.api.md +8 -2
  48. package/src/molecules/list/list.md +4 -3
  49. package/src/molecules/list/types.ts +5 -2
  50. package/src/utils/warnDeprecated.ts +21 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frappe-ui",
3
- "version": "1.0.0-beta.65",
3
+ "version": "1.0.0-beta.67",
4
4
  "description": "A set of components and utilities for rapid UI development",
5
5
  "engines": {
6
6
  "node": ">=20.19.0"
@@ -20,7 +20,9 @@
20
20
  },
21
21
  "bin": {
22
22
  "tokens-v2": "./tailwind/migrate-tokens-v2.js",
23
- "shortcuts-v1": "./scripts/migrate-shortcuts-v1.js"
23
+ "shortcuts-v1": "./scripts/migrate-shortcuts-v1.js",
24
+ "editor-v1": "./scripts/migrate-editor-v1.js",
25
+ "destinations-v1": "./scripts/migrate-destinations-v1.js"
24
26
  },
25
27
  "scripts": {
26
28
  "test": "vitest --run",
@@ -102,6 +104,7 @@
102
104
  "tailwind",
103
105
  "vitepress",
104
106
  "scripts/migrate-shortcuts-v1.js",
107
+ "scripts/migrate-editor-v1.js",
105
108
  "experimental",
106
109
  "experimental.ts",
107
110
  "tsconfig.base.json"
@@ -0,0 +1,229 @@
1
+ #!/usr/bin/env node
2
+ /** Rename pre-v1 navigation props to the v1 destination vocabulary.
3
+ * Usage: destinations-v1 [--dry-run] <dir-or-file...>
4
+ */
5
+ import fs from 'node:fs'
6
+ import path from 'node:path'
7
+ import { fileURLToPath } from 'node:url'
8
+
9
+ const USAGE = 'Usage: destinations-v1 [--dry-run] <dir-or-file...>'
10
+ const EXTENSIONS = new Set(['.vue', '.md'])
11
+ const PROP_RENAMES = {
12
+ Button: { link: 'href' },
13
+ PageHeaderBackButton: { to: 'fallback-route' },
14
+ ListRow: { to: 'route' },
15
+ SidebarItem: { to: 'route' },
16
+ SidebarRailItem: { to: 'route' },
17
+ MobileNavItem: { to: 'route' },
18
+ }
19
+ const kebab = (name) =>
20
+ name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
21
+
22
+ function componentAliases(source) {
23
+ const aliases = new Map()
24
+ const imports =
25
+ /import\s*\{([^}]+)\}\s*from\s*['"]frappe-ui(?:\/[^'"]*)?['"]/g
26
+ for (const match of source.matchAll(imports)) {
27
+ for (const binding of match[1].split(',')) {
28
+ const [imported, local] = binding
29
+ .trim()
30
+ .replace(/^type\s+/, '')
31
+ .split(/\s+as\s+/)
32
+ if (PROP_RENAMES[imported]) {
33
+ const binding = local || imported
34
+ aliases.set(binding, imported)
35
+ aliases.set(kebab(binding), imported)
36
+ }
37
+ }
38
+ }
39
+ // Local template bindings shadow imports with the same local name.
40
+ const localImports =
41
+ /import\s+([^'";]+?)\s+from\s*['"](?!frappe-ui(?:\/|['"]))[^'"]+['"]/g
42
+ for (const match of source.matchAll(localImports)) {
43
+ const clause = match[1].trim()
44
+ const bindings = []
45
+ if (!clause.startsWith('{') && !clause.startsWith('*')) {
46
+ bindings.push(clause.split(',')[0].trim())
47
+ }
48
+ const named = clause.match(/\{([^}]+)\}/)?.[1]
49
+ if (named) {
50
+ for (const binding of named.split(',')) {
51
+ bindings.push(
52
+ binding
53
+ .trim()
54
+ .split(/\s+as\s+/)
55
+ .at(-1),
56
+ )
57
+ }
58
+ }
59
+ for (const name of bindings) {
60
+ if (!PROP_RENAMES[name]) continue
61
+ aliases.delete(name)
62
+ aliases.delete(kebab(name))
63
+ }
64
+ }
65
+ return aliases
66
+ }
67
+
68
+ function renameAttributes(tag, renames) {
69
+ let migrated = ''
70
+ let cursor = 0
71
+ let quote
72
+ while (cursor < tag.length) {
73
+ const character = tag[cursor]
74
+ if (quote) {
75
+ migrated += character
76
+ if (character === quote && tag[cursor - 1] !== '\\') quote = undefined
77
+ cursor += 1
78
+ continue
79
+ }
80
+ if (character === '"' || character === "'") {
81
+ quote = character
82
+ migrated += character
83
+ cursor += 1
84
+ continue
85
+ }
86
+ if (!/\s/.test(character)) {
87
+ migrated += character
88
+ cursor += 1
89
+ continue
90
+ }
91
+ migrated += character
92
+ cursor += 1
93
+ const match = tag
94
+ .slice(cursor)
95
+ .match(
96
+ /^(v-bind:|[:.]?)([A-Za-z][\w-]*)((?:\.[A-Za-z][\w-]*)*)(?=[\s=/>])/,
97
+ )
98
+ if (!match) continue
99
+ const replacement = renames[match[2]]
100
+ if (replacement) {
101
+ const isShorthandBinding =
102
+ match[1] && tag[cursor + match[0].length] !== '='
103
+ migrated += `${match[1]}${replacement}${match[3]}`
104
+ if (isShorthandBinding) migrated += `="${match[2]}"`
105
+ } else {
106
+ migrated += match[0]
107
+ }
108
+ cursor += match[0].length
109
+ }
110
+ return migrated
111
+ }
112
+
113
+ function migrateMarkup(source, aliases) {
114
+ let migrated = ''
115
+ let cursor = 0
116
+ while (cursor < source.length) {
117
+ const start = source.indexOf('<', cursor)
118
+ if (start === -1) return migrated + source.slice(cursor)
119
+ migrated += source.slice(cursor, start)
120
+ if (source.startsWith('<!--', start)) {
121
+ const commentEnd = source.indexOf('-->', start + 4)
122
+ const next = commentEnd === -1 ? source.length : commentEnd + 3
123
+ migrated += source.slice(start, next)
124
+ cursor = next
125
+ continue
126
+ }
127
+ const nameMatch = source.slice(start + 1).match(/^([A-Za-z][\w.-]*)/)
128
+ if (!nameMatch) {
129
+ migrated += '<'
130
+ cursor = start + 1
131
+ continue
132
+ }
133
+ let end = start + 1 + nameMatch[1].length
134
+ let quote
135
+ for (; end < source.length; end += 1) {
136
+ const character = source[end]
137
+ if (quote) {
138
+ if (character === quote && source[end - 1] !== '\\') quote = undefined
139
+ } else if (character === '"' || character === "'") {
140
+ quote = character
141
+ } else if (character === '>') {
142
+ break
143
+ }
144
+ }
145
+ if (end === source.length) return migrated + source.slice(start)
146
+ const tag = source.slice(start, end + 1)
147
+ const component = aliases.get(nameMatch[1])
148
+ if (!component) {
149
+ migrated += tag
150
+ cursor = end + 1
151
+ continue
152
+ }
153
+ migrated += renameAttributes(tag, PROP_RENAMES[component])
154
+ cursor = end + 1
155
+ }
156
+ return migrated
157
+ }
158
+
159
+ /**
160
+ * Rename destination attributes on template components explicitly imported
161
+ * from frappe-ui, leaving scripts and ambiguous global components unchanged.
162
+ */
163
+ export function migrateDestinations(source) {
164
+ const aliases = componentAliases(source)
165
+ const templateStart = source.indexOf('<template')
166
+ const templateEnd = source.lastIndexOf('</template>')
167
+ if (templateStart === -1 || templateEnd < templateStart)
168
+ return migrateMarkup(source, aliases)
169
+ const end = templateEnd + '</template>'.length
170
+ return (
171
+ source.slice(0, templateStart) +
172
+ migrateMarkup(source.slice(templateStart, end), aliases) +
173
+ source.slice(end)
174
+ )
175
+ }
176
+
177
+ function filesIn(target, seen = new Set()) {
178
+ const stat = fs.lstatSync(target)
179
+ if (stat.isSymbolicLink()) return []
180
+ if (stat.isFile()) return EXTENSIONS.has(path.extname(target)) ? [target] : []
181
+ const real = fs.realpathSync(target)
182
+ if (seen.has(real)) return []
183
+ seen.add(real)
184
+ return fs
185
+ .readdirSync(target, { withFileTypes: true })
186
+ .sort((a, b) => a.name.localeCompare(b.name))
187
+ .flatMap((entry) => {
188
+ if (['node_modules', '.git', 'dist', 'build'].includes(entry.name))
189
+ return []
190
+ return filesIn(path.join(target, entry.name), seen)
191
+ })
192
+ }
193
+
194
+ /** Run the destinations-v1 CLI and return its process exit code. */
195
+ export function run(argv) {
196
+ const dryRun = argv.includes('--dry-run')
197
+ const unknown = argv.find((arg) => arg.startsWith('-') && arg !== '--dry-run')
198
+ if (unknown) {
199
+ console.error(`Unknown option: ${unknown}\n${USAGE}`)
200
+ return 1
201
+ }
202
+ const targets = argv.filter((arg) => arg !== '--dry-run')
203
+ if (!targets.length) {
204
+ console.error(USAGE)
205
+ return 1
206
+ }
207
+ let changed = 0
208
+ const files = [
209
+ ...new Set(targets.flatMap((target) => filesIn(path.resolve(target)))),
210
+ ]
211
+ for (const file of files) {
212
+ const source = fs.readFileSync(file, 'utf8')
213
+ const migrated = migrateDestinations(source)
214
+ if (migrated === source) continue
215
+ changed += 1
216
+ if (!dryRun) fs.writeFileSync(file, migrated)
217
+ console.log(`${dryRun ? 'Would migrate' : 'Migrated'} ${file}`)
218
+ }
219
+ console.log(
220
+ `${dryRun ? 'Would migrate' : 'Migrated'} ${changed} file${changed === 1 ? '' : 's'}.`,
221
+ )
222
+ return 0
223
+ }
224
+
225
+ const script = fileURLToPath(import.meta.url)
226
+ const isMain =
227
+ process.argv[1] &&
228
+ fs.realpathSync(process.argv[1]) === fs.realpathSync(script)
229
+ if (isMain) process.exitCode = run(process.argv.slice(2))