Merge origin/master (PR #41 data-test-ids)
This commit is contained in:
commit
d060e32784
|
|
@ -174,10 +174,11 @@ const topMenus = [
|
|||
<template>
|
||||
<div class="shrink-0 border-b border-border">
|
||||
<div class="flex items-center gap-2 px-2 py-1.5">
|
||||
<img src="/favicon-32.png" class="size-4" alt="OpenPencil" />
|
||||
<img data-test-id="app-logo" src="/favicon-32.png" class="size-4" alt="OpenPencil" />
|
||||
<input
|
||||
v-if="editingName"
|
||||
data-doc-name-edit
|
||||
data-test-id="app-document-name-input"
|
||||
class="min-w-0 flex-1 rounded border border-accent bg-input px-1 py-0.5 text-xs text-surface outline-none"
|
||||
:value="store.state.documentName"
|
||||
@blur="commitRename($event.target as HTMLInputElement)"
|
||||
|
|
@ -186,11 +187,13 @@ const topMenus = [
|
|||
/>
|
||||
<span
|
||||
v-else
|
||||
data-test-id="app-document-name"
|
||||
class="min-w-0 flex-1 cursor-default truncate rounded px-1 py-0.5 text-xs text-surface hover:bg-hover"
|
||||
@dblclick="startRename"
|
||||
>{{ store.state.documentName }}</span
|
||||
>
|
||||
<button
|
||||
data-test-id="app-toggle-ui"
|
||||
class="flex size-6 shrink-0 cursor-pointer items-center justify-center rounded text-muted transition-colors hover:bg-hover hover:text-surface"
|
||||
title="Toggle UI (⌘\)"
|
||||
@click="store.state.showUI = !store.state.showUI"
|
||||
|
|
@ -202,6 +205,7 @@ const topMenus = [
|
|||
<MenubarRoot class="flex items-center gap-0.5 overflow-x-auto scrollbar-none">
|
||||
<MenubarMenu v-for="menu in topMenus" :key="menu.label">
|
||||
<MenubarTrigger
|
||||
:data-test-id="`menubar-${menu.label.toLowerCase()}`"
|
||||
class="flex cursor-pointer items-center rounded px-2 py-1 text-xs text-muted transition-colors select-none hover:bg-hover hover:text-surface data-[state=open]:bg-hover data-[state=open]:text-surface"
|
||||
>
|
||||
{{ menu.label }}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ const modelValue = defineModel<T>({ required: true })
|
|||
<template>
|
||||
<SelectRoot v-model="modelValue">
|
||||
<SelectTrigger
|
||||
data-test-id="app-select-trigger"
|
||||
class="flex min-w-0 flex-1 cursor-pointer items-center justify-between rounded border border-border bg-input px-1.5 py-1 text-xs text-surface outline-none hover:bg-hover"
|
||||
>
|
||||
<SelectValue :placeholder="placeholder" />
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ const { copy, copied } = useClipboard({ copiedDuring: 1500 })
|
|||
<ToastRoot
|
||||
v-for="t in toast.toasts.value"
|
||||
:key="t.id"
|
||||
data-test-id="toast-item"
|
||||
:duration="t.variant === 'error' ? 0 : toast.TOAST_DURATION"
|
||||
class="flex max-w-sm items-start gap-1.5 rounded-md px-2.5 py-1.5 text-xs text-white shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:fade-in data-[state=open]:slide-in-from-top-1 data-[state=closed]:fade-out data-[state=closed]:slide-out-to-top-1 data-[swipe=move]:translate-y-[var(--reka-toast-swipe-move-y)] data-[swipe=cancel]:translate-y-0 data-[swipe=cancel]:transition-transform"
|
||||
:class="t.variant === 'error' ? 'bg-red-600' : 'bg-blue-600'"
|
||||
|
|
@ -27,6 +28,7 @@ const { copy, copied } = useClipboard({ copiedDuring: 1500 })
|
|||
<ToastDescription class="min-w-0 flex-1 select-text">{{ t.message }}</ToastDescription>
|
||||
<button
|
||||
v-if="t.variant === 'error'"
|
||||
data-test-id="toast-copy-error"
|
||||
class="mt-0.5 shrink-0 cursor-pointer rounded p-0.5 opacity-70 hover:opacity-100"
|
||||
:title="copied ? 'Copied!' : 'Copy error'"
|
||||
@click="copy(t.message)"
|
||||
|
|
@ -36,6 +38,7 @@ const { copy, copied } = useClipboard({ copiedDuring: 1500 })
|
|||
</button>
|
||||
<ToastClose
|
||||
v-if="t.variant === 'error'"
|
||||
data-test-id="toast-close"
|
||||
class="mt-0.5 shrink-0 cursor-pointer rounded p-0.5 opacity-70 hover:opacity-100"
|
||||
>
|
||||
<icon-lucide-x class="size-3" />
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ function handleStop() {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex min-w-0 flex-1 flex-col overflow-hidden">
|
||||
<div data-test-id="chat-panel" class="flex min-w-0 flex-1 flex-col overflow-hidden">
|
||||
<APIKeySetup v-if="!isConfigured" />
|
||||
|
||||
<template v-else>
|
||||
|
|
@ -49,6 +49,7 @@ function handleStop() {
|
|||
<!-- Empty state -->
|
||||
<div
|
||||
v-if="messages.length === 0"
|
||||
data-test-id="chat-empty-state"
|
||||
class="flex h-full flex-col items-center justify-center gap-3 text-muted"
|
||||
>
|
||||
<icon-lucide-message-circle class="size-8 opacity-50" />
|
||||
|
|
@ -56,11 +57,15 @@ function handleStop() {
|
|||
</div>
|
||||
|
||||
<!-- Messages -->
|
||||
<div v-else class="flex flex-col gap-3">
|
||||
<div v-else data-test-id="chat-messages" class="flex flex-col gap-3">
|
||||
<ChatMessage v-for="msg in messages" :key="msg.id" :message="msg" />
|
||||
|
||||
<!-- Typing indicator -->
|
||||
<div v-if="status === 'submitted'" class="flex gap-2">
|
||||
<div
|
||||
v-if="status === 'submitted'"
|
||||
data-test-id="chat-typing-indicator"
|
||||
class="flex gap-2"
|
||||
>
|
||||
<div
|
||||
class="flex size-6 shrink-0 items-center justify-center rounded-full bg-muted/20 text-[10px] font-bold text-muted"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -38,14 +38,22 @@ watch(jsxCode, () => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="!jsxCode" class="flex flex-1 items-center justify-center px-4 text-center">
|
||||
<div
|
||||
v-if="!jsxCode"
|
||||
data-test-id="code-panel-empty"
|
||||
class="flex flex-1 items-center justify-center px-4 text-center"
|
||||
>
|
||||
<span class="text-xs text-muted">Select a layer to see its JSX code</span>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex min-h-0 flex-1 flex-col">
|
||||
<div class="flex shrink-0 items-center justify-between border-b border-border px-3 py-1.5">
|
||||
<div v-else data-test-id="code-panel" class="flex min-h-0 flex-1 flex-col">
|
||||
<div
|
||||
data-test-id="code-panel-header"
|
||||
class="flex shrink-0 items-center justify-between border-b border-border px-3 py-1.5"
|
||||
>
|
||||
<span class="text-[11px] text-muted">JSX</span>
|
||||
<button
|
||||
data-test-id="code-panel-copy"
|
||||
class="flex items-center gap-1 rounded px-1.5 py-0.5 text-[11px] text-muted hover:bg-hover hover:text-surface"
|
||||
@click="copyCode"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ function initials(name: string): string {
|
|||
<TooltipRoot>
|
||||
<TooltipTrigger as-child>
|
||||
<div
|
||||
data-test-id="collab-local-avatar"
|
||||
class="flex size-6 items-center justify-center rounded-full border-2 border-panel text-[10px] font-semibold text-white"
|
||||
:style="{ background: colorToCSS(state.localColor) }"
|
||||
>
|
||||
|
|
@ -111,6 +112,7 @@ function initials(name: string): string {
|
|||
<TooltipRoot v-for="peer in peers" :key="peer.clientId">
|
||||
<TooltipTrigger as-child>
|
||||
<div
|
||||
data-test-id="collab-peer-avatar"
|
||||
class="flex size-6 cursor-pointer items-center justify-center rounded-full border-2 text-[10px] font-semibold text-white transition-all"
|
||||
:class="
|
||||
followingPeer === peer.clientId
|
||||
|
|
@ -145,6 +147,7 @@ function initials(name: string): string {
|
|||
<PopoverRoot v-model:open="popoverOpen">
|
||||
<PopoverTrigger as-child>
|
||||
<button
|
||||
data-test-id="collab-share-button"
|
||||
class="flex h-7 cursor-pointer items-center gap-1.5 rounded-md border-none px-3 text-xs font-medium transition-colors"
|
||||
:class="
|
||||
state.connected
|
||||
|
|
@ -161,6 +164,7 @@ function initials(name: string): string {
|
|||
|
||||
<PopoverPortal>
|
||||
<PopoverContent
|
||||
data-test-id="collab-popover"
|
||||
class="z-50 w-72 rounded-lg border border-border bg-panel p-3 shadow-xl"
|
||||
:side-offset="8"
|
||||
side="bottom"
|
||||
|
|
@ -173,10 +177,12 @@ function initials(name: string): string {
|
|||
<input
|
||||
:value="shareUrl"
|
||||
readonly
|
||||
data-test-id="collab-room-link"
|
||||
class="min-w-0 flex-1 rounded border border-border bg-input px-2 py-1 text-xs text-surface"
|
||||
@focus="($event.target as HTMLInputElement).select()"
|
||||
/>
|
||||
<button
|
||||
data-test-id="collab-copy-link"
|
||||
class="flex h-7 cursor-pointer items-center gap-1 rounded border-none bg-accent px-2 text-xs text-white hover:bg-accent/90"
|
||||
@click="copyLink"
|
||||
>
|
||||
|
|
@ -191,6 +197,7 @@ function initials(name: string): string {
|
|||
</div>
|
||||
|
||||
<button
|
||||
data-test-id="collab-disconnect"
|
||||
class="flex h-7 w-full cursor-pointer items-center justify-center rounded border border-border bg-transparent text-xs text-muted hover:bg-hover hover:text-surface"
|
||||
@click="emit('disconnect')"
|
||||
>
|
||||
|
|
@ -209,6 +216,7 @@ function initials(name: string): string {
|
|||
<label class="mb-1 block text-xs text-muted">Your name</label>
|
||||
<input
|
||||
v-model="nameDraft"
|
||||
data-test-id="collab-name-input"
|
||||
class="w-full rounded border border-border bg-input px-2 py-1 text-xs text-surface"
|
||||
placeholder="Enter your name"
|
||||
autofocus
|
||||
|
|
@ -217,6 +225,7 @@ function initials(name: string): string {
|
|||
</div>
|
||||
|
||||
<button
|
||||
data-test-id="collab-join-button"
|
||||
class="flex h-8 w-full cursor-pointer items-center justify-center gap-1.5 rounded border-none bg-accent text-xs font-medium text-white hover:bg-accent/90 disabled:opacity-50"
|
||||
:disabled="!nameDraft.trim()"
|
||||
@click="onJoin"
|
||||
|
|
@ -232,6 +241,7 @@ function initials(name: string): string {
|
|||
<label class="mb-1 block text-xs text-muted">Your name</label>
|
||||
<input
|
||||
v-model="nameDraft"
|
||||
data-test-id="collab-name-input"
|
||||
class="w-full rounded border border-border bg-input px-2 py-1 text-xs text-surface"
|
||||
placeholder="Enter your name"
|
||||
@keydown.enter="onShare"
|
||||
|
|
@ -239,6 +249,7 @@ function initials(name: string): string {
|
|||
</div>
|
||||
|
||||
<button
|
||||
data-test-id="collab-share-file"
|
||||
class="mb-3 flex h-8 w-full cursor-pointer items-center justify-center gap-1.5 rounded border-none bg-accent text-xs font-medium text-white hover:bg-accent/90 disabled:opacity-50"
|
||||
:disabled="!nameDraft.trim()"
|
||||
@click="onShare"
|
||||
|
|
@ -256,11 +267,13 @@ function initials(name: string): string {
|
|||
<div class="flex items-center gap-1.5">
|
||||
<input
|
||||
v-model="joinInput"
|
||||
data-test-id="collab-join-input"
|
||||
class="min-w-0 flex-1 rounded border border-border bg-input px-2 py-1 text-xs text-surface"
|
||||
placeholder="Paste room link or ID"
|
||||
@keydown.enter="onJoin"
|
||||
/>
|
||||
<button
|
||||
data-test-id="collab-join-room-button"
|
||||
class="flex h-7 cursor-pointer items-center rounded border-none bg-accent px-3 text-xs text-white hover:bg-accent/90 disabled:opacity-50"
|
||||
:disabled="!joinInput.trim() || !nameDraft.trim()"
|
||||
@click="onJoin"
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ function onHexChange(e: Event) {
|
|||
<ColorPicker :color="color" @update="emit('update', $event)" />
|
||||
<input
|
||||
v-if="editable"
|
||||
data-test-id="color-hex-input"
|
||||
class="min-w-0 flex-1 border-none bg-transparent font-mono text-xs text-surface outline-none"
|
||||
:value="colorToHexRaw(color)"
|
||||
maxlength="6"
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ const swatchColor = computed(() => {
|
|||
<PopoverRoot>
|
||||
<PopoverTrigger as-child>
|
||||
<button
|
||||
data-test-id="color-picker-swatch"
|
||||
class="size-5 shrink-0 cursor-pointer rounded border border-border p-0"
|
||||
:style="{ background: swatchColor }"
|
||||
/>
|
||||
|
|
@ -31,6 +32,7 @@ const swatchColor = computed(() => {
|
|||
|
||||
<PopoverPortal>
|
||||
<PopoverContent
|
||||
data-test-id="color-picker-popover"
|
||||
class="z-[100] w-56 rounded-lg border border-border bg-panel p-2 shadow-xl"
|
||||
:side-offset="4"
|
||||
side="left"
|
||||
|
|
|
|||
|
|
@ -28,8 +28,15 @@ const isComponentType = computed(() => {
|
|||
|
||||
<template>
|
||||
<!-- Multi-select summary -->
|
||||
<div v-if="multiCount > 1" class="flex-1 overflow-x-hidden overflow-y-auto scrollbar-thin pb-4">
|
||||
<div class="flex items-center gap-1.5 border-b border-border px-3 py-2">
|
||||
<div
|
||||
v-if="multiCount > 1"
|
||||
data-test-id="design-panel-multi"
|
||||
class="flex-1 overflow-x-hidden overflow-y-auto scrollbar-thin pb-4"
|
||||
>
|
||||
<div
|
||||
data-test-id="design-multi-header"
|
||||
class="flex items-center gap-1.5 border-b border-border px-3 py-2"
|
||||
>
|
||||
<span class="text-[11px] text-muted">Mixed</span>
|
||||
<span class="text-xs font-semibold">{{ multiCount }} layers</span>
|
||||
</div>
|
||||
|
|
@ -41,9 +48,15 @@ const isComponentType = computed(() => {
|
|||
</div>
|
||||
|
||||
<!-- Single selection -->
|
||||
<div v-else-if="node" class="flex-1 overflow-x-hidden overflow-y-auto scrollbar-thin pb-4">
|
||||
<!-- Node header -->
|
||||
<div class="flex items-center gap-1.5 border-b border-border px-3 py-2">
|
||||
<div
|
||||
v-else-if="node"
|
||||
data-test-id="design-panel-single"
|
||||
class="flex-1 overflow-x-hidden overflow-y-auto scrollbar-thin pb-4"
|
||||
>
|
||||
<div
|
||||
data-test-id="design-node-header"
|
||||
class="flex items-center gap-1.5 border-b border-border px-3 py-2"
|
||||
>
|
||||
<span class="text-[11px]" :class="isComponentType ? 'text-[#9747ff]' : 'text-muted'">{{
|
||||
node.type
|
||||
}}</span>
|
||||
|
|
@ -56,12 +69,14 @@ const isComponentType = computed(() => {
|
|||
class="flex flex-col gap-1 border-b border-border px-3 py-2"
|
||||
>
|
||||
<button
|
||||
data-test-id="design-go-to-component"
|
||||
class="rounded bg-[#9747ff]/10 px-2 py-1 text-left text-[11px] text-[#9747ff] hover:bg-[#9747ff]/20"
|
||||
@click="store.goToMainComponent()"
|
||||
>
|
||||
Go to Main Component
|
||||
</button>
|
||||
<button
|
||||
data-test-id="design-detach-instance"
|
||||
class="rounded px-2 py-1 text-left text-[11px] text-muted hover:bg-hover"
|
||||
@click="store.detachInstance()"
|
||||
>
|
||||
|
|
@ -80,7 +95,11 @@ const isComponentType = computed(() => {
|
|||
<ExportSection />
|
||||
</div>
|
||||
|
||||
<div v-else class="flex-1 overflow-x-hidden overflow-y-auto scrollbar-thin pb-4">
|
||||
<div
|
||||
v-else
|
||||
data-test-id="design-panel-empty"
|
||||
class="flex-1 overflow-x-hidden overflow-y-auto scrollbar-thin pb-4"
|
||||
>
|
||||
<PageSection />
|
||||
<VariablesSection @open-dialog="variablesOpen = true" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -40,11 +40,20 @@ const cursor = computed(() => {
|
|||
|
||||
<template>
|
||||
<CanvasContextMenu>
|
||||
<div class="canvas-area relative flex-1 min-w-0 min-h-0 overflow-hidden">
|
||||
<canvas ref="canvasRef" :style="{ cursor }" class="block size-full touch-none" />
|
||||
<div
|
||||
data-test-id="canvas-area"
|
||||
class="canvas-area relative flex-1 min-w-0 min-h-0 overflow-hidden"
|
||||
>
|
||||
<canvas
|
||||
ref="canvasRef"
|
||||
data-test-id="canvas-element"
|
||||
:style="{ cursor }"
|
||||
class="block size-full touch-none"
|
||||
/>
|
||||
<Transition leave-active-class="transition-opacity duration-300" leave-to-class="opacity-0">
|
||||
<div
|
||||
v-if="store.state.loading"
|
||||
data-test-id="canvas-loading"
|
||||
class="absolute inset-0 z-50 flex items-center justify-center bg-canvas"
|
||||
>
|
||||
<svg
|
||||
|
|
|
|||
|
|
@ -231,6 +231,7 @@ function stopSwatchColor(stop: GradientStop) {
|
|||
<PopoverRoot>
|
||||
<PopoverTrigger as-child>
|
||||
<button
|
||||
data-test-id="fill-picker-swatch"
|
||||
class="size-5 shrink-0 cursor-pointer rounded border border-border p-0"
|
||||
:style="{ background: swatchBackground }"
|
||||
/>
|
||||
|
|
@ -247,6 +248,7 @@ function stopSwatchColor(stop: GradientStop) {
|
|||
<button
|
||||
class="flex size-6 cursor-pointer items-center justify-center rounded border-none p-0 text-muted transition-colors hover:bg-hover hover:text-surface"
|
||||
:class="{ 'bg-hover text-surface': fillCategory === 'SOLID' }"
|
||||
data-test-id="fill-picker-tab-solid"
|
||||
title="Solid"
|
||||
@click="setCategory('SOLID')"
|
||||
>
|
||||
|
|
@ -257,6 +259,7 @@ function stopSwatchColor(stop: GradientStop) {
|
|||
<button
|
||||
class="flex size-6 cursor-pointer items-center justify-center rounded border-none p-0 text-muted transition-colors hover:bg-hover hover:text-surface"
|
||||
:class="{ 'bg-hover text-surface': fillCategory === 'GRADIENT' }"
|
||||
data-test-id="fill-picker-tab-gradient"
|
||||
title="Gradient"
|
||||
@click="setCategory('GRADIENT')"
|
||||
>
|
||||
|
|
@ -273,6 +276,7 @@ function stopSwatchColor(stop: GradientStop) {
|
|||
<button
|
||||
class="flex size-6 cursor-pointer items-center justify-center rounded border-none p-0 text-muted transition-colors hover:bg-hover hover:text-surface"
|
||||
:class="{ 'bg-hover text-surface': fillCategory === 'IMAGE' }"
|
||||
data-test-id="fill-picker-tab-image"
|
||||
title="Image"
|
||||
@click="setCategory('IMAGE')"
|
||||
>
|
||||
|
|
@ -319,6 +323,7 @@ function stopSwatchColor(stop: GradientStop) {
|
|||
<div
|
||||
v-if="isGradient && fill.gradientStops?.length"
|
||||
ref="gradientStopBarRef"
|
||||
data-test-id="fill-picker-gradient-bar"
|
||||
class="relative mb-2 h-6 rounded"
|
||||
:style="{ background: gradientBarBackground }"
|
||||
@pointermove="onStopBarPointerMove"
|
||||
|
|
@ -343,6 +348,7 @@ function stopSwatchColor(stop: GradientStop) {
|
|||
<span class="text-[11px] text-muted">Stops</span>
|
||||
<button
|
||||
class="flex size-4 cursor-pointer items-center justify-center rounded border-none bg-transparent p-0 text-muted hover:text-surface"
|
||||
data-test-id="fill-picker-add-stop"
|
||||
title="Add stop"
|
||||
@click="addStop"
|
||||
>
|
||||
|
|
@ -399,6 +405,7 @@ function stopSwatchColor(stop: GradientStop) {
|
|||
<!-- Image placeholder -->
|
||||
<div
|
||||
v-if="fill.type === 'IMAGE'"
|
||||
data-test-id="fill-picker-image-placeholder"
|
||||
class="flex h-24 items-center justify-center rounded border border-dashed border-border text-xs text-muted"
|
||||
>
|
||||
Image fill (coming soon)
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ function onSelect(val: string) {
|
|||
<PopoverRoot v-model:open="open">
|
||||
<PopoverAnchor>
|
||||
<button
|
||||
data-test-id="font-picker-trigger"
|
||||
class="flex w-full cursor-pointer items-center justify-between rounded border border-border bg-input px-2 py-1 text-xs text-surface hover:bg-hover"
|
||||
@click="open = !open"
|
||||
>
|
||||
|
|
@ -69,6 +70,7 @@ function onSelect(val: string) {
|
|||
<ListboxFilter
|
||||
ref="filterRef"
|
||||
v-model="searchTerm"
|
||||
data-test-id="font-picker-search"
|
||||
class="min-w-0 flex-1 border-none bg-transparent text-xs text-surface outline-none placeholder:text-muted"
|
||||
placeholder="Search fonts…"
|
||||
autocomplete="off"
|
||||
|
|
@ -86,6 +88,7 @@ function onSelect(val: string) {
|
|||
>
|
||||
<ListboxItem
|
||||
:value="option"
|
||||
data-test-id="font-picker-item"
|
||||
class="flex w-full cursor-pointer items-center gap-2 px-2 py-2 text-sm text-surface outline-none data-[highlighted]:bg-hover"
|
||||
:style="{ fontFamily: `'${option}', sans-serif` }"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@ const hueColor = computed(() => {
|
|||
<!-- SV area -->
|
||||
<div
|
||||
ref="svAreaRef"
|
||||
data-test-id="hsv-sv-area"
|
||||
class="relative h-[140px] w-full cursor-crosshair overflow-hidden rounded"
|
||||
:style="{ background: hueColor }"
|
||||
@pointerdown="onSvPointerDown"
|
||||
|
|
@ -146,7 +147,15 @@ const hueColor = computed(() => {
|
|||
|
||||
<!-- Hue slider -->
|
||||
<div class="mt-2">
|
||||
<input type="range" class="hue-slider" :value="hue" min="0" max="360" @input="onHueInput" />
|
||||
<input
|
||||
type="range"
|
||||
data-test-id="hsv-hue-slider"
|
||||
class="hue-slider"
|
||||
:value="hue"
|
||||
min="0"
|
||||
max="360"
|
||||
@input="onHueInput"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Alpha slider -->
|
||||
|
|
@ -157,6 +166,7 @@ const hueColor = computed(() => {
|
|||
/>
|
||||
<input
|
||||
type="range"
|
||||
data-test-id="hsv-alpha-slider"
|
||||
class="alpha-slider"
|
||||
:value="alpha * 100"
|
||||
min="0"
|
||||
|
|
@ -170,6 +180,7 @@ const hueColor = computed(() => {
|
|||
<span class="text-[11px] text-muted">#</span>
|
||||
<input
|
||||
type="text"
|
||||
data-test-id="hsv-hex-input"
|
||||
class="min-w-0 flex-1 rounded border border-border bg-input px-1.5 py-0.5 font-mono text-xs text-surface"
|
||||
:value="hexValue"
|
||||
maxlength="6"
|
||||
|
|
@ -177,6 +188,7 @@ const hueColor = computed(() => {
|
|||
/>
|
||||
<input
|
||||
type="number"
|
||||
data-test-id="hsv-alpha-input"
|
||||
class="w-10 rounded border border-border bg-input px-1 py-0.5 text-right text-xs text-surface"
|
||||
:value="Math.round(alpha * 100)"
|
||||
min="0"
|
||||
|
|
|
|||
|
|
@ -260,6 +260,7 @@ function updateDropTarget(ev: PointerEvent) {
|
|||
|
||||
<template>
|
||||
<aside
|
||||
data-test-id="layers-panel"
|
||||
class="flex min-w-0 flex-1 flex-col overflow-hidden border-r border-border bg-panel"
|
||||
style="contain: paint layout style"
|
||||
>
|
||||
|
|
@ -279,12 +280,19 @@ function updateDropTarget(ev: PointerEvent) {
|
|||
/>
|
||||
</SplitterResizeHandle>
|
||||
<SplitterPanel :default-size="70" :min-size="20" class="flex flex-col overflow-hidden">
|
||||
<header class="shrink-0 px-3 py-2 text-[11px] uppercase tracking-wider text-muted">
|
||||
<header
|
||||
data-test-id="layers-header"
|
||||
class="shrink-0 px-3 py-2 text-[11px] uppercase tracking-wider text-muted"
|
||||
>
|
||||
Layers
|
||||
</header>
|
||||
<ContextMenuRoot :modal="false">
|
||||
<ContextMenuTrigger as-child @contextmenu="onLayerRightClick">
|
||||
<div ref="listRef" class="relative flex-1 overflow-y-auto scrollbar-thin px-1">
|
||||
<div
|
||||
ref="listRef"
|
||||
data-test-id="layers-tree"
|
||||
class="relative flex-1 overflow-y-auto scrollbar-thin px-1"
|
||||
>
|
||||
<TreeRoot
|
||||
:key="treeKey"
|
||||
v-slot="{ flattenItems }"
|
||||
|
|
@ -301,6 +309,7 @@ function updateDropTarget(ev: PointerEvent) {
|
|||
>
|
||||
<TreeItem v-slot="{ isExpanded }" v-bind="item.bind" as-child @select="onSelect">
|
||||
<button
|
||||
data-test-id="layers-item"
|
||||
class="group/row flex w-full cursor-pointer items-center gap-1 rounded border-none py-1 text-left text-xs"
|
||||
:class="[
|
||||
store.state.selectedIds.has(item.value.id)
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ const menuClass =
|
|||
<template>
|
||||
<ContextMenuContent :class="menuClass" :side-offset="2" align="start">
|
||||
<ContextMenuItem
|
||||
data-test-id="context-copy"
|
||||
:class="itemClass"
|
||||
:disabled="!hasSelection"
|
||||
@select="document.execCommand('copy')"
|
||||
|
|
@ -80,6 +81,7 @@ const menuClass =
|
|||
<span class="text-[11px] text-muted">⌘C</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
data-test-id="context-cut"
|
||||
:class="itemClass"
|
||||
:disabled="!hasSelection"
|
||||
@select="document.execCommand('cut')"
|
||||
|
|
@ -87,11 +89,16 @@ const menuClass =
|
|||
<span>Cut</span>
|
||||
<span class="text-[11px] text-muted">⌘X</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem :class="itemClass" @select="document.execCommand('paste')">
|
||||
<ContextMenuItem
|
||||
data-test-id="context-paste"
|
||||
:class="itemClass"
|
||||
@select="document.execCommand('paste')"
|
||||
>
|
||||
<span>Paste here</span>
|
||||
<span class="text-[11px] text-muted">⌘V</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
data-test-id="context-duplicate"
|
||||
:class="itemClass"
|
||||
:disabled="!hasSelection"
|
||||
@select="store.duplicateSelected()"
|
||||
|
|
@ -99,7 +106,12 @@ const menuClass =
|
|||
<span>Duplicate</span>
|
||||
<span class="text-[11px] text-muted">⌘D</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem :class="itemClass" :disabled="!hasSelection" @select="store.deleteSelected()">
|
||||
<ContextMenuItem
|
||||
data-test-id="context-delete"
|
||||
:class="itemClass"
|
||||
:disabled="!hasSelection"
|
||||
@select="store.deleteSelected()"
|
||||
>
|
||||
<span>Delete</span>
|
||||
<span class="text-[11px] text-muted">⌫</span>
|
||||
</ContextMenuItem>
|
||||
|
|
@ -107,7 +119,7 @@ const menuClass =
|
|||
<ContextMenuSeparator class="my-1 h-px bg-border" />
|
||||
|
||||
<ContextMenuSub v-if="otherPages.length > 0 && hasSelection">
|
||||
<ContextMenuSubTrigger :class="itemClass">
|
||||
<ContextMenuSubTrigger data-test-id="context-move-to-page" :class="itemClass">
|
||||
<span>Move to page</span>
|
||||
<span class="text-sm text-muted">›</span>
|
||||
</ContextMenuSubTrigger>
|
||||
|
|
@ -125,26 +137,51 @@ const menuClass =
|
|||
</ContextMenuPortal>
|
||||
</ContextMenuSub>
|
||||
|
||||
<ContextMenuItem :class="itemClass" :disabled="!hasSelection" @select="store.bringToFront()">
|
||||
<ContextMenuItem
|
||||
data-test-id="context-bring-to-front"
|
||||
:class="itemClass"
|
||||
:disabled="!hasSelection"
|
||||
@select="store.bringToFront()"
|
||||
>
|
||||
<span>Bring to front</span>
|
||||
<span class="text-[11px] text-muted">]</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem :class="itemClass" :disabled="!hasSelection" @select="store.sendToBack()">
|
||||
<ContextMenuItem
|
||||
data-test-id="context-send-to-back"
|
||||
:class="itemClass"
|
||||
:disabled="!hasSelection"
|
||||
@select="store.sendToBack()"
|
||||
>
|
||||
<span>Send to back</span>
|
||||
<span class="text-[11px] text-muted">[</span>
|
||||
</ContextMenuItem>
|
||||
|
||||
<ContextMenuSeparator class="my-1 h-px bg-border" />
|
||||
|
||||
<ContextMenuItem :class="itemClass" :disabled="multiCount < 2" @select="store.groupSelected()">
|
||||
<ContextMenuItem
|
||||
data-test-id="context-group"
|
||||
:class="itemClass"
|
||||
:disabled="multiCount < 2"
|
||||
@select="store.groupSelected()"
|
||||
>
|
||||
<span>Group</span>
|
||||
<span class="text-[11px] text-muted">⌘G</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem v-if="isGroup" :class="itemClass" @select="store.ungroupSelected()">
|
||||
<ContextMenuItem
|
||||
v-if="isGroup"
|
||||
data-test-id="context-ungroup"
|
||||
:class="itemClass"
|
||||
@select="store.ungroupSelected()"
|
||||
>
|
||||
<span>Ungroup</span>
|
||||
<span class="text-[11px] text-muted">⇧⌘G</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem v-if="hasSelection" :class="itemClass" @select="store.wrapInAutoLayout()">
|
||||
<ContextMenuItem
|
||||
v-if="hasSelection"
|
||||
data-test-id="context-auto-layout"
|
||||
:class="itemClass"
|
||||
@select="store.wrapInAutoLayout()"
|
||||
>
|
||||
<span>Add auto layout</span>
|
||||
<span class="text-[11px] text-muted">⇧A</span>
|
||||
</ContextMenuItem>
|
||||
|
|
@ -152,6 +189,7 @@ const menuClass =
|
|||
<ContextMenuSeparator class="my-1 h-px bg-border" />
|
||||
|
||||
<ContextMenuItem
|
||||
data-test-id="context-create-component"
|
||||
:class="componentItemClass"
|
||||
:disabled="!hasSelection"
|
||||
@select="store.createComponentFromSelection()"
|
||||
|
|
@ -161,6 +199,7 @@ const menuClass =
|
|||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
v-if="canCreateComponentSet"
|
||||
data-test-id="context-create-component-set"
|
||||
:class="componentItemClass"
|
||||
@select="store.createComponentSetFromComponents()"
|
||||
>
|
||||
|
|
@ -169,6 +208,7 @@ const menuClass =
|
|||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
v-if="isComponent"
|
||||
data-test-id="context-create-instance"
|
||||
:class="componentItemClass"
|
||||
@select="store.createInstanceFromComponent(singleNode!.id)"
|
||||
>
|
||||
|
|
@ -176,12 +216,18 @@ const menuClass =
|
|||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
v-if="isInstance"
|
||||
data-test-id="context-go-to-component"
|
||||
:class="componentItemClass"
|
||||
@select="store.goToMainComponent()"
|
||||
>
|
||||
<span>Go to main component</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem v-if="isInstance" :class="itemClass" @select="store.detachInstance()">
|
||||
<ContextMenuItem
|
||||
v-if="isInstance"
|
||||
data-test-id="context-detach-instance"
|
||||
:class="itemClass"
|
||||
@select="store.detachInstance()"
|
||||
>
|
||||
<span>Detach instance</span>
|
||||
<span class="text-[11px] text-muted">⌥⌘B</span>
|
||||
</ContextMenuItem>
|
||||
|
|
@ -189,18 +235,30 @@ const menuClass =
|
|||
<template v-if="hasSelection">
|
||||
<ContextMenuSeparator class="my-1 h-px bg-border" />
|
||||
|
||||
<ContextMenuItem :class="itemClass" @select="store.toggleVisibility()">
|
||||
<ContextMenuItem
|
||||
data-test-id="context-toggle-visibility"
|
||||
:class="itemClass"
|
||||
@select="store.toggleVisibility()"
|
||||
>
|
||||
<span>{{ isVisible ? 'Hide' : 'Show' }}</span>
|
||||
<span class="text-[11px] text-muted">⇧⌘H</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem :class="itemClass" @select="store.toggleLock()">
|
||||
<ContextMenuItem
|
||||
data-test-id="context-toggle-lock"
|
||||
:class="itemClass"
|
||||
@select="store.toggleLock()"
|
||||
>
|
||||
<span>{{ isLocked ? 'Unlock' : 'Lock' }}</span>
|
||||
<span class="text-[11px] text-muted">⇧⌘L</span>
|
||||
</ContextMenuItem>
|
||||
|
||||
<ContextMenuSeparator class="my-1 h-px bg-border" />
|
||||
|
||||
<ContextMenuItem :class="itemClass" @select="store.exportSelection(1, 'PNG')">
|
||||
<ContextMenuItem
|
||||
data-test-id="context-export-png"
|
||||
:class="itemClass"
|
||||
@select="store.exportSelection(1, 'PNG')"
|
||||
>
|
||||
<span>Export as PNG</span>
|
||||
<span class="text-[11px] text-muted">⇧⌘E</span>
|
||||
</ContextMenuItem>
|
||||
|
|
|
|||
|
|
@ -44,10 +44,13 @@ function onKeydown(e: KeyboardEvent, pageId: string) {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex min-h-0 flex-1 flex-col">
|
||||
<div data-test-id="pages-panel" class="flex min-h-0 flex-1 flex-col">
|
||||
<div class="flex shrink-0 items-center justify-between px-3 py-1.5">
|
||||
<span class="text-[11px] uppercase tracking-wider text-muted">Pages</span>
|
||||
<span data-test-id="pages-header" class="text-[11px] uppercase tracking-wider text-muted"
|
||||
>Pages</span
|
||||
>
|
||||
<button
|
||||
data-test-id="pages-add"
|
||||
class="cursor-pointer rounded border-none bg-transparent px-1 text-base leading-none text-muted hover:bg-hover hover:text-surface"
|
||||
title="Add page"
|
||||
@click="store.addPage()"
|
||||
|
|
@ -60,6 +63,7 @@ function onKeydown(e: KeyboardEvent, pageId: string) {
|
|||
<input
|
||||
v-if="editingPageId === pg.id"
|
||||
data-page-edit
|
||||
data-test-id="pages-item-input"
|
||||
class="w-full rounded border border-accent bg-input px-2 py-1 text-xs text-surface outline-none"
|
||||
:value="pg.name"
|
||||
@blur="commitRename(pg.id, $event.target as HTMLInputElement)"
|
||||
|
|
@ -74,6 +78,7 @@ function onKeydown(e: KeyboardEvent, pageId: string) {
|
|||
</div>
|
||||
<button
|
||||
v-else
|
||||
data-test-id="pages-item"
|
||||
class="flex w-full cursor-pointer items-center gap-1.5 rounded border-none px-2 py-1 text-left text-xs"
|
||||
:class="
|
||||
pg.id === store.state.currentPageId
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ const { activeTab } = useAIChat()
|
|||
|
||||
<template>
|
||||
<aside
|
||||
data-test-id="properties-panel"
|
||||
class="flex min-w-0 flex-1 flex-col overflow-hidden border-l border-border bg-panel"
|
||||
style="contain: paint layout style"
|
||||
>
|
||||
|
|
@ -21,12 +22,14 @@ const { activeTab } = useAIChat()
|
|||
<TabsList class="flex h-10 shrink-0 items-center gap-1 border-b border-border px-2">
|
||||
<TabsTrigger
|
||||
value="design"
|
||||
data-test-id="properties-tab-design"
|
||||
class="rounded px-2.5 py-1 text-xs text-muted hover:text-surface data-[state=active]:font-semibold data-[state=active]:text-surface"
|
||||
>
|
||||
Design
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="code"
|
||||
data-test-id="properties-tab-code"
|
||||
class="flex items-center gap-1 rounded px-2.5 py-1 text-xs text-muted hover:text-surface data-[state=active]:font-semibold data-[state=active]:text-surface"
|
||||
>
|
||||
<icon-lucide-code class="size-3" />
|
||||
|
|
@ -34,6 +37,7 @@ const { activeTab } = useAIChat()
|
|||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="ai"
|
||||
data-test-id="properties-tab-ai"
|
||||
class="flex items-center gap-1 rounded px-2.5 py-1 text-xs text-muted hover:text-surface data-[state=active]:font-semibold data-[state=active]:text-surface"
|
||||
>
|
||||
<icon-lucide-sparkles class="size-3" />
|
||||
|
|
@ -41,6 +45,7 @@ const { activeTab } = useAIChat()
|
|||
</TabsTrigger>
|
||||
<span
|
||||
v-if="activeTab === 'design'"
|
||||
data-test-id="properties-zoom"
|
||||
class="ml-auto cursor-pointer rounded px-1.5 py-0.5 text-[11px] text-muted hover:bg-hover"
|
||||
>
|
||||
{{ Math.round(store.state.zoom * 100) }}%
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ const show = !IS_TAURI && typeof window !== 'undefined' && !window.showSaveFileP
|
|||
<template>
|
||||
<div
|
||||
v-if="show && !dismissed"
|
||||
data-test-id="safari-banner"
|
||||
class="flex items-center gap-2 border-b border-amber-500/30 bg-amber-500/10 px-3 py-1.5 text-xs text-amber-200"
|
||||
>
|
||||
<span class="flex-1">
|
||||
|
|
@ -19,6 +20,7 @@ const show = !IS_TAURI && typeof window !== 'undefined' && !window.showSaveFileP
|
|||
Edge for full support.
|
||||
</span>
|
||||
<button
|
||||
data-test-id="safari-banner-dismiss"
|
||||
class="shrink-0 rounded px-1.5 py-0.5 text-amber-300 transition-colors hover:bg-amber-500/20"
|
||||
@click="dismissed = true"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -111,6 +111,7 @@ function onKeydown(e: KeyboardEvent) {
|
|||
|
||||
<template>
|
||||
<div
|
||||
data-test-id="scrub-input"
|
||||
class="flex min-w-0 flex-1 items-center rounded border border-border bg-input h-[26px] focus-within:border-accent"
|
||||
:style="{ cursor: editing ? 'auto' : 'ew-resize' }"
|
||||
@pointerdown="!editing && startScrub($event)"
|
||||
|
|
@ -127,6 +128,7 @@ function onKeydown(e: KeyboardEvent) {
|
|||
v-if="editing"
|
||||
ref="inputRef"
|
||||
type="number"
|
||||
data-test-id="scrub-input-field"
|
||||
class="min-w-0 flex-1 cursor-text border-none bg-transparent pr-1.5 font-[inherit] text-xs text-surface outline-none"
|
||||
:value="isMixed ? '' : displayValue"
|
||||
:placeholder="placeholder"
|
||||
|
|
|
|||
|
|
@ -36,12 +36,14 @@ function onClose(e: MouseEvent, tabId: string) {
|
|||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
:value="tab.id"
|
||||
data-test-id="tabbar-tab"
|
||||
class="group/tab flex h-full max-w-48 min-w-0 cursor-pointer items-center gap-1.5 border-r border-border px-3 text-xs transition-colors select-none outline-none focus-visible:ring-1 focus-visible:ring-accent data-[state=active]:bg-panel data-[state=active]:text-surface data-[state=inactive]:text-muted data-[state=inactive]:hover:text-surface"
|
||||
@mousedown="onMiddleClick($event, tab.id)"
|
||||
>
|
||||
<icon-lucide-file class="size-3 shrink-0 opacity-50" />
|
||||
<span class="min-w-0 flex-1 truncate">{{ tab.name }}</span>
|
||||
<button
|
||||
data-test-id="tabbar-close"
|
||||
class="flex size-4 shrink-0 cursor-pointer items-center justify-center rounded opacity-0 transition-opacity hover:bg-hover group-hover/tab:opacity-100 data-[state=active]:opacity-100"
|
||||
:class="tab.isActive ? 'opacity-100' : ''"
|
||||
:title="`Close ${tab.name}`"
|
||||
|
|
@ -54,6 +56,7 @@ function onClose(e: MouseEvent, tabId: string) {
|
|||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<button
|
||||
data-test-id="tabbar-new"
|
||||
class="flex size-9 shrink-0 cursor-pointer items-center justify-center text-muted transition-colors hover:text-surface"
|
||||
title="New tab"
|
||||
aria-label="New tab"
|
||||
|
|
|
|||
|
|
@ -81,11 +81,15 @@ function activeKeyForTool(tool: (typeof TOOLS)[number]): Tool {
|
|||
|
||||
<template>
|
||||
<div class="absolute bottom-4 left-1/2 z-10 flex -translate-x-1/2 items-center">
|
||||
<div class="flex gap-0.5 rounded-xl border border-border bg-panel p-1 shadow-lg">
|
||||
<div
|
||||
data-test-id="toolbar"
|
||||
class="flex gap-0.5 rounded-xl border border-border bg-panel p-1 shadow-lg"
|
||||
>
|
||||
<template v-for="tool in TOOLS" :key="tool.key">
|
||||
<!-- Tool with flyout: split button + chevron -->
|
||||
<div v-if="tool.flyout && tool.flyout.length > 1" class="flex items-center">
|
||||
<button
|
||||
:data-test-id="`toolbar-tool-${activeKeyForTool(tool).toLowerCase()}`"
|
||||
class="flex size-8 cursor-pointer items-center justify-center rounded-lg border-none transition-colors"
|
||||
:class="
|
||||
isActive(tool)
|
||||
|
|
@ -101,6 +105,7 @@ function activeKeyForTool(tool: (typeof TOOLS)[number]): Tool {
|
|||
<DropdownMenuRoot>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<button
|
||||
:data-test-id="`toolbar-flyout-${tool.key.toLowerCase()}`"
|
||||
class="flex h-8 w-3 cursor-pointer items-center justify-center rounded-lg border-none transition-colors"
|
||||
:class="
|
||||
isActive(tool)
|
||||
|
|
@ -122,6 +127,7 @@ function activeKeyForTool(tool: (typeof TOOLS)[number]): Tool {
|
|||
<DropdownMenuItem
|
||||
v-for="sub in tool.flyout"
|
||||
:key="sub"
|
||||
:data-test-id="`toolbar-flyout-item-${sub.toLowerCase()}`"
|
||||
class="flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-xs outline-none transition-colors"
|
||||
:class="
|
||||
store.state.activeTool === sub
|
||||
|
|
@ -144,6 +150,7 @@ function activeKeyForTool(tool: (typeof TOOLS)[number]): Tool {
|
|||
<!-- Simple tool button -->
|
||||
<button
|
||||
v-else
|
||||
:data-test-id="`toolbar-tool-${tool.key.toLowerCase()}`"
|
||||
class="flex size-8 cursor-pointer items-center justify-center rounded-lg border-none transition-colors"
|
||||
:class="
|
||||
isActive(tool)
|
||||
|
|
|
|||
|
|
@ -378,6 +378,7 @@ const table = useVueTable({
|
|||
<DialogPortal>
|
||||
<DialogOverlay class="fixed inset-0 z-40 bg-black/50" />
|
||||
<DialogContent
|
||||
data-test-id="variables-dialog"
|
||||
class="fixed left-1/2 top-1/2 z-50 flex h-[75vh] w-[800px] max-w-[90vw] -translate-x-1/2 -translate-y-1/2 flex-col rounded-xl border border-border bg-panel shadow-2xl outline-none"
|
||||
>
|
||||
<div v-if="collections.length === 0" class="flex flex-1 flex-col">
|
||||
|
|
@ -393,6 +394,7 @@ const table = useVueTable({
|
|||
<div class="text-center">
|
||||
<p class="text-sm text-muted">No variable collections</p>
|
||||
<button
|
||||
data-test-id="variables-create-collection"
|
||||
class="mt-2 cursor-pointer rounded bg-hover px-3 py-1.5 text-xs text-surface hover:bg-border"
|
||||
@click="addCollection"
|
||||
>
|
||||
|
|
@ -420,6 +422,7 @@ const table = useVueTable({
|
|||
<TabsTrigger
|
||||
v-else
|
||||
:value="col.id"
|
||||
data-test-id="variables-collection-tab"
|
||||
class="cursor-pointer whitespace-nowrap rounded border-none px-2.5 py-1 text-xs text-muted data-[state=active]:bg-hover data-[state=active]:text-surface"
|
||||
@dblclick="startRenameCollection(col.id)"
|
||||
>
|
||||
|
|
@ -438,6 +441,7 @@ const table = useVueTable({
|
|||
/>
|
||||
</div>
|
||||
<button
|
||||
data-test-id="variables-add-collection"
|
||||
class="flex size-6 cursor-pointer items-center justify-center rounded border-none bg-transparent text-muted hover:bg-hover hover:text-surface"
|
||||
title="Add collection"
|
||||
@click="addCollection"
|
||||
|
|
@ -521,6 +525,7 @@ const table = useVueTable({
|
|||
|
||||
<!-- Footer -->
|
||||
<button
|
||||
data-test-id="variables-add-variable"
|
||||
class="flex w-full shrink-0 cursor-pointer items-center gap-1.5 border-t border-border bg-transparent px-4 py-2 text-xs text-muted hover:bg-hover hover:text-surface"
|
||||
@click="addVariable"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -14,18 +14,23 @@ function save() {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-1 flex-col items-center justify-center gap-4 px-4">
|
||||
<div
|
||||
data-test-id="api-key-setup"
|
||||
class="flex flex-1 flex-col items-center justify-center gap-4 px-4"
|
||||
>
|
||||
<icon-lucide-key-round class="size-8 text-muted" />
|
||||
<p class="text-center text-xs text-muted">Enter your OpenRouter API key to start chatting.</p>
|
||||
<form class="flex w-full gap-1.5" @submit.prevent="save">
|
||||
<input
|
||||
v-model="input"
|
||||
type="password"
|
||||
data-test-id="api-key-input"
|
||||
placeholder="sk-or-…"
|
||||
class="min-w-0 flex-1 rounded border border-border bg-input px-2 py-1 text-xs text-surface outline-none focus:border-accent"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
data-test-id="api-key-save"
|
||||
class="shrink-0 rounded bg-accent px-2.5 py-1 text-xs font-medium text-white hover:bg-accent/90"
|
||||
:disabled="!input.trim()"
|
||||
>
|
||||
|
|
@ -35,6 +40,7 @@ function save() {
|
|||
<a
|
||||
href="https://openrouter.ai/keys"
|
||||
target="_blank"
|
||||
data-test-id="api-key-get-link"
|
||||
class="text-[10px] text-muted underline hover:text-surface"
|
||||
>
|
||||
Get an API key →
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ function handleSubmit(e: Event) {
|
|||
<div class="mb-1.5 flex items-center">
|
||||
<SelectRoot v-model="modelId">
|
||||
<SelectTrigger
|
||||
data-test-id="chat-model-selector"
|
||||
class="flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] text-muted hover:bg-hover hover:text-surface"
|
||||
>
|
||||
<icon-lucide-bot class="size-3" />
|
||||
|
|
@ -90,6 +91,7 @@ function handleSubmit(e: Event) {
|
|||
<input
|
||||
v-model="input"
|
||||
type="text"
|
||||
data-test-id="chat-input"
|
||||
placeholder="Describe a change…"
|
||||
class="min-w-0 flex-1 rounded border border-border bg-input px-2.5 py-1.5 text-xs text-surface outline-none placeholder:text-muted focus:border-accent"
|
||||
:disabled="status === 'submitted'"
|
||||
|
|
@ -98,6 +100,7 @@ function handleSubmit(e: Event) {
|
|||
<TooltipTrigger as-child>
|
||||
<button
|
||||
type="button"
|
||||
data-test-id="chat-stop-button"
|
||||
class="shrink-0 rounded border border-border px-2 py-1.5 text-xs text-muted hover:bg-hover"
|
||||
@click="emit('stop')"
|
||||
>
|
||||
|
|
@ -118,6 +121,7 @@ function handleSubmit(e: Event) {
|
|||
<TooltipTrigger as-child>
|
||||
<button
|
||||
type="submit"
|
||||
data-test-id="chat-send-button"
|
||||
class="shrink-0 rounded bg-accent px-2.5 py-1.5 text-xs font-medium text-white hover:bg-accent/90"
|
||||
:disabled="!input.trim()"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -55,11 +55,15 @@ function toolState(part: ToolPart): 'pending' | 'done' | 'error' {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="message.role === 'user' ? 'flex justify-end' : ''">
|
||||
<div
|
||||
:data-test-id="`chat-message-${message.role}`"
|
||||
:class="message.role === 'user' ? 'flex justify-end' : ''"
|
||||
>
|
||||
<div class="min-w-0 space-y-1.5" :class="message.role === 'user' ? 'max-w-[85%]' : ''">
|
||||
<!-- Tool timeline -->
|
||||
<div
|
||||
v-if="message.role === 'assistant' && getToolParts(message).length > 0"
|
||||
data-test-id="chat-tool-timeline"
|
||||
class="space-y-0.5 rounded-lg border border-border bg-canvas p-2"
|
||||
>
|
||||
<CollapsibleRoot v-for="tool in getToolParts(message)" :key="tool.toolCallId">
|
||||
|
|
@ -114,6 +118,7 @@ function toolState(part: ToolPart): 'pending' | 'done' | 'error' {
|
|||
<!-- Text bubble -->
|
||||
<div
|
||||
v-if="getTextContent(message)"
|
||||
data-test-id="chat-text-bubble"
|
||||
class="rounded-xl px-3 py-2 text-xs leading-relaxed"
|
||||
:class="
|
||||
message.role === 'user'
|
||||
|
|
|
|||
|
|
@ -147,10 +147,11 @@ const cornerRadiusValue = computed(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="active" class="border-b border-border px-3 py-2">
|
||||
<div v-if="active" data-test-id="appearance-section" class="border-b border-border px-3 py-2">
|
||||
<div class="mb-1.5 flex items-center justify-between">
|
||||
<label class="text-[11px] text-muted">Appearance</label>
|
||||
<button
|
||||
data-test-id="appearance-visibility"
|
||||
class="flex cursor-pointer items-center justify-center rounded border-none bg-transparent p-0.5 text-muted hover:bg-hover hover:text-surface"
|
||||
:class="{ 'text-accent': visibilityState === 'hidden' }"
|
||||
title="Toggle visibility"
|
||||
|
|
|
|||
|
|
@ -143,10 +143,11 @@ function toggleExpand(index: number) {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="active" class="border-b border-border px-3 py-2">
|
||||
<div v-if="active" data-test-id="effects-section" class="border-b border-border px-3 py-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="mb-1 block text-[11px] text-muted">Effects</label>
|
||||
<button
|
||||
data-test-id="effects-section-add"
|
||||
class="flex size-5 cursor-pointer items-center justify-center rounded border-none bg-transparent text-sm leading-none text-muted hover:bg-hover hover:text-surface"
|
||||
@click="add"
|
||||
>
|
||||
|
|
@ -156,7 +157,12 @@ function toggleExpand(index: number) {
|
|||
|
||||
<p v-if="effectsAreMixed" class="text-[11px] text-muted">Click + to replace mixed effects</p>
|
||||
|
||||
<div v-for="(effect, i) in effectsAreMixed ? [] : ((node ?? nodes[0])?.effects ?? [])" :key="i">
|
||||
<div
|
||||
v-for="(effect, i) in effectsAreMixed ? [] : ((node ?? nodes[0])?.effects ?? [])"
|
||||
:key="i"
|
||||
data-test-id="effects-item"
|
||||
:data-test-index="i"
|
||||
>
|
||||
<!-- Collapsed row: color swatch | type dropdown | eye | minus -->
|
||||
<div class="group flex items-center gap-1.5 py-0.5">
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -95,10 +95,11 @@ onUnmounted(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="border-b border-border px-3 py-2">
|
||||
<div data-test-id="export-section" class="border-b border-border px-3 py-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="mb-1 block text-[11px] text-muted">Export</label>
|
||||
<button
|
||||
data-test-id="export-section-add"
|
||||
class="flex size-5 cursor-pointer items-center justify-center rounded border-none bg-transparent text-sm leading-none text-muted hover:bg-hover hover:text-surface"
|
||||
@click="addSetting"
|
||||
>
|
||||
|
|
@ -106,7 +107,13 @@ onUnmounted(() => {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div v-for="(setting, i) in settings" :key="i" class="flex items-center gap-1.5 py-0.5">
|
||||
<div
|
||||
v-for="(setting, i) in settings"
|
||||
:key="i"
|
||||
data-test-id="export-item"
|
||||
:data-test-index="i"
|
||||
class="flex items-center gap-1.5 py-0.5"
|
||||
>
|
||||
<AppSelect
|
||||
:model-value="setting.scale"
|
||||
:options="SCALE_OPTIONS"
|
||||
|
|
@ -128,6 +135,7 @@ onUnmounted(() => {
|
|||
|
||||
<button
|
||||
v-if="settings.length > 0"
|
||||
data-test-id="export-button"
|
||||
class="mt-1.5 w-full cursor-pointer truncate rounded bg-blue-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-blue-700 disabled:cursor-default disabled:opacity-50"
|
||||
:disabled="exporting"
|
||||
@click="doExport"
|
||||
|
|
@ -137,6 +145,7 @@ onUnmounted(() => {
|
|||
|
||||
<button
|
||||
v-if="settings.length > 0"
|
||||
data-test-id="export-preview-toggle"
|
||||
class="mt-1 flex w-full cursor-pointer items-center gap-1 rounded border-none bg-transparent px-0 py-1 text-[11px] text-muted hover:text-surface"
|
||||
@click="showPreview = !showPreview"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -116,10 +116,11 @@ const filteredVariables = computed(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="active" class="border-b border-border px-3 py-2">
|
||||
<div v-if="active" data-test-id="fill-section" class="border-b border-border px-3 py-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="mb-1 block text-[11px] text-muted">Fill</label>
|
||||
<button
|
||||
data-test-id="fill-section-add"
|
||||
class="flex size-5 cursor-pointer items-center justify-center rounded border-none bg-transparent text-sm leading-none text-muted hover:bg-hover hover:text-surface"
|
||||
@click="add"
|
||||
>
|
||||
|
|
@ -130,6 +131,8 @@ const filteredVariables = computed(() => {
|
|||
<div
|
||||
v-for="(fill, i) in fillsAreMixed ? [] : (activeNode?.fills ?? [])"
|
||||
:key="i"
|
||||
data-test-id="fill-item"
|
||||
:data-test-index="i"
|
||||
class="group flex items-center gap-1.5 py-0.5"
|
||||
>
|
||||
<FillPicker :fill="fill" @update="updateFill(i, $event)" />
|
||||
|
|
@ -142,6 +145,7 @@ const filteredVariables = computed(() => {
|
|||
{{ getBoundVariable(i)!.name }}
|
||||
</span>
|
||||
<button
|
||||
data-test-id="fill-unbind-variable"
|
||||
class="cursor-pointer border-none bg-transparent p-0 text-violet-400 hover:text-surface"
|
||||
title="Detach variable"
|
||||
@click="unbindVariable(i)"
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ function setAlignment(primary: LayoutAlign, counter: LayoutCounterAlign) {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="node" class="border-b border-border px-3 py-2">
|
||||
<div v-if="node" data-test-id="layout-section" class="border-b border-border px-3 py-2">
|
||||
<label class="mb-1.5 block text-[11px] text-muted">Layout</label>
|
||||
<div class="flex gap-1.5">
|
||||
<!-- Width -->
|
||||
|
|
@ -147,6 +147,7 @@ function setAlignment(primary: LayoutAlign, counter: LayoutCounterAlign) {
|
|||
</button>
|
||||
<div
|
||||
v-if="widthSizingOpen"
|
||||
data-test-id="layout-width-sizing-dropdown"
|
||||
class="absolute top-full left-0 right-0 z-10 min-w-40 rounded-md border border-border bg-panel p-1 shadow-lg"
|
||||
>
|
||||
<button
|
||||
|
|
@ -194,6 +195,7 @@ function setAlignment(primary: LayoutAlign, counter: LayoutCounterAlign) {
|
|||
</button>
|
||||
<div
|
||||
v-if="heightSizingOpen"
|
||||
data-test-id="layout-height-sizing-dropdown"
|
||||
class="absolute top-full left-0 right-0 z-10 min-w-40 rounded-md border border-border bg-panel p-1 shadow-lg"
|
||||
>
|
||||
<button
|
||||
|
|
@ -233,6 +235,7 @@ function setAlignment(primary: LayoutAlign, counter: LayoutCounterAlign) {
|
|||
<button
|
||||
v-if="node.layoutMode === 'NONE'"
|
||||
class="cursor-pointer rounded border-none bg-transparent px-1 text-base leading-none text-muted hover:bg-hover hover:text-surface"
|
||||
data-test-id="layout-add-auto"
|
||||
title="Add auto layout (Shift+A)"
|
||||
@click="store.setLayoutMode(node.id, 'VERTICAL')"
|
||||
>
|
||||
|
|
@ -241,6 +244,7 @@ function setAlignment(primary: LayoutAlign, counter: LayoutCounterAlign) {
|
|||
<button
|
||||
v-else
|
||||
class="cursor-pointer rounded border-none bg-transparent px-1 text-base leading-none text-muted hover:bg-hover hover:text-surface"
|
||||
data-test-id="layout-remove-auto"
|
||||
title="Remove auto layout"
|
||||
@click="store.setLayoutMode(node.id, 'NONE')"
|
||||
>
|
||||
|
|
@ -258,6 +262,7 @@ function setAlignment(primary: LayoutAlign, counter: LayoutCounterAlign) {
|
|||
? 'border-accent bg-accent text-white'
|
||||
: 'border-border bg-input text-muted hover:bg-hover hover:text-surface'
|
||||
"
|
||||
data-test-id="layout-direction-vertical"
|
||||
title="Vertical layout"
|
||||
@click="store.setLayoutMode(node.id, 'VERTICAL')"
|
||||
>
|
||||
|
|
@ -274,6 +279,7 @@ function setAlignment(primary: LayoutAlign, counter: LayoutCounterAlign) {
|
|||
? 'border-accent bg-accent text-white'
|
||||
: 'border-border bg-input text-muted hover:bg-hover hover:text-surface'
|
||||
"
|
||||
data-test-id="layout-direction-horizontal"
|
||||
title="Horizontal layout"
|
||||
@click="store.setLayoutMode(node.id, 'HORIZONTAL')"
|
||||
>
|
||||
|
|
@ -290,6 +296,7 @@ function setAlignment(primary: LayoutAlign, counter: LayoutCounterAlign) {
|
|||
? 'border-accent bg-accent text-white'
|
||||
: 'border-border bg-input text-muted hover:bg-hover hover:text-surface'
|
||||
"
|
||||
data-test-id="layout-direction-wrap"
|
||||
title="Wrap"
|
||||
@click="updateProp('layoutWrap', node.layoutWrap === 'WRAP' ? 'NO_WRAP' : 'WRAP')"
|
||||
>
|
||||
|
|
@ -303,7 +310,10 @@ function setAlignment(primary: LayoutAlign, counter: LayoutCounterAlign) {
|
|||
|
||||
<!-- Alignment grid + Gap -->
|
||||
<div class="mt-1.5 flex items-center gap-2">
|
||||
<div class="grid grid-cols-3 gap-0.5 rounded border border-border bg-input p-1">
|
||||
<div
|
||||
data-test-id="layout-alignment-grid"
|
||||
class="grid grid-cols-3 gap-0.5 rounded border border-border bg-input p-1"
|
||||
>
|
||||
<button
|
||||
v-for="(a, i) in ALIGN_GRID"
|
||||
:key="i"
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ function updateColor(color: Color) {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="border-b border-border px-3 py-2">
|
||||
<div data-test-id="page-section" class="border-b border-border px-3 py-2">
|
||||
<label class="mb-1.5 block text-[11px] text-muted">Page</label>
|
||||
<ColorInput :color="store.state.pageColor" editable @update="updateColor" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ function rotate90() {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="active" class="border-b border-border px-3 py-2">
|
||||
<div v-if="active" data-test-id="position-section" class="border-b border-border px-3 py-2">
|
||||
<label class="mb-1.5 block text-[11px] text-muted">Position</label>
|
||||
|
||||
<!-- Alignment buttons -->
|
||||
|
|
@ -132,6 +132,7 @@ function rotate90() {
|
|||
<div class="flex gap-0.5">
|
||||
<button
|
||||
class="flex size-7 cursor-pointer items-center justify-center rounded border border-border bg-input text-muted hover:bg-hover hover:text-surface"
|
||||
data-test-id="position-align-left"
|
||||
title="Align left"
|
||||
@click="alignHorizontal('left')"
|
||||
>
|
||||
|
|
@ -139,6 +140,7 @@ function rotate90() {
|
|||
</button>
|
||||
<button
|
||||
class="flex size-7 cursor-pointer items-center justify-center rounded border border-border bg-input text-muted hover:bg-hover hover:text-surface"
|
||||
data-test-id="position-align-center-h"
|
||||
title="Align center horizontally"
|
||||
@click="alignHorizontal('center')"
|
||||
>
|
||||
|
|
@ -146,6 +148,7 @@ function rotate90() {
|
|||
</button>
|
||||
<button
|
||||
class="flex size-7 cursor-pointer items-center justify-center rounded border border-border bg-input text-muted hover:bg-hover hover:text-surface"
|
||||
data-test-id="position-align-right"
|
||||
title="Align right"
|
||||
@click="alignHorizontal('right')"
|
||||
>
|
||||
|
|
@ -155,6 +158,7 @@ function rotate90() {
|
|||
<div class="flex gap-0.5">
|
||||
<button
|
||||
class="flex size-7 cursor-pointer items-center justify-center rounded border border-border bg-input text-muted hover:bg-hover hover:text-surface"
|
||||
data-test-id="position-align-top"
|
||||
title="Align top"
|
||||
@click="alignVertical('top')"
|
||||
>
|
||||
|
|
@ -162,6 +166,7 @@ function rotate90() {
|
|||
</button>
|
||||
<button
|
||||
class="flex size-7 cursor-pointer items-center justify-center rounded border border-border bg-input text-muted hover:bg-hover hover:text-surface"
|
||||
data-test-id="position-align-center-v"
|
||||
title="Align center vertically"
|
||||
@click="alignVertical('center')"
|
||||
>
|
||||
|
|
@ -169,6 +174,7 @@ function rotate90() {
|
|||
</button>
|
||||
<button
|
||||
class="flex size-7 cursor-pointer items-center justify-center rounded border border-border bg-input text-muted hover:bg-hover hover:text-surface"
|
||||
data-test-id="position-align-bottom"
|
||||
title="Align bottom"
|
||||
@click="alignVertical('bottom')"
|
||||
>
|
||||
|
|
@ -228,6 +234,7 @@ function rotate90() {
|
|||
</ScrubInput>
|
||||
<button
|
||||
class="flex size-7 shrink-0 cursor-pointer items-center justify-center rounded border border-border bg-input text-muted hover:bg-hover hover:text-surface"
|
||||
data-test-id="position-flip-horizontal"
|
||||
title="Flip horizontal"
|
||||
@click="flipHorizontal"
|
||||
>
|
||||
|
|
@ -235,6 +242,7 @@ function rotate90() {
|
|||
</button>
|
||||
<button
|
||||
class="flex size-7 shrink-0 cursor-pointer items-center justify-center rounded border border-border bg-input text-muted hover:bg-hover hover:text-surface"
|
||||
data-test-id="position-flip-vertical"
|
||||
title="Flip vertical"
|
||||
@click="flipVertical"
|
||||
>
|
||||
|
|
@ -242,6 +250,7 @@ function rotate90() {
|
|||
</button>
|
||||
<button
|
||||
class="flex size-7 shrink-0 cursor-pointer items-center justify-center rounded border border-border bg-input text-muted hover:bg-hover hover:text-surface"
|
||||
data-test-id="position-rotate-90"
|
||||
title="Rotate 90°"
|
||||
@click="rotate90"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -231,18 +231,16 @@ const borderWeights = computed(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="active" class="border-b border-border px-3 py-2">
|
||||
<!-- Header: label + add button -->
|
||||
<div v-if="active" data-test-id="stroke-section" class="border-b border-border px-3 py-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="mb-1 block text-[11px] text-muted">Stroke</label>
|
||||
<div class="flex items-center gap-0.5">
|
||||
<button
|
||||
class="flex size-5 cursor-pointer items-center justify-center rounded border-none bg-transparent text-sm leading-none text-muted hover:bg-hover hover:text-surface"
|
||||
@click="add"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
data-test-id="stroke-section-add"
|
||||
class="flex size-5 cursor-pointer items-center justify-center rounded border-none bg-transparent text-sm leading-none text-muted hover:bg-hover hover:text-surface"
|
||||
@click="add"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p v-if="strokesAreMixed" class="text-[11px] text-muted">Click + to replace mixed strokes</p>
|
||||
|
|
@ -251,6 +249,8 @@ const borderWeights = computed(() => {
|
|||
<div
|
||||
v-for="(stroke, i) in strokesAreMixed ? [] : (activeNode?.strokes ?? [])"
|
||||
:key="i"
|
||||
data-test-id="stroke-item"
|
||||
:data-test-index="i"
|
||||
class="group flex items-center gap-1.5 py-0.5"
|
||||
>
|
||||
<ColorInput
|
||||
|
|
|
|||
|
|
@ -73,13 +73,14 @@ onMounted(async () => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="node" class="border-b border-border px-3 py-2">
|
||||
<div v-if="node" data-test-id="typography-section" class="border-b border-border px-3 py-2">
|
||||
<label class="mb-1.5 block text-[11px] text-muted">Typography</label>
|
||||
|
||||
<div class="mb-1.5 flex items-center gap-1.5">
|
||||
<FontPicker class="min-w-0 flex-1" :model-value="node.fontFamily" @select="selectFamily" />
|
||||
<icon-lucide-alert-triangle
|
||||
v-if="hasMissingFonts"
|
||||
data-test-id="typography-missing-font"
|
||||
class="size-3.5 shrink-0 text-amber-400"
|
||||
:title="
|
||||
'Missing font' + (missingFonts.length > 1 ? 's' : '') + ': ' + missingFonts.join(', ')
|
||||
|
|
|
|||
|
|
@ -18,10 +18,11 @@ const variableCount = computed(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="border-b border-border px-3 py-2">
|
||||
<div data-test-id="variables-section" class="border-b border-border px-3 py-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-[11px] font-medium text-surface">Variables</label>
|
||||
<button
|
||||
data-test-id="variables-section-open"
|
||||
class="flex size-5 cursor-pointer items-center justify-center rounded border-none bg-transparent text-muted hover:bg-hover hover:text-surface"
|
||||
title="Open variables"
|
||||
@click="emit('openDialog')"
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ function onDisconnect() {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-screen w-screen flex-col">
|
||||
<div data-test-id="editor-root" class="flex h-screen w-screen flex-col">
|
||||
<SafariBanner />
|
||||
<TabBar />
|
||||
<SplitterGroup
|
||||
|
|
@ -122,8 +122,11 @@ function onDisconnect() {
|
|||
class="absolute left-7 top-7 z-10 flex items-center gap-2 rounded-lg border border-border bg-panel px-2 py-1 shadow-sm"
|
||||
>
|
||||
<img src="/favicon-32.png" class="size-4" alt="OpenPencil" />
|
||||
<span class="text-xs text-surface">{{ store.state.documentName }}</span>
|
||||
<span data-test-id="editor-document-name" class="text-xs text-surface">{{
|
||||
store.state.documentName
|
||||
}}</span>
|
||||
<button
|
||||
data-test-id="editor-show-ui"
|
||||
class="ml-1 flex size-6 cursor-pointer items-center justify-center rounded text-muted transition-colors hover:bg-hover hover:text-surface"
|
||||
title="Show UI (⌘\)"
|
||||
@click="store.state.showUI = true"
|
||||
|
|
|
|||
Loading…
Reference in a new issue