From 0770b084f5f72e713017d5d715aaf11a1ac00a8c Mon Sep 17 00:00:00 2001 From: bangwu Date: Mon, 9 Mar 2026 15:33:25 +0800 Subject: [PATCH] refactor(frontend): migrate desktop app to solid --- AGENTS.md | 55 +- README.md | 288 +- components.json | 21 - components/app-shell.tsx | 35 - components/app-sidebar.tsx | 181 - components/chart-area-interactive.tsx | 292 - components/data-table.tsx | 807 -- components/file-area.tsx | 235 - components/i18n-provider.tsx | 54 - components/language-toggle.tsx | 26 - components/login-form.tsx | 104 - components/markdown-editor.tsx | 157 - components/nav-documents.tsx | 92 - components/nav-main.tsx | 58 - components/nav-secondary.tsx | 42 - components/nav-user.tsx | 110 - components/s3-config-dialog.tsx | 219 - components/section-cards.tsx | 102 - components/site-header.tsx | 30 - components/theme-toggle.tsx | 83 - components/ui/accordion.tsx | 66 - components/ui/alert-dialog.tsx | 157 - components/ui/alert.tsx | 66 - components/ui/aspect-ratio.tsx | 11 - components/ui/avatar.tsx | 53 - components/ui/badge.tsx | 46 - components/ui/breadcrumb.tsx | 109 - components/ui/button.tsx | 59 - components/ui/calendar.tsx | 91 - components/ui/card.tsx | 92 - components/ui/carousel.tsx | 241 - components/ui/chart.tsx | 353 - components/ui/checkbox.tsx | 32 - components/ui/collapsible.tsx | 33 - components/ui/command.tsx | 177 - components/ui/context-menu.tsx | 252 - components/ui/dialog.tsx | 135 - components/ui/drawer.tsx | 132 - components/ui/dropdown-menu.tsx | 257 - components/ui/form.tsx | 167 - components/ui/hover-card.tsx | 44 - components/ui/input-otp.tsx | 77 - components/ui/input.tsx | 20 - components/ui/label.tsx | 24 - components/ui/menubar.tsx | 276 - components/ui/navigation-menu.tsx | 168 - components/ui/pagination.tsx | 127 - components/ui/popover.tsx | 48 - components/ui/progress.tsx | 31 - components/ui/radio-group.tsx | 45 - components/ui/resizable.tsx | 56 - components/ui/scroll-area.tsx | 58 - components/ui/select.tsx | 185 - components/ui/separator.tsx | 28 - components/ui/sheet.tsx | 139 - components/ui/sidebar.tsx | 728 - components/ui/skeleton.tsx | 13 - components/ui/slider.tsx | 63 - components/ui/sonner.tsx | 58 - components/ui/switch.tsx | 31 - components/ui/table.tsx | 116 - components/ui/tabs.tsx | 66 - components/ui/textarea.tsx | 18 - components/ui/toggle-group.tsx | 73 - components/ui/toggle.tsx | 47 - components/ui/tooltip.tsx | 61 - components/writing-area.tsx | 329 - eslint.config.mjs | 23 - hooks/use-mobile.ts | 19 - hooks/useAccessibility.ts | 260 - hooks/useGlobalEventListeners.ts | 48 - hooks/useI18n.ts | 10 - hooks/useS3Config.ts | 61 - hooks/useTauriApp.ts | 60 - hooks/useTheme.ts | 1 - hooks/useTheme.tsx | 1 - index.html | 24 + lib/accessibility-utils.ts | 399 - lib/i18n.ts | 273 - lib/stores/editor-store.ts | 109 - lib/stores/index.ts | 27 - lib/stores/settings-store.ts | 149 - lib/stores/ui-store.ts | 73 - lib/stores/workspace-store.ts | 80 - lib/updater.ts | 39 - lib/utils.ts | 6 - lib/ux-feedback.ts | 118 - package.json | 91 +- playwright.config.ts | 35 +- pnpm-lock.yaml | 11447 ++-------------- postcss.config.mjs | 7 - src-tauri/tauri.conf.json | 2 +- src/app.tsx | 32 + src/components/file-browser.tsx | 171 + src/components/i18n-provider.tsx | 69 + src/components/language-toggle.tsx | 20 + src/components/s3-config-dialog.tsx | 203 + src/components/theme-toggle.tsx | 32 + src/components/toast-viewport.tsx | 36 + src/components/ui.tsx | 118 + src/components/writing-area.tsx | 492 + src/index.tsx | 13 + src/lib/i18n.ts | 395 + src/lib/markdown.ts | 12 + src/lib/s3.ts | 29 + src/lib/storage.ts | 22 + src/lib/tauri.ts | 202 + src/lib/utils.ts | 44 + src/routeTree.gen.ts | 104 - src/router.tsx | 22 +- src/routes/__root.tsx | 84 +- src/routes/dashboard-data.json | 614 - src/routes/dashboard.tsx | 337 +- src/routes/editor-theme.css | 583 +- src/routes/globals.css | 1619 +-- src/routes/index.tsx | 413 +- src/routes/login.tsx | 107 +- src/state/editor.ts | 95 + src/state/settings.ts | 90 + src/state/toast.ts | 42 + src/state/workspace.ts | 146 + src/styles.css | 50 + src/types.d.ts | 13 +- test/IMPLEMENTATION_SUMMARY.md | 181 +- test/README.md | 25 +- test/components/color-harmony.test.tsx | 243 +- test/components/comprehensive-theme.test.tsx | 268 +- test/components/interaction-states.test.tsx | 198 +- test/components/theme-consistency.test.tsx | 119 +- test/components/theme-core.test.tsx | 217 +- test/components/ui/button.test.tsx | 212 +- test/components/ui/input.test.tsx | 147 +- test/hooks/useI18n.test.tsx | 80 +- test/lib/i18n.test.ts | 213 +- test/lib/utils.test.ts | 98 +- test/setup.ts | 33 +- test/utils/theme-test-utils.tsx | 77 +- test/visual/theme-visual-regression.spec.ts | 210 +- .../dashboard-dark-chromium-linux.png | Bin 0 -> 61427 bytes .../landing-dark-chromium-linux.png | Bin 0 -> 55161 bytes .../landing-light-chromium-linux.png | Bin 0 -> 55004 bytes ...ing-select-folder-hover-chromium-linux.png | Bin 0 -> 1851 bytes .../login-zh-chromium-linux.png | Bin 0 -> 35524 bytes tsconfig.json | 20 +- vite-env.d.ts | 1 + vite.config.ts | 37 +- vitest.config.ts | 19 +- 147 files changed, 5143 insertions(+), 25275 deletions(-) delete mode 100644 components.json delete mode 100644 components/app-shell.tsx delete mode 100644 components/app-sidebar.tsx delete mode 100644 components/chart-area-interactive.tsx delete mode 100644 components/data-table.tsx delete mode 100644 components/file-area.tsx delete mode 100644 components/i18n-provider.tsx delete mode 100644 components/language-toggle.tsx delete mode 100644 components/login-form.tsx delete mode 100644 components/markdown-editor.tsx delete mode 100644 components/nav-documents.tsx delete mode 100644 components/nav-main.tsx delete mode 100644 components/nav-secondary.tsx delete mode 100644 components/nav-user.tsx delete mode 100644 components/s3-config-dialog.tsx delete mode 100644 components/section-cards.tsx delete mode 100644 components/site-header.tsx delete mode 100644 components/theme-toggle.tsx delete mode 100644 components/ui/accordion.tsx delete mode 100644 components/ui/alert-dialog.tsx delete mode 100644 components/ui/alert.tsx delete mode 100644 components/ui/aspect-ratio.tsx delete mode 100644 components/ui/avatar.tsx delete mode 100644 components/ui/badge.tsx delete mode 100644 components/ui/breadcrumb.tsx delete mode 100644 components/ui/button.tsx delete mode 100644 components/ui/calendar.tsx delete mode 100644 components/ui/card.tsx delete mode 100644 components/ui/carousel.tsx delete mode 100644 components/ui/chart.tsx delete mode 100644 components/ui/checkbox.tsx delete mode 100644 components/ui/collapsible.tsx delete mode 100644 components/ui/command.tsx delete mode 100644 components/ui/context-menu.tsx delete mode 100644 components/ui/dialog.tsx delete mode 100644 components/ui/drawer.tsx delete mode 100644 components/ui/dropdown-menu.tsx delete mode 100644 components/ui/form.tsx delete mode 100644 components/ui/hover-card.tsx delete mode 100644 components/ui/input-otp.tsx delete mode 100644 components/ui/input.tsx delete mode 100644 components/ui/label.tsx delete mode 100644 components/ui/menubar.tsx delete mode 100644 components/ui/navigation-menu.tsx delete mode 100644 components/ui/pagination.tsx delete mode 100644 components/ui/popover.tsx delete mode 100644 components/ui/progress.tsx delete mode 100644 components/ui/radio-group.tsx delete mode 100644 components/ui/resizable.tsx delete mode 100644 components/ui/scroll-area.tsx delete mode 100644 components/ui/select.tsx delete mode 100644 components/ui/separator.tsx delete mode 100644 components/ui/sheet.tsx delete mode 100644 components/ui/sidebar.tsx delete mode 100644 components/ui/skeleton.tsx delete mode 100644 components/ui/slider.tsx delete mode 100644 components/ui/sonner.tsx delete mode 100644 components/ui/switch.tsx delete mode 100644 components/ui/table.tsx delete mode 100644 components/ui/tabs.tsx delete mode 100644 components/ui/textarea.tsx delete mode 100644 components/ui/toggle-group.tsx delete mode 100644 components/ui/toggle.tsx delete mode 100644 components/ui/tooltip.tsx delete mode 100644 components/writing-area.tsx delete mode 100644 eslint.config.mjs delete mode 100644 hooks/use-mobile.ts delete mode 100644 hooks/useAccessibility.ts delete mode 100644 hooks/useGlobalEventListeners.ts delete mode 100644 hooks/useI18n.ts delete mode 100644 hooks/useS3Config.ts delete mode 100644 hooks/useTauriApp.ts delete mode 100644 hooks/useTheme.ts delete mode 100644 hooks/useTheme.tsx create mode 100644 index.html delete mode 100644 lib/accessibility-utils.ts delete mode 100644 lib/i18n.ts delete mode 100644 lib/stores/editor-store.ts delete mode 100644 lib/stores/index.ts delete mode 100644 lib/stores/settings-store.ts delete mode 100644 lib/stores/ui-store.ts delete mode 100644 lib/stores/workspace-store.ts delete mode 100644 lib/updater.ts delete mode 100644 lib/utils.ts delete mode 100644 lib/ux-feedback.ts delete mode 100644 postcss.config.mjs create mode 100644 src/app.tsx create mode 100644 src/components/file-browser.tsx create mode 100644 src/components/i18n-provider.tsx create mode 100644 src/components/language-toggle.tsx create mode 100644 src/components/s3-config-dialog.tsx create mode 100644 src/components/theme-toggle.tsx create mode 100644 src/components/toast-viewport.tsx create mode 100644 src/components/ui.tsx create mode 100644 src/components/writing-area.tsx create mode 100644 src/index.tsx create mode 100644 src/lib/i18n.ts create mode 100644 src/lib/markdown.ts create mode 100644 src/lib/s3.ts create mode 100644 src/lib/storage.ts create mode 100644 src/lib/tauri.ts create mode 100644 src/lib/utils.ts delete mode 100644 src/routeTree.gen.ts delete mode 100644 src/routes/dashboard-data.json create mode 100644 src/state/editor.ts create mode 100644 src/state/settings.ts create mode 100644 src/state/toast.ts create mode 100644 src/state/workspace.ts create mode 100644 src/styles.css create mode 100644 test/visual/theme-visual-regression.spec.ts-snapshots/dashboard-dark-chromium-linux.png create mode 100644 test/visual/theme-visual-regression.spec.ts-snapshots/landing-dark-chromium-linux.png create mode 100644 test/visual/theme-visual-regression.spec.ts-snapshots/landing-light-chromium-linux.png create mode 100644 test/visual/theme-visual-regression.spec.ts-snapshots/landing-select-folder-hover-chromium-linux.png create mode 100644 test/visual/theme-visual-regression.spec.ts-snapshots/login-zh-chromium-linux.png diff --git a/AGENTS.md b/AGENTS.md index aee3620..cf200c7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,45 +1,48 @@ # Repository Guidelines ## Project Structure & Module Organization -- `app/`: Next.js App Router pages, layouts, and global styles (`app/globals.css`, `app/editor-theme.css`). -- `components/`: shared UI components and providers. -- `hooks/`: reusable React hooks (keep hook names `useThing`). -- `lib/`: utilities and shared helpers. -- `public/`: static assets served by Next.js. -- `test/`: unit and visual tests plus utilities and setup. -- `src-tauri/`: Tauri desktop shell and Rust configuration. +- `src/`: Solid frontend application. + - `src/components/`: shared Solid UI and feature components. + - `src/routes/`: route-level screens and global route styles. + - `src/state/`: app state stores. + - `src/lib/`: utilities, Tauri helpers, markdown helpers, and i18n. +- `public/`: static assets served by Vite. +- `src-tauri/`: Tauri desktop shell and Rust commands. +- `test/`: Vitest and Playwright coverage, helpers, and docs. ## Build, Test, and Development Commands -- `pnpm dev`: start the Next.js dev server (Turbopack). -- `pnpm build`: production build. -- `pnpm start`: serve the production build. -- `pnpm lint`: run ESLint (`next lint`). +- `pnpm dev`: start the Solid + Vite dev server. +- `pnpm build`: production build and TypeScript check. +- `pnpm start`: preview the production build. +- `pnpm lint`: run TypeScript checking. - `pnpm test`: run Vitest once. - `pnpm test:watch`: Vitest watch mode. - `pnpm test:ui`: Vitest UI. - `pnpm test:visual`: Playwright visual regression tests. - `pnpm test:visual:ui`: Playwright UI runner. -- `npx tsx test/run-all-tests.ts`: run the full test suite with reporting. +- `pnpm tauri dev`: run the desktop shell in development. +- `pnpm tauri build`: build the desktop app. ## Coding Style & Naming Conventions -- TypeScript + React (Next.js). Follow existing file patterns and keep diffs minimal. -- Indentation: 2 spaces in TS/TSX; align JSX props vertically when multi-line. -- Components: `PascalCase` exports; files are typically kebab-case (match the folder). -- Hooks: functions named `useThing`, stored in `hooks/`. -- Linting: follow ESLint rules from `eslint.config.mjs`; fix lint before PRs. +- TypeScript + Solid. Keep diffs focused and consistent with nearby files. +- Indentation: 2 spaces in TS/TSX and CSS. +- Components: `PascalCase` exports; filenames are typically kebab-case or grouped by feature. +- Shared state lives in `src/state/`; generic helpers live in `src/lib/`. +- Prefer small composable helpers over framework-heavy abstractions. ## Testing Guidelines -- Unit tests live in `test/components/*.test.tsx` (Vitest + React Testing Library). -- Visual regression tests live in `test/visual/*.spec.ts` (Playwright). -- Use `test/setup.ts` and `test/utils/` for shared helpers. -- If visuals change intentionally, update snapshots: `pnpm test:visual --update-snapshots`. +- Unit and integration tests live in `test/components`, `test/hooks`, and `test/lib`. +- Visual regression tests live in `test/visual/*.spec.ts`. +- Use `test/setup.ts` and `test/utils/` for shared test setup. +- If visuals change intentionally, update snapshots with `pnpm test:visual --update-snapshots`. +- WebKit snapshots are optional locally and can be enabled with `PLAYWRIGHT_ENABLE_WEBKIT=1`. ## Commit & Pull Request Guidelines - Commit messages follow conventional commits: `type(scope): subject`. - Examples: `chore(deps): ...`, `ci(test): ...`. -- PRs should include: a concise description, linked issue (if any), and screenshots for UI changes. -- Note test coverage in the PR body (e.g., `pnpm test`, `pnpm test:visual`). +- PRs should include a concise description, linked issue if any, and screenshots for UI changes. +- Mention validation in the PR body, for example `pnpm test`, `pnpm test:visual`, or `pnpm build`. ## Configuration Notes -- App config is in `next.config.ts`; tooling config in `vitest.config.ts` and `playwright.config.ts`. -- Prefer `pnpm` for dependency changes; keep `pnpm-lock.yaml` in sync. +- Frontend config lives in `vite.config.ts`, `vitest.config.ts`, and `playwright.config.ts`. +- Tauri config lives in `src-tauri/tauri.conf.json` and `src-tauri/Cargo.toml`. +- Prefer `pnpm` for dependency changes and keep `pnpm-lock.yaml` in sync. diff --git a/README.md b/README.md index 85db0f3..fadee1c 100644 --- a/README.md +++ b/README.md @@ -6,265 +6,123 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/) -[![Next.js](https://img.shields.io/badge/Next.js-15.4-black)](https://nextjs.org/) -[![React](https://img.shields.io/badge/React-19.0-61dafb)](https://reactjs.org/) +[![Solid](https://img.shields.io/badge/Solid-1.9-2c4f7c)](https://www.solidjs.com/) +[![Vite](https://img.shields.io/badge/Vite-7.3-646cff)](https://vite.dev/) [![Tauri](https://img.shields.io/badge/Tauri-2.4-24C8DB)](https://tauri.app/) -[![pnpm](https://img.shields.io/badge/pnpm-latest-F69220)](https://pnpm.io/) -[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](http://makeapullrequest.com) - -[Features](#features) · [Installation](#installation) · [Documentation](#documentation) · [Contributing](#contributing) ---- - ## Overview -**OnlyWrite** is a modern, distraction-free writing application built with efficiency and user experience at its core. Designed for writers, developers, and content creators who value clean interfaces and powerful functionality. - -Built with cutting-edge technologies including Next.js 15, React 19, Tauri 2.4, and TypeScript, OnlyWrite delivers a native desktop experience with web-level flexibility. +OnlyWrite is a local-first desktop writing app built with **Solid**, **Vite**, **Tauri 2**, and **TypeScript**. +It focuses on a distraction-free workspace, fast file-based writing, markdown preview, bilingual UI, and desktop-friendly workflows. ## Features -### Core Writing Experience -- **Markdown Editor**: Full-featured markdown editing with live preview -- **Split View**: Edit and preview simultaneously with resizable panels -- **Auto-save**: Never lose your work with automatic 5-second intervals -- **File Management**: Organize your workspace with folder-based navigation -- **Syntax Highlighting**: Beautiful code blocks with theme integration - -### User Interface -- **Dark/Light Themes**: Seamless theme switching with consistent color harmony -- **Responsive Design**: Works beautifully on desktop and mobile -- **Accessibility First**: WCAG compliant with keyboard navigation and screen reader support -- **Modern Components**: Built with Radix UI and Tailwind CSS for a polished experience - -### Platform Features -- **Cross-platform**: Native desktop apps for Windows, macOS, and Linux via Tauri -- **Local-first**: All your data stays on your device -- **Fast Performance**: Optimized with Turbopack for lightning-fast dev and build times -- **Internationalization**: Support for English and Chinese (more languages coming) +- Markdown writing with live preview +- Edit / Render / Split view modes +- Local folder-based workspace +- Manual save, restore, and autosave +- Light and dark themes +- English and Chinese UI +- Optional S3 image upload configuration +- Tauri desktop packaging for Windows, macOS, and Linux -### Technical Highlights -- **Type-safe**: Full TypeScript coverage for reliable code -- **Tested**: Comprehensive test suite with Vitest and Playwright -- **Modern Stack**: React 19, Next.js 15, Tailwind CSS 4 -- **Developer Experience**: Hot reload, ESLint, and pre-configured dev environment +## Tech Stack -## Installation - -### Prerequisites -- Node.js 20+ -- pnpm (recommended package manager) -- Rust (for Tauri desktop builds) - -### Quick Start +### Frontend +- Solid 1.9 +- Vite 7 +- Tailwind CSS 4 +- Lucide Solid +- marked + DOMPurify -```bash -# Clone the repository -git clone https://github.com/yourusername/OnlyWrite.git -cd OnlyWrite +### Desktop +- Tauri 2 +- Rust -# Install dependencies -pnpm install +### Testing +- Vitest +- Solid Testing Library +- Playwright -# Start development server -pnpm dev +## Project Structure -# Open http://localhost:3000 +```text +OnlyWrite/ +├── src/ # Solid frontend +│ ├── components/ # Shared and feature components +│ ├── lib/ # Utilities and Tauri helpers +│ ├── routes/ # Route screens and route CSS +│ └── state/ # Solid stores +├── public/ # Static assets +├── src-tauri/ # Tauri shell and Rust commands +└── test/ # Unit and visual tests ``` -### Desktop App Development +## Scripts ```bash -# Install Tauri CLI -cargo install tauri-cli +# Frontend +pnpm dev +pnpm build +pnpm start +pnpm lint -# Run desktop app in development -pnpm tauri dev +# Tests +pnpm test +pnpm test:watch +pnpm test:ui +pnpm test:visual +pnpm test:visual:ui -# Build desktop app for production +# Desktop +pnpm tauri dev pnpm tauri build ``` -## Documentation - -### Project Structure - -``` -OnlyWrite/ -├── app/ # Next.js App Router pages and layouts -├── components/ # React components -│ ├── ui/ # Reusable UI components (Radix + Tailwind) -│ └── ... # Feature-specific components -├── hooks/ # Custom React hooks -├── lib/ # Utility functions and helpers -├── public/ # Static assets -├── src-tauri/ # Tauri desktop shell -├── test/ # Test suites -│ ├── components/ # Component tests -│ ├── lib/ # Utility tests -│ ├── hooks/ # Hook tests -│ └── visual/ # Visual regression tests -└── ... -``` - -### Available Scripts +## Development ```bash -# Development -pnpm dev # Start Next.js dev server with Turbopack -pnpm build # Production build -pnpm start # Serve production build -pnpm lint # Run ESLint - -# Testing -pnpm test # Run unit tests (Vitest) -pnpm test:watch # Run tests in watch mode -pnpm test:ui # Open Vitest UI -pnpm test:visual # Run visual regression tests (Playwright) -pnpm test:visual:ui # Open Playwright UI - -# Desktop -pnpm tauri dev # Run Tauri desktop app -pnpm tauri build # Build desktop app +git clone https://github.com/yourusername/OnlyWrite.git +cd OnlyWrite +pnpm install +pnpm dev ``` -### Coding Guidelines - -- **TypeScript**: All code must be type-safe -- **Components**: Use PascalCase for component names -- **Hooks**: Prefix with `use` (e.g., `useI18n`, `useS3Config`) -- **Styling**: Use Tailwind CSS utility classes -- **Testing**: Write tests for new features and bug fixes -- **Commits**: Follow [Conventional Commits](https://www.conventionalcommits.org/) - -## Technology Stack +Then open `http://localhost:3000`. -### Frontend -- **Framework**: Next.js 15 (App Router) -- **UI Library**: React 19 -- **Styling**: Tailwind CSS 4 -- **Components**: Radix UI -- **Icons**: Tabler Icons + Lucide React -- **Editor**: MDXEditor +For the desktop shell: -### Desktop -- **Framework**: Tauri 2.4 -- **Language**: Rust - -### Development -- **Language**: TypeScript 5 -- **Package Manager**: pnpm -- **Build Tool**: Turbopack -- **Linting**: ESLint 9 -- **Testing**: Vitest + Playwright -- **Testing Library**: React Testing Library - -### Key Dependencies -- `next-themes` - Theme management -- `react-markdown` - Markdown rendering -- `react-resizable-panels` - Split view -- `@dnd-kit` - Drag and drop -- `sonner` - Toast notifications -- `zod` - Schema validation +```bash +pnpm tauri dev +``` ## Testing -OnlyWrite maintains high code quality with comprehensive test coverage: - -### Unit Tests (Vitest) -- Component functionality tests -- Hook behavior tests -- Utility function tests -- Theme consistency tests - -### Visual Regression Tests (Playwright) -- Cross-browser screenshot comparisons -- Theme switching validation -- Responsive design verification - -### Running Tests - ```bash -# Run all tests -npx tsx test/run-all-tests.ts - -# Run unit tests only +# Unit tests pnpm test -# Run visual tests only +# Visual regression snapshots pnpm test:visual -# Update visual snapshots +# Update visual baselines pnpm test:visual --update-snapshots ``` -## Contributing - -We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) and [Code of Conduct](CODE_OF_CONDUCT.md). - -### Development Workflow +By default visual tests run on Chromium. To enable WebKit locally after installing its system dependencies: -1. Fork the repository -2. Create a feature branch (`git checkout -b feature/amazing-feature`) -3. Make your changes -4. Write or update tests -5. Ensure all tests pass (`pnpm test && pnpm test:visual`) -6. Run linting (`pnpm lint`) -7. Commit your changes (`git commit -m 'feat: add amazing feature'`) -8. Push to the branch (`git push origin feature/amazing-feature`) -9. Open a Pull Request - -### PR Guidelines - -- Include a clear description of the changes -- Link related issues -- Add screenshots for UI changes -- Ensure tests pass and coverage is maintained -- Follow the existing code style +```bash +PLAYWRIGHT_ENABLE_WEBKIT=1 pnpm test:visual +``` -## Roadmap +## Notes -- [ ] Cloud sync support (S3, Dropbox, etc.) -- [ ] Real-time collaboration -- [ ] Plugin system -- [ ] Custom themes -- [ ] Export to PDF/DOCX -- [ ] Version history -- [ ] Mobile apps (iOS/Android) -- [ ] More language support +- `pnpm lint` currently runs TypeScript checking. +- Visual snapshots live under `test/visual/theme-visual-regression.spec.ts-snapshots/`. +- The repository has been cleaned up to remove the old React / TanStack / Radix frontend. ## License -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. - -## Acknowledgments - -**This project was developed with the assistance of AI tools**, including: -- Code generation and refactoring assistance -- Documentation writing -- Test case generation -- Code review suggestions - -All AI-generated code has been reviewed, tested, and validated by human developers. - -### Built With - -- [Next.js](https://nextjs.org/) - React framework -- [Tauri](https://tauri.app/) - Desktop framework -- [Tailwind CSS](https://tailwindcss.com/) - Utility-first CSS -- [Radix UI](https://www.radix-ui.com/) - Unstyled UI primitives -- [MDXEditor](https://mdxeditor.dev/) - Markdown editor component -- [Vitest](https://vitest.dev/) - Unit testing framework -- [Playwright](https://playwright.dev/) - E2E and visual testing - ---- - -
- -**Made with ❤️ by the OnlyWrite Team** - -[Report Bug](https://github.com/yourusername/OnlyWrite/issues) · [Request Feature](https://github.com/yourusername/OnlyWrite/issues) - -
+MIT diff --git a/components.json b/components.json deleted file mode 100644 index 5a3c750..0000000 --- a/components.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "new-york", - "rsc": true, - "tsx": true, - "tailwind": { - "config": "", - "css": "app/globals.css", - "baseColor": "zinc", - "cssVariables": true, - "prefix": "" - }, - "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" - }, - "iconLibrary": "lucide" -} \ No newline at end of file diff --git a/components/app-shell.tsx b/components/app-shell.tsx deleted file mode 100644 index 2f10791..0000000 --- a/components/app-shell.tsx +++ /dev/null @@ -1,35 +0,0 @@ -"use client" - -import React from "react" -import { useI18n } from "@/hooks/useI18n" - -export function AppShell({ children }: { children: React.ReactNode }) { - const { t } = useI18n() - - return ( - <> - {/* Skip to main content link for keyboard navigation */} - - {t("a11y.skipToContent")} - - - {/* ARIA live region for status announcements */} -
- - {/* ARIA live region for urgent announcements */} -
- -
{children}
- - ) -} diff --git a/components/app-sidebar.tsx b/components/app-sidebar.tsx deleted file mode 100644 index 7cea20b..0000000 --- a/components/app-sidebar.tsx +++ /dev/null @@ -1,181 +0,0 @@ -"use client" - -import * as React from "react" -import { - IconCamera, - IconChartBar, - IconDashboard, - IconDatabase, - IconFileAi, - IconFileDescription, - IconFileWord, - IconFolder, - IconHelp, - IconInnerShadowTop, - IconListDetails, - IconReport, - IconSearch, - IconSettings, - IconUsers, -} from "@tabler/icons-react" - -import { NavDocuments } from "@/components/nav-documents" -import { NavMain } from "@/components/nav-main" -import { NavSecondary } from "@/components/nav-secondary" -import { NavUser } from "@/components/nav-user" -import { - Sidebar, - SidebarContent, - SidebarFooter, - SidebarHeader, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, -} from "@/components/ui/sidebar" - -const data = { - user: { - name: "shadcn", - email: "m@example.com", - avatar: "/avatars/shadcn.jpg", - }, - navMain: [ - { - title: "Dashboard", - url: "#", - icon: IconDashboard, - }, - { - title: "Lifecycle", - url: "#", - icon: IconListDetails, - }, - { - title: "Analytics", - url: "#", - icon: IconChartBar, - }, - { - title: "Projects", - url: "#", - icon: IconFolder, - }, - { - title: "Team", - url: "#", - icon: IconUsers, - }, - ], - navClouds: [ - { - title: "Capture", - icon: IconCamera, - isActive: true, - url: "#", - items: [ - { - title: "Active Proposals", - url: "#", - }, - { - title: "Archived", - url: "#", - }, - ], - }, - { - title: "Proposal", - icon: IconFileDescription, - url: "#", - items: [ - { - title: "Active Proposals", - url: "#", - }, - { - title: "Archived", - url: "#", - }, - ], - }, - { - title: "Prompts", - icon: IconFileAi, - url: "#", - items: [ - { - title: "Active Proposals", - url: "#", - }, - { - title: "Archived", - url: "#", - }, - ], - }, - ], - navSecondary: [ - { - title: "Settings", - url: "#", - icon: IconSettings, - }, - { - title: "Get Help", - url: "#", - icon: IconHelp, - }, - { - title: "Search", - url: "#", - icon: IconSearch, - }, - ], - documents: [ - { - name: "Data Library", - url: "#", - icon: IconDatabase, - }, - { - name: "Reports", - url: "#", - icon: IconReport, - }, - { - name: "Word Assistant", - url: "#", - icon: IconFileWord, - }, - ], -} - -export function AppSidebar({ ...props }: React.ComponentProps) { - return ( - - - - - - - - Acme Inc. - - - - - - - - - - - - - - - ) -} diff --git a/components/chart-area-interactive.tsx b/components/chart-area-interactive.tsx deleted file mode 100644 index 4753f83..0000000 --- a/components/chart-area-interactive.tsx +++ /dev/null @@ -1,292 +0,0 @@ -"use client" - -import * as React from "react" -import { Area, AreaChart, CartesianGrid, XAxis } from "recharts" - -import { useIsMobile } from "@/hooks/use-mobile" -import { - Card, - CardAction, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card" -import { - ChartConfig, - ChartContainer, - ChartTooltip, - ChartTooltipContent, -} from "@/components/ui/chart" -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" -import { - ToggleGroup, - ToggleGroupItem, -} from "@/components/ui/toggle-group" - -export const description = "An interactive area chart" - -const chartData = [ - { date: "2024-04-01", desktop: 222, mobile: 150 }, - { date: "2024-04-02", desktop: 97, mobile: 180 }, - { date: "2024-04-03", desktop: 167, mobile: 120 }, - { date: "2024-04-04", desktop: 242, mobile: 260 }, - { date: "2024-04-05", desktop: 373, mobile: 290 }, - { date: "2024-04-06", desktop: 301, mobile: 340 }, - { date: "2024-04-07", desktop: 245, mobile: 180 }, - { date: "2024-04-08", desktop: 409, mobile: 320 }, - { date: "2024-04-09", desktop: 59, mobile: 110 }, - { date: "2024-04-10", desktop: 261, mobile: 190 }, - { date: "2024-04-11", desktop: 327, mobile: 350 }, - { date: "2024-04-12", desktop: 292, mobile: 210 }, - { date: "2024-04-13", desktop: 342, mobile: 380 }, - { date: "2024-04-14", desktop: 137, mobile: 220 }, - { date: "2024-04-15", desktop: 120, mobile: 170 }, - { date: "2024-04-16", desktop: 138, mobile: 190 }, - { date: "2024-04-17", desktop: 446, mobile: 360 }, - { date: "2024-04-18", desktop: 364, mobile: 410 }, - { date: "2024-04-19", desktop: 243, mobile: 180 }, - { date: "2024-04-20", desktop: 89, mobile: 150 }, - { date: "2024-04-21", desktop: 137, mobile: 200 }, - { date: "2024-04-22", desktop: 224, mobile: 170 }, - { date: "2024-04-23", desktop: 138, mobile: 230 }, - { date: "2024-04-24", desktop: 387, mobile: 290 }, - { date: "2024-04-25", desktop: 215, mobile: 250 }, - { date: "2024-04-26", desktop: 75, mobile: 130 }, - { date: "2024-04-27", desktop: 383, mobile: 420 }, - { date: "2024-04-28", desktop: 122, mobile: 180 }, - { date: "2024-04-29", desktop: 315, mobile: 240 }, - { date: "2024-04-30", desktop: 454, mobile: 380 }, - { date: "2024-05-01", desktop: 165, mobile: 220 }, - { date: "2024-05-02", desktop: 293, mobile: 310 }, - { date: "2024-05-03", desktop: 247, mobile: 190 }, - { date: "2024-05-04", desktop: 385, mobile: 420 }, - { date: "2024-05-05", desktop: 481, mobile: 390 }, - { date: "2024-05-06", desktop: 498, mobile: 520 }, - { date: "2024-05-07", desktop: 388, mobile: 300 }, - { date: "2024-05-08", desktop: 149, mobile: 210 }, - { date: "2024-05-09", desktop: 227, mobile: 180 }, - { date: "2024-05-10", desktop: 293, mobile: 330 }, - { date: "2024-05-11", desktop: 335, mobile: 270 }, - { date: "2024-05-12", desktop: 197, mobile: 240 }, - { date: "2024-05-13", desktop: 197, mobile: 160 }, - { date: "2024-05-14", desktop: 448, mobile: 490 }, - { date: "2024-05-15", desktop: 473, mobile: 380 }, - { date: "2024-05-16", desktop: 338, mobile: 400 }, - { date: "2024-05-17", desktop: 499, mobile: 420 }, - { date: "2024-05-18", desktop: 315, mobile: 350 }, - { date: "2024-05-19", desktop: 235, mobile: 180 }, - { date: "2024-05-20", desktop: 177, mobile: 230 }, - { date: "2024-05-21", desktop: 82, mobile: 140 }, - { date: "2024-05-22", desktop: 81, mobile: 120 }, - { date: "2024-05-23", desktop: 252, mobile: 290 }, - { date: "2024-05-24", desktop: 294, mobile: 220 }, - { date: "2024-05-25", desktop: 201, mobile: 250 }, - { date: "2024-05-26", desktop: 213, mobile: 170 }, - { date: "2024-05-27", desktop: 420, mobile: 460 }, - { date: "2024-05-28", desktop: 233, mobile: 190 }, - { date: "2024-05-29", desktop: 78, mobile: 130 }, - { date: "2024-05-30", desktop: 340, mobile: 280 }, - { date: "2024-05-31", desktop: 178, mobile: 230 }, - { date: "2024-06-01", desktop: 178, mobile: 200 }, - { date: "2024-06-02", desktop: 470, mobile: 410 }, - { date: "2024-06-03", desktop: 103, mobile: 160 }, - { date: "2024-06-04", desktop: 439, mobile: 380 }, - { date: "2024-06-05", desktop: 88, mobile: 140 }, - { date: "2024-06-06", desktop: 294, mobile: 250 }, - { date: "2024-06-07", desktop: 323, mobile: 370 }, - { date: "2024-06-08", desktop: 385, mobile: 320 }, - { date: "2024-06-09", desktop: 438, mobile: 480 }, - { date: "2024-06-10", desktop: 155, mobile: 200 }, - { date: "2024-06-11", desktop: 92, mobile: 150 }, - { date: "2024-06-12", desktop: 492, mobile: 420 }, - { date: "2024-06-13", desktop: 81, mobile: 130 }, - { date: "2024-06-14", desktop: 426, mobile: 380 }, - { date: "2024-06-15", desktop: 307, mobile: 350 }, - { date: "2024-06-16", desktop: 371, mobile: 310 }, - { date: "2024-06-17", desktop: 475, mobile: 520 }, - { date: "2024-06-18", desktop: 107, mobile: 170 }, - { date: "2024-06-19", desktop: 341, mobile: 290 }, - { date: "2024-06-20", desktop: 408, mobile: 450 }, - { date: "2024-06-21", desktop: 169, mobile: 210 }, - { date: "2024-06-22", desktop: 317, mobile: 270 }, - { date: "2024-06-23", desktop: 480, mobile: 530 }, - { date: "2024-06-24", desktop: 132, mobile: 180 }, - { date: "2024-06-25", desktop: 141, mobile: 190 }, - { date: "2024-06-26", desktop: 434, mobile: 380 }, - { date: "2024-06-27", desktop: 448, mobile: 490 }, - { date: "2024-06-28", desktop: 149, mobile: 200 }, - { date: "2024-06-29", desktop: 103, mobile: 160 }, - { date: "2024-06-30", desktop: 446, mobile: 400 }, -] - -const chartConfig = { - visitors: { - label: "Visitors", - }, - desktop: { - label: "Desktop", - color: "var(--primary)", - }, - mobile: { - label: "Mobile", - color: "var(--primary)", - }, -} satisfies ChartConfig - -export function ChartAreaInteractive() { - const isMobile = useIsMobile() - const [timeRange, setTimeRange] = React.useState("90d") - - React.useEffect(() => { - if (isMobile) { - setTimeRange("7d") - } - }, [isMobile]) - - const filteredData = chartData.filter((item) => { - const date = new Date(item.date) - const referenceDate = new Date("2024-06-30") - let daysToSubtract = 90 - if (timeRange === "30d") { - daysToSubtract = 30 - } else if (timeRange === "7d") { - daysToSubtract = 7 - } - const startDate = new Date(referenceDate) - startDate.setDate(startDate.getDate() - daysToSubtract) - return date >= startDate - }) - - return ( - - - Total Visitors - - - Total for the last 3 months - - Last 3 months - - - - Last 3 months - Last 30 days - Last 7 days - - - - - - - - - - - - - - - - - - - { - const date = new Date(value) - return date.toLocaleDateString("en-US", { - month: "short", - day: "numeric", - }) - }} - /> - { - return new Date(value).toLocaleDateString("en-US", { - month: "short", - day: "numeric", - }) - }} - indicator="dot" - /> - } - /> - - - - - - - ) -} diff --git a/components/data-table.tsx b/components/data-table.tsx deleted file mode 100644 index f55cb49..0000000 --- a/components/data-table.tsx +++ /dev/null @@ -1,807 +0,0 @@ -"use client" - -import * as React from "react" -import { - DndContext, - KeyboardSensor, - MouseSensor, - TouchSensor, - closestCenter, - useSensor, - useSensors, - type DragEndEvent, - type UniqueIdentifier, -} from "@dnd-kit/core" -import { restrictToVerticalAxis } from "@dnd-kit/modifiers" -import { - SortableContext, - arrayMove, - useSortable, - verticalListSortingStrategy, -} from "@dnd-kit/sortable" -import { CSS } from "@dnd-kit/utilities" -import { - IconChevronDown, - IconChevronLeft, - IconChevronRight, - IconChevronsLeft, - IconChevronsRight, - IconCircleCheckFilled, - IconDotsVertical, - IconGripVertical, - IconLayoutColumns, - IconLoader, - IconPlus, - IconTrendingUp, -} from "@tabler/icons-react" -import { - ColumnDef, - ColumnFiltersState, - Row, - SortingState, - VisibilityState, - flexRender, - getCoreRowModel, - getFacetedRowModel, - getFacetedUniqueValues, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable, -} from "@tanstack/react-table" -import { Area, AreaChart, CartesianGrid, XAxis } from "recharts" -import { toast } from "sonner" -import { z } from "zod" - -import { useIsMobile } from "@/hooks/use-mobile" -import { Badge } from "@/components/ui/badge" -import { Button } from "@/components/ui/button" -import { - ChartConfig, - ChartContainer, - ChartTooltip, - ChartTooltipContent, -} from "@/components/ui/chart" -import { Checkbox } from "@/components/ui/checkbox" -import { - Drawer, - DrawerClose, - DrawerContent, - DrawerDescription, - DrawerFooter, - DrawerHeader, - DrawerTitle, - DrawerTrigger, -} from "@/components/ui/drawer" -import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" -import { Separator } from "@/components/ui/separator" -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table" -import { - Tabs, - TabsContent, - TabsList, - TabsTrigger, -} from "@/components/ui/tabs" - -export const schema = z.object({ - id: z.number(), - header: z.string(), - type: z.string(), - status: z.string(), - target: z.string(), - limit: z.string(), - reviewer: z.string(), -}) - -// Create a separate component for the drag handle -function DragHandle({ id }: { id: number }) { - const { attributes, listeners } = useSortable({ - id, - }) - - return ( - - ) -} - -const columns: ColumnDef>[] = [ - { - id: "drag", - header: () => null, - cell: ({ row }) => , - }, - { - id: "select", - header: ({ table }) => ( -
- table.toggleAllPageRowsSelected(!!value)} - aria-label="Select all" - /> -
- ), - cell: ({ row }) => ( -
- row.toggleSelected(!!value)} - aria-label="Select row" - /> -
- ), - enableSorting: false, - enableHiding: false, - }, - { - accessorKey: "header", - header: "Header", - cell: ({ row }) => { - return - }, - enableHiding: false, - }, - { - accessorKey: "type", - header: "Section Type", - cell: ({ row }) => ( -
- - {row.original.type} - -
- ), - }, - { - accessorKey: "status", - header: "Status", - cell: ({ row }) => ( - - {row.original.status === "Done" ? ( - - ) : ( - - )} - {row.original.status} - - ), - }, - { - accessorKey: "target", - header: () =>
Target
, - cell: ({ row }) => ( -
{ - e.preventDefault() - toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), { - loading: `Saving ${row.original.header}`, - success: "Done", - error: "Error", - }) - }} - > - - -
- ), - }, - { - accessorKey: "limit", - header: () =>
Limit
, - cell: ({ row }) => ( -
{ - e.preventDefault() - toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), { - loading: `Saving ${row.original.header}`, - success: "Done", - error: "Error", - }) - }} - > - - -
- ), - }, - { - accessorKey: "reviewer", - header: "Reviewer", - cell: ({ row }) => { - const isAssigned = row.original.reviewer !== "Assign reviewer" - - if (isAssigned) { - return row.original.reviewer - } - - return ( - <> - - - - ) - }, - }, - { - id: "actions", - cell: () => ( - - - - - - Edit - Make a copy - Favorite - - Delete - - - ), - }, -] - -function DraggableRow({ row }: { row: Row> }) { - const { transform, transition, setNodeRef, isDragging } = useSortable({ - id: row.original.id, - }) - - return ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - ) -} - -export function DataTable({ - data: initialData, -}: { - data: z.infer[] -}) { - const [data, setData] = React.useState(() => initialData) - const [rowSelection, setRowSelection] = React.useState({}) - const [columnVisibility, setColumnVisibility] = - React.useState({}) - const [columnFilters, setColumnFilters] = React.useState( - [] - ) - const [sorting, setSorting] = React.useState([]) - const [pagination, setPagination] = React.useState({ - pageIndex: 0, - pageSize: 10, - }) - const sortableId = React.useId() - const sensors = useSensors( - useSensor(MouseSensor, {}), - useSensor(TouchSensor, {}), - useSensor(KeyboardSensor, {}) - ) - - const dataIds = React.useMemo( - () => data?.map(({ id }) => id) || [], - [data] - ) - - const table = useReactTable({ - data, - columns, - state: { - sorting, - columnVisibility, - rowSelection, - columnFilters, - pagination, - }, - getRowId: (row) => row.id.toString(), - enableRowSelection: true, - onRowSelectionChange: setRowSelection, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - onColumnVisibilityChange: setColumnVisibility, - onPaginationChange: setPagination, - getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFacetedRowModel: getFacetedRowModel(), - getFacetedUniqueValues: getFacetedUniqueValues(), - }) - - function handleDragEnd(event: DragEndEvent) { - const { active, over } = event - if (active && over && active.id !== over.id) { - setData((data) => { - const oldIndex = dataIds.indexOf(active.id) - const newIndex = dataIds.indexOf(over.id) - return arrayMove(data, oldIndex, newIndex) - }) - } - } - - return ( - -
- - - - Outline - - Past Performance 3 - - - Key Personnel 2 - - Focus Documents - -
- - - - - - {table - .getAllColumns() - .filter( - (column) => - typeof column.accessorFn !== "undefined" && - column.getCanHide() - ) - .map((column) => { - return ( - - column.toggleVisibility(!!value) - } - > - {column.id} - - ) - })} - - - -
-
- -
- - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ) - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - - {table.getRowModel().rows.map((row) => ( - - ))} - - ) : ( - - - No results. - - - )} - -
-
-
-
-
- {table.getFilteredSelectedRowModel().rows.length} of{" "} - {table.getFilteredRowModel().rows.length} row(s) selected. -
-
-
- - -
-
- Page {table.getState().pagination.pageIndex + 1} of{" "} - {table.getPageCount()} -
-
- - - - -
-
-
-
- -
-
- -
-
- -
-
-
- ) -} - -const chartData = [ - { month: "January", desktop: 186, mobile: 80 }, - { month: "February", desktop: 305, mobile: 200 }, - { month: "March", desktop: 237, mobile: 120 }, - { month: "April", desktop: 73, mobile: 190 }, - { month: "May", desktop: 209, mobile: 130 }, - { month: "June", desktop: 214, mobile: 140 }, -] - -const chartConfig = { - desktop: { - label: "Desktop", - color: "var(--primary)", - }, - mobile: { - label: "Mobile", - color: "var(--primary)", - }, -} satisfies ChartConfig - -function TableCellViewer({ item }: { item: z.infer }) { - const isMobile = useIsMobile() - - return ( - - - - - - - {item.header} - - Showing total visitors for the last 6 months - - -
- {!isMobile && ( - <> - - - - value.slice(0, 3)} - hide - /> - } - /> - - - - - -
-
- Trending up by 5.2% this month{" "} - -
-
- Showing total visitors for the last 6 months. This is just - some random text to test the layout. It spans multiple lines - and should wrap around. -
-
- - - )} -
-
- - -
-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
-
- - -
-
-
- - - - - - -
-
- ) -} diff --git a/components/file-area.tsx b/components/file-area.tsx deleted file mode 100644 index 1e383ed..0000000 --- a/components/file-area.tsx +++ /dev/null @@ -1,235 +0,0 @@ -'use client' - -import React, { useEffect, useCallback } from 'react' -import { readDir, writeTextFile } from '@tauri-apps/plugin-fs' -import { join } from '@tauri-apps/api/path' -import { Button } from './ui/button' -import { Card, CardContent, CardHeader, CardTitle } from './ui/card' -import { ScrollArea } from './ui/scroll-area' -import { Badge } from './ui/badge' -import { FolderOpen, File as FileIcon, Plus, RefreshCw } from 'lucide-react' -import { cn } from '@/lib/utils' -import { useI18n } from '@/hooks/useI18n' -import { useWorkspaceStore } from '@/lib/stores' -import { selectFileCount, selectActiveFileName, selectFolderName } from '@/lib/stores/workspace-store' -import { UXFeedback } from '@/lib/ux-feedback' - -export function FileArea({ className }: { className?: string }) { - const { t } = useI18n() - - const workspaceState = useWorkspaceStore() - - const fileCount = selectFileCount(workspaceState) - const activeFileName = selectActiveFileName(workspaceState) - const folderName = selectFolderName(workspaceState) - - const { - folderPath, - files, - isLoadingFiles, - fileError, - setFiles, - setSelectedFile, - setIsLoadingFiles, - setFileError, - } = workspaceState - - const loadFiles = useCallback(async () => { - if (!folderPath) return - - setIsLoadingFiles(true) - setFileError(null) - - try { - const entries = await readDir(folderPath) - const fileEntries = entries.map((entry) => ({ - name: entry.name, - isFile: entry.isFile, - })) - - fileEntries.sort((a, b) => { - if (a.isFile !== b.isFile) { - return a.isFile ? 1 : -1 - } - return a.name.localeCompare(b.name) - }) - - setFiles(fileEntries) - } catch (error) { - UXFeedback.handleError(error, 'Failed to load files') - } - }, [folderPath, setFiles, setIsLoadingFiles, setFileError]) - - useEffect(() => { - loadFiles() - }, [folderPath, loadFiles]) - - const handleFileClick = useCallback( - async (fileName: string, isFile: boolean) => { - if (folderPath && isFile) { - const filePath = await join(folderPath, fileName) - setSelectedFile(filePath) - } - }, - [folderPath, setSelectedFile], - ) - - const handleCreateFile = useCallback(async () => { - if (!folderPath) return - - try { - const fileName = await UXFeedback.showSaveDialog({ - filters: [ - { - name: 'Markdown', - extensions: ['md'], - }, - ], - }) - - if (!fileName) return - - const finalFileName = fileName.endsWith('.md') ? fileName : `${fileName}.md` - - const filePath = await join(folderPath, finalFileName) - - await writeTextFile( - filePath, - '# ' + finalFileName.replace('.md', '') + '\n\n' + (t('file.newFileContent') || 'Start writing here...') + '\n' - ) - - await loadFiles() - setSelectedFile(filePath) - UXFeedback.success(t('file.created') || `Created ${finalFileName}`) - } catch (error) { - UXFeedback.handleError(error, 'Failed to create file') - } - }, [folderPath, loadFiles, setSelectedFile, t]) - - const getFileIcon = (fileName: string, isFile: boolean) => { - if (!isFile) { - return ( - - - - ) - } - - if (fileName.endsWith('.md')) { - return - } - - return - } - - return ( - - -
-
-

- {t('file.folderLabel') || 'FOLDER'} -

- - {folderName || (t('file.noFolder') || 'No folder selected')} - -
-
- {folderPath && ( - - {t('file.filesCount', { count: fileCount }) || `${fileCount} files`} - - )} - {folderPath && ( - - )} -
-
-
- - - {isLoadingFiles ? ( -
- -

- {t('file.loading') || 'Loading files...'} -

-
- ) : fileError ? ( -
- -

- {fileError} -

- -
- ) : ( - -
- {folderPath ? ( -
- {files.map((entry, index) => { - const isEntryActive = entry.isFile && entry.name === activeFileName - return ( - - ) - })} - {files.length === 0 && ( -
-
- -

- {t('file.emptyFolder') || 'Empty folder'} -

-
-
- )} -
- ) : ( -
- -

- {t('file.chooseFolder') || 'Choose a folder'} -

-
- )} -
-
- )} -
-
- ) -} diff --git a/components/i18n-provider.tsx b/components/i18n-provider.tsx deleted file mode 100644 index db5fb0b..0000000 --- a/components/i18n-provider.tsx +++ /dev/null @@ -1,54 +0,0 @@ -"use client" - -import React, { useCallback, useEffect, useMemo, useState } from "react" -import { - detectLocale, - formatMessage, - getMessage, - persistLocale, - type Locale, -} from "@/lib/i18n" - -type I18nContextValue = { - locale: Locale - setLocale: (locale: Locale) => void - t: (key: string, params?: Record) => string -} - -export const I18nContext = React.createContext(null) - -export function I18nProvider({ children }: { children: React.ReactNode }) { - const [locale, setLocaleState] = useState("en") - - const setLocale = useCallback((next: Locale) => { - setLocaleState(next) - persistLocale(next) - }, []) - - const t = useCallback( - (key: string, params?: Record) => { - const message = getMessage(locale, key) - return formatMessage(message, params) - }, - [locale] - ) - - useEffect(() => { - setLocaleState(detectLocale()) - }, []) - - useEffect(() => { - document.documentElement.lang = locale - }, [locale]) - - const value = useMemo( - () => ({ - locale, - setLocale, - t, - }), - [locale, setLocale, t] - ) - - return {children} -} diff --git a/components/language-toggle.tsx b/components/language-toggle.tsx deleted file mode 100644 index 0319866..0000000 --- a/components/language-toggle.tsx +++ /dev/null @@ -1,26 +0,0 @@ -"use client" - -import { useI18n } from "@/hooks/useI18n" -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" - -export function LanguageToggle() { - const { locale, setLocale, t } = useI18n() - - return ( - - ) -} diff --git a/components/login-form.tsx b/components/login-form.tsx deleted file mode 100644 index cec1031..0000000 --- a/components/login-form.tsx +++ /dev/null @@ -1,104 +0,0 @@ -'use client' - -import { cn } from "@/lib/utils" -import { Button } from "@/components/ui/button" -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { useI18n } from "@/hooks/useI18n" - -export function LoginForm({ - className, - ...props -}: React.ComponentProps<"div">) { - const { t } = useI18n() - - return ( -
- - - {t('login.title')} - - {t('login.subtitle')} - - - -
-
-
- - -
-
- - {t('login.orEmail')} - -
-
-
- - -
-
-
- - - {t('login.forgot')} - -
- -
- -
-
- {t('login.noAccount')}{" "} - - {t('login.signup')} - -
-
-
-
-
-
- {t('login.terms')} - {t('login.termsLink')} - {t('login.termsJoin')} - {t('login.privacyLink')} - {t('login.termsEnd')} -
-
- ) -} diff --git a/components/markdown-editor.tsx b/components/markdown-editor.tsx deleted file mode 100644 index 4f889dd..0000000 --- a/components/markdown-editor.tsx +++ /dev/null @@ -1,157 +0,0 @@ -'use client' - -import React, { useState } from 'react' -import { - MDXEditor, - type MDXEditorMethods, - type MDXEditorProps, - headingsPlugin, - listsPlugin, - quotePlugin, - thematicBreakPlugin, - toolbarPlugin, - BoldItalicUnderlineToggles, - imagePlugin, - linkPlugin, - linkDialogPlugin, - tablePlugin, - InsertTable, - BlockTypeSelect, - CreateLink, - UndoRedo, - codeBlockPlugin, - codeMirrorPlugin, - InsertCodeBlock, -} from '@mdxeditor/editor' -import '@mdxeditor/editor/style.css' -import { join } from '@tauri-apps/api/path' -import { writeFile } from '@tauri-apps/plugin-fs' -import { Separator } from './ui/separator' -import { useS3Config } from '@/hooks/useS3Config' -import { toast } from 'sonner' - -interface MarkdownEditorProps extends MDXEditorProps { - editorRef?: React.ForwardedRef - folderPath: string | null -} - -const MarkdownEditor: React.FC = ({ - editorRef, - folderPath, - ...props -}) => { - const { hasConfig, uploadImage } = useS3Config() - const [uploading, setUploading] = useState(false) - - const imageUploadHandler = async (image: File) => { - setUploading(true) - try { - if (hasConfig()) { - const imageName = `${Date.now()}-${image.name}` - const fileData = await image.arrayBuffer() - const url = await uploadImage(imageName, fileData) - toast.success('Image uploaded to S3 successfully') - return url - } else if (folderPath) { - const imageName = `${Date.now()}-${image.name}` - const imagePath = await join(folderPath, 'assets', imageName) - - const reader = new FileReader() - reader.readAsArrayBuffer(image) - await new Promise((resolve, reject) => { - reader.onload = async () => { - try { - await writeFile(imagePath, new Uint8Array(reader.result as ArrayBuffer)) - resolve(undefined) - } catch (e) { - reject(e) - } - } - reader.onerror = reject - }) - - return imagePath - } else { - toast.error('No S3 configuration or folder path available') - return '' - } - } catch (error) { - toast.error(`Failed to upload image: ${error}`) - return '' - } finally { - setUploading(false) - } - } - - if (uploading) { - return ( -
-
-
-

Uploading image...

-
-
- ) - } - - return ( -
- ( -
- - - - - - - - - - -
- ), - }), - headingsPlugin(), - listsPlugin(), - quotePlugin(), - thematicBreakPlugin(), - linkPlugin(), - linkDialogPlugin(), - tablePlugin(), - codeBlockPlugin({ defaultCodeBlockLanguage: 'js' }), - codeMirrorPlugin({ - codeBlockLanguages: { - js: 'JavaScript', - ts: 'TypeScript', - tsx: 'TypeScript React', - css: 'CSS', - html: 'HTML', - python: 'Python', - rust: 'Rust', - go: 'Go', - java: 'Java', - c: 'C', - cpp: 'C++', - sh: 'Shell', - json: 'JSON', - yaml: 'YAML', - xml: 'XML', - sql: 'SQL', - md: 'Markdown', - } - }), - imagePlugin({ imageUploadHandler }), - ]} - contentEditableClassName="markdown-editor max-w-none p-6 min-h-screen focus:outline-none bg-background text-foreground" - {...props} - ref={editorRef} - className="h-full overflow-auto" - /> -
- ) -} - -export default MarkdownEditor diff --git a/components/nav-documents.tsx b/components/nav-documents.tsx deleted file mode 100644 index 66f6ee7..0000000 --- a/components/nav-documents.tsx +++ /dev/null @@ -1,92 +0,0 @@ -"use client" - -import { - IconDots, - IconFolder, - IconShare3, - IconTrash, - type Icon, -} from "@tabler/icons-react" - -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" -import { - SidebarGroup, - SidebarGroupLabel, - SidebarMenu, - SidebarMenuAction, - SidebarMenuButton, - SidebarMenuItem, - useSidebar, -} from "@/components/ui/sidebar" - -export function NavDocuments({ - items = [], -}: { - items?: { - name: string - url: string - icon: Icon - }[] -}) { - const { isMobile } = useSidebar() - - return ( - - Documents - - {items.map((item) => ( - - - - - {item.name} - - - - - - - More - - - - - - Open - - - - Share - - - - - Delete - - - - - ))} - - - - More - - - - - ) -} diff --git a/components/nav-main.tsx b/components/nav-main.tsx deleted file mode 100644 index 694c230..0000000 --- a/components/nav-main.tsx +++ /dev/null @@ -1,58 +0,0 @@ -"use client" - -import { IconCirclePlusFilled, IconMail, type Icon } from "@tabler/icons-react" - -import { Button } from "@/components/ui/button" -import { - SidebarGroup, - SidebarGroupContent, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, -} from "@/components/ui/sidebar" - -export function NavMain({ - items, -}: { - items: { - title: string - url: string - icon?: Icon - }[] -}) { - return ( - - - - - - - Quick Create - - - - - - {items.map((item) => ( - - - {item.icon && } - {item.title} - - - ))} - - - - ) -} diff --git a/components/nav-secondary.tsx b/components/nav-secondary.tsx deleted file mode 100644 index 3f3636f..0000000 --- a/components/nav-secondary.tsx +++ /dev/null @@ -1,42 +0,0 @@ -"use client" - -import * as React from "react" -import { type Icon } from "@tabler/icons-react" - -import { - SidebarGroup, - SidebarGroupContent, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, -} from "@/components/ui/sidebar" - -export function NavSecondary({ - items, - ...props -}: { - items: { - title: string - url: string - icon: Icon - }[] -} & React.ComponentPropsWithoutRef) { - return ( - - - - {items.map((item) => ( - - - - - {item.title} - - - - ))} - - - - ) -} diff --git a/components/nav-user.tsx b/components/nav-user.tsx deleted file mode 100644 index 7c49dc7..0000000 --- a/components/nav-user.tsx +++ /dev/null @@ -1,110 +0,0 @@ -"use client" - -import { - IconCreditCard, - IconDotsVertical, - IconLogout, - IconNotification, - IconUserCircle, -} from "@tabler/icons-react" - -import { - Avatar, - AvatarFallback, - AvatarImage, -} from "@/components/ui/avatar" -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" -import { - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - useSidebar, -} from "@/components/ui/sidebar" - -export function NavUser({ - user, -}: { - user: { - name: string - email: string - avatar: string - } -}) { - const { isMobile } = useSidebar() - - return ( - - - - - - - - CN - -
- {user.name} - - {user.email} - -
- -
-
- - -
- - - CN - -
- {user.name} - - {user.email} - -
-
-
- - - - - Account - - - - Billing - - - - Notifications - - - - - - Log out - -
-
-
-
- ) -} diff --git a/components/s3-config-dialog.tsx b/components/s3-config-dialog.tsx deleted file mode 100644 index 16eb8a5..0000000 --- a/components/s3-config-dialog.tsx +++ /dev/null @@ -1,219 +0,0 @@ -'use client' - -import React, { useState, useEffect } from 'react' -import { invoke } from '@tauri-apps/api/core' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from './ui/dialog' -import { Button } from './ui/button' -import { Input } from './ui/input' -import { Label } from './ui/label' -import { toast } from 'sonner' - -interface S3Config { - bucket_name: string - region: string - access_key_id: string - secret_access_key: string - endpoint_url?: string - path_prefix?: string -} - -interface S3ConfigDialogProps { - trigger?: React.ReactNode - onConfigSaved?: () => void -} - -export function S3ConfigDialog({ trigger, onConfigSaved }: S3ConfigDialogProps) { - const [open, setOpen] = useState(false) - const [loading, setLoading] = useState(false) - const [hasConfig, setHasConfig] = useState(false) - const [formData, setFormData] = useState({ - bucket_name: '', - region: 'us-east-1', - access_key_id: '', - secret_access_key: '', - endpoint_url: '', - path_prefix: '', - }) - - const loadConfig = async () => { - try { - const config = await invoke('load_s3_config') - if (config) { - setFormData(config) - setHasConfig(true) - } else { - setHasConfig(false) - } - } catch (error) { - console.error('Failed to load S3 config:', error) - } - } - - useEffect(() => { - if (open) { - loadConfig() - } - }, [open]) - - const handleSave = async () => { - if (!formData.bucket_name || !formData.region || !formData.access_key_id || !formData.secret_access_key) { - toast.error('Please fill in all required fields') - return - } - - setLoading(true) - try { - await invoke('save_s3_config', { config: formData }) - toast.success('S3 configuration saved successfully') - setHasConfig(true) - onConfigSaved?.() - setOpen(false) - } catch (error) { - toast.error(`Failed to save S3 config: ${error}`) - } finally { - setLoading(false) - } - } - - const handleDelete = async () => { - if (!confirm('Are you sure you want to delete the S3 configuration?')) { - return - } - - setLoading(true) - try { - await invoke('delete_s3_config') - toast.success('S3 configuration deleted') - setHasConfig(false) - setFormData({ - bucket_name: '', - region: 'us-east-1', - access_key_id: '', - secret_access_key: '', - endpoint_url: '', - path_prefix: '', - }) - onConfigSaved?.() - } catch (error) { - toast.error(`Failed to delete S3 config: ${error}`) - } finally { - setLoading(false) - } - } - - const handleInputChange = (field: keyof S3Config, value: string) => { - setFormData((prev) => ({ ...prev, [field]: value })) - } - - return ( - - {trigger && {trigger}} - - - S3 Configuration - - Configure your S3-compatible storage for automatic image uploads. - - - -
-
- - handleInputChange('bucket_name', e.target.value)} - placeholder="my-bucket" - /> -
- -
- - handleInputChange('region', e.target.value)} - placeholder="us-east-1" - /> -
- -
- - handleInputChange('access_key_id', e.target.value)} - placeholder="AKIAIOSFODNN7EXAMPLE" - type="password" - /> -
- -
- - handleInputChange('secret_access_key', e.target.value)} - placeholder="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" - type="password" - /> -
- -
- - handleInputChange('endpoint_url', e.target.value)} - placeholder="https://s3.amazonaws.com" - /> -

- Leave empty for AWS S3. Use this for S3-compatible services like MinIO, Wasabi, etc. -

-
- -
- - handleInputChange('path_prefix', e.target.value)} - placeholder="images/blog" - /> -

- Uploads will be stored under this prefix in the bucket -

-
-
- - - {hasConfig && ( - - )} -
- - -
-
-
-
- ) -} diff --git a/components/section-cards.tsx b/components/section-cards.tsx deleted file mode 100644 index bb2e93f..0000000 --- a/components/section-cards.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { IconTrendingDown, IconTrendingUp } from "@tabler/icons-react" - -import { Badge } from "@/components/ui/badge" -import { - Card, - CardAction, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card" - -export function SectionCards() { - return ( -
- - - Total Revenue - - $1,250.00 - - - - - +12.5% - - - - -
- Trending up this month -
-
- Visitors for the last 6 months -
-
-
- - - New Customers - - 1,234 - - - - - -20% - - - - -
- Down 20% this period -
-
- Acquisition needs attention -
-
-
- - - Active Accounts - - 45,678 - - - - - +12.5% - - - - -
- Strong user retention -
-
Engagement exceed targets
-
-
- - - Growth Rate - - 4.5% - - - - - +4.5% - - - - -
- Steady performance increase -
-
Meets growth projections
-
-
-
- ) -} diff --git a/components/site-header.tsx b/components/site-header.tsx deleted file mode 100644 index 59c4f02..0000000 --- a/components/site-header.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { Button } from "@/components/ui/button" -import { Separator } from "@/components/ui/separator" -import { SidebarTrigger } from "@/components/ui/sidebar" - -export function SiteHeader() { - return ( -
-
- - -

Documents

-
- -
-
-
- ) -} diff --git a/components/theme-toggle.tsx b/components/theme-toggle.tsx deleted file mode 100644 index ae7c9b6..0000000 --- a/components/theme-toggle.tsx +++ /dev/null @@ -1,83 +0,0 @@ -'use client' - -import { Button } from '@/components/ui/button' -import { useEffect, useState } from 'react' -import { useI18n } from '@/hooks/useI18n' - -export function ThemeToggle() { - const { t } = useI18n() - const [theme, setTheme] = useState<'dark' | 'light'>('light') - - useEffect(() => { - // 从本地存储获取主题设置 - const savedTheme = localStorage.getItem('onlywrite-theme') as 'dark' | 'light' - if (savedTheme) { - setTheme(savedTheme) - } else { - // 检查系统主题偏好 - const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' - setTheme(systemTheme) - } - }, []) - - useEffect(() => { - // 更新文档类 - const root = document.documentElement - root.classList.remove('light', 'dark') - root.classList.add(theme) - - // 保存到本地存储 - localStorage.setItem('onlywrite-theme', theme) - }, [theme]) - - const toggleTheme = () => { - // Add a brief delay to prevent flash during theme transition - document.body.style.transition = 'none'; - setTheme(prev => prev === 'light' ? 'dark' : 'light'); - - // Re-enable transitions after theme change - requestAnimationFrame(() => { - document.body.style.transition = ''; - }); - } - - return ( - - ) -} diff --git a/components/ui/accordion.tsx b/components/ui/accordion.tsx deleted file mode 100644 index 4a8cca4..0000000 --- a/components/ui/accordion.tsx +++ /dev/null @@ -1,66 +0,0 @@ -"use client" - -import * as React from "react" -import * as AccordionPrimitive from "@radix-ui/react-accordion" -import { ChevronDownIcon } from "lucide-react" - -import { cn } from "@/lib/utils" - -function Accordion({ - ...props -}: React.ComponentProps) { - return -} - -function AccordionItem({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function AccordionTrigger({ - className, - children, - ...props -}: React.ComponentProps) { - return ( - - svg]:rotate-180", - className - )} - {...props} - > - {children} - - - - ) -} - -function AccordionContent({ - className, - children, - ...props -}: React.ComponentProps) { - return ( - -
{children}
-
- ) -} - -export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/components/ui/alert-dialog.tsx b/components/ui/alert-dialog.tsx deleted file mode 100644 index 71ee6e1..0000000 --- a/components/ui/alert-dialog.tsx +++ /dev/null @@ -1,157 +0,0 @@ -"use client" - -import * as React from "react" -import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" - -import { cn } from "@/lib/utils" -import { buttonVariants } from "@/components/ui/button" - -function AlertDialog({ - ...props -}: React.ComponentProps) { - return -} - -function AlertDialogTrigger({ - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function AlertDialogPortal({ - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function AlertDialogOverlay({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function AlertDialogContent({ - className, - ...props -}: React.ComponentProps) { - return ( - - - - - ) -} - -function AlertDialogHeader({ - className, - ...props -}: React.ComponentProps<"div">) { - return ( -
- ) -} - -function AlertDialogFooter({ - className, - ...props -}: React.ComponentProps<"div">) { - return ( -
- ) -} - -function AlertDialogTitle({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function AlertDialogDescription({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function AlertDialogAction({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function AlertDialogCancel({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -export { - AlertDialog, - AlertDialogPortal, - AlertDialogOverlay, - AlertDialogTrigger, - AlertDialogContent, - AlertDialogHeader, - AlertDialogFooter, - AlertDialogTitle, - AlertDialogDescription, - AlertDialogAction, - AlertDialogCancel, -} diff --git a/components/ui/alert.tsx b/components/ui/alert.tsx deleted file mode 100644 index 7ec3faf..0000000 --- a/components/ui/alert.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" - -import { cn } from "@/lib/utils" - -const alertVariants = cva( - "relative w-full border border-foreground px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", - { - variants: { - variant: { - default: "bg-card text-card-foreground", - destructive: - "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -function Alert({ - className, - variant, - ...props -}: React.ComponentProps<"div"> & VariantProps) { - return ( -
- ) -} - -function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function AlertDescription({ - className, - ...props -}: React.ComponentProps<"div">) { - return ( -
- ) -} - -export { Alert, AlertTitle, AlertDescription } diff --git a/components/ui/aspect-ratio.tsx b/components/ui/aspect-ratio.tsx deleted file mode 100644 index 3df3fd0..0000000 --- a/components/ui/aspect-ratio.tsx +++ /dev/null @@ -1,11 +0,0 @@ -"use client" - -import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" - -function AspectRatio({ - ...props -}: React.ComponentProps) { - return -} - -export { AspectRatio } diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx deleted file mode 100644 index 71e428b..0000000 --- a/components/ui/avatar.tsx +++ /dev/null @@ -1,53 +0,0 @@ -"use client" - -import * as React from "react" -import * as AvatarPrimitive from "@radix-ui/react-avatar" - -import { cn } from "@/lib/utils" - -function Avatar({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function AvatarImage({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function AvatarFallback({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -export { Avatar, AvatarImage, AvatarFallback } diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx deleted file mode 100644 index 1d23972..0000000 --- a/components/ui/badge.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" - -import { cn } from "@/lib/utils" - -const badgeVariants = cva( - "inline-flex items-center justify-center border-2 border-foreground px-2 py-0.5 text-[0.65rem] font-mono tracking-widest w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none transition-colors duration-100 overflow-hidden", - { - variants: { - variant: { - default: - "bg-foreground text-background", - secondary: - "bg-background text-foreground", - destructive: - "bg-foreground text-background", - outline: - "bg-transparent text-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -function Badge({ - className, - variant, - asChild = false, - ...props -}: React.ComponentProps<"span"> & - VariantProps & { asChild?: boolean }) { - const Comp = asChild ? Slot : "span" - - return ( - - ) -} - -export { Badge, badgeVariants } diff --git a/components/ui/breadcrumb.tsx b/components/ui/breadcrumb.tsx deleted file mode 100644 index eb88f32..0000000 --- a/components/ui/breadcrumb.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { ChevronRight, MoreHorizontal } from "lucide-react" - -import { cn } from "@/lib/utils" - -function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { - return