From d339f6f5f8715e1297b2eb546d8f0954762732ed Mon Sep 17 00:00:00 2001 From: Ryota Ikezawa Date: Sat, 3 Jan 2026 11:38:43 +0900 Subject: [PATCH 1/3] feat: Add responsive preview panel with fullscreen dialog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Dialog component using Radix UI for fullscreen preview - Scale down preview images to fit mobile screens - Enable click/tap to open fullscreen view with original size - Add keyboard accessibility support (Enter/Space to open) - Add i18n keys for fullscreen button label 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- package.json | 1 + pnpm-lock.yaml | 38 ++++++ .../components/generators/PreviewPanel.tsx | 128 +++++++++++++----- src/client/components/ui/dialog.tsx | 115 ++++++++++++++++ src/client/i18n/locales/en.json | 3 +- src/client/i18n/locales/ja.json | 3 +- 6 files changed, 250 insertions(+), 38 deletions(-) create mode 100644 src/client/components/ui/dialog.tsx diff --git a/package.json b/package.json index 2c33ec3..e31da1b 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "dependencies": { "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-collapsible": "^1.1.3", + "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-slot": "^1.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3eb17cc..4395097 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@radix-ui/react-collapsible': specifier: ^1.1.3 version: 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-dialog': + specifier: 1.1.15 + version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-label': specifier: ^2.1.2 version: 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -867,6 +870,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-dialog@1.1.15': + resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-direction@1.1.1': resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} peerDependencies: @@ -2793,6 +2809,28 @@ snapshots: optionalDependencies: '@types/react': 19.2.7 + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.3) + aria-hidden: 1.2.6 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-remove-scroll: 2.7.2(@types/react@19.2.7)(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.7 + '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-direction@1.1.1(@types/react@19.2.7)(react@19.2.3)': dependencies: react: 19.2.3 diff --git a/src/client/components/generators/PreviewPanel.tsx b/src/client/components/generators/PreviewPanel.tsx index 69216aa..8b7e318 100644 --- a/src/client/components/generators/PreviewPanel.tsx +++ b/src/client/components/generators/PreviewPanel.tsx @@ -1,7 +1,8 @@ -import { ImageIcon } from 'lucide-react'; +import { ImageIcon, Maximize2 } from 'lucide-react'; import { memo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'; import { Skeleton } from '@/components/ui/skeleton'; import { CodeOutput } from './CodeOutput'; @@ -17,10 +18,14 @@ function ImagePreview({ url, alt, onStateChange, + onClick, + scaledDown = false, }: { url: string; alt: string; onStateChange: (state: ImageState) => void; + onClick?: () => void; + scaledDown?: boolean; }) { const [state, setState] = useState('loading'); @@ -34,19 +39,37 @@ function ImagePreview({ onStateChange('error'); }; + const handleKeyDown = (e: React.KeyboardEvent) => { + if (onClick && (e.key === 'Enter' || e.key === ' ')) { + e.preventDefault(); + onClick(); + } + }; + return ( {alt} ); } export const PreviewPanel = memo(function PreviewPanel({ url, alt }: PreviewPanelProps) { const { t } = useTranslation(); + const [dialogOpen, setDialogOpen] = useState(false); // Track both the state and which URL it belongs to const [stateForUrl, setStateForUrl] = useState<{ url: string | null; state: ImageState }>({ url: null, @@ -67,42 +90,75 @@ export const PreviewPanel = memo(function PreviewPanel({ url, alt }: PreviewPane const showImage = url && imageState === 'loaded'; return ( - - - - {t('generator.preview')} - - - -
- {showEmpty && ( -
- -

{t('generator.previewEmpty')}

-
- )} - {showLoading && ( -
- - -
- - - + <> + + +
+ + {t('generator.preview')} + + {showImage && ( + + )} +
+
+ +
+ {showEmpty && ( +
+ +

{t('generator.previewEmpty')}

-
- )} - {showError && ( -

{t('generator.previewError')}

- )} + )} + {showLoading && ( +
+ + +
+ + + +
+
+ )} + {showError && ( +

{t('generator.previewError')}

+ )} + {url && ( + // key={url} resets the ImagePreview component when URL changes + setDialogOpen(true)} + scaledDown + /> + )} +
+ + {showImage && } + + + + {/* Full-size preview dialog */} + + + {t('generator.preview')} {url && ( - // key={url} resets the ImagePreview component when URL changes - +
+ {alt} +
)} -
- - {showImage && } - - + + + ); }); diff --git a/src/client/components/ui/dialog.tsx b/src/client/components/ui/dialog.tsx new file mode 100644 index 0000000..9f2e5f9 --- /dev/null +++ b/src/client/components/ui/dialog.tsx @@ -0,0 +1,115 @@ +import * as DialogPrimitive from '@radix-ui/react-dialog'; +import { X } from 'lucide-react'; +import type { ComponentProps } from 'react'; +import { cn } from '@/lib/utils'; + +function Dialog({ ...props }: ComponentProps) { + return ; +} + +function DialogTrigger({ ...props }: ComponentProps) { + return ; +} + +function DialogPortal({ ...props }: ComponentProps) { + return ; +} + +function DialogClose({ ...props }: ComponentProps) { + return ; +} + +function DialogOverlay({ className, ...props }: ComponentProps) { + return ( + + ); +} + +function DialogContent({ + className, + children, + ...props +}: ComponentProps) { + return ( + + + + {children} + + + Close + + + + ); +} + +function DialogHeader({ className, ...props }: ComponentProps<'div'>) { + return ( +
+ ); +} + +function DialogFooter({ className, ...props }: ComponentProps<'div'>) { + return ( +
+ ); +} + +function DialogTitle({ className, ...props }: ComponentProps) { + return ( + + ); +} + +function DialogDescription({ + className, + ...props +}: ComponentProps) { + return ( + + ); +} + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +}; diff --git a/src/client/i18n/locales/en.json b/src/client/i18n/locales/en.json index 559c2e1..f5f2bfb 100644 --- a/src/client/i18n/locales/en.json +++ b/src/client/i18n/locales/en.json @@ -69,7 +69,8 @@ "preview": "Preview", "previewEmpty": "Enter details to generate preview", "previewLoading": "Loading...", - "previewError": "Failed to load. Check the username." + "previewError": "Failed to load. Check the username.", + "fullscreen": "View full size" }, "stats": { "title": "Stats Card Generator", diff --git a/src/client/i18n/locales/ja.json b/src/client/i18n/locales/ja.json index bdd03d3..c372f6c 100644 --- a/src/client/i18n/locales/ja.json +++ b/src/client/i18n/locales/ja.json @@ -69,7 +69,8 @@ "preview": "プレビュー", "previewEmpty": "詳細を入力してプレビュー", "previewLoading": "読み込み中...", - "previewError": "読み込み失敗。ユーザー名を確認。" + "previewError": "読み込み失敗。ユーザー名を確認。", + "fullscreen": "フルサイズで表示" }, "stats": { "title": "統計カード作成", From e096c3a10814e11d3812cce275f0c5bd13fe38af Mon Sep 17 00:00:00 2001 From: Ryota Ikezawa Date: Sat, 3 Jan 2026 11:42:11 +0900 Subject: [PATCH 2/3] perf: Optimize bundle size with manual chunks and fix Analytics Engine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Disable Analytics Engine binding (requires Dashboard enablement) - Add Vite manualChunks to split vendor code: - react-vendor: React core runtime - router: react-router-dom - radix-ui: All Radix UI primitives - i18n: i18next and react-i18next - Reduces main bundle from 330KB to 231KB (-30%) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- vite.config.ts | 23 +++++++++++++++++++++++ wrangler.toml | 8 +++++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index d4e11c5..845adc6 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -9,6 +9,29 @@ export default defineConfig({ build: { outDir: '../../dist', emptyOutDir: true, + rollupOptions: { + output: { + manualChunks: { + // Core React runtime + 'react-vendor': ['react', 'react-dom'], + // Routing + 'router': ['react-router-dom'], + // UI primitives (Radix UI) + 'radix-ui': [ + '@radix-ui/react-checkbox', + '@radix-ui/react-collapsible', + '@radix-ui/react-dialog', + '@radix-ui/react-label', + '@radix-ui/react-select', + '@radix-ui/react-slot', + '@radix-ui/react-tabs', + '@radix-ui/react-tooltip', + ], + // Internationalization + 'i18n': ['i18next', 'react-i18next'], + }, + }, + }, }, esbuild: { jsx: 'automatic', diff --git a/wrangler.toml b/wrangler.toml index 0a0ffd2..8bc8d06 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -42,6 +42,8 @@ preview_id = "607a7597605c4984aae071103c16a817" # ============================================ # Analytics Engine (for metrics and observability) # ============================================ -[[analytics_engine_datasets]] -binding = "ANALYTICS" -dataset = "devcard_metrics" +# NOTE: Uncomment after enabling Analytics Engine in Cloudflare Dashboard: +# https://dash.cloudflare.com -> Workers & Pages -> Analytics Engine +# [[analytics_engine_datasets]] +# binding = "ANALYTICS" +# dataset = "devcard_metrics" From f6ba594026cb98948cb2b735c2819d99671ddc0b Mon Sep 17 00:00:00 2001 From: Ryota Ikezawa Date: Sat, 3 Jan 2026 11:43:40 +0900 Subject: [PATCH 3/3] chore: Add persist option to observability logs config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- wrangler.toml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/wrangler.toml b/wrangler.toml index 8bc8d06..e58c6d7 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -6,16 +6,11 @@ compatibility_flags = ["nodejs_compat"] # ============================================ # Observability Settings # ============================================ -[observability] -enabled = true - -# Logs configuration - enables Workers Logs in Cloudflare Dashboard [observability.logs] enabled = true -# Sample rate: 1.0 = 100% of requests logged (adjust for high-traffic) -head_sampling_rate = 1.0 -# Invocation logs capture console.log output +head_sampling_rate = 1 invocation_logs = true +persist = true # ============================================ # Static Assets