Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"format:backend": "cd src-tauri && cargo fmt",
"test:playwright": "playwright test",
"test:playwright:ui": "playwright test --ui",
"check:translations": "bun scripts/check-translations.ts"
"check:translations": "bun scripts/check-translations.ts",
"storybook": "vite --config vite.storybook.config.ts"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

README.md and CLAUDE.md should be updated to document this new command and how to use the storybook. right now the only mention is in the PR description

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the bun run storybook command to both README.md and CLAUDE.md with a short description of what it does and the URL to open.

},
"dependencies": {
"@tailwindcss/vite": "^4.1.16",
Expand Down
57 changes: 57 additions & 0 deletions src/components/ui/IconButton.tsx
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a new component introduced in this PR but it's not used anywhere in the actual app — only in the react storybook. a storybook PR shouldn't introduce new components that aren't consumed by the app yet. i'd recommend either removing this from this PR (and adding it in a separate PR where app code actually uses it) or updating existing app code to use it here

leaning towards the latter

screenshots would be super important here!

Copy link
Copy Markdown
Author

@edwche10 edwche10 Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed IconButton from this PR to keep the scope focused. I noticed the existing ResetButton does a similar job — once the real Storybook setup is in place, we can revisit consolidating them into IconButton in a follow-up PR.

I had an image of the ResetButton that exists today in the app, but at the end, it's an icon bottom.

Image

Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from "react";

export type IconButtonVariant = "primary" | "secondary" | "danger" | "ghost";
export type IconButtonSize = "sm" | "md" | "lg";

interface IconButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
/** Icon element to render (e.g. <ResetIcon />). No label text. */
icon: React.ReactNode;
/** Accessible name for the button (required when no visible text). */
"aria-label": string;
variant?: IconButtonVariant;
size?: IconButtonSize;
}

const variantClasses: Record<IconButtonVariant, string> = {
primary:
"text-white bg-background-ui border-background-ui hover:bg-background-ui/80 hover:border-background-ui/80 focus:ring-1 focus:ring-background-ui",
secondary:
"text-text bg-mid-gray/10 border-mid-gray/20 hover:bg-logo-primary/30 hover:border-logo-primary focus:outline-none focus:ring-1 focus:ring-logo-primary",
danger:
"text-white bg-red-600 border-mid-gray/20 hover:bg-red-700 hover:border-red-700 focus:ring-1 focus:ring-red-500",
ghost:
"text-current border-transparent hover:bg-mid-gray/10 hover:border-logo-primary focus:bg-mid-gray/20 focus:ring-1 focus:ring-logo-primary",
};

const sizeClasses: Record<IconButtonSize, string> = {
sm: "p-1 rounded [&_svg]:w-3 [&_svg]:h-3",
md: "p-2 rounded [&_svg]:w-4 [&_svg]:h-4",
lg: "p-2 rounded [&_svg]:w-5 [&_svg]:h-5",
};

export const IconButton: React.FC<IconButtonProps> = ({
icon,
"aria-label": ariaLabel,
variant = "secondary",
size = "md",
className = "",
disabled = false,
...props
}) => (
<button
type="button"
aria-label={ariaLabel}
disabled={disabled}
className={[
"inline-flex items-center justify-center border border-solid font-medium transition-colors duration-150",
"disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer",
"focus:outline-none",
variantClasses[variant],
sizeClasses[size],
className,
].join(" ")}
{...props}
>
{icon}
</button>
);
1 change: 1 addition & 0 deletions storybook/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.vite/
42 changes: 42 additions & 0 deletions storybook/index.html
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this references http://localhost:1422/HandyMain/storybook/ but the actual URL is http://localhost:1422/

minor but would confuse someone opening the html file directly

this will anyway get removed once we use storybook properly

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed! The URL now correctly points to http://localhost:1422/

Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Handy Storybook</title>
</head>
<body>
<div id="root">
<div
style="
max-width: 640px;
margin: 48px auto;
padding: 16px;
border: 1px solid #d9d9d9;
border-radius: 12px;
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
line-height: 1.5;
"
>
<h1 style="margin: 0 0 8px; font-size: 20px">Handy Storybook</h1>
<p style="margin: 0 0 8px">
This page must be served with Vite. Opening this file directly will
not render React components.
</p>
<p style="margin: 0 0 8px">Run:</p>
<pre
style="
margin: 0;
padding: 10px;
background: #f6f6f6;
border-radius: 8px;
overflow: auto;
"
><code>bun install
bun run storybook</code></pre>
<p style="margin: 8px 0 0">Then open: <code>http://localhost:1422/HandyMain/storybook/</code></p>
</div>
</div>
<script type="module" src="./main.tsx"></script>
</body>
</html>
11 changes: 11 additions & 0 deletions storybook/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from "react";
import ReactDOM from "react-dom/client";
import "@/i18n";
import "./storybook.css";
import { StorybookApp } from "./storybook";

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<StorybookApp />
</React.StrictMode>,
);
85 changes: 85 additions & 0 deletions storybook/mocks/bindings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import {
MOCK_APP_DIR,
MOCK_AUDIO_FILE,
MOCK_HISTORY,
MOCK_LOG_DIR,
MOCK_MODELS,
MOCK_POST_PROCESS_PROVIDERS,
MOCK_PROMPTS,
MOCK_RECORDINGS_DIR,
MOCK_SETTINGS,
} from "./data";

const ok = <T>(data: T) => ({ status: "ok" as const, data });

export const commands = {
// App + system
getAppSettings: async () => ok(MOCK_SETTINGS),
getDefaultSettings: async () => ok(MOCK_SETTINGS),
getAppDirPath: async () => ok(MOCK_APP_DIR),
openAppDataDir: async () => ok(null),
getLogDirPath: async () => ok(MOCK_LOG_DIR),
openLogDir: async () => ok(null),

// Permissions + shortcuts
initializeEnigo: async () => ok(null),
initializeShortcuts: async () => ok(null),
suspendBinding: async () => ok(null),
resumeBinding: async () => ok(null),
startHandyKeysRecording: async () => ok(null),
stopHandyKeysRecording: async () => ok(null),
changeKeyboardImplementationSetting: async () =>
ok({ success: true, reset_bindings: [] }),

// Models
getTranscriptionModelStatus: async () => ok(MOCK_SETTINGS.selected_model),
isRecording: async () => false,

// Hardware + environment
isLaptop: async () => ok(true),

// Post-processing
addPostProcessPrompt: async (name: string, prompt: string) =>
ok({ id: `prompt_${Date.now()}`, name, prompt }),
updatePostProcessPrompt: async () => ok(null),
deletePostProcessPrompt: async () => ok(null),
checkAppleIntelligenceAvailable: async () => false,

// History
getHistoryEntries: async () => ok(MOCK_HISTORY),
toggleHistoryEntrySaved: async () => ok(null),
getAudioFilePath: async () => ok(MOCK_AUDIO_FILE),
deleteHistoryEntry: async () => ok(null),
openRecordingsFolder: async () => ok(MOCK_RECORDINGS_DIR),

// Misc settings
setModelUnloadTimeout: async () => ok(null),

// Models list for storybook previews (not used by components directly)
getAvailableModels: async () => ok(MOCK_MODELS),

// Provider data for storybook previews (not used by components directly)
getPostProcessProviders: async () => ok(MOCK_POST_PROCESS_PROVIDERS),
getPostProcessPrompts: async () => ok(MOCK_PROMPTS),
};

export type {
AppSettings,
AudioDevice,
ClipboardHandling,
EngineType,
HistoryEntry,
ImplementationChangeResult,
KeyboardImplementation,
LLMPrompt,
LogLevel,
ModelInfo,
ModelUnloadTimeout,
OverlayPosition,
PasteMethod,
PostProcessProvider,
RecordingRetentionPeriod,
Result,
ShortcutBinding,
SoundTheme,
} from "../../../src/bindings";
Loading