fix(ui): refine export scale control
This commit is contained in:
parent
bcf18b8022
commit
3accbbb835
|
|
@ -182,7 +182,13 @@ function vueTemplateAst(source, filename) {
|
|||
}
|
||||
|
||||
function isVueSourceFile(file) {
|
||||
return file.endsWith('.vue') && (file.includes('/src/') || file.includes('/packages/vue/src/'))
|
||||
return (
|
||||
file.endsWith('.vue') &&
|
||||
(file.startsWith('src/') ||
|
||||
file.includes('/src/') ||
|
||||
file.startsWith('packages/vue/src/') ||
|
||||
file.includes('/packages/vue/src/'))
|
||||
)
|
||||
}
|
||||
|
||||
function sourceLineCount(source) {
|
||||
|
|
@ -272,8 +278,7 @@ const noVueStyleBlocks = {
|
|||
},
|
||||
create(context) {
|
||||
const file = normalizedFilename(context)
|
||||
if (!file.endsWith('.vue')) return {}
|
||||
if (!file.includes('/src/') && !file.includes('/packages/vue/src/')) return {}
|
||||
if (!isVueSourceFile(file)) return {}
|
||||
|
||||
return {
|
||||
Program(node) {
|
||||
|
|
@ -297,8 +302,7 @@ const noNativeTitleAttributesInVue = {
|
|||
},
|
||||
create(context) {
|
||||
const file = normalizedFilename(context)
|
||||
if (!file.endsWith('.vue')) return {}
|
||||
if (!file.includes('/src/') && !file.includes('/packages/vue/src/')) return {}
|
||||
if (!isVueSourceFile(file)) return {}
|
||||
|
||||
return {
|
||||
Program(node) {
|
||||
|
|
@ -329,8 +333,7 @@ const noHardcodedTipLabelsInVue = {
|
|||
},
|
||||
create(context) {
|
||||
const file = normalizedFilename(context)
|
||||
if (!file.endsWith('.vue')) return {}
|
||||
if (!file.includes('/src/') && !file.includes('/packages/vue/src/')) return {}
|
||||
if (!isVueSourceFile(file)) return {}
|
||||
|
||||
return {
|
||||
Program(node) {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { vTestId, type TestIdProps } from '@open-pencil/vue'
|
|||
|
||||
import { useInputUI } from '@/components/ui/input'
|
||||
import { menuItem, useMenuUI } from '@/components/ui/menu'
|
||||
import Tip from '@/components/ui/Tip.vue'
|
||||
|
||||
interface ExportScaleInputProps extends TestIdProps {
|
||||
presets: readonly number[]
|
||||
|
|
@ -35,7 +36,10 @@ const text = ref('')
|
|||
// Keep the editable text in sync with the committed scale (e.g. 1.5 -> "1.5x").
|
||||
watch(modelValue, (value) => (text.value = `${value}x`), { immediate: true })
|
||||
|
||||
const inputClass = useInputUI({ size: 'sm', ui: { base: 'min-w-0 flex-1' } }).base
|
||||
const inputClass = useInputUI({
|
||||
size: 'sm',
|
||||
ui: { base: 'min-w-0 flex-1 rounded-none border-0 bg-transparent focus:border-transparent' }
|
||||
}).base
|
||||
const menuCls = useMenuUI({ content: 'min-w-[7rem]' })
|
||||
const itemCls = menuItem({ justify: 'between' })
|
||||
|
||||
|
|
@ -58,46 +62,50 @@ function isActive(scale: number) {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex min-w-0 flex-1 items-center gap-1">
|
||||
<input
|
||||
ref="inputRef"
|
||||
v-model="text"
|
||||
v-test-id="testId"
|
||||
type="text"
|
||||
:title="label"
|
||||
:class="inputClass"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
@focus="inputRef?.select()"
|
||||
@blur="commit"
|
||||
@keydown.enter.prevent="inputRef?.blur()"
|
||||
@keydown.escape.prevent="((text = `${modelValue}x`), inputRef?.blur())"
|
||||
/>
|
||||
<DropdownMenuRoot v-model:open="open">
|
||||
<DropdownMenuTrigger as-child>
|
||||
<button
|
||||
type="button"
|
||||
:title="label"
|
||||
class="flex shrink-0 cursor-pointer items-center rounded border border-border bg-input px-1 py-1 text-surface hover:bg-hover"
|
||||
>
|
||||
<icon-lucide-chevron-down class="size-3 text-muted" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent side="bottom" align="end" :side-offset="4" :class="menuCls.content">
|
||||
<DropdownMenuItem
|
||||
v-for="scale in presets"
|
||||
:key="scale"
|
||||
:class="itemCls"
|
||||
@select="pick(scale)"
|
||||
<Tip :label="label" :disabled="!label">
|
||||
<div
|
||||
class="flex min-w-0 flex-1 overflow-hidden rounded border border-border bg-input focus-within:border-accent"
|
||||
>
|
||||
<input
|
||||
ref="inputRef"
|
||||
v-model="text"
|
||||
v-test-id="testId"
|
||||
type="text"
|
||||
:aria-label="label"
|
||||
:class="inputClass"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
@focus="inputRef?.select()"
|
||||
@blur="commit"
|
||||
@keydown.enter.prevent="inputRef?.blur()"
|
||||
@keydown.escape.prevent="((text = `${modelValue}x`), inputRef?.blur())"
|
||||
/>
|
||||
<DropdownMenuRoot v-model:open="open">
|
||||
<DropdownMenuTrigger as-child>
|
||||
<button
|
||||
type="button"
|
||||
:aria-label="label"
|
||||
class="flex shrink-0 cursor-pointer items-center border-l border-border px-1.5 text-surface hover:bg-hover"
|
||||
>
|
||||
<span>{{ scale }}x</span>
|
||||
<icon-lucide-check v-if="isActive(scale)" class="size-3 text-accent" />
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuRoot>
|
||||
</div>
|
||||
<icon-lucide-chevron-down class="size-3 text-muted" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent side="bottom" align="end" :side-offset="4" :class="menuCls.content">
|
||||
<DropdownMenuItem
|
||||
v-for="scale in presets"
|
||||
:key="scale"
|
||||
:class="itemCls"
|
||||
@select="pick(scale)"
|
||||
>
|
||||
<span>{{ scale }}x</span>
|
||||
<icon-lucide-check v-if="isActive(scale)" class="size-3 text-accent" />
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuRoot>
|
||||
</div>
|
||||
</Tip>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ const indicator = ui?.indicator ?? 'absolute left-1.5 inline-flex items-center j
|
|||
|
||||
<template>
|
||||
<SelectRoot v-model="modelValue">
|
||||
<SelectTrigger v-test-id="testId" :class="select.trigger" :aria-label="label" :title="label">
|
||||
<SelectTrigger v-test-id="testId" :class="select.trigger" :aria-label="label">
|
||||
<SelectValue :placeholder="placeholder" />
|
||||
<icon-lucide-chevron-down class="ml-1 size-3 shrink-0 text-muted" />
|
||||
</SelectTrigger>
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ export default defineConfig([
|
|||
'open-pencil/no-non-ui-imports-in-shared-ui': 'error',
|
||||
'open-pencil/no-app-imports-in-shared-ui': 'error',
|
||||
'open-pencil/no-property-panel-internals-outside-panel': 'error',
|
||||
'open-pencil/no-native-title-attributes-in-vue': 'error',
|
||||
'open-pencil/no-ui-imports-in-core': 'error',
|
||||
'open-pencil/scripts-are-entrypoint-shims': 'error',
|
||||
'open-pencil/strict-tools-layout': 'error'
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import path from 'node:path'
|
||||
|
||||
import { parse as parseVueSfc } from 'vue/compiler-sfc'
|
||||
|
||||
import {
|
||||
collectFolders,
|
||||
createFileRule,
|
||||
|
|
@ -360,6 +362,57 @@ const noHardcodedMacOSShortcutGlyphs = createTextRule(
|
|||
}
|
||||
)
|
||||
|
||||
const VUE_ATTRIBUTE_NODE = 6
|
||||
const VUE_DIRECTIVE_NODE = 7
|
||||
|
||||
type VueTemplateNode = {
|
||||
type?: number
|
||||
name?: string
|
||||
arg?: { content?: string }
|
||||
props?: VueTemplateNode[]
|
||||
children?: VueTemplateNode[]
|
||||
loc?: { start?: { line?: number; column?: number } }
|
||||
}
|
||||
|
||||
function walkVueTemplateAst(node: VueTemplateNode, visitor: (node: VueTemplateNode) => void) {
|
||||
visitor(node)
|
||||
for (const prop of node.props ?? []) walkVueTemplateAst(prop, visitor)
|
||||
for (const child of node.children ?? []) walkVueTemplateAst(child, visitor)
|
||||
}
|
||||
|
||||
function vuePropName(prop: VueTemplateNode) {
|
||||
if (prop.type === VUE_ATTRIBUTE_NODE) return prop.name ?? null
|
||||
if (prop.type === VUE_DIRECTIVE_NODE && prop.name === 'bind') return prop.arg?.content ?? null
|
||||
return null
|
||||
}
|
||||
|
||||
const noNativeTitleAttributesInVue = createTextRule(
|
||||
'open-pencil/no-native-title-attributes-in-vue',
|
||||
(sourceRel, content) => {
|
||||
if (
|
||||
!sourceRel.endsWith('.vue') ||
|
||||
(!sourceRel.startsWith('src/') && !sourceRel.startsWith('packages/vue/src/'))
|
||||
) {
|
||||
return []
|
||||
}
|
||||
|
||||
const template = parseVueSfc(content, { filename: sourceRel }).descriptor.template?.ast
|
||||
if (!template) return []
|
||||
|
||||
const diagnostics: Array<{ message: string; line?: number; column?: number }> = []
|
||||
walkVueTemplateAst(template as VueTemplateNode, (node) => {
|
||||
if (vuePropName(node) !== 'title') return
|
||||
const loc = node.loc?.start
|
||||
diagnostics.push({
|
||||
message: 'Use Tip/Reka tooltip patterns instead of native title attributes.',
|
||||
line: loc?.line,
|
||||
column: loc?.column
|
||||
})
|
||||
})
|
||||
return diagnostics
|
||||
}
|
||||
)
|
||||
|
||||
const SHORTCUT_LABEL_PATTERN = /(?:Shift|Ctrl|Alt|Option|Cmd|Command|⌘|⇧|⌥|⌃)\s*[+)\w]/u
|
||||
|
||||
const noShortcutTextInLabels = createTextRule(
|
||||
|
|
@ -429,6 +482,7 @@ export const openPencilArchitecturePlugin = {
|
|||
noNonUiImportsInSharedUi,
|
||||
noAppImportsInSharedUi,
|
||||
noPropertyPanelInternalsOutsidePanel,
|
||||
noNativeTitleAttributesInVue,
|
||||
noShortcutTextInLabels,
|
||||
noHardcodedMacOSShortcutGlyphs,
|
||||
noUiImportsInCore
|
||||
|
|
|
|||
Loading…
Reference in a new issue