From f1fc5fa9b3857e2e7d5d0fe839b55725720e4ee1 Mon Sep 17 00:00:00 2001 From: Dante Date: Thu, 12 Mar 2026 15:39:08 +0900 Subject: [PATCH] feat: add DropZone Storybook coverage for file upload states (#9690) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - align the linear-mode `DropZone` upload indicator with the Figma file upload states - add a co-located Storybook story for the default and hover variants - add a `forceHovered` preview prop so Storybook can render the hover state deterministically ## Validation - `pnpm typecheck` (run in the original workspace with dependencies installed) - `pnpm lint` (passes with one pre-existing warning in `src/lib/litegraph/src/ContextMenu.ts`) - Storybook smoke check is currently blocked by an existing workspace issue: `vite-plugin-inspect` fails with `Can not found environment context for client` ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9690-feat-add-DropZone-Storybook-coverage-for-file-upload-states-31f6d73d365081ae9eabdde6b5915f26) by [Unito](https://www.unito.io) --------- Co-authored-by: Claude Opus 4.6 Co-authored-by: GitHub Action --- .../extensions/linearMode/DropZone.stories.ts | 131 ++++++++++++++++++ .../extensions/linearMode/DropZone.vue | 76 +++++++--- 2 files changed, 187 insertions(+), 20 deletions(-) create mode 100644 src/renderer/extensions/linearMode/DropZone.stories.ts diff --git a/src/renderer/extensions/linearMode/DropZone.stories.ts b/src/renderer/extensions/linearMode/DropZone.stories.ts new file mode 100644 index 0000000000..74f21081c8 --- /dev/null +++ b/src/renderer/extensions/linearMode/DropZone.stories.ts @@ -0,0 +1,131 @@ +import type { + ComponentPropsAndSlots, + Meta, + StoryObj +} from '@storybook/vue3-vite' +import { ref } from 'vue' + +import DropZone from './DropZone.vue' + +type StoryArgs = ComponentPropsAndSlots + +const defaultLabel = 'Click to browse or drag an image' +const defaultIconClass = 'icon-[lucide--image]' + +function createFileInput(onFile: (file: File) => void) { + const input = document.createElement('input') + input.type = 'file' + input.accept = 'image/*' + input.addEventListener('change', () => { + const file = input.files?.[0] + if (file) onFile(file) + }) + return input +} + +function fileToObjectUrl(file: File): string { + return URL.createObjectURL(file) +} + +function extractDroppedImageFile(e: DragEvent): File | undefined { + return Array.from(e.dataTransfer?.files ?? []).find((f) => + f.type.startsWith('image/') + ) +} + +const renderStory = (args: StoryArgs) => ({ + components: { DropZone }, + setup() { + const imageUrl = ref(undefined) + const hovered = ref(false) + + function handleFile(file: File) { + imageUrl.value = fileToObjectUrl(file) + } + + const onDragOver = (e: DragEvent) => { + if (!e.dataTransfer?.items) return false + return Array.from(e.dataTransfer.items).some( + (item) => item.kind === 'file' && item.type.startsWith('image/') + ) + } + + const onDragDrop = (e: DragEvent) => { + const file = extractDroppedImageFile(e) + if (file) handleFile(file) + return !!file + } + + const onClick = () => { + createFileInput(handleFile).click() + } + + const dropIndicator = ref({ + ...args.dropIndicator, + onClick + }) + + return { args, onDragOver, onDragDrop, dropIndicator, imageUrl, hovered } + }, + template: ` +
+ +
+ ` +}) + +const meta: Meta = { + title: 'Components/FileUpload', + component: DropZone, + tags: ['autodocs'], + parameters: { + layout: 'centered', + docs: { + description: { + component: + 'Linear mode drag-and-drop target with a file upload indicator. Click to browse or drag an image file to upload.' + } + } + }, + argTypes: { + onDragOver: { table: { disable: true } }, + onDragDrop: { table: { disable: true } }, + dropIndicator: { control: false }, + forceHovered: { table: { disable: true } } + }, + args: { + dropIndicator: { + label: defaultLabel, + iconClass: defaultIconClass + } + }, + decorators: [ + (story) => ({ + components: { story }, + template: ` +
+ +
+ ` + }) + ] +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + render: renderStory +} diff --git a/src/renderer/extensions/linearMode/DropZone.vue b/src/renderer/extensions/linearMode/DropZone.vue index 349534c33e..4819e39faa 100644 --- a/src/renderer/extensions/linearMode/DropZone.vue +++ b/src/renderer/extensions/linearMode/DropZone.vue @@ -1,10 +1,15 @@