refactor(ui): unify chat composer controls
- Add a canonical 24, 28, and 32 pixel control scale - Rebuild the AI prompt as a multiline compound input - Keep property-panel icon actions at the explicit compact size
This commit is contained in:
parent
62f3594819
commit
2f88ef4827
|
|
@ -5,9 +5,8 @@ import { computed, onBeforeUnmount, ref } from 'vue'
|
|||
|
||||
import ChatProfileSelect from '@/components/chat/ChatProfileSelect.vue'
|
||||
import ProviderModelSelect from '@/components/chat/ProviderModelSelect.vue'
|
||||
import AppInput from '@/components/ui/AppInput.vue'
|
||||
import Tip from '@/components/ui/Tip.vue'
|
||||
import { useButtonUI } from '@/components/ui/button'
|
||||
import IconButton from '@/components/ui/IconButton.vue'
|
||||
import InputGroup from '@/components/ui/InputGroup.vue'
|
||||
import { useAIChat } from '@/app/ai/chat/use'
|
||||
import { designModelProfile, designModelProfiles } from '@/app/ai/models'
|
||||
import { validateReferenceImageFile } from '@/app/ai/reference-image/prepare'
|
||||
|
|
@ -62,18 +61,6 @@ const acpAgentName = computed(() => {
|
|||
const isCustomProvider = computed(
|
||||
() => providerID.value === 'openai-compatible' || providerID.value === 'anthropic-compatible'
|
||||
)
|
||||
const stopButton = useButtonUI({
|
||||
tone: 'ghost',
|
||||
shape: 'rounded',
|
||||
size: 'sm',
|
||||
ui: { base: 'shrink-0 border border-border px-2 py-1.5' }
|
||||
})
|
||||
const sendButton = useButtonUI({
|
||||
tone: 'accent',
|
||||
shape: 'rounded',
|
||||
size: 'sm',
|
||||
ui: { base: 'shrink-0 px-2.5 py-1.5 font-medium' }
|
||||
})
|
||||
const customModelName = computed(() => customModelID.value.trim())
|
||||
const usesCustomModel = computed(
|
||||
() => !!providerDef.value.supportsCustomModel && !!customModelName.value
|
||||
|
|
@ -113,6 +100,13 @@ function handlePaste(event: ClipboardEvent) {
|
|||
|
||||
onBeforeUnmount(clearReference)
|
||||
|
||||
function handleInputKeydown(event: KeyboardEvent) {
|
||||
if (event.code !== 'Enter' || event.shiftKey || event.isComposing) return
|
||||
event.preventDefault()
|
||||
const target = event.currentTarget
|
||||
if (target instanceof HTMLElement) target.closest('form')?.requestSubmit()
|
||||
}
|
||||
|
||||
function handleSubmit(e: Event) {
|
||||
e.preventDefault()
|
||||
const text = input.value.trim()
|
||||
|
|
@ -127,113 +121,114 @@ function handleSubmit(e: Event) {
|
|||
|
||||
<template>
|
||||
<TooltipProvider>
|
||||
<div class="shrink-0 border-t border-border px-3 py-2">
|
||||
<!-- Model selector & settings -->
|
||||
<div class="mb-1.5 flex items-center gap-1">
|
||||
<template v-if="isACPProvider">
|
||||
<div class="flex items-center gap-1 px-1.5 py-0.5 text-[10px] text-muted">
|
||||
<icon-lucide-bot class="size-3" />
|
||||
{{ acpAgentName }}
|
||||
</div>
|
||||
</template>
|
||||
<ChatProfileSelect v-else-if="canSwitchProfile && (isCustomProvider || usesCustomModel)">
|
||||
<template #value>
|
||||
<span class="min-w-0 truncate">{{ selectedProfileName }}</span>
|
||||
<div class="shrink-0 border-t border-border p-2.5">
|
||||
<form @submit="handleSubmit" @paste.stop="handlePaste">
|
||||
<InputGroup :disabled="isStreaming">
|
||||
<template v-if="reference" #attachment>
|
||||
<div class="flex items-center gap-2 rounded-lg bg-canvas p-1.5">
|
||||
<img
|
||||
:src="reference.previewURL"
|
||||
alt="Reference image"
|
||||
width="40"
|
||||
height="40"
|
||||
class="size-10 rounded object-cover"
|
||||
/>
|
||||
<span class="min-w-0 flex-1 truncate text-[10px] text-surface">
|
||||
{{ reference.file.name }}
|
||||
</span>
|
||||
<IconButton label="Remove reference image" size="xs" @click="clearReference">
|
||||
<icon-lucide-x class="size-3" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</template>
|
||||
</ChatProfileSelect>
|
||||
<template v-else-if="isCustomProvider || usesCustomModel">
|
||||
<div
|
||||
class="flex items-center gap-1 px-1.5 py-0.5 text-[10px] text-muted"
|
||||
data-test-id="chat-custom-model-label"
|
||||
>
|
||||
<icon-lucide-bot class="size-3" />
|
||||
{{ selectedModelName }}
|
||||
</div>
|
||||
</template>
|
||||
<ProviderModelSelect v-else>
|
||||
<template #value>{{ selectedModelName }}</template>
|
||||
</ProviderModelSelect>
|
||||
|
||||
<div class="ml-auto">
|
||||
<Tip :label="dialogs.providerSettings">
|
||||
<button
|
||||
type="button"
|
||||
<textarea
|
||||
v-model="input"
|
||||
data-test-id="chat-input"
|
||||
:placeholder="dialogs.describeChange"
|
||||
:disabled="isStreaming"
|
||||
rows="2"
|
||||
aria-label="Describe a change"
|
||||
class="block min-h-12 w-full resize-none bg-transparent px-3 pt-2.5 pb-1 text-xs leading-relaxed text-surface outline-none placeholder:text-muted disabled:cursor-not-allowed disabled:opacity-60"
|
||||
@keydown="handleInputKeydown"
|
||||
@copy.stop
|
||||
@cut.stop
|
||||
/>
|
||||
|
||||
<template #leading>
|
||||
<IconButton
|
||||
label="Attach reference image"
|
||||
size="sm"
|
||||
:disabled="isStreaming"
|
||||
@click="openReferenceDialog()"
|
||||
>
|
||||
<icon-lucide-image-plus class="size-4" />
|
||||
</IconButton>
|
||||
</template>
|
||||
|
||||
<template #model>
|
||||
<div class="flex min-w-0 items-center">
|
||||
<template v-if="isACPProvider">
|
||||
<div class="flex min-w-0 items-center gap-1 px-1.5 text-[10px] text-muted">
|
||||
<icon-lucide-bot class="size-3 shrink-0" />
|
||||
<span class="truncate">{{ acpAgentName }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<ChatProfileSelect
|
||||
v-else-if="canSwitchProfile && (isCustomProvider || usesCustomModel)"
|
||||
>
|
||||
<template #value>
|
||||
<span class="min-w-0 truncate">{{ selectedProfileName }}</span>
|
||||
</template>
|
||||
</ChatProfileSelect>
|
||||
<div
|
||||
v-else-if="isCustomProvider || usesCustomModel"
|
||||
class="flex min-w-0 items-center gap-1 px-1.5 text-[10px] text-muted"
|
||||
data-test-id="chat-custom-model-label"
|
||||
>
|
||||
<icon-lucide-bot class="size-3 shrink-0" />
|
||||
<span class="truncate">{{ selectedModelName }}</span>
|
||||
</div>
|
||||
<ProviderModelSelect v-else>
|
||||
<template #value>
|
||||
<span class="min-w-0 truncate">{{ selectedModelName }}</span>
|
||||
</template>
|
||||
</ProviderModelSelect>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<IconButton
|
||||
:label="dialogs.providerSettings"
|
||||
size="sm"
|
||||
data-test-id="provider-settings-trigger"
|
||||
:aria-label="dialogs.providerSettings"
|
||||
class="rounded p-0.5 text-muted hover:bg-hover hover:text-surface"
|
||||
@click="openSettingsDialog('ai')"
|
||||
>
|
||||
<icon-lucide-settings class="size-3" />
|
||||
</button>
|
||||
</Tip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input form -->
|
||||
<div
|
||||
v-if="reference"
|
||||
class="mb-2 flex items-center gap-2 rounded-lg border border-border bg-canvas p-1.5"
|
||||
>
|
||||
<img
|
||||
:src="reference.previewURL"
|
||||
alt="Reference image"
|
||||
class="size-10 rounded object-cover"
|
||||
/>
|
||||
<span class="min-w-0 flex-1 truncate text-[10px] text-surface">
|
||||
{{ reference.file.name }}
|
||||
</span>
|
||||
<Tip label="Remove reference image">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Remove reference image"
|
||||
class="rounded p-1 text-muted hover:bg-hover hover:text-surface"
|
||||
@click="clearReference"
|
||||
>
|
||||
<icon-lucide-x class="size-3" />
|
||||
</button>
|
||||
</Tip>
|
||||
</div>
|
||||
<form class="flex gap-1.5" @submit="handleSubmit" @paste.stop="handlePaste">
|
||||
<Tip label="Attach reference image">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Attach reference image"
|
||||
class="shrink-0 rounded p-1.5 text-muted hover:bg-hover hover:text-surface"
|
||||
:disabled="isStreaming"
|
||||
@click="openReferenceDialog()"
|
||||
>
|
||||
<icon-lucide-image-plus class="size-4" />
|
||||
</button>
|
||||
</Tip>
|
||||
<AppInput
|
||||
v-model="input"
|
||||
data-test-id="chat-input"
|
||||
:placeholder="dialogs.describeChange"
|
||||
class="min-w-0 flex-1 placeholder:text-muted"
|
||||
:disabled="isStreaming"
|
||||
@copy.stop
|
||||
@cut.stop
|
||||
/>
|
||||
<Tip v-if="isStreaming" :label="dialogs.stopGenerating">
|
||||
<button
|
||||
type="button"
|
||||
data-test-id="chat-stop-button"
|
||||
:class="stopButton.base"
|
||||
@click="emit('stop')"
|
||||
>
|
||||
<icon-lucide-square class="size-3" />
|
||||
</button>
|
||||
</Tip>
|
||||
<Tip v-else :label="dialogs.sendMessage">
|
||||
<button
|
||||
type="submit"
|
||||
data-test-id="chat-send-button"
|
||||
:class="sendButton.base"
|
||||
:disabled="!input.trim()"
|
||||
>
|
||||
<icon-lucide-send class="size-3" />
|
||||
</button>
|
||||
</Tip>
|
||||
<icon-lucide-settings class="size-3.5" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
v-if="isStreaming"
|
||||
:label="dialogs.stopGenerating"
|
||||
size="sm"
|
||||
data-test-id="chat-stop-button"
|
||||
class="border border-border"
|
||||
@click="emit('stop')"
|
||||
>
|
||||
<icon-lucide-square class="size-3" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
v-else
|
||||
:label="dialogs.sendMessage"
|
||||
size="sm"
|
||||
type="submit"
|
||||
data-test-id="chat-send-button"
|
||||
class="bg-accent text-white hover:bg-accent/90 hover:text-white"
|
||||
:disabled="!input.trim()"
|
||||
>
|
||||
<icon-lucide-send class="size-3.5" />
|
||||
</IconButton>
|
||||
</template>
|
||||
</InputGroup>
|
||||
</form>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ function blendModeOptions(value: BlendMode | typeof MIXED) {
|
|||
<div class="flex h-6 items-center justify-end">
|
||||
<IconButton
|
||||
:label="panels.independentCornerRadii"
|
||||
size="md"
|
||||
size="xs"
|
||||
:active="independentCorners === true"
|
||||
@click="actions.toggleIndependentCorners"
|
||||
>
|
||||
|
|
@ -189,7 +189,7 @@ function blendModeOptions(value: BlendMode | typeof MIXED) {
|
|||
<template #actions>
|
||||
<IconButton
|
||||
:label="panels.independentCornerRadii"
|
||||
size="md"
|
||||
size="xs"
|
||||
active
|
||||
@click="actions.toggleIndependentCorners"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ function setLayoutMode(mode: string) {
|
|||
<IconButton
|
||||
v-if="ctx.isFlex"
|
||||
:label="panels.layoutWrap"
|
||||
size="md"
|
||||
size="xs"
|
||||
:active="ctx.node.layoutWrap === 'WRAP'"
|
||||
@click="ctx.updateProp('layoutWrap', ctx.node.layoutWrap === 'WRAP' ? 'NO_WRAP' : 'WRAP')"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const CONTAINER_TYPES = ['FRAME', 'COMPONENT', 'COMPONENT_SET', 'INSTANCE']
|
|||
<template v-if="CONTAINER_TYPES.includes(ctx.node.type)" #actions>
|
||||
<IconButton
|
||||
:label="ctx.node.layoutMode === 'NONE' ? panels.addAutoLayout : panels.removeAutoLayout"
|
||||
size="md"
|
||||
size="xs"
|
||||
:active="ctx.node.layoutMode !== 'NONE'"
|
||||
class="data-[state=on]:bg-accent/15"
|
||||
@click="
|
||||
|
|
|
|||
|
|
@ -34,21 +34,21 @@ function handleAlign(
|
|||
<div class="flex gap-0.5">
|
||||
<IconButton
|
||||
:label="panels.alignLeft"
|
||||
size="md"
|
||||
size="xs"
|
||||
@click="handleAlign(actions.align, 'horizontal', 'min')"
|
||||
>
|
||||
<icon-lucide-align-start-vertical class="size-3.5" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
:label="panels.alignCenterHorizontally"
|
||||
size="md"
|
||||
size="xs"
|
||||
@click="handleAlign(actions.align, 'horizontal', 'center')"
|
||||
>
|
||||
<icon-lucide-align-center-vertical class="size-3.5" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
:label="panels.alignRight"
|
||||
size="md"
|
||||
size="xs"
|
||||
@click="handleAlign(actions.align, 'horizontal', 'max')"
|
||||
>
|
||||
<icon-lucide-align-end-vertical class="size-3.5" />
|
||||
|
|
@ -57,21 +57,21 @@ function handleAlign(
|
|||
<div class="flex gap-0.5">
|
||||
<IconButton
|
||||
:label="panels.alignTop"
|
||||
size="md"
|
||||
size="xs"
|
||||
@click="handleAlign(actions.align, 'vertical', 'min')"
|
||||
>
|
||||
<icon-lucide-align-start-horizontal class="size-3.5" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
:label="panels.alignCenterVertically"
|
||||
size="md"
|
||||
size="xs"
|
||||
@click="handleAlign(actions.align, 'vertical', 'center')"
|
||||
>
|
||||
<icon-lucide-align-center-horizontal class="size-3.5" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
:label="panels.alignBottom"
|
||||
size="md"
|
||||
size="xs"
|
||||
@click="handleAlign(actions.align, 'vertical', 'max')"
|
||||
>
|
||||
<icon-lucide-align-end-horizontal class="size-3.5" />
|
||||
|
|
@ -145,13 +145,13 @@ function handleAlign(
|
|||
</NumberField>
|
||||
</Tip>
|
||||
<div class="flex h-6 items-center justify-end gap-0.5">
|
||||
<IconButton :label="panels.flipHorizontal" size="md" @click="actions.flip('horizontal')">
|
||||
<IconButton :label="panels.flipHorizontal" size="xs" @click="actions.flip('horizontal')">
|
||||
<icon-lucide-flip-horizontal-2 class="size-3.5" />
|
||||
</IconButton>
|
||||
<IconButton :label="panels.flipVertical" size="md" @click="actions.flip('vertical')">
|
||||
<IconButton :label="panels.flipVertical" size="xs" @click="actions.flip('vertical')">
|
||||
<icon-lucide-flip-vertical-2 class="size-3.5" />
|
||||
</IconButton>
|
||||
<IconButton :label="panels.rotate90" size="md" @click="actions.rotate(90)">
|
||||
<IconButton :label="panels.rotate90" size="xs" @click="actions.rotate(90)">
|
||||
<icon-lucide-rotate-cw-square class="size-3.5" />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -222,7 +222,7 @@ function onToggleSides(activeNode: SceneNode | null) {
|
|||
</Tip>
|
||||
<IconButton
|
||||
:label="panels.strokeSides"
|
||||
size="md"
|
||||
size="xs"
|
||||
class="size-[26px] shrink-0"
|
||||
:active="expandedSides"
|
||||
data-property="stroke-sides"
|
||||
|
|
@ -235,7 +235,7 @@ function onToggleSides(activeNode: SceneNode | null) {
|
|||
<div v-if="!isMixed && items.length > 0" class="mt-1.5 flex items-center gap-1.5">
|
||||
<IconButton
|
||||
:label="panels.strokeDash"
|
||||
size="md"
|
||||
size="xs"
|
||||
class="shrink-0"
|
||||
:active="strokeCtx.dashState(items[0]).on"
|
||||
data-property="stroke-dash"
|
||||
|
|
|
|||
|
|
@ -208,7 +208,7 @@ function featureEnabled(features: Array<{ tag: string; enabled: boolean }>, tag:
|
|||
>
|
||||
<IconButton
|
||||
:label="`${menu.bold} (${appMenuShortcutLabel('text.bold')})`"
|
||||
size="md"
|
||||
size="xs"
|
||||
:active="ctx.activeFormatting.value.includes('bold')"
|
||||
@click="ctx.actions.toggleBold"
|
||||
>
|
||||
|
|
@ -216,7 +216,7 @@ function featureEnabled(features: Array<{ tag: string; enabled: boolean }>, tag:
|
|||
</IconButton>
|
||||
<IconButton
|
||||
:label="`${menu.italic} (${appMenuShortcutLabel('text.italic')})`"
|
||||
size="md"
|
||||
size="xs"
|
||||
:active="ctx.activeFormatting.value.includes('italic')"
|
||||
@click="ctx.actions.toggleItalic"
|
||||
>
|
||||
|
|
@ -224,7 +224,7 @@ function featureEnabled(features: Array<{ tag: string; enabled: boolean }>, tag:
|
|||
</IconButton>
|
||||
<IconButton
|
||||
:label="`${menu.underline} (${appMenuShortcutLabel('text.underline')})`"
|
||||
size="md"
|
||||
size="xs"
|
||||
:active="ctx.activeFormatting.value.includes('underline')"
|
||||
@click="ctx.actions.toggleDecoration('UNDERLINE')"
|
||||
>
|
||||
|
|
@ -232,7 +232,7 @@ function featureEnabled(features: Array<{ tag: string; enabled: boolean }>, tag:
|
|||
</IconButton>
|
||||
<IconButton
|
||||
:label="menu.strikethrough"
|
||||
size="md"
|
||||
size="xs"
|
||||
:active="ctx.activeFormatting.value.includes('strikethrough')"
|
||||
@click="ctx.actions.toggleDecoration('STRIKETHROUGH')"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { computed } from 'vue'
|
||||
import { tv } from 'tailwind-variants'
|
||||
|
||||
import type { ControlSize } from '@/theme/control'
|
||||
import theme from '@/theme/input'
|
||||
|
||||
interface AppInputProps {
|
||||
|
|
@ -16,7 +17,7 @@ interface AppInputProps {
|
|||
max?: number
|
||||
step?: number
|
||||
tone?: 'default' | 'panel'
|
||||
size?: 'sm' | 'md'
|
||||
size?: ControlSize
|
||||
state?: 'idle' | 'mixed' | 'bound' | 'invalid'
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { computed, normalizeClass, useAttrs } from 'vue'
|
|||
import { tv } from 'tailwind-variants'
|
||||
|
||||
import theme from '@/theme/icon-button'
|
||||
import type { ControlSize } from '@/theme/control'
|
||||
import Tip from '@/components/ui/Tip.vue'
|
||||
|
||||
const {
|
||||
|
|
@ -10,14 +11,14 @@ const {
|
|||
disabled = false,
|
||||
label,
|
||||
side = 'top',
|
||||
size = 'sm',
|
||||
size = 'xs',
|
||||
type = 'button'
|
||||
} = defineProps<{
|
||||
active?: boolean
|
||||
disabled?: boolean
|
||||
label?: string
|
||||
side?: 'top' | 'bottom' | 'left' | 'right'
|
||||
size?: 'sm' | 'md'
|
||||
size?: ControlSize
|
||||
type?: 'button' | 'submit' | 'reset'
|
||||
}>()
|
||||
|
||||
|
|
|
|||
49
src/components/ui/InputGroup.vue
Normal file
49
src/components/ui/InputGroup.vue
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { tv, type VariantProps } from 'tailwind-variants'
|
||||
|
||||
import theme from '@/theme/input-group'
|
||||
import type { ComponentUI } from '@/components/ui/types'
|
||||
|
||||
const inputGroup = tv(theme)
|
||||
type InputGroupVariants = VariantProps<typeof inputGroup>
|
||||
|
||||
type InputGroupUI = ComponentUI<typeof theme>
|
||||
|
||||
const {
|
||||
size = 'sm',
|
||||
disabled = false,
|
||||
ui
|
||||
} = defineProps<{
|
||||
size?: NonNullable<InputGroupVariants['size']>
|
||||
disabled?: boolean
|
||||
ui?: InputGroupUI
|
||||
}>()
|
||||
|
||||
const cls = computed(() => inputGroup({ size, disabled }))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="input-group"
|
||||
:data-size="size"
|
||||
:data-disabled="disabled || undefined"
|
||||
:class="cls.root({ class: ui?.root })"
|
||||
>
|
||||
<div v-if="$slots.attachment" data-slot="input-group-attachment" :class="ui?.attachment">
|
||||
<slot name="attachment" />
|
||||
</div>
|
||||
<div data-slot="input-group-control" :class="ui?.control">
|
||||
<slot />
|
||||
</div>
|
||||
<div data-slot="input-group-toolbar" :class="cls.toolbar({ class: ui?.toolbar })">
|
||||
<slot name="leading" />
|
||||
<div data-slot="input-group-model" :class="cls.model({ class: ui?.model })">
|
||||
<slot name="model" />
|
||||
</div>
|
||||
<div data-slot="input-group-actions" :class="cls.actions({ class: ui?.actions })">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
13
src/theme/control.ts
Normal file
13
src/theme/control.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
export type ControlSize = 'xs' | 'sm' | 'md'
|
||||
|
||||
export const controlHeight = {
|
||||
xs: 'h-6',
|
||||
sm: 'h-7',
|
||||
md: 'h-8'
|
||||
} satisfies Record<ControlSize, string>
|
||||
|
||||
export const squareControlSize = {
|
||||
xs: 'size-6',
|
||||
sm: 'size-7',
|
||||
md: 'size-8'
|
||||
} satisfies Record<ControlSize, string>
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
import { panelIconButtonBase } from './panel/field'
|
||||
|
||||
export default {
|
||||
base: 'flex cursor-pointer items-center justify-center bg-transparent text-muted outline-none hover:bg-hover hover:text-surface focus-visible:border-panel-focus',
|
||||
variants: {
|
||||
size: {
|
||||
sm: 'size-5 rounded border-none text-sm leading-none',
|
||||
md: panelIconButtonBase
|
||||
xs: 'size-6 rounded border-none text-sm leading-none',
|
||||
sm: 'size-7 rounded-md border-none text-sm leading-none',
|
||||
md: 'size-8 rounded-md border-none text-base leading-none'
|
||||
},
|
||||
active: {
|
||||
true: 'border-accent text-accent'
|
||||
|
|
|
|||
27
src/theme/input-group.ts
Normal file
27
src/theme/input-group.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { controlHeight } from '@/theme/control'
|
||||
|
||||
export default {
|
||||
slots: {
|
||||
root: 'min-w-0 rounded-xl border border-border bg-input transition-colors hover:border-muted/60 focus-within:border-panel-focus focus-within:ring-1 focus-within:ring-accent/30',
|
||||
attachment: 'px-2 pt-2',
|
||||
control:
|
||||
'block min-h-12 w-full resize-none bg-transparent px-3 pt-2.5 pb-1 text-xs leading-relaxed text-surface outline-none placeholder:text-muted disabled:cursor-not-allowed disabled:opacity-60',
|
||||
toolbar: 'flex min-w-0 items-center gap-1 px-1.5 pb-1.5',
|
||||
model: 'min-w-0 flex-1',
|
||||
actions: 'ml-auto flex shrink-0 items-center gap-1'
|
||||
},
|
||||
variants: {
|
||||
size: {
|
||||
xs: { toolbar: controlHeight.xs },
|
||||
sm: { toolbar: controlHeight.sm },
|
||||
md: { toolbar: controlHeight.md }
|
||||
},
|
||||
disabled: {
|
||||
true: { root: 'opacity-60' }
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'sm' as const,
|
||||
disabled: false
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +1,25 @@
|
|||
import { panelFieldBase, panelFieldState } from './panel/field'
|
||||
|
||||
const defaultInputBase =
|
||||
'min-w-0 rounded-md border border-border bg-input text-surface outline-none hover:border-muted/60 focus:border-panel-focus focus:ring-1 focus:ring-accent/25 disabled:cursor-not-allowed disabled:opacity-60'
|
||||
|
||||
export default {
|
||||
base: 'w-full tabular-nums',
|
||||
variants: {
|
||||
tone: {
|
||||
default: panelFieldBase,
|
||||
default: defaultInputBase,
|
||||
panel: panelFieldBase
|
||||
},
|
||||
size: {
|
||||
sm: 'px-2 text-[11px]',
|
||||
md: 'px-2 text-[11px]'
|
||||
xs: 'h-6 px-2 text-[11px]',
|
||||
sm: 'h-7 px-2.5 text-xs',
|
||||
md: 'h-8 px-3 text-xs'
|
||||
},
|
||||
state: panelFieldState
|
||||
},
|
||||
defaultVariants: {
|
||||
tone: 'default' as const,
|
||||
size: 'md' as const,
|
||||
size: 'xs' as const,
|
||||
state: 'idle' as const
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue