diff --git a/.agent/rules/ia-rules.md b/.agent/rules/ia-rules.md new file mode 100644 index 0000000..3ea7ee0 --- /dev/null +++ b/.agent/rules/ia-rules.md @@ -0,0 +1,109 @@ +--- +trigger: always_on +--- + +# AI Guidelines for OpenTimeTracker + +This document provides context and rules for AI agents contributing to OpenTimeTracker. Follow these guidelines to ensure consistency, quality, and adherence to project standards. + +## Project Overview + +- **Name**: OpenTimeTracker +- **Purpose**: Free, open-source, local-first time tracking application. +- **Key Features**: Offline-first (SQLite), no subscriptions, cross-platform (Windows, macOS, Linux). + +## Tech Stack + +- **Frontend**: Angular 21 (Signals, Standalone Components, strict mode). +- **Desktop Wrapper**: Electron 37 (IPC for Node.js access). +- **Database**: SQLite via Prisma (local file `timetracker.db`). +- **UI Component Library**: PrimeNG 21 + PrimeFlex. +- **State Management**: Angular Signals + Services (avoid NgRx unless strictly necessary). +- **Internationalization**: `@ngx-translate/core`. + +## Coding Standards + +### TypeScript & Angular + +- **Strict Typing**: No `any`. Define interfaces for all data structures. +- **Signals**: Use Angular Signals for state reactivity. Avoid `BehaviorSubject` where Signals suffice. +- **Standalone Components**: All new components must be `standalone: true`. +- **Control Flow**: Use modern Angular control flow (`@if`, `@for`, `@switch`) instead of `*ngIf`, `*ngFor`. +- **Change Detection**: Use `OnPush` strategy where possible. + +### Electron & IPC + +- **Security**: Enable context isolation and sandbox. +- **IPC**: Use `ipcMain.handle` and `ipcRenderer.invoke` for bidirectional communication. +- **Preload Scripts**: Expose typed APIs via `contextBridge`. + +### CSS & Styling + +- **PrimeFlex**: Use PrimeFlex utility classes for layout and spacing (e.g., `flex`, `gap-2`, `p-3`). +- **SCSS**: Use component-specific SCSS for custom styles not covered by PrimeFlex. +- **Variables**: Use CSS variables for theming (e.g., `var(--primary-color)`). + +### Git & Commits + +- **Conventional Commits**: `type(scope): description`. + - Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`. + - Example: `feat(calendar): add weekly view support`. + +## Architecture + +### Directory Structure + +- `src/app`: Angular application code. +- `src/assets`: Static assets and i18n files. +- `electron/src`: Electron main process and preload scripts. +- `electron/src/services`: IPC handlers and Node.js logic. +- `prisma`: Database schema and migrations. + +### Internationalization (i18n) + +- All user-facing text must be translatable. +- Keys: `SECTION.FEATURE.KEY` (e.g., `SETTINGS.UPDATES.CHECK_NOW`). +- Files: `src/assets/i18n/en.json`, `src/assets/i18n/es.json`. +- Tools: `TranslateService`, `TranslatePipe`. + +### Testing + +- **Unit Tests**: Jasmine + Karma (Angular), Vitest (Electron). +- **Coverage**: Maintain high coverage (>80%). +- **SonarQube**: Respect quality gates. Run `npm run sonar:check` before pushing. + +### UI Development (Storybook) + +- Create stories for new dumb/presentational components. +- Path: `src/app/components/[name]/[name].stories.ts`. +- Run: `npm run storybook`. + +## Workflows + +### Database Changes + +1. Modify `prisma/schema.prisma`. +2. Run `npx prisma migrate dev --name `. +3. Run `npm run prisma:template` to update production template. + +### Adding a New Feature + +1. Design component/service hierarchy. +2. Implement core logic and state. +3. Add UI with PrimeNG/PrimeFlex. +4. Add i18n keys to English and Spanish. +5. Add Unit Tests. +6. (Optional) Add Storybook story. +7. Verify with `npm run sonar:check`. + +## Context for AI + +- **Do not** suggest cloud-based solutions unless explicitly asked (Local-first philosophy). +- **Do not** introduce new heavy dependencies without approval. +- **Always** check `package.json` scripts (`dev`, `build`, `test`, `sonar:check`) for running tasks. +- **Always** favor modern Angular syntax (Signals, Control Flow). + +## Essential Commands + +- **Run Project**: `npm run dev` (Runs Angular + Electron in development mode). +- **Run Tests & Quality Checks**: `npm run sonar:check` (Runs Unit Tests, Coverage, and SonarQube analysis). Use this to verify your changes. diff --git a/.storybook/main.ts b/.storybook/main.ts index 99fd12d..d5b7476 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,12 +1,21 @@ import type { StorybookConfig } from '@storybook/angular'; const config: StorybookConfig = { - stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], - addons: [ - '@storybook/addon-a11y', - '@storybook/addon-docs', - '@storybook/addon-onboarding', + stories: [ + './*.mdx', + '../src/**/*.mdx', + '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)', ], - framework: '@storybook/angular', + addons: ['@storybook/addon-a11y', '@storybook/addon-docs'], + framework: { + name: '@storybook/angular', + options: {}, + }, + staticDirs: ['../public'], + /* Explicitly include global styles from Angular project */ + previewHead: (head) => ` + ${head} + + `, }; export default config; diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 0af8a75..9695efb 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -1,9 +1,79 @@ import type { Preview } from '@storybook/angular'; import { setCompodocJson } from '@storybook/addon-docs/angular'; +import { applicationConfig } from '@storybook/angular'; import docJson from '../documentation.json'; + +/* Import PrimeNG configuration */ +import { provideAnimations } from '@angular/platform-browser/animations'; +import { providePrimeNG } from 'primeng/config'; +import { AuraOpen } from '../src/app/themes/aura-open.preset'; + +/* Import i18n configuration */ +import { importProvidersFrom } from '@angular/core'; +import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; +import { of } from 'rxjs'; + +/* + * Mock TranslateLoader for Storybook + * Empty - all translations are handled in individual stories + */ +class StoryTranslateLoader implements TranslateLoader { + getTranslation() { + return of({}); + } +} + setCompodocJson(docJson); const preview: Preview = { + decorators: [ + applicationConfig({ + providers: [ + provideAnimations(), + providePrimeNG({ + theme: { + preset: AuraOpen, + options: { + darkModeSelector: '.my-app-dark', + cssLayer: false, + }, + }, + inputVariant: 'outlined', + ripple: true, + }), + importProvidersFrom( + TranslateModule.forRoot({ + defaultLanguage: 'es', + loader: { + provide: TranslateLoader, + useClass: StoryTranslateLoader, + }, + }), + ), + ], + }), + (story, context) => { + /* Handle theme switching from story args */ + const theme = context.args['theme'] || 'dark'; + if (theme === 'dark') { + document.documentElement.classList.add('my-app-dark'); + document.documentElement.classList.remove('my-app-light'); + } else { + document.documentElement.classList.remove('my-app-dark'); + document.documentElement.classList.add('my-app-light'); + } + + /* + * Handle language switching from story args + * Note: TranslateService is configured with the selected locale + * Components using computed signals with translate.instant() will + * need to use the locale from args in their render functions + */ + const locale = context.args['locale'] || 'es'; + + return story({ locale }); + }, + ], parameters: { controls: { matchers: { @@ -11,6 +81,19 @@ const preview: Preview = { date: /Date$/i, }, }, + backgrounds: { + default: 'dark', + values: [ + { + name: 'dark', + value: '#09090b', + }, + { + name: 'light', + value: '#ffffff', + }, + ], + }, }, }; diff --git a/COLLABORATION.md b/COLLABORATION.md index 57b71a6..c244de2 100644 --- a/COLLABORATION.md +++ b/COLLABORATION.md @@ -23,6 +23,7 @@ Brief guide to contribute consistently. English first, Spanish version below. ```bash npm start # Angular dev server on 4200 npm run dev # build + Electron dev mode + npm run storybook # UI component explorer ``` ## Workflow @@ -68,6 +69,7 @@ Brief guide to contribute consistently. English first, Spanish version below. ## UI and i18n - Uses PrimeNG/PrimeFlex, dark theme by default. +- Use Storybook to develop components in isolation. - Add strings in src/assets/i18n/en.json and src/assets/i18n/es.json. - Mind accessibility: labels, visible focus, contrast. @@ -113,6 +115,7 @@ Guía breve para contribuir de forma consistente. ```bash npm start # servidor Angular en 4200 npm run dev # build + Electron en modo desarrollo + npm run storybook # explorador de componentes UI ``` ### Flujo de trabajo @@ -158,6 +161,7 @@ Guía breve para contribuir de forma consistente. ### UI e i18n - Componentes con PrimeNG/PrimeFlex, tema oscuro por defecto. +- Usa Storybook para desarrollar componentes de forma aislada. - Añade cadenas en src/assets/i18n/en.json y src/assets/i18n/es.json. - Cuida accesibilidad: labels, focus visible, contrastes. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7642270..817cb1a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -254,6 +254,13 @@ When modifying the Prisma schema: - Dark theme (Aura Black) is the default - Follow existing component patterns +* +### Storybook +* +We use [Storybook](https://storybook.js.org/) for UI component development and documentation. +* +- **Run Storybook**: `npm run storybook` (accessible at http://localhost:6006) + +- **Build Storybook**: `npm run build-storybook` + +- **Creating Stories**: Add `*.stories.ts` files alongside your components. + +- **Documentation**: Storybook documentation is auto-generated using Compodoc. Ensure `documentation.json` is up-to-date if you encounter issues. + ### Adding Translations Add new strings to both language files: @@ -540,6 +547,13 @@ Al modificar el esquema de Prisma: - El tema oscuro (Aura Black) es el predeterminado - Sigue los patrones de componentes existentes +* +### Storybook +* +Usamos [Storybook](https://storybook.js.org/) para el desarrollo y documentación de componentes de UI. +* +- **Ejecutar Storybook**: `npm run storybook` (accesible en http://localhost:6006) + +- **Construir Storybook**: `npm run build-storybook` + +- **Crear Historias**: Añade archivos `*.stories.ts` junto a tus componentes. + +- **Documentación**: La documentación de Storybook se genera automáticamente usando Compodoc. Asegúrate de que `documentation.json` esté actualizado si encuentras problemas. + ### Añadir Traducciones Añade nuevas cadenas a ambos archivos de idioma: diff --git a/README.md b/README.md index 37c0acc..dd2a573 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,9 @@ cd OpenTimeTracker npm install npm run prisma:generate npm run dev + +# Run Storybook (Component Explorer) +npm run storybook ``` npm run dev is the recommended and supported development command. @@ -105,6 +108,8 @@ All contribution-related documentation lives outside this README to keep things 🧭 COLLABORATION.md — technical and architectural details +🤖 AI_GUIDELINES.md — context and rules for AI assistants + 🛡️ SECURITY.md — how to report vulnerabilities 📜 CODE_OF_CONDUCT.md — community guidelines diff --git a/electron.vite.config.ts b/electron.vite.config.ts new file mode 100644 index 0000000..3c42607 --- /dev/null +++ b/electron.vite.config.ts @@ -0,0 +1,102 @@ +import { defineConfig, externalizeDepsPlugin } from 'electron-vite'; +import { resolve } from 'path'; +import { existsSync, cpSync, createReadStream } from 'fs'; +import angular from '@analogjs/vite-plugin-angular'; + +export default defineConfig({ + main: { + plugins: [externalizeDepsPlugin()], + build: { + outDir: 'dist/main', + rollupOptions: { + input: { + main: resolve(__dirname, 'electron/src/main/main.ts'), + }, + output: { + entryFileNames: '[name].js', + }, + }, + }, + }, + preload: { + plugins: [externalizeDepsPlugin()], + build: { + outDir: 'dist/preload', + rollupOptions: { + input: { + preload: resolve(__dirname, 'electron/src/preload/preload.ts'), + }, + output: { + format: 'cjs', + entryFileNames: '[name].js', + }, + }, + }, + }, + renderer: { + root: '.', + publicDir: false, + plugins: [ + angular({ + jit: false, + workspaceRoot: process.cwd(), + }), + // Serve src/assets at /assets/ in dev, copy on build + { + name: 'serve-and-copy-assets', + configureServer(server) { + server.middlewares.use('/assets', (req, res, next) => { + const filePath = resolve( + __dirname, + 'src/assets', + req.url!.replace(/^\//, ''), + ); + if (existsSync(filePath)) { + const ext = filePath.split('.').pop(); + const mimeTypes: Record = { + json: 'application/json', + png: 'image/png', + svg: 'image/svg+xml', + ico: 'image/x-icon', + }; + res.setHeader( + 'Content-Type', + mimeTypes[ext || ''] || 'application/octet-stream', + ); + createReadStream(filePath).pipe(res); + } else { + next(); + } + }); + }, + closeBundle() { + const src = resolve(__dirname, 'src/assets'); + const dest = resolve(__dirname, 'dist/renderer/assets'); + if (existsSync(src)) { + cpSync(src, dest, { recursive: true }); + } + }, + }, + ], + base: './', + build: { + outDir: 'dist/renderer', + rollupOptions: { + input: { + index: resolve(__dirname, 'index.html'), + }, + }, + }, + server: { + fs: { + allow: [resolve(__dirname, '.'), resolve(__dirname, 'src')], + }, + }, + resolve: { + alias: { + src: resolve(__dirname, 'src'), + '/assets': resolve(__dirname, 'src/assets'), + }, + }, + }, +}); diff --git a/electron/build/tsconfig.json b/electron/build/tsconfig.json deleted file mode 100644 index 262e79c..0000000 --- a/electron/build/tsconfig.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "lib": ["ES2022"], - "types": ["node"], - "outDir": "../../dist/electron", - "rootDir": "../src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "moduleResolution": "NodeNext", - "resolveJsonModule": true, - "declaration": true, - "sourceMap": true, - "baseUrl": "../..", - "paths": { - "@prisma/generated/*": ["prisma/generated/client/*"] - } - }, - "include": ["../src/**/*"], - "exclude": ["node_modules", "dist", "../src/preload/**/*"] -} diff --git a/electron/build/tsconfig.preload.json b/electron/build/tsconfig.preload.json deleted file mode 100644 index bbb9171..0000000 --- a/electron/build/tsconfig.preload.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "CommonJS", - "lib": ["ES2022"], - "types": ["node"], - "outDir": "../../dist/electron/preload", - "rootDir": "../src/preload", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "moduleResolution": "Node", - "resolveJsonModule": true, - "declaration": false, - "sourceMap": true - }, - "include": ["../src/preload/**/*"], - "exclude": ["node_modules", "dist", "**/*.spec.ts"] -} diff --git a/electron/src/main/window.ts b/electron/src/main/window.ts index f26ae30..d0b4f0e 100644 --- a/electron/src/main/window.ts +++ b/electron/src/main/window.ts @@ -38,18 +38,25 @@ export class WindowManager { * Loads the Angular application */ private async loadApplication(): Promise { - const indexPath = getIndexPath(); - console.log('Index path:', indexPath); + const rendererUrl = process.env['ELECTRON_RENDERER_URL']; - if (fs.existsSync(indexPath)) { - this.navigationHandler = new NavigationHandler(this.mainWindow!); - this.navigationHandler.setupNavigationHandlers(); - this.navigationHandler.loadIndex(); + if (rendererUrl) { + console.log('Loading renderer from:', rendererUrl); + await this.mainWindow?.loadURL(rendererUrl); } else { - console.error('index.html not found at:', indexPath); - this.mainWindow?.loadURL( - `data:text/html,

Error: index.html not found

Path: ${indexPath}

`, - ); + const indexPath = getIndexPath(); + console.log('Index path:', indexPath); + + if (fs.existsSync(indexPath)) { + this.navigationHandler = new NavigationHandler(this.mainWindow!); + this.navigationHandler.setupNavigationHandlers(); + this.navigationHandler.loadIndex(); + } else { + console.error('index.html not found at:', indexPath); + this.mainWindow?.loadURL( + `data:text/html,

Error: index.html not found

Path: ${indexPath}

`, + ); + } } } diff --git a/electron/src/utils/paths.spec.ts b/electron/src/utils/paths.spec.ts index 6c954d1..b78dc8d 100644 --- a/electron/src/utils/paths.spec.ts +++ b/electron/src/utils/paths.spec.ts @@ -166,7 +166,7 @@ describe('Paths Utility', () => { const result = getIndexPath(); expect(result).toContain('OpenTimeTracker'); - expect(result).toContain('browser'); + expect(result).toContain('renderer'); expect(result).toContain('index.html'); }); diff --git a/electron/src/utils/paths.ts b/electron/src/utils/paths.ts index d1c1062..eed2d0f 100644 --- a/electron/src/utils/paths.ts +++ b/electron/src/utils/paths.ts @@ -53,9 +53,8 @@ export const getBackupPath = (): string => { /** * Gets the path to the Angular index.html file. - * In development: dist/OpenTimeTracker/browser/index.html - * In production: resources/app.asar/dist/OpenTimeTracker/browser/index.html - * Note: Uses process.resourcesPath for cross-platform compatibility (Windows/macOS/Linux) + * In development: dist/renderer/index.html (relative to main.js via __dirname) + * In production: resources/app.asar/dist/renderer/index.html */ export const getIndexPath = (): string => { if (isPackaged()) { @@ -63,24 +62,17 @@ export const getIndexPath = (): string => { process.resourcesPath, 'app.asar', 'dist', - 'OpenTimeTracker', - 'browser', + 'renderer', 'index.html', ); } - return path.join( - __dirname, - '..', - '..', - 'OpenTimeTracker', - 'browser', - 'index.html', - ); + return path.join(__dirname, '../renderer/index.html'); }; /** * Gets the path to the preload script. - * Note: Uses process.resourcesPath for cross-platform compatibility (Windows/macOS/Linux) + * In development: dist/preload/preload.js (relative to main.js via __dirname) + * In production: resources/app.asar/dist/preload/preload.js */ export const getPreloadPath = (): string => { if (isPackaged()) { @@ -88,12 +80,11 @@ export const getPreloadPath = (): string => { process.resourcesPath, 'app.asar', 'dist', - 'electron', 'preload', 'preload.js', ); } - return path.join(__dirname, '..', 'preload', 'preload.js'); + return path.join(__dirname, '../preload/preload.js'); }; /** diff --git a/electron/tsconfig.spec.json b/electron/tsconfig.spec.json index 322d283..769cdee 100644 --- a/electron/tsconfig.spec.json +++ b/electron/tsconfig.spec.json @@ -1,14 +1,24 @@ { - "extends": "./build/tsconfig.json", "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], "types": ["node"], + "baseUrl": "..", + "outDir": "./dist", + "strict": true, "esModuleInterop": true, - "composite": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, "declaration": true, - "outDir": "./dist", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "isolatedModules": true + "composite": true, + "sourceMap": true, + "isolatedModules": true, + "paths": { + "@prisma/generated/*": ["prisma/generated/client/*"] + } }, "include": ["src/**/*.spec.ts", "src/**/*.ts"] } diff --git a/src/index.html b/index.html similarity index 86% rename from src/index.html rename to index.html index 6331635..65245f6 100644 --- a/src/index.html +++ b/index.html @@ -9,5 +9,6 @@ + diff --git a/package-lock.json b/package-lock.json index c9e4d99..6f8721e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "open-time-tracker", - "version": "1.0.0-alpha.6", + "version": "1.0.0-alpha.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "open-time-tracker", - "version": "1.0.0-alpha.6", + "version": "1.0.0-alpha.7", "hasInstallScript": true, "license": "GPL-3.0", "dependencies": { @@ -20,7 +20,7 @@ "@angular/router": "^21.0.1", "@ngx-translate/core": "^17.0.0", "@ngx-translate/http-loader": "^17.0.0", - "@primeuix/themes": "^1.2.0", + "@primeuix/themes": "^2.0.0", "@prisma/adapter-better-sqlite3": "^7.1.0", "@prisma/client": "^7.1.0", "@types/dompurify": "^3.0.5", @@ -29,12 +29,13 @@ "marked": "^17.0.1", "primeflex": "^4.0.0", "primeicons": "^7.0.0", - "primeng": "^20.3.0", + "primeng": "^21.0.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.15.0" }, "devDependencies": { + "@analogjs/vite-plugin-angular": "^2.2.3", "@angular-devkit/architect": "^0.2100.1", "@angular-devkit/build-angular": "^21.0.1", "@angular-devkit/core": "^21.0.1", @@ -48,8 +49,10 @@ "@electron/rebuild": "^4.0.3", "@storybook/addon-a11y": "^10.2.8", "@storybook/addon-docs": "^10.2.8", + "@storybook/addon-interactions": "^8.6.14", "@storybook/addon-onboarding": "^10.2.8", "@storybook/angular": "^10.2.8", + "@storybook/test": "^8.6.15", "@types/better-sqlite3": "^7.6.13", "@types/jasmine": "~5.1.0", "@types/node": "^24.0.10", @@ -59,6 +62,7 @@ "dotenv": "^17.2.3", "electron": "^37.1.0", "electron-builder": "^26.0.12", + "electron-vite": "^5.0.0", "eslint": "^9.39.1", "husky": "^9.1.7", "jasmine-core": "~5.7.0", @@ -75,6 +79,7 @@ "storybook": "^10.2.8", "typescript": "~5.9.3", "typescript-eslint": "8.46.4", + "vite": "^7.3.1", "vitest": "^4.0.15" } }, @@ -315,6 +320,79 @@ "node": ">=6.0.0" } }, + "node_modules/@analogjs/vite-plugin-angular": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@analogjs/vite-plugin-angular/-/vite-plugin-angular-2.2.3.tgz", + "integrity": "sha512-OqVfiJsaHdHMxzvK0heVvp8MenSXh+xib6/p+v3d44kJ3J7ooD4gRx/jKC350zkgRKwcZc3a0ybGUnG6LEF7mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ts-morph": "^21.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/brandonroberts" + }, + "peerDependencies": { + "@angular-devkit/build-angular": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0", + "@angular/build": "^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0" + }, + "peerDependenciesMeta": { + "@angular-devkit/build-angular": { + "optional": true + }, + "@angular/build": { + "optional": true + } + } + }, + "node_modules/@analogjs/vite-plugin-angular/node_modules/@ts-morph/common": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.22.0.tgz", + "integrity": "sha512-HqNBuV/oIlMKdkLshXd1zKBqNQCsuPEsgQOkfFQ/eUKjRlwndXW1AjN9LVkBEIukm00gGXSRmfkl0Wv5VXLnlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.3.2", + "minimatch": "^9.0.3", + "mkdirp": "^3.0.1", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@analogjs/vite-plugin-angular/node_modules/code-block-writer": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-12.0.0.tgz", + "integrity": "sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@analogjs/vite-plugin-angular/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@analogjs/vite-plugin-angular/node_modules/ts-morph": { + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-21.0.1.tgz", + "integrity": "sha512-dbDtVdEAncKctzrVZ+Nr7kHpHkv+0JDJb2MjjpBaj8bFeCkePU9rHfMklmhuLFnpeq/EJZk2IhStY6NzqgjOkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ts-morph/common": "~0.22.0", + "code-block-writer": "^12.0.0" + } + }, "node_modules/@angular-devkit/architect": { "version": "0.2100.6", "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2100.6.tgz", @@ -937,6 +1015,81 @@ } } }, + "node_modules/@angular/build/node_modules/vite": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", + "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, "node_modules/@angular/cdk": { "version": "21.1.3", "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-21.1.3.tgz", @@ -7903,6 +8056,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@primeuix/motion": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@primeuix/motion/-/motion-0.0.10.tgz", + "integrity": "sha512-PsZwOPq79Scp7/ionshRcQ5xKVf9+zuLcyY5mf6onK8chHT5C9JGphmcIZ4CzcqxuGEpsm8AIbTGy+zS3RtzLA==", + "license": "MIT", + "dependencies": { + "@primeuix/utils": "^0.6.3" + }, + "engines": { + "node": ">=12.11.0" + } + }, "node_modules/@primeuix/styled": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.7.4.tgz", @@ -7916,21 +8081,21 @@ } }, "node_modules/@primeuix/styles": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@primeuix/styles/-/styles-1.2.5.tgz", - "integrity": "sha512-nypFRct/oaaBZqP4jinT0puW8ZIfs4u+l/vqUFmJEPU332fl5ePj6DoOpQgTLzo3OfmvSmz5a5/5b4OJJmmi7Q==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@primeuix/styles/-/styles-2.0.3.tgz", + "integrity": "sha512-2ykAB6BaHzR/6TwF8ShpJTsZrid6cVIEBVlookSdvOdmlWuevGu5vWOScgIwqWwlZcvkFYAGR/SUV3OHCTBMdw==", "license": "MIT", "dependencies": { - "@primeuix/styled": "^0.7.3" + "@primeuix/styled": "^0.7.4" } }, "node_modules/@primeuix/themes": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@primeuix/themes/-/themes-1.2.5.tgz", - "integrity": "sha512-n3YkwJrHQaEESc/D/A/iD815sxp8cKnmzscA6a8Tm8YvMtYU32eCahwLLe6h5rywghVwxASWuG36XBgISYOIjQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@primeuix/themes/-/themes-2.0.3.tgz", + "integrity": "sha512-3fS1883mtCWhgUgNf/feiaaDSOND4EBIOu9tZnzJlJ8QtYyL6eFLcA6V3ymCWqLVXQ1+lTVEZv1gl47FIdXReg==", "license": "MIT", "dependencies": { - "@primeuix/styled": "^0.7.3" + "@primeuix/styled": "^0.7.4" } }, "node_modules/@primeuix/utils": { @@ -9067,144 +9232,332 @@ "storybook": "^10.2.8" } }, - "node_modules/@storybook/addon-onboarding": { - "version": "10.2.8", - "resolved": "https://registry.npmjs.org/@storybook/addon-onboarding/-/addon-onboarding-10.2.8.tgz", - "integrity": "sha512-/+TD055ZDmM325RYrDKqle51P1iT3GiFyDrcCYNOGTUEp3lAu/qplgOC0xMZudiv2y4ExlNYD26lJoGSTNHfHg==", + "node_modules/@storybook/addon-interactions": { + "version": "8.6.14", + "resolved": "https://registry.npmjs.org/@storybook/addon-interactions/-/addon-interactions-8.6.14.tgz", + "integrity": "sha512-8VmElhm2XOjh22l/dO4UmXxNOolGhNiSpBcls2pqWSraVh4a670EyYBZsHpkXqfNHo2YgKyZN3C91+9zfH79qQ==", "dev": true, "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "@storybook/instrumenter": "8.6.14", + "@storybook/test": "8.6.14", + "polished": "^4.2.2", + "ts-dedent": "^2.2.0" + }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^10.2.8" + "storybook": "^8.6.14" } }, - "node_modules/@storybook/angular": { - "version": "10.2.8", - "resolved": "https://registry.npmjs.org/@storybook/angular/-/angular-10.2.8.tgz", - "integrity": "sha512-YN3qEa7lIsvBVMF7lGCzPySNrLz/fkosP0fBKRty2hgl61r0eSGo3Nm4m2kQg+qcck5AYMTXCFKCRqUS1ViKgQ==", + "node_modules/@storybook/addon-interactions/node_modules/@storybook/test": { + "version": "8.6.14", + "resolved": "https://registry.npmjs.org/@storybook/test/-/test-8.6.14.tgz", + "integrity": "sha512-GkPNBbbZmz+XRdrhMtkxPotCLOQ1BaGNp/gFZYdGDk2KmUWBKmvc5JxxOhtoXM2703IzNFlQHSSNnhrDZYuLlw==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/builder-webpack5": "10.2.8", "@storybook/global": "^5.0.0", - "telejson": "8.0.0", - "ts-dedent": "^2.0.0", - "tsconfig-paths-webpack-plugin": "^4.0.1", - "webpack": "5" + "@storybook/instrumenter": "8.6.14", + "@testing-library/dom": "10.4.0", + "@testing-library/jest-dom": "6.5.0", + "@testing-library/user-event": "14.5.2", + "@vitest/expect": "2.0.5", + "@vitest/spy": "2.0.5" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "@angular-devkit/architect": ">=0.1800.0 < 0.2200.0", - "@angular-devkit/build-angular": ">=18.0.0 < 22.0.0", - "@angular-devkit/core": ">=18.0.0 < 22.0.0", - "@angular/animations": ">=18.0.0 < 22.0.0", - "@angular/cli": ">=18.0.0 < 22.0.0", - "@angular/common": ">=18.0.0 < 22.0.0", - "@angular/compiler": ">=18.0.0 < 22.0.0", - "@angular/compiler-cli": ">=18.0.0 < 22.0.0", - "@angular/core": ">=18.0.0 < 22.0.0", - "@angular/platform-browser": ">=18.0.0 < 22.0.0", - "@angular/platform-browser-dynamic": ">=18.0.0 < 22.0.0", - "rxjs": "^6.5.3 || ^7.4.0", - "storybook": "^10.2.8", - "typescript": "^4.9.0 || ^5.0.0", - "zone.js": ">=0.14.0" - }, - "peerDependenciesMeta": { - "@angular/animations": { - "optional": true - }, - "@angular/cli": { - "optional": true - }, - "zone.js": { - "optional": true - } + "storybook": "^8.6.14" } }, - "node_modules/@storybook/builder-webpack5": { - "version": "10.2.8", - "resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-10.2.8.tgz", - "integrity": "sha512-77i/is0a4HIRwkcxs3wQnQCnIahLONKxSp0cURjBU38kj/M0ukOOlOPIIJOm4HgI202yLjvGNiaMcLWFxHfl8w==", + "node_modules/@storybook/addon-interactions/node_modules/@testing-library/jest-dom": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.5.0.tgz", + "integrity": "sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/core-webpack": "10.2.8", - "case-sensitive-paths-webpack-plugin": "^2.4.0", - "cjs-module-lexer": "^1.2.3", - "css-loader": "^7.1.2", - "es-module-lexer": "^1.5.0", - "fork-ts-checker-webpack-plugin": "^9.1.0", - "html-webpack-plugin": "^5.5.0", - "magic-string": "^0.30.5", - "style-loader": "^4.0.0", - "terser-webpack-plugin": "^5.3.14", - "ts-dedent": "^2.0.0", - "webpack": "5", - "webpack-dev-middleware": "^6.1.2", - "webpack-hot-middleware": "^2.25.1", - "webpack-virtual-modules": "^0.6.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "storybook": "^10.2.8" + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "redent": "^3.0.0" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" } }, - "node_modules/@storybook/builder-webpack5/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "node_modules/@storybook/addon-interactions/node_modules/@testing-library/user-event": { + "version": "14.5.2", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz", + "integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" } }, - "node_modules/@storybook/builder-webpack5/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "node_modules/@storybook/addon-interactions/node_modules/@vitest/expect": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz", + "integrity": "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==", "dev": true, "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "@vitest/spy": "2.0.5", + "@vitest/utils": "2.0.5", + "chai": "^5.1.1", + "tinyrainbow": "^1.2.0" }, - "engines": { - "node": ">= 0.6" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@storybook/builder-webpack5/node_modules/webpack-dev-middleware": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.3.tgz", - "integrity": "sha512-A4ChP0Qj8oGociTs6UdlRUGANIGrCDL3y+pmQMc+dSsraXHCatFpmMey4mYELA+juqwUqwQsUgJJISXl1KWmiw==", + "node_modules/@storybook/addon-interactions/node_modules/@vitest/pretty-format": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz", + "integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==", "dev": true, "license": "MIT", "dependencies": { - "colorette": "^2.0.10", - "memfs": "^3.4.12", - "mime-types": "^2.1.31", - "range-parser": "^1.2.1", - "schema-utils": "^4.0.0" + "tinyrainbow": "^1.2.0" }, - "engines": { - "node": ">= 14.15.0" + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@storybook/addon-interactions/node_modules/@vitest/spy": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz", + "integrity": "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.0" }, "funding": { - "type": "opencollective", + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@storybook/addon-interactions/node_modules/@vitest/utils": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.5.tgz", + "integrity": "sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.0.5", + "estree-walker": "^3.0.3", + "loupe": "^3.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@storybook/addon-interactions/node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@storybook/addon-interactions/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@storybook/addon-interactions/node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@storybook/addon-interactions/node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@storybook/addon-onboarding": { + "version": "10.2.8", + "resolved": "https://registry.npmjs.org/@storybook/addon-onboarding/-/addon-onboarding-10.2.8.tgz", + "integrity": "sha512-/+TD055ZDmM325RYrDKqle51P1iT3GiFyDrcCYNOGTUEp3lAu/qplgOC0xMZudiv2y4ExlNYD26lJoGSTNHfHg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^10.2.8" + } + }, + "node_modules/@storybook/angular": { + "version": "10.2.8", + "resolved": "https://registry.npmjs.org/@storybook/angular/-/angular-10.2.8.tgz", + "integrity": "sha512-YN3qEa7lIsvBVMF7lGCzPySNrLz/fkosP0fBKRty2hgl61r0eSGo3Nm4m2kQg+qcck5AYMTXCFKCRqUS1ViKgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/builder-webpack5": "10.2.8", + "@storybook/global": "^5.0.0", + "telejson": "8.0.0", + "ts-dedent": "^2.0.0", + "tsconfig-paths-webpack-plugin": "^4.0.1", + "webpack": "5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "@angular-devkit/architect": ">=0.1800.0 < 0.2200.0", + "@angular-devkit/build-angular": ">=18.0.0 < 22.0.0", + "@angular-devkit/core": ">=18.0.0 < 22.0.0", + "@angular/animations": ">=18.0.0 < 22.0.0", + "@angular/cli": ">=18.0.0 < 22.0.0", + "@angular/common": ">=18.0.0 < 22.0.0", + "@angular/compiler": ">=18.0.0 < 22.0.0", + "@angular/compiler-cli": ">=18.0.0 < 22.0.0", + "@angular/core": ">=18.0.0 < 22.0.0", + "@angular/platform-browser": ">=18.0.0 < 22.0.0", + "@angular/platform-browser-dynamic": ">=18.0.0 < 22.0.0", + "rxjs": "^6.5.3 || ^7.4.0", + "storybook": "^10.2.8", + "typescript": "^4.9.0 || ^5.0.0", + "zone.js": ">=0.14.0" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + }, + "@angular/cli": { + "optional": true + }, + "zone.js": { + "optional": true + } + } + }, + "node_modules/@storybook/builder-webpack5": { + "version": "10.2.8", + "resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-10.2.8.tgz", + "integrity": "sha512-77i/is0a4HIRwkcxs3wQnQCnIahLONKxSp0cURjBU38kj/M0ukOOlOPIIJOm4HgI202yLjvGNiaMcLWFxHfl8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/core-webpack": "10.2.8", + "case-sensitive-paths-webpack-plugin": "^2.4.0", + "cjs-module-lexer": "^1.2.3", + "css-loader": "^7.1.2", + "es-module-lexer": "^1.5.0", + "fork-ts-checker-webpack-plugin": "^9.1.0", + "html-webpack-plugin": "^5.5.0", + "magic-string": "^0.30.5", + "style-loader": "^4.0.0", + "terser-webpack-plugin": "^5.3.14", + "ts-dedent": "^2.0.0", + "webpack": "5", + "webpack-dev-middleware": "^6.1.2", + "webpack-hot-middleware": "^2.25.1", + "webpack-virtual-modules": "^0.6.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^10.2.8" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@storybook/builder-webpack5/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@storybook/builder-webpack5/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@storybook/builder-webpack5/node_modules/webpack-dev-middleware": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.3.tgz", + "integrity": "sha512-A4ChP0Qj8oGociTs6UdlRUGANIGrCDL3y+pmQMc+dSsraXHCatFpmMey4mYELA+juqwUqwQsUgJJISXl1KWmiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.12", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { @@ -9286,77 +9639,383 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/@storybook/react-dom-shim": { - "version": "10.2.8", - "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.2.8.tgz", - "integrity": "sha512-Xde9X3VszFV1pTXfc2ZFM89XOCGRxJD8MUIzDwkcT9xaki5a+8srs/fsXj75fMY6gMYfcL5lNRZvCqg37HOmcQ==", + "node_modules/@storybook/instrumenter": { + "version": "8.6.14", + "resolved": "https://registry.npmjs.org/@storybook/instrumenter/-/instrumenter-8.6.14.tgz", + "integrity": "sha512-iG4MlWCcz1L7Yu8AwgsnfVAmMbvyRSk700Mfy2g4c8y5O+Cv1ejshE1LBBsCwHgkuqU0H4R0qu4g23+6UnUemQ==", "dev": true, "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "@vitest/utils": "^2.1.1" + }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.2.8" + "storybook": "^8.6.14" } }, - "node_modules/@szmarczak/http-timer": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "node_modules/@storybook/instrumenter/node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", "dev": true, "license": "MIT", "dependencies": { - "defer-to-connect": "^2.0.0" + "tinyrainbow": "^1.2.0" }, - "engines": { - "node": ">=10" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@testing-library/jest-dom": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", - "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "node_modules/@storybook/instrumenter/node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", "dev": true, "license": "MIT", "dependencies": { - "@adobe/css-tools": "^4.4.0", - "aria-query": "^5.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.6.3", - "picocolors": "^1.1.1", - "redent": "^3.0.0" + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" }, - "engines": { - "node": ">=14", - "npm": ">=6", - "yarn": ">=1" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@testing-library/user-event": { - "version": "14.6.1", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", - "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "node_modules/@storybook/instrumenter/node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=12", - "npm": ">=6" + "node": ">=14.0.0" + } + }, + "node_modules/@storybook/react-dom-shim": { + "version": "10.2.8", + "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.2.8.tgz", + "integrity": "sha512-Xde9X3VszFV1pTXfc2ZFM89XOCGRxJD8MUIzDwkcT9xaki5a+8srs/fsXj75fMY6gMYfcL5lNRZvCqg37HOmcQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "@testing-library/dom": ">=7.21.4" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "storybook": "^10.2.8" } }, - "node_modules/@thednp/event-listener": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/@thednp/event-listener/-/event-listener-2.0.12.tgz", - "integrity": "sha512-PbW05+EwNfGVy2uwz0vL2xbEmcLhpuBZ2nm0pdLT088gjmY9dySfJOZUtWCmzSPJcVFn3BkgH1m1MiS11AubJA==", + "node_modules/@storybook/test": { + "version": "8.6.15", + "resolved": "https://registry.npmjs.org/@storybook/test/-/test-8.6.15.tgz", + "integrity": "sha512-EwquDRUDVvWcZds3T2abmB5wSN/Vattal4YtZ6fpBlIUqONV4o/cOBX39cFfQSUCBrIXIjQ6RmapQCHK/PvBYw==", "dev": true, "license": "MIT", - "engines": { - "node": ">=16", + "dependencies": { + "@storybook/global": "^5.0.0", + "@storybook/instrumenter": "8.6.15", + "@testing-library/dom": "10.4.0", + "@testing-library/jest-dom": "6.5.0", + "@testing-library/user-event": "14.5.2", + "@vitest/expect": "2.0.5", + "@vitest/spy": "2.0.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.6.15" + } + }, + "node_modules/@storybook/test/node_modules/@storybook/instrumenter": { + "version": "8.6.15", + "resolved": "https://registry.npmjs.org/@storybook/instrumenter/-/instrumenter-8.6.15.tgz", + "integrity": "sha512-TvHR/+yyIAOp/1bLulFai2kkhIBtAlBw7J6Jd9DKyInoGhTWNE1G1Y61jD5GWXX29AlwaHfzGUaX5NL1K+FJpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "@vitest/utils": "^2.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.6.15" + } + }, + "node_modules/@storybook/test/node_modules/@testing-library/jest-dom": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.5.0.tgz", + "integrity": "sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@storybook/test/node_modules/@testing-library/user-event": { + "version": "14.5.2", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz", + "integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@storybook/test/node_modules/@vitest/expect": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz", + "integrity": "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.0.5", + "@vitest/utils": "2.0.5", + "chai": "^5.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@storybook/test/node_modules/@vitest/expect/node_modules/@vitest/pretty-format": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz", + "integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@storybook/test/node_modules/@vitest/expect/node_modules/@vitest/utils": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.5.tgz", + "integrity": "sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.0.5", + "estree-walker": "^3.0.3", + "loupe": "^3.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@storybook/test/node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@storybook/test/node_modules/@vitest/spy": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz", + "integrity": "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@storybook/test/node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@storybook/test/node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@storybook/test/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@storybook/test/node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@storybook/test/node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/dom/node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@thednp/event-listener": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@thednp/event-listener/-/event-listener-2.0.12.tgz", + "integrity": "sha512-PbW05+EwNfGVy2uwz0vL2xbEmcLhpuBZ2nm0pdLT088gjmY9dySfJOZUtWCmzSPJcVFn3BkgH1m1MiS11AubJA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16", "pnpm": ">=8.6.0" } }, @@ -9516,6 +10175,13 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/better-sqlite3": { "version": "7.6.13", "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", @@ -12298,6 +12964,16 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/cacache": { "version": "19.0.1", "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", @@ -13818,6 +14494,16 @@ "node": ">= 0.8" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/destr": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", @@ -14329,169 +15015,683 @@ "wrap-ansi": "^7.0.0" }, "engines": { - "node": ">=12" + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-builder/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-builder/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/electron-builder/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-publish": { + "version": "26.6.0", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.6.0.tgz", + "integrity": "sha512-LsyHMMqbvJ2vsOvuWJ19OezgF2ANdCiHpIucDHNiLhuI+/F3eW98ouzWSRmXXi82ZOPZXC07jnIravY4YYwCLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "26.4.1", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "form-data": "^4.0.5", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-publish/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-publish/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-publish/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "dev": true, + "license": "ISC" + }, + "node_modules/electron-vite": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/electron-vite/-/electron-vite-5.0.0.tgz", + "integrity": "sha512-OHp/vjdlubNlhNkPkL/+3JD34ii5ov7M0GpuXEVdQeqdQ3ulvVR7Dg/rNBLfS5XPIFwgoBLDf9sjjrL+CuDyRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.4", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "cac": "^6.7.14", + "esbuild": "^0.25.11", + "magic-string": "^0.30.19", + "picocolors": "^1.1.1" + }, + "bin": { + "electron-vite": "bin/electron-vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@swc/core": "^1.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + } + } + }, + "node_modules/electron-vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/electron-builder/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "node_modules/electron-vite/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/electron-builder/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "node_modules/electron-vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/electron-builder/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/electron-vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/electron-builder/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "node_modules/electron-vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">= 10.0.0" + "node": ">=18" } }, - "node_modules/electron-builder/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "node_modules/electron-vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=18" } }, - "node_modules/electron-builder/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "node_modules/electron-vite/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, + "optional": true, + "os": [ + "openharmony" + ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/electron-builder/node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "node_modules/electron-vite/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], "dev": true, - "license": "ISC", + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/electron-publish": { - "version": "26.6.0", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.6.0.tgz", - "integrity": "sha512-LsyHMMqbvJ2vsOvuWJ19OezgF2ANdCiHpIucDHNiLhuI+/F3eW98ouzWSRmXXi82ZOPZXC07jnIravY4YYwCLQ==", + "node_modules/electron-vite/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/fs-extra": "^9.0.11", - "builder-util": "26.4.1", - "builder-util-runtime": "9.5.1", - "chalk": "^4.1.2", - "form-data": "^4.0.5", - "fs-extra": "^10.1.0", - "lazy-val": "^1.0.5", - "mime": "^2.5.2" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, - "node_modules/electron-publish/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "node_modules/electron-vite/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/electron-publish/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "node_modules/electron-vite/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, - "node_modules/electron-publish/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "node_modules/electron-vite/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, + "hasInstallScript": true, "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, "engines": { - "node": ">= 10.0.0" + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, - "node_modules/electron-to-chromium": { - "version": "1.5.286", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", - "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", - "dev": true, - "license": "ISC" - }, "node_modules/electron/node_modules/@types/node": { "version": "22.19.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.10.tgz", @@ -19173,6 +20373,16 @@ "dev": true, "license": "MIT" }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/macos-release": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.1.tgz", @@ -21505,6 +22715,19 @@ "node": ">=10.4.0" } }, + "node_modules/polished": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/polished/-/polished-4.3.1.tgz", + "integrity": "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.17.8" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/polka": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/polka/-/polka-0.5.2.tgz", @@ -21770,6 +22993,44 @@ "renderkid": "^3.0.0" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/primeflex": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/primeflex/-/primeflex-4.0.0.tgz", @@ -21783,24 +23044,24 @@ "license": "MIT" }, "node_modules/primeng": { - "version": "20.4.0", - "resolved": "https://registry.npmjs.org/primeng/-/primeng-20.4.0.tgz", - "integrity": "sha512-vXUD1G4/uet4rDkPW8xx7yZWj7RmsmexEJ3+GhpQgsNaLtPFsTCVfQq8v4FQ4tIs7shoD0hz76d3jtjGWZ49QQ==", + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/primeng/-/primeng-21.1.1.tgz", + "integrity": "sha512-ArX5X+psJjL37/5mRzIbeedYOtxDwjLrbBoTTw9hV6+kQt4v8xKy9R1dKuiIFG9OeQTQwzIaJ74OREBAJJmvoA==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { + "@primeuix/motion": "^0.0.10", "@primeuix/styled": "^0.7.4", - "@primeuix/styles": "^1.2.5", - "@primeuix/utils": "^0.6.2", + "@primeuix/styles": "^2.0.3", + "@primeuix/utils": "^0.6.3", "tslib": "^2.3.0" }, "peerDependencies": { - "@angular/animations": "^20.0.4", - "@angular/cdk": "^20.0.3", - "@angular/common": "^20.0.4", - "@angular/core": "^20.0.4", - "@angular/forms": "^20.0.4", - "@angular/platform-browser": "^20.0.4", - "@angular/router": "^20.0.4", + "@angular/cdk": "^21.0.0", + "@angular/common": "^21.0.0", + "@angular/core": "^21.0.7", + "@angular/forms": "^21.0.0", + "@angular/platform-browser": "^21.0.0", + "@angular/router": "^21.0.0", "rxjs": "^6.0.0 || ^7.8.1" } }, @@ -22151,6 +23412,13 @@ "react": "^19.2.4" } }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, "node_modules/read-binary-file-arch": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", @@ -25834,9 +27102,9 @@ } }, "node_modules/vite": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", - "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index f0842ce..02fc4c5 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { "name": "open-time-tracker", - "version": "1.0.0-alpha.6", + "version": "1.0.0-alpha.7", "author": "altaskur", "license": "GPL-3.0", "type": "module", "description": "Time tracking application for managing projects and tasks", - "main": "dist/electron/main/main.js", + "main": "dist/main/main.js", "overrides": { "node-forge": "^1.3.2", "hono": "4.11.8", @@ -13,31 +13,30 @@ "axios": "1.13.5" }, "scripts": { - "start": "ng serve", - "build": "npm run prisma:generate && ng build && tsc --project electron/build/tsconfig.json && tsc --project electron/build/tsconfig.preload.json", - "dev": "npm run prisma:generate && ng build --configuration=development && tsc --project electron/build/tsconfig.json && tsc --project electron/build/tsconfig.preload.json && electron .", + "start": "electron-vite preview", + "dev": "npm run prisma:generate && electron-vite dev", + "build": "npm run prisma:generate && electron-vite build", "dist": "npm run build && electron-builder", "dist:win": "npm run build && electron-builder --win", "dist:linux": "npm run build && electron-builder --linux", "dist:mac": "npm run build && electron-builder --mac", - "pack": "electron-builder --dir", + "postinstall": "npx @electron/rebuild", "prisma:generate": "cross-env DATABASE_URL=file:./dist/data/timetracker.db prisma generate", "prisma:push": "cross-env DATABASE_URL=file:./dist/data/timetracker.db prisma db push", "prisma:studio": "cross-env DATABASE_URL=file:./dist/data/timetracker.db prisma studio", "prisma:migrate": "cross-env DATABASE_URL=file:./dist/data/timetracker.db prisma migrate dev", "prisma:template": "node scripts/update-db-template.mjs", - "postinstall": "npx @electron/rebuild", "test": "ng test", "test:coverage": "ng test --code-coverage --watch=false --browsers=ChromeHeadless", "test:electron": "vitest run", "test:electron:watch": "vitest", "test:electron:coverage": "vitest run --coverage", - "electron": "electron .", + "lint": "ng lint", + "lint:fix": "ng lint --fix", + "format": "prettier --write \"src/**/*.{ts,html,scss}\"", "sonar": "node scripts/run-sonar.mjs", "sonar:check": "npm run test:coverage && npm run test:electron:coverage && npm run sonar && node scripts/check-sonar-quality-gate.mjs", "prepare": "husky", - "lint": "ng lint", - "lint:fix": "ng lint --fix", "storybook": "ng run OpenTimeTracker:storybook", "build-storybook": "ng run OpenTimeTracker:build-storybook" }, @@ -64,7 +63,7 @@ "node_modules/@prisma/client/**/*", "node_modules/@prisma/adapter-better-sqlite3/**/*", "node_modules/better-sqlite3/**/*", - "dist/electron/generated/**/*", + "dist/main/generated/**/*", "prisma/template.db" ], "asar": true, @@ -166,7 +165,7 @@ "@angular/router": "^21.0.1", "@ngx-translate/core": "^17.0.0", "@ngx-translate/http-loader": "^17.0.0", - "@primeuix/themes": "^1.2.0", + "@primeuix/themes": "^2.0.0", "@prisma/adapter-better-sqlite3": "^7.1.0", "@prisma/client": "^7.1.0", "@types/dompurify": "^3.0.5", @@ -175,18 +174,30 @@ "marked": "^17.0.1", "primeflex": "^4.0.0", "primeicons": "^7.0.0", - "primeng": "^20.3.0", + "primeng": "^21.0.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.15.0" }, "devDependencies": { + "@analogjs/vite-plugin-angular": "^2.2.3", + "@angular-devkit/architect": "^0.2100.1", + "@angular-devkit/build-angular": "^21.0.1", + "@angular-devkit/core": "^21.0.1", "@angular/build": "^21.0.1", "@angular/cli": "^21.0.1", "@angular/compiler-cli": "^21.0.1", + "@angular/platform-browser-dynamic": "^21.0.1", "@commitlint/cli": "^20.1.0", "@commitlint/config-conventional": "^20.0.0", + "@compodoc/compodoc": "^1.2.1", "@electron/rebuild": "^4.0.3", + "@storybook/addon-a11y": "^10.2.8", + "@storybook/addon-docs": "^10.2.8", + "@storybook/addon-interactions": "^8.6.14", + "@storybook/addon-onboarding": "^10.2.8", + "@storybook/angular": "^10.2.8", + "@storybook/test": "^8.6.15", "@types/better-sqlite3": "^7.6.13", "@types/jasmine": "~5.1.0", "@types/node": "^24.0.10", @@ -196,6 +207,7 @@ "dotenv": "^17.2.3", "electron": "^37.1.0", "electron-builder": "^26.0.12", + "electron-vite": "^5.0.0", "eslint": "^9.39.1", "husky": "^9.1.7", "jasmine-core": "~5.7.0", @@ -209,19 +221,11 @@ "prettier": "^3.6.2", "prisma": "^7.1.0", "sonarqube-scanner": "^4.3.0", + "storybook": "^10.2.8", "typescript": "~5.9.3", "typescript-eslint": "8.46.4", - "vitest": "^4.0.15", - "storybook": "^10.2.8", - "@storybook/angular": "^10.2.8", - "@storybook/addon-a11y": "^10.2.8", - "@storybook/addon-docs": "^10.2.8", - "@storybook/addon-onboarding": "^10.2.8", - "@angular-devkit/build-angular": "^21.0.1", - "@angular-devkit/architect": "^0.2100.1", - "@angular-devkit/core": "^21.0.1", - "@angular/platform-browser-dynamic": "^21.0.1", - "@compodoc/compodoc": "^1.2.1" + "vite": "^7.3.1", + "vitest": "^4.0.15" }, "lint-staged": { "*.{ts,js}": [ diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 806994f..109251a 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -17,7 +17,7 @@ import { routes } from './app.routes'; import { providePrimeNG } from 'primeng/config'; import { MessageService } from 'primeng/api'; -import { AuraBlack } from './themes/aura-black.preset'; +import { AuraOpen } from './themes/aura-open.preset'; import { GlobalErrorHandler, ThemeService, @@ -40,7 +40,7 @@ export const appConfig: ApplicationConfig = { }), providePrimeNG({ theme: { - preset: AuraBlack, + preset: AuraOpen, options: { darkModeSelector: '.my-app-dark', }, diff --git a/src/app/components/open-calendar/open-calendar.scss b/src/app/components/open-calendar/open-calendar.scss index 066289d..e6c4707 100644 --- a/src/app/components/open-calendar/open-calendar.scss +++ b/src/app/components/open-calendar/open-calendar.scss @@ -25,8 +25,8 @@ .calendar__title { margin: 0; - font-size: 1.25rem; - font-weight: 600; + font-size: var(--font-size-xl); + font-weight: var(--font-weight-semibold); text-transform: capitalize; min-width: 200px; text-align: center; @@ -43,20 +43,21 @@ flex-direction: column; align-items: center; padding: 0.25rem 0.75rem; - border-radius: var(--p-border-radius); + border-radius: var(--radius-md); background: var(--c-surface-muted); } .calendar__stat-label { - font-size: 0.625rem; + font-size: var(--font-size-xs); text-transform: uppercase; color: var(--p-text-muted-color); letter-spacing: 0.5px; + font-weight: var(--font-weight-medium); } .calendar__stat-value { - font-size: 0.875rem; - font-weight: 600; + font-size: var(--font-size-sm); + font-weight: var(--font-weight-semibold); } .calendar__stat.balance-positive { @@ -127,11 +128,15 @@ .calendar__cell { cursor: pointer; - transition: background-color 0.2s; + transition: + background-color var(--transition-base), + box-shadow var(--transition-base); height: 100%; &:hover { background: var(--p-surface-hover); + box-shadow: inset 0 0 0 1px + color-mix(in srgb, var(--p-primary-color) 20%, transparent); } } @@ -166,12 +171,13 @@ display: flex; align-items: center; justify-content: center; + box-shadow: var(--elevation-xs); } } .calendar__day-number { - font-size: 0.875rem; - font-weight: 500; + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); margin-bottom: 0.25rem; } @@ -187,14 +193,14 @@ align-items: center; gap: 0; opacity: 0; - transition: opacity 0.2s; + transition: opacity var(--transition-base); ::ng-deep .p-button { width: 1.5rem; height: 1.5rem; .p-button-icon { - font-size: 0.75rem; + font-size: var(--font-size-xs); } } } @@ -204,10 +210,10 @@ } .calendar__day-type { - font-size: 0.5rem; + font-size: var(--font-size-xs); padding: 0.125rem 0.375rem; - border-radius: 0.75rem; - font-weight: 500; + border-radius: var(--radius-full); + font-weight: var(--font-weight-medium); text-transform: uppercase; letter-spacing: 0.25px; opacity: 0.7; @@ -274,9 +280,11 @@ } .calendar__day--has-override { - background: color-mix(in srgb, - var(--day-type-color, var(--p-primary-color)) 8%, - transparent); + background: color-mix( + in srgb, + var(--day-type-color, var(--p-primary-color)) 8%, + transparent + ); .calendar__day-number { color: var(--p-text-muted-color); @@ -289,6 +297,7 @@ &:hover { background: var(--p-surface-hover); + box-shadow: var(--elevation-xs); } } @@ -306,17 +315,22 @@ justify-content: space-between; gap: 0.25rem; padding: 0.25rem 0.5rem; - border-radius: var(--p-border-radius-sm); - font-size: 0.75rem; + border-radius: var(--radius-sm); + font-size: var(--font-size-xs); cursor: pointer; - transition: transform 0.1s; + transition: + transform var(--transition-fast), + box-shadow var(--transition-fast); border-left: 3px solid var(--status-color, var(--p-surface-400)); - background: color-mix(in srgb, - var(--status-color, var(--p-surface-400)) 15%, - transparent); + background: color-mix( + in srgb, + var(--status-color, var(--p-surface-400)) 15%, + transparent + ); &:hover { transform: translateX(2px); + box-shadow: var(--elevation-sm); } } @@ -469,18 +483,22 @@ padding: 0.75rem 1rem; border-radius: var(--p-border-radius); border-left: 3px solid var(--status-color, var(--p-surface-300)); - background: color-mix(in srgb, - var(--status-color, var(--p-surface-400)) 15%, - transparent); + background: color-mix( + in srgb, + var(--status-color, var(--p-surface-400)) 15%, + transparent + ); cursor: pointer; transition: background-color 0.2s, transform 0.1s; &:hover { - background: color-mix(in srgb, - var(--status-color, var(--p-surface-400)) 25%, - transparent); + background: color-mix( + in srgb, + var(--status-color, var(--p-surface-400)) 25%, + transparent + ); transform: translateX(2px); } @@ -540,4 +558,4 @@ padding: 2rem; font-style: italic; } -} \ No newline at end of file +} diff --git a/src/app/components/open-card/open-card-progressbar-aria-fix.directive.ts b/src/app/components/open-card/open-card-progressbar-aria-fix.directive.ts new file mode 100644 index 0000000..af507c0 --- /dev/null +++ b/src/app/components/open-card/open-card-progressbar-aria-fix.directive.ts @@ -0,0 +1,43 @@ +import { + AfterViewInit, + Directive, + ElementRef, + inject, + OnDestroy, +} from '@angular/core'; + +@Directive({ + selector: '[appProgressbarAriaFix]', + standalone: true, +}) +export class OpenCardProgressbarAriaFixDirective + implements AfterViewInit, OnDestroy +{ + private readonly elementRef = inject>(ElementRef); + private observer?: MutationObserver; + + ngAfterViewInit(): void { + this.removeInvalidAriaLevel(); + + this.observer = new MutationObserver(() => { + this.removeInvalidAriaLevel(); + }); + + this.observer.observe(this.elementRef.nativeElement, { + attributes: true, + attributeFilter: ['aria-level'], + }); + } + + ngOnDestroy(): void { + this.observer?.disconnect(); + } + + private removeInvalidAriaLevel(): void { + const element = this.elementRef.nativeElement; + + if (element.hasAttribute('aria-level')) { + element.removeAttribute('aria-level'); + } + } +} diff --git a/src/app/components/open-card/open-card.html b/src/app/components/open-card/open-card.html new file mode 100644 index 0000000..b2e9bc5 --- /dev/null +++ b/src/app/components/open-card/open-card.html @@ -0,0 +1,92 @@ +
+ @switch (variant()) { + + @case ("project") { + +
+ @if (icon()) { + + } +

{{ title() }}

+
+
+ } + + + @case ("stats-time") { + +
+ @if (icon()) { + + } + @if (iconLabel()) { + {{ iconLabel() }} + } +
+
+
+ {{ worked() }} + / + {{ target() }} +
+ + @if (remaining()) { +
+ {{ remaining() }} +
+ } +
+
+ } + + + @case ("stats-count") { + +
+ @if (icon()) { + + } + @if (iconLabel()) { + {{ iconLabel() }} + } +
+
+
+ {{ bigNumber() }} +
+
+
+ } + + + @case ("task") { + + @if (title()) { +

{{ title() }}

+ } + @if (subtitle()) { +

{{ subtitle() }}

+ } + +
+ } + } +
diff --git a/src/app/components/open-card/open-card.scss b/src/app/components/open-card/open-card.scss new file mode 100644 index 0000000..f3d8a7e --- /dev/null +++ b/src/app/components/open-card/open-card.scss @@ -0,0 +1,205 @@ +/* Base card styles - common to all variants */ +.open-card { + height: 134px; + width: 100%; + max-width: 356px; + + ::ng-deep .p-card { + height: 100%; + box-shadow: var(--elevation-sm); + border: 1px solid var(--p-surface-border); + border-radius: var(--radius-lg); + transition: + box-shadow var(--transition-base), + transform var(--transition-base); + overflow: hidden; + + &:hover { + box-shadow: var(--elevation-md); + transform: translateY(-2px); + } + } + + ::ng-deep .p-card-body { + height: 100%; + padding: 0.75rem 1rem; + } + + ::ng-deep .p-card-content { + padding: 0; + } + + /* Common header styles */ + &__header { + display: flex; + align-items: center; + gap: 0.75rem; + + i { + font-size: 1.5rem; + color: var(--p-primary-color); + flex-shrink: 0; + } + } + + &__name { + margin: 0; + font-size: var(--font-size-lg); + font-weight: var(--font-weight-semibold); + color: var(--p-text-color); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +} + +/* Stats time variant specific styles */ +.open-card--stats-time, +.open-card--stats-count { + .stats-card { + height: 100%; + + &__header { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.75rem; + font-size: var(--font-size-sm); + font-weight: var(--font-weight-semibold); + color: var(--p-text-muted-color); + + i { + font-size: var(--font-size-xl); + } + } + + &__content { + display: flex; + flex-direction: column; + gap: 0.5rem; + } + + &__time { + display: flex; + align-items: baseline; + gap: 0.25rem; + } + + &__worked { + font-size: var(--font-size-2xl); + font-weight: var(--font-weight-bold); + color: var(--p-text-color); + } + + &__separator { + color: var(--p-text-muted-color); + } + + &__target { + font-size: var(--font-size-base); + color: var(--p-text-muted-color); + } + + &__remaining { + font-size: var(--font-size-sm); + color: var(--p-text-muted-color); + } + + &__big-number { + font-size: var(--font-size-4xl); + font-weight: var(--font-weight-bold); + color: var(--p-text-color); + text-align: center; + padding: 0.5rem 0; + } + + &--today { + ::ng-deep .p-progressbar .p-progressbar-value { + background: var(--p-primary-color); + } + } + + &--week { + ::ng-deep .p-progressbar .p-progressbar-value { + background: var(--p-blue-500); + } + } + + &--tasks { + .stats-card__header i { + color: var(--p-green-500); + } + } + } +} + +/* Task variant specific styles */ +.open-card--task { + ::ng-deep .p-card { + height: 100%; + display: flex; + flex-direction: column; + } + + ::ng-deep .p-card-body { + flex: 1; + display: flex; + flex-direction: column; + } + + ::ng-deep .p-card-content { + flex: 1; + display: flex; + flex-direction: column; + } + + .task-card { + &__project { + font-size: var(--font-size-xs); + color: var(--p-primary-color); + font-weight: var(--font-weight-medium); + text-transform: uppercase; + letter-spacing: 0.5px; + } + + &__name { + margin: 0.25rem 0 0.5rem; + font-size: var(--font-size-base); + font-weight: var(--font-weight-semibold); + color: var(--p-text-color); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &__description { + margin: 0 0 0.75rem; + font-size: var(--font-size-sm); + color: var(--p-text-muted-color); + display: -webkit-box; + -webkit-line-clamp: 2; + line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + } + + &__footer { + margin-top: auto; + } + + &__meta { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem; + } + + &__hours { + font-size: var(--font-size-xs); + color: var(--p-text-muted-color); + background: var(--p-surface-100); + padding: 0.25rem 0.5rem; + border-radius: var(--radius-md); + } + } +} \ No newline at end of file diff --git a/src/app/components/open-card/open-card.spec.ts b/src/app/components/open-card/open-card.spec.ts new file mode 100644 index 0000000..e3ff80c --- /dev/null +++ b/src/app/components/open-card/open-card.spec.ts @@ -0,0 +1,184 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { OpenCard } from './open-card'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; + +describe('OpenCard', () => { + let component: OpenCard; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [OpenCard], + providers: [provideNoopAnimations()], + }).compileComponents(); + + fixture = TestBed.createComponent(OpenCard); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('Project Variant', () => { + beforeEach(() => { + fixture.componentRef.setInput('variant', 'project'); + fixture.componentRef.setInput('icon', 'pi-folder'); + fixture.componentRef.setInput('title', 'Test Project'); + fixture.detectChanges(); + }); + + it('should render project card', () => { + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('.open-card--project')).toBeTruthy(); + }); + + it('should display project title', () => { + const compiled = fixture.nativeElement as HTMLElement; + const nameElement = compiled.querySelector('.open-card__name'); + expect(nameElement?.textContent?.trim()).toBe('Test Project'); + }); + + it('should display project icon', () => { + const compiled = fixture.nativeElement as HTMLElement; + const iconElement = compiled.querySelector('.pi-folder'); + expect(iconElement).toBeTruthy(); + }); + }); + + describe('Stats Time Variant', () => { + beforeEach(() => { + fixture.componentRef.setInput('variant', 'stats-time'); + fixture.componentRef.setInput('statsModifier', 'today'); + fixture.componentRef.setInput('icon', 'pi-sun'); + fixture.componentRef.setInput('iconLabel', 'Today'); + fixture.componentRef.setInput('worked', '4:30'); + fixture.componentRef.setInput('target', '8:00'); + fixture.componentRef.setInput('remaining', '3:30'); + fixture.componentRef.setInput('progress', 56); + fixture.detectChanges(); + }); + + it('should render stats-time card', () => { + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('.open-card--stats-time')).toBeTruthy(); + expect(compiled.querySelector('.stats-card--today')).toBeTruthy(); + }); + + it('should display worked and target time', () => { + const compiled = fixture.nativeElement as HTMLElement; + const workedElement = compiled.querySelector('.stats-card__worked'); + const targetElement = compiled.querySelector('.stats-card__target'); + expect(workedElement?.textContent?.trim()).toBe('4:30'); + expect(targetElement?.textContent?.trim()).toBe('8:00'); + }); + + it('should display progress bar', () => { + const compiled = fixture.nativeElement as HTMLElement; + const progressBar = compiled.querySelector('p-progressbar'); + expect(progressBar).toBeTruthy(); + }); + + it('should display icon label', () => { + const compiled = fixture.nativeElement as HTMLElement; + const labelElement = compiled.querySelector('.stats-card__header span'); + expect(labelElement?.textContent?.trim()).toBe('Today'); + }); + + it('should expose accessible progress label with worked and target', () => { + expect(component.progressLabel()).toBe('Progress: 4:30 of 8:00 (56%)'); + }); + + it('should render ARIA attributes for progressbar', () => { + const compiled = fixture.nativeElement as HTMLElement; + const progressBar = compiled.querySelector('p-progressbar'); + expect(progressBar?.getAttribute('aria-label')).toBe( + 'Progress: 4:30 of 8:00 (56%)', + ); + expect(progressBar?.getAttribute('aria-valuetext')).toBe('56%'); + }); + + it('should fallback progress label when worked and target are empty', () => { + fixture.componentRef.setInput('worked', ''); + fixture.componentRef.setInput('target', ''); + fixture.componentRef.setInput('progress', 10); + fixture.detectChanges(); + + expect(component.progressLabel()).toBe('Progress: 10%'); + }); + }); + + describe('Stats Count Variant', () => { + beforeEach(() => { + fixture.componentRef.setInput('variant', 'stats-count'); + fixture.componentRef.setInput('icon', 'pi-check-square'); + fixture.componentRef.setInput('iconLabel', 'Tasks today'); + fixture.componentRef.setInput('bigNumber', 12); + fixture.detectChanges(); + }); + + it('should render stats-count card', () => { + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('.open-card--stats-count')).toBeTruthy(); + expect(compiled.querySelector('.stats-card--tasks')).toBeTruthy(); + }); + + it('should display big number', () => { + const compiled = fixture.nativeElement as HTMLElement; + const numberElement = compiled.querySelector('.stats-card__big-number'); + expect(numberElement?.textContent?.trim()).toBe('12'); + }); + + it('should display icon label', () => { + const compiled = fixture.nativeElement as HTMLElement; + const labelElement = compiled.querySelector('.stats-card__header span'); + expect(labelElement?.textContent?.trim()).toBe('Tasks today'); + }); + }); + + describe('Task Variant', () => { + beforeEach(() => { + fixture.componentRef.setInput('variant', 'task'); + fixture.componentRef.setInput('title', 'Add calendar view'); + fixture.componentRef.setInput( + 'subtitle', + 'Develop calendar component for tracking', + ); + fixture.componentRef.setInput('status', 'In Progress'); + fixture.componentRef.setInput('statusSeverity', 'info'); + fixture.componentRef.setInput('tags', ['feature', 'ui']); + fixture.detectChanges(); + }); + + it('should render task card', () => { + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('.open-card--task')).toBeTruthy(); + }); + + it('should display task title', () => { + const compiled = fixture.nativeElement as HTMLElement; + const nameElement = compiled.querySelector('.task-card__name'); + expect(nameElement?.textContent?.trim()).toBe('Add calendar view'); + }); + + it('should display task subtitle', () => { + const compiled = fixture.nativeElement as HTMLElement; + const descElement = compiled.querySelector('.task-card__description'); + expect(descElement?.textContent?.trim()).toBe( + 'Develop calendar component for tracking', + ); + }); + + it('should display status tag', () => { + const compiled = fixture.nativeElement as HTMLElement; + const statusTag = compiled.querySelector('p-tag'); + expect(statusTag).toBeTruthy(); + }); + + it('should display task tags', () => { + const compiled = fixture.nativeElement as HTMLElement; + const chips = compiled.querySelectorAll('p-chip'); + expect(chips.length).toBe(2); + }); + }); +}); diff --git a/src/app/components/open-card/open-card.stories.ts b/src/app/components/open-card/open-card.stories.ts new file mode 100644 index 0000000..413be3e --- /dev/null +++ b/src/app/components/open-card/open-card.stories.ts @@ -0,0 +1,356 @@ +import type { Meta, StoryObj } from '@storybook/angular'; +import { applicationConfig } from '@storybook/angular'; +import { provideAnimations } from '@angular/platform-browser/animations'; +import { OpenCard } from './open-card'; + +/** + * Extended type for story args including theme and locale controls + */ +interface StoryArgs { + variant?: 'project' | 'task' | 'stats-time' | 'stats-count'; + statsModifier?: 'today' | 'week' | 'tasks'; + icon?: string; + iconLabel?: string; + title?: string; + subtitle?: string; + status?: string; + statusSeverity?: 'success' | 'info' | 'warn' | 'danger' | 'secondary'; + tags?: string[]; + progress?: number; + worked?: string; + target?: string; + remaining?: string; + bigNumber?: number; + theme?: 'light' | 'dark'; + locale?: 'es' | 'en'; +} + +/** + * Translation records for story content + * Maps prop names to locale-specific values + */ +type I18nKeys = Record; + +/** + * Helper function to create i18n-reactive stories + * Creates a render function that updates props when locale changes in toolbar + */ +function createI18nStory( + baseArgs: Record, + i18nKeys: I18nKeys = {}, +): StoryObj { + return { + render: (args) => { + const locale = (args.locale || 'es') as 'es' | 'en'; + const translatedProps: Record = {}; + + /* Apply explicit translations based on current locale */ + Object.entries(i18nKeys).forEach(([key, values]) => { + translatedProps[key] = values[locale]; + }); + + /* + * Auto-translate iconLabel based on statsModifier + * Each story manages its own translations inline + */ + const statsModifier = baseArgs['statsModifier'] as string | undefined; + if (statsModifier && !i18nKeys['iconLabel']) { + const iconLabelTranslations: Record< + string, + { es: string; en: string } + > = { + today: { es: 'Hoy', en: 'Today' }, + week: { es: 'Esta semana', en: 'This week' }, + tasks: { es: 'Tareas hoy', en: 'Tasks today' }, + }; + + if (iconLabelTranslations[statsModifier]) { + translatedProps['iconLabel'] = + iconLabelTranslations[statsModifier][locale]; + } + } + + /* + * Auto-calculate progress from worked/target if both are provided + * Parses time strings like "4h 30m" and "8h" to calculate percentage + */ + const worked = baseArgs['worked'] as string | undefined; + const target = baseArgs['target'] as string | undefined; + if (worked && target && !baseArgs['progress']) { + const parseTime = (timeStr: string): number => { + const hourRegex = /(\d+)h/; + const minuteRegex = /(\d+)m/; + const hoursMatch = hourRegex.exec(timeStr); + const minutesMatch = minuteRegex.exec(timeStr); + return ( + (hoursMatch ? parseInt(hoursMatch[1]) * 60 : 0) + + (minutesMatch ? parseInt(minutesMatch[1]) : 0) + ); + }; + + const workedMinutes = parseTime(worked); + const targetMinutes = parseTime(target); + + if (targetMinutes > 0) { + translatedProps['progress'] = Math.round( + (workedMinutes / targetMinutes) * 100, + ); + } + } + + /* + * Auto-translate remaining prefix (Restante: / Remaining:) + * Story manages the full translated string + */ + const remaining = baseArgs['remaining'] as string | undefined; + if (remaining && !i18nKeys['remaining']) { + const prefix = locale === 'es' ? 'Restante' : 'Remaining'; + translatedProps['remaining'] = `${prefix}: ${remaining}`; + } + + return { + props: { + ...args, + ...translatedProps, + }, + }; + }, + args: baseArgs, + }; +} + +const meta: Meta = { + title: 'Components/OpenCard', + component: OpenCard, + tags: ['autodocs'], + decorators: [ + applicationConfig({ + providers: [provideAnimations()], + }), + ], + parameters: { + layout: 'padded', + docs: { + description: { + component: ` +Generic card component for displaying projects, tasks, and statistics. +This component replaces the previous ProjectCard, TaskCard, and StatsCard components +with a unified implementation using 4 variants. + +### Variants +- **project**: Displays a project card with icon and title +- **task**: Displays a task card with title, subtitle, status, and tags +- **stats-time**: Displays time statistics with progress bars (today/week) +- **stats-count**: Displays count statistics with a large number (tasks) + +### Features +- **Themeable**: Supports light and dark modes (use the theme toolbar) +- **i18n Ready**: Supports Spanish and English (use the locale toolbar) +- **Responsive**: Adapts to different screen sizes +- **Accessible**: ARIA labels and semantic HTML + `, + }, + }, + }, + argTypes: { + variant: { + control: 'select', + options: ['project', 'task', 'stats-time', 'stats-count'], + description: 'Card variant type', + }, + statsModifier: { + control: 'select', + options: ['today', 'week', 'tasks'], + description: 'Modifier for stats variant (today/week/tasks colors)', + }, + icon: { + control: 'text', + description: 'PrimeIcons class name', + }, + progress: { + control: false, + description: 'Progress percentage (auto-calculated from worked/target)', + }, + theme: { + control: { type: 'select' }, + options: ['light', 'dark'], + description: 'Theme mode', + table: { category: 'Story' }, + }, + locale: { + control: { type: 'select' }, + options: ['es', 'en'], + description: 'Language', + table: { category: 'Story' }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +/** + * Today's time statistics + * Displays worked time vs target with auto-calculated progress + */ +export const StatsToday: Story = { + args: { + theme: 'light', + }, + + ...createI18nStory({ + variant: 'stats-time', + statsModifier: 'today', + icon: 'pi-sun', + worked: '4h 30m', + target: '8h', + remaining: '3h 30m', + theme: 'dark', + locale: 'es', + }), + + argTypes: { + // Show only stats-time relevant controls + worked: { control: 'text' }, + target: { control: 'text' }, + remaining: { control: 'text' }, + // Hide other variant controls + title: { table: { disable: true } }, + subtitle: { table: { disable: true } }, + status: { table: { disable: true } }, + statusSeverity: { table: { disable: true } }, + tags: { table: { disable: true } }, + bigNumber: { table: { disable: true } }, + }, +}; + +/** + * Weekly time statistics + * Displays worked time vs target with auto-calculated progress + */ +export const StatsWeek: Story = { + ...createI18nStory({ + variant: 'stats-time', + statsModifier: 'week', + icon: 'pi-calendar-clock', + worked: '22h 15m', + target: '40h', + remaining: '17h 45m', + theme: 'dark', + locale: 'es', + }), + argTypes: { + // Show only stats-time relevant controls + worked: { control: 'text' }, + target: { control: 'text' }, + remaining: { control: 'text' }, + // Hide other variant controls + title: { table: { disable: true } }, + subtitle: { table: { disable: true } }, + status: { table: { disable: true } }, + statusSeverity: { table: { disable: true } }, + tags: { table: { disable: true } }, + bigNumber: { table: { disable: true } }, + }, +}; + +/** + * Task count statistics + * Displays total number of tasks completed today + */ +export const StatsTask: Story = { + ...createI18nStory({ + variant: 'stats-count', + statsModifier: 'tasks', + icon: 'pi-check-square', + bigNumber: 12, + theme: 'dark', + locale: 'es', + }), + argTypes: { + // Show only stats-count relevant controls + bigNumber: { control: 'number' }, + // Hide other variant controls + title: { table: { disable: true } }, + subtitle: { table: { disable: true } }, + status: { table: { disable: true } }, + statusSeverity: { table: { disable: true } }, + tags: { table: { disable: true } }, + worked: { table: { disable: true } }, + target: { table: { disable: true } }, + remaining: { table: { disable: true } }, + }, +}; + +/** + * Task card example + * Displays a task with title, description, status, and tags + */ +export const Task: Story = { + ...createI18nStory( + { + variant: 'task', + statusSeverity: 'info', + tags: ['feature', 'ui', 'calendar'], + theme: 'dark', + locale: 'es', + }, + { + title: { + es: 'Añadir vista de calendario', + en: 'Add calendar view', + }, + subtitle: { + es: 'Desarrollar componente de calendario para seguimiento de entradas de tiempo', + en: 'Develop calendar component for tracking time entries', + }, + status: { + es: 'En progreso', + en: 'In Progress', + }, + }, + ), + argTypes: { + // Show only task relevant controls + title: { control: 'text' }, + subtitle: { control: 'text' }, + status: { control: 'text' }, + statusSeverity: { + control: 'select', + options: ['success', 'info', 'warn', 'danger', 'secondary'], + }, + tags: { control: 'object' }, + // Hide other variant controls + worked: { table: { disable: true } }, + target: { table: { disable: true } }, + remaining: { table: { disable: true } }, + bigNumber: { table: { disable: true } }, + }, +}; + +/** + * Project card example + * Displays a project with icon and name + */ +export const Project: Story = { + ...createI18nStory({ + variant: 'project', + icon: 'pi-folder', + title: 'OpenTimeTracker', + theme: 'dark', + locale: 'es', + }), + argTypes: { + // Show only project relevant controls + title: { control: 'text' }, + // Hide other variant controls + subtitle: { table: { disable: true } }, + status: { table: { disable: true } }, + statusSeverity: { table: { disable: true } }, + tags: { table: { disable: true } }, + worked: { table: { disable: true } }, + target: { table: { disable: true } }, + remaining: { table: { disable: true } }, + bigNumber: { table: { disable: true } }, + }, +}; diff --git a/src/app/components/open-card/open-card.ts b/src/app/components/open-card/open-card.ts new file mode 100644 index 0000000..20cf82a --- /dev/null +++ b/src/app/components/open-card/open-card.ts @@ -0,0 +1,172 @@ +import { Component, input, computed } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { CardModule } from 'primeng/card'; +import { TagModule } from 'primeng/tag'; +import { ChipModule } from 'primeng/chip'; +import { ProgressBarModule } from 'primeng/progressbar'; +import { TranslateModule } from '@ngx-translate/core'; +import { OpenCardProgressbarAriaFixDirective } from './open-card-progressbar-aria-fix.directive'; + +/** + * Card variant type + */ +export type CardVariant = 'project' | 'task' | 'stats-time' | 'stats-count'; + +/** + * Generic card component for displaying projects, tasks, and statistics + * + * @remarks + * This component replaces the previous ProjectCard, TaskCard, and StatsCard + * components with a unified implementation using 4 variants: + * - **project**: Simple project card with icon and title + * - **task**: Task card with title, subtitle, status, and tags + * - **stats-time**: Statistics with time worked/target and progress bar + * - **stats-count**: Statistics with a large number display + * + * @example Project variant + * ```html + * + * ``` + * + * @example Task variant + * ```html + * + * ``` + * + * @example Stats time variant + * ```html + * + * ``` + * Note: iconLabel is automatically derived from statsModifier using i18n. + * + * @example Statscount variant + * ```html + * + * ``` + */ +@Component({ + selector: 'app-open-card', + standalone: true, + imports: [ + CommonModule, + CardModule, + TagModule, + ChipModule, + ProgressBarModule, + TranslateModule, + OpenCardProgressbarAriaFixDirective, + ], + templateUrl: './open-card.html', + styleUrl: './open-card.scss', +}) +export class OpenCard { + /** + * Card variant type + */ + variant = input.required(); + + /** + * Icon class (PrimeIcons) + */ + icon = input(); + + /** + * Icon label (automatically translated in stories) + */ + iconLabel = input(); + + /** + * Title (project name or task title) + */ + title = input(); + + /** + * Subtitle (task description) + */ + subtitle = input(); + + /** + * Stats modifier for CSS classes (today/week/tasks) + */ + statsModifier = input<'today' | 'week' | 'tasks'>(); + + /** + * Worked time formatted string + */ + worked = input(''); + + /** + * Target time formatted string + */ + target = input(''); + + /** + * Remaining time formatted string + */ + remaining = input(''); + + /** + * Progress percentage 0-100 + */ + progress = input(0); + + /** + * Accessible label for progress bar + */ + progressLabel = computed(() => { + const worked = this.worked(); + const target = this.target(); + const progressValue = this.progress(); + + if (worked && target) { + return `Progress: ${worked} of ${target} (${progressValue}%)`; + } + return `Progress: ${progressValue}%`; + }); + + /** + * Big number to display + */ + bigNumber = input(0); + + /** + * Task status label + */ + status = input(); + + /** + * Task status severity + */ + statusSeverity = input<'success' | 'info' | 'warn' | 'danger' | 'secondary'>( + 'secondary', + ); + + /** + * Task tags + */ + tags = input([]); +} diff --git a/src/app/components/open-day-override-dialog/open-day-override-dialog.scss b/src/app/components/open-day-override-dialog/open-day-override-dialog.scss index 969a55f..913cd3c 100644 --- a/src/app/components/open-day-override-dialog/open-day-override-dialog.scss +++ b/src/app/components/open-day-override-dialog/open-day-override-dialog.scss @@ -1,3 +1,7 @@ +@use "../../themes/dialog-polish" as *; + +@include dialog-polish; + .day-override { display: flex; flex-direction: column; @@ -10,9 +14,9 @@ gap: 0.75rem; padding: 1rem; background: var(--c-surface-subtle); - border-radius: var(--p-border-radius); - font-size: 1rem; - font-weight: 500; + border-radius: var(--radius-md); + font-size: var(--font-size-base); + font-weight: var(--font-weight-medium); color: var(--p-text-color); text-transform: capitalize; diff --git a/src/app/components/open-day-types-dialog/open-day-types-dialog.scss b/src/app/components/open-day-types-dialog/open-day-types-dialog.scss index dbd0b01..5f41c17 100644 --- a/src/app/components/open-day-types-dialog/open-day-types-dialog.scss +++ b/src/app/components/open-day-types-dialog/open-day-types-dialog.scss @@ -1,3 +1,7 @@ +@use "../../themes/dialog-polish" as *; + +@include dialog-polish; + .day-types { display: flex; flex-direction: column; @@ -7,7 +11,7 @@ .day-types__form { padding: 1rem; background: var(--p-surface-50); - border-radius: var(--p-border-radius); + border-radius: var(--radius-md); } .day-types__form-row { diff --git a/src/app/components/open-time-entry-dialog/open-time-entry-dialog.scss b/src/app/components/open-time-entry-dialog/open-time-entry-dialog.scss index ea721c2..7af2a35 100644 --- a/src/app/components/open-time-entry-dialog/open-time-entry-dialog.scss +++ b/src/app/components/open-time-entry-dialog/open-time-entry-dialog.scss @@ -1,3 +1,7 @@ +@use "../../themes/dialog-polish" as *; + +@include dialog-polish; + .time-entry { display: flex; flex-direction: column; @@ -11,8 +15,8 @@ } .time-entry__label { - font-weight: 600; - font-size: 0.875rem; + font-weight: var(--font-weight-semibold); + font-size: var(--font-size-sm); color: var(--p-text-color); } diff --git a/src/app/components/open-work-config-dialog/open-work-config-dialog.scss b/src/app/components/open-work-config-dialog/open-work-config-dialog.scss index a343e32..9128df4 100644 --- a/src/app/components/open-work-config-dialog/open-work-config-dialog.scss +++ b/src/app/components/open-work-config-dialog/open-work-config-dialog.scss @@ -1,3 +1,7 @@ +@use "../../themes/dialog-polish" as *; + +@include dialog-polish; + .work-config { display: flex; flex-direction: column; @@ -11,8 +15,8 @@ } .work-config__label { - font-weight: 600; - font-size: 0.875rem; + font-weight: var(--font-weight-semibold); + font-size: var(--font-size-sm); color: var(--p-text-color); } diff --git a/src/app/pages/open-calendar-page/open-calendar-page.scss b/src/app/pages/open-calendar-page/open-calendar-page.scss index 4059e89..f8ef6f9 100644 --- a/src/app/pages/open-calendar-page/open-calendar-page.scss +++ b/src/app/pages/open-calendar-page/open-calendar-page.scss @@ -15,14 +15,14 @@ align-items: center; background: rgba(0, 0, 0, 0.5); z-index: 100; - border-radius: 8px; + border-radius: var(--radius-lg); } .loading-text { color: var(--text-color); - font-size: 1.1rem; + font-size: var(--font-size-lg); background: var(--surface-card); padding: 1rem 2rem; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + border-radius: var(--radius-lg); + box-shadow: var(--elevation-lg); } diff --git a/src/app/pages/open-history/open-history.scss b/src/app/pages/open-history/open-history.scss index ae84580..3a1f8b4 100644 --- a/src/app/pages/open-history/open-history.scss +++ b/src/app/pages/open-history/open-history.scss @@ -108,7 +108,7 @@ tr.undone { .json-display { background: var(--p-surface-100); border: 1px solid var(--p-surface-200); - border-radius: 6px; + border-radius: var(--radius-md); padding: 1rem; margin: 0; font-family: "Fira Code", "Consolas", monospace; diff --git a/src/app/pages/open-home/components/project-card/project-card.html b/src/app/pages/open-home/components/project-card/project-card.html deleted file mode 100644 index 5ebccd8..0000000 --- a/src/app/pages/open-home/components/project-card/project-card.html +++ /dev/null @@ -1,8 +0,0 @@ -
- -
- -

{{ project().name }}

-
-
-
diff --git a/src/app/pages/open-home/components/project-card/project-card.scss b/src/app/pages/open-home/components/project-card/project-card.scss deleted file mode 100644 index cf87c0a..0000000 --- a/src/app/pages/open-home/components/project-card/project-card.scss +++ /dev/null @@ -1,30 +0,0 @@ -.project-card { - height: 100%; - width: 100%; - - ::ng-deep .p-card { - height: 100%; - } - - ::ng-deep .p-card-body { - height: 100%; - } - - &__header { - display: flex; - align-items: center; - gap: 0.75rem; - - i { - font-size: 1.5rem; - color: var(--p-primary-color); - } - } - - &__name { - margin: 0; - font-size: 1rem; - font-weight: 600; - color: var(--p-text-color); - } -} diff --git a/src/app/pages/open-home/components/project-card/project-card.spec.ts b/src/app/pages/open-home/components/project-card/project-card.spec.ts deleted file mode 100644 index 6e01930..0000000 --- a/src/app/pages/open-home/components/project-card/project-card.spec.ts +++ /dev/null @@ -1,216 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { provideNoopAnimations } from '@angular/platform-browser/animations'; -import { ProjectCard } from './project-card'; -import { Project } from '../../../../../types/electron'; - -describe('ProjectCard', () => { - let component: ProjectCard; - let fixture: ComponentFixture; - - const mockProject: Project = { - id: '1', - name: 'Test Project', - description: 'A test project for unit testing', - createdAt: new Date('2026-01-01'), - updatedAt: new Date('2026-01-15'), - isClosed: false, - }; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ProjectCard], - providers: [provideNoopAnimations()], - }).compileComponents(); - - fixture = TestBed.createComponent(ProjectCard); - component = fixture.componentInstance; - }); - - describe('component creation', () => { - it('should create', () => { - fixture.componentRef.setInput('project', mockProject); - fixture.detectChanges(); - expect(component).toBeTruthy(); - }); - }); - - describe('project input', () => { - it('should accept project input', () => { - fixture.componentRef.setInput('project', mockProject); - fixture.detectChanges(); - - expect(component.project()).toEqual(mockProject); - }); - - it('should reflect project name changes', () => { - fixture.componentRef.setInput('project', mockProject); - fixture.detectChanges(); - - expect(component.project().name).toBe('Test Project'); - - const updatedProject = { ...mockProject, name: 'Updated Project' }; - fixture.componentRef.setInput('project', updatedProject); - fixture.detectChanges(); - - expect(component.project().name).toBe('Updated Project'); - }); - - it('should handle different project data', () => { - const anotherProject: Project = { - id: '2', - name: 'Another Project', - description: 'Different project', - createdAt: new Date('2026-02-01'), - updatedAt: new Date('2026-02-05'), - isClosed: false, - }; - - fixture.componentRef.setInput('project', anotherProject); - fixture.detectChanges(); - - expect(component.project()).toEqual(anotherProject); - expect(component.project().id).toBe('2'); - expect(component.project().name).toBe('Another Project'); - }); - }); - - describe('rendering', () => { - beforeEach(() => { - fixture.componentRef.setInput('project', mockProject); - fixture.detectChanges(); - }); - - it('should display project name', () => { - const compiled = fixture.nativeElement as HTMLElement; - const nameElement = compiled.querySelector('.project-card__name'); - - expect(nameElement).toBeTruthy(); - expect(nameElement?.textContent?.trim()).toBe('Test Project'); - }); - - it('should have project-card article element', () => { - const compiled = fixture.nativeElement as HTMLElement; - const articleElement = compiled.querySelector('article.project-card'); - - expect(articleElement).toBeTruthy(); - }); - - it('should contain p-card component', () => { - const compiled = fixture.nativeElement as HTMLElement; - const cardElement = compiled.querySelector('p-card'); - - expect(cardElement).toBeTruthy(); - }); - - it('should have project-card__header div', () => { - const compiled = fixture.nativeElement as HTMLElement; - const headerElement = compiled.querySelector('.project-card__header'); - - expect(headerElement).toBeTruthy(); - }); - - it('should display folder icon', () => { - const compiled = fixture.nativeElement as HTMLElement; - const iconElement = compiled.querySelector('.pi.pi-folder'); - - expect(iconElement).toBeTruthy(); - }); - - it('should update project name in DOM when input changes', () => { - const compiled = fixture.nativeElement as HTMLElement; - let nameElement = compiled.querySelector('.project-card__name'); - - expect(nameElement?.textContent?.trim()).toBe('Test Project'); - - const updatedProject = { ...mockProject, name: 'New Name' }; - fixture.componentRef.setInput('project', updatedProject); - fixture.detectChanges(); - - nameElement = compiled.querySelector('.project-card__name'); - expect(nameElement?.textContent?.trim()).toBe('New Name'); - }); - - it('should render with minimal project data', () => { - const minimalProject: Project = { - id: '999', - name: 'Minimal', - description: '', - createdAt: new Date(), - updatedAt: new Date(), - isClosed: false, - }; - - fixture.componentRef.setInput('project', minimalProject); - fixture.detectChanges(); - - const compiled = fixture.nativeElement as HTMLElement; - const nameElement = compiled.querySelector('.project-card__name'); - - expect(nameElement?.textContent?.trim()).toBe('Minimal'); - }); - - it('should handle project with long name', () => { - const longNameProject: Project = { - ...mockProject, - name: 'Project with a very long name that might need special handling', - }; - - fixture.componentRef.setInput('project', longNameProject); - fixture.detectChanges(); - - const compiled = fixture.nativeElement as HTMLElement; - const nameElement = compiled.querySelector('.project-card__name'); - - expect(nameElement?.textContent?.trim()).toBe( - 'Project with a very long name that might need special handling', - ); - }); - - it('should handle project with special characters in name', () => { - const specialNameProject: Project = { - ...mockProject, - name: 'Project #1 @ 2026 & Co.', - }; - - fixture.componentRef.setInput('project', specialNameProject); - fixture.detectChanges(); - - const compiled = fixture.nativeElement as HTMLElement; - const nameElement = compiled.querySelector('.project-card__name'); - - expect(nameElement?.textContent?.trim()).toBe('Project #1 @ 2026 & Co.'); - }); - }); - - describe('structure', () => { - beforeEach(() => { - fixture.componentRef.setInput('project', mockProject); - fixture.detectChanges(); - }); - - it('should have header as direct child of p-card', () => { - const compiled = fixture.nativeElement as HTMLElement; - const cardElement = compiled.querySelector('p-card'); - const headerElement = cardElement?.querySelector('.project-card__header'); - - expect(headerElement).toBeTruthy(); - }); - - it('should have icon and name inside header', () => { - const compiled = fixture.nativeElement as HTMLElement; - const headerElement = compiled.querySelector('.project-card__header'); - const icon = headerElement?.querySelector('.pi.pi-folder'); - const name = headerElement?.querySelector('.project-card__name'); - - expect(icon).toBeTruthy(); - expect(name).toBeTruthy(); - }); - - it('should have h3 element for project name', () => { - const compiled = fixture.nativeElement as HTMLElement; - const nameElement = compiled.querySelector('.project-card__name'); - - expect(nameElement?.tagName.toLowerCase()).toBe('h3'); - }); - }); -}); diff --git a/src/app/pages/open-home/components/project-card/project-card.ts b/src/app/pages/open-home/components/project-card/project-card.ts deleted file mode 100644 index cb97935..0000000 --- a/src/app/pages/open-home/components/project-card/project-card.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Component, input } from '@angular/core'; -import { CardModule } from 'primeng/card'; -import { Project } from '../../../../../types/electron'; - -/** - * Project card component for displaying an open project - */ -@Component({ - selector: 'app-project-card', - standalone: true, - imports: [CardModule], - templateUrl: './project-card.html', - styleUrl: './project-card.scss', -}) -export class ProjectCard { - /** - * Project to display - */ - project = input.required(); -} diff --git a/src/app/pages/open-home/components/stats-card/stats-card.html b/src/app/pages/open-home/components/stats-card/stats-card.html deleted file mode 100644 index be8c123..0000000 --- a/src/app/pages/open-home/components/stats-card/stats-card.html +++ /dev/null @@ -1,23 +0,0 @@ - -
- - {{ labelKey() | translate }} -
-
- @if (type() === "tasks") { -
- {{ bigNumber() }} -
- } @else { -
- {{ worked() }} - / - {{ target() }} -
- -
- {{ "home.stats.remaining" | translate }}: {{ remaining() }} -
- } -
-
diff --git a/src/app/pages/open-home/components/stats-card/stats-card.scss b/src/app/pages/open-home/components/stats-card/stats-card.scss deleted file mode 100644 index 7781055..0000000 --- a/src/app/pages/open-home/components/stats-card/stats-card.scss +++ /dev/null @@ -1,74 +0,0 @@ -.stats-card { - height: 100%; - - &__header { - display: flex; - align-items: center; - gap: 0.5rem; - margin-bottom: 0.75rem; - font-weight: 600; - color: var(--p-text-muted-color); - - i { - font-size: 1.25rem; - } - } - - &__content { - display: flex; - flex-direction: column; - gap: 0.5rem; - } - - &__time { - display: flex; - align-items: baseline; - gap: 0.25rem; - } - - &__worked { - font-size: 1.5rem; - font-weight: 700; - color: var(--p-text-color); - } - - &__separator { - color: var(--p-text-muted-color); - } - - &__target { - font-size: 1rem; - color: var(--p-text-muted-color); - } - - &__remaining { - font-size: 0.875rem; - color: var(--p-text-muted-color); - } - - &__big-number { - font-size: 2.5rem; - font-weight: 700; - color: var(--p-text-color); - text-align: center; - padding: 0.5rem 0; - } - - &--today { - ::ng-deep .p-progressbar .p-progressbar-value { - background: var(--p-primary-color); - } - } - - &--week { - ::ng-deep .p-progressbar .p-progressbar-value { - background: var(--p-blue-500); - } - } - - &--tasks { - .stats-card__header i { - color: var(--p-green-500); - } - } -} diff --git a/src/app/pages/open-home/components/stats-card/stats-card.ts b/src/app/pages/open-home/components/stats-card/stats-card.ts deleted file mode 100644 index 8cf1cc3..0000000 --- a/src/app/pages/open-home/components/stats-card/stats-card.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Component, input } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { CardModule } from 'primeng/card'; -import { ProgressBarModule } from 'primeng/progressbar'; -import { TranslateModule } from '@ngx-translate/core'; - -/** - * Type of stats card to display - */ -export type StatsCardType = 'today' | 'week' | 'tasks'; - -/** - * Stats card component for displaying time tracking statistics - */ -@Component({ - selector: 'app-stats-card', - standalone: true, - imports: [CommonModule, CardModule, ProgressBarModule, TranslateModule], - templateUrl: './stats-card.html', - styleUrl: './stats-card.scss', -}) -export class StatsCard { - /** - * Type of card (today, week, or tasks) - */ - type = input.required(); - - /** - * Icon to display in header - */ - icon = input.required(); - - /** - * Translation key for header label - */ - labelKey = input.required(); - - /** - * Worked time formatted string - */ - worked = input(''); - - /** - * Target time formatted string - */ - target = input(''); - - /** - * Remaining time formatted string - */ - remaining = input(''); - - /** - * Progress percentage (0-100) - */ - progress = input(0); - - /** - * Big number to display (for tasks card) - */ - bigNumber = input(0); -} diff --git a/src/app/pages/open-home/components/task-card/task-card.html b/src/app/pages/open-home/components/task-card/task-card.html deleted file mode 100644 index 5b218bd..0000000 --- a/src/app/pages/open-home/components/task-card/task-card.html +++ /dev/null @@ -1,27 +0,0 @@ -
- - {{ - task().project?.name || ("home.noProject" | translate) - }} -

{{ task().name }}

- @if (task().description) { -

{{ task().description }}

- } - -
-
diff --git a/src/app/pages/open-home/components/task-card/task-card.scss b/src/app/pages/open-home/components/task-card/task-card.scss deleted file mode 100644 index 71bffa1..0000000 --- a/src/app/pages/open-home/components/task-card/task-card.scss +++ /dev/null @@ -1,67 +0,0 @@ -.task-card { - height: 100%; - width: 100%; - - ::ng-deep .p-card { - height: 100%; - display: flex; - flex-direction: column; - } - - ::ng-deep .p-card-body { - flex: 1; - display: flex; - flex-direction: column; - } - - ::ng-deep .p-card-content { - flex: 1; - display: flex; - flex-direction: column; - } - - &__project { - font-size: 0.75rem; - color: var(--p-primary-color); - font-weight: 500; - text-transform: uppercase; - letter-spacing: 0.5px; - } - - &__name { - margin: 0.25rem 0 0.5rem; - font-size: 1rem; - font-weight: 600; - color: var(--p-text-color); - } - - &__description { - margin: 0 0 0.75rem; - font-size: 0.875rem; - color: var(--p-text-muted-color); - display: -webkit-box; - -webkit-line-clamp: 2; - line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - } - - &__footer { - margin-top: auto; - } - - &__meta { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 0.5rem; - } - - &__hours { - font-size: 0.75rem; - color: var(--p-text-muted-color); - background: var(--p-surface-100); - padding: 0.25rem 0.5rem; - border-radius: var(--p-border-radius); - } -} diff --git a/src/app/pages/open-home/components/task-card/task-card.spec.ts b/src/app/pages/open-home/components/task-card/task-card.spec.ts deleted file mode 100644 index 3b2c51b..0000000 --- a/src/app/pages/open-home/components/task-card/task-card.spec.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { TaskCard } from './task-card'; -import { TranslateModule, TranslateService } from '@ngx-translate/core'; -import { Task } from '../../../../../types/electron'; - -describe('TaskCard', () => { - let component: TaskCard; - let fixture: ComponentFixture; - let translateService: TranslateService; - - const mockTask: Task = { - id: '1', - name: 'Test Task', - projectId: 'p1', - statusId: 's1', - description: 'Test description', - estimatedHours: 8, - createdAt: new Date(), - updatedAt: new Date(), - status: { - id: 's1', - name: 'status.pending', - color: '#f59e0b', - isDefault: true, - }, - project: { - id: 'p1', - name: 'Test Project', - description: null, - isClosed: false, - createdAt: new Date(), - updatedAt: new Date(), - }, - tags: [{ tag: { id: 't1', name: 'Bug' } }], - }; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [TaskCard, TranslateModule.forRoot()], - }).compileComponents(); - - fixture = TestBed.createComponent(TaskCard); - component = fixture.componentInstance; - translateService = TestBed.inject(TranslateService); - fixture.componentRef.setInput('task', mockTask); - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - describe('getStatusDisplayName', () => { - it('should return empty string for undefined status', () => { - expect(component.getStatusDisplayName(undefined)).toBe(''); - }); - - it('should return empty string for empty status', () => { - expect(component.getStatusDisplayName('')).toBe(''); - }); - - it('should translate status keys starting with status.', () => { - spyOn(translateService, 'instant').and.returnValue('Pendiente'); - const result = component.getStatusDisplayName('status.pending'); - expect(translateService.instant).toHaveBeenCalledWith('status.pending'); - expect(result).toBe('Pendiente'); - }); - - it('should return original name for non-translation keys', () => { - const result = component.getStatusDisplayName('Custom Status'); - expect(result).toBe('Custom Status'); - }); - }); - - describe('getStatusSeverity', () => { - it('should return success for completed statuses', () => { - expect(component.getStatusSeverity('status.completed')).toBe('success'); - expect(component.getStatusSeverity('Completada')).toBe('success'); - expect(component.getStatusSeverity('Completed')).toBe('success'); - }); - - it('should return info for in-progress statuses', () => { - expect(component.getStatusSeverity('status.inProgress')).toBe('info'); - expect(component.getStatusSeverity('En progreso')).toBe('info'); - expect(component.getStatusSeverity('In Progress')).toBe('info'); - }); - - it('should return warn for pending statuses', () => { - expect(component.getStatusSeverity('status.pending')).toBe('warn'); - expect(component.getStatusSeverity('Pendiente')).toBe('warn'); - expect(component.getStatusSeverity('Pending')).toBe('warn'); - }); - - it('should return danger for blocked statuses', () => { - expect(component.getStatusSeverity('status.blocked')).toBe('danger'); - expect(component.getStatusSeverity('Bloqueada')).toBe('danger'); - expect(component.getStatusSeverity('Blocked')).toBe('danger'); - }); - - it('should return secondary for unknown statuses', () => { - expect(component.getStatusSeverity('Unknown')).toBe('secondary'); - expect(component.getStatusSeverity(undefined)).toBe('secondary'); - expect(component.getStatusSeverity('')).toBe('secondary'); - }); - }); -}); diff --git a/src/app/pages/open-home/components/task-card/task-card.ts b/src/app/pages/open-home/components/task-card/task-card.ts deleted file mode 100644 index 7346c4e..0000000 --- a/src/app/pages/open-home/components/task-card/task-card.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Component, inject, input } from '@angular/core'; -import { CardModule } from 'primeng/card'; -import { TagModule } from 'primeng/tag'; -import { ChipModule } from 'primeng/chip'; -import { TranslateModule, TranslateService } from '@ngx-translate/core'; -import { Task } from '../../../../../types/electron'; - -/** - * Task card component for displaying a pending task - */ -@Component({ - selector: 'app-task-card', - standalone: true, - imports: [CardModule, TagModule, ChipModule, TranslateModule], - templateUrl: './task-card.html', - styleUrl: './task-card.scss', -}) -export class TaskCard { - private readonly translate = inject(TranslateService); - - /** - * Task to display - */ - task = input.required(); - - /** - * Gets translated display name for status - */ - getStatusDisplayName(statusName?: string): string { - if (!statusName) return ''; - if (statusName.startsWith('status.')) { - return this.translate.instant(statusName); - } - return statusName; - } - - /** - * Gets severity color for task status - */ - getStatusSeverity( - statusName?: string, - ): 'success' | 'info' | 'warn' | 'danger' | 'secondary' { - switch (statusName) { - case 'status.completed': - case 'Completada': - case 'Completed': - return 'success'; - case 'status.inProgress': - case 'En progreso': - case 'In Progress': - return 'info'; - case 'status.pending': - case 'Pendiente': - case 'Pending': - return 'warn'; - case 'status.blocked': - case 'Bloqueada': - case 'Blocked': - return 'danger'; - default: - return 'secondary'; - } - } -} diff --git a/src/app/pages/open-home/open-home.html b/src/app/pages/open-home/open-home.html index c8a05f2..ee00233 100644 --- a/src/app/pages/open-home/open-home.html +++ b/src/app/pages/open-home/open-home.html @@ -4,73 +4,89 @@

{{ "home.title" | translate }}

- - -
-

{{ "home.pendingTasks" | translate }}

+
+

{{ "home.pendingTasks" | translate }}

- @if (loading()) { -

{{ "home.loading" | translate }}

- } @else if (pendingTasks().length === 0) { - -
- -

{{ "home.empty" | translate }}

-
-
- } @else { -
- @for (task of pendingTasks(); track task.id) { -
- + @if (loading()) { +

{{ "home.loading" | translate }}

+ } @else if (pendingTasks().length === 0) { + +
+ +

{{ "home.empty" | translate }}

- } -
- } - - -

{{ "home.openProjects" | translate }}

- - @if (openProjects().length === 0) { - -
- -

{{ "home.emptyProjects" | translate }}

+ + } @else { +
+ @for (task of pendingTasks(); track task.id) { +
+ +
+ }
- - } @else { -
- @for (project of openProjects(); track project.id) { -
- + } +
+
+

{{ "home.openProjects" | translate }}

+ + @if (openProjects().length === 0) { + +
+ +

{{ "home.emptyProjects" | translate }}

- } -
- } + + } @else { +
+ @for (project of openProjects(); track project.id) { +
+ +
+ } +
+ } +
diff --git a/src/app/pages/open-home/open-home.scss b/src/app/pages/open-home/open-home.scss index 37c8d41..f5243a9 100644 --- a/src/app/pages/open-home/open-home.scss +++ b/src/app/pages/open-home/open-home.scss @@ -4,12 +4,14 @@ h1 { margin: 0 0 0.5rem; - font-size: 2rem; + font-size: var(--font-size-3xl); + font-weight: var(--font-weight-semibold); } p { margin: 0 0 1.5rem; - opacity: 0.8; + font-size: var(--font-size-lg); + color: var(--p-text-muted-color); } } @@ -25,11 +27,14 @@ h1 { margin: 0 0 1rem; + font-size: var(--font-size-2xl); + font-weight: var(--font-weight-semibold); } h2 { margin: 0 0 1rem; - font-size: 1.25rem; + font-size: var(--font-size-xl); + font-weight: var(--font-weight-semibold); } } @@ -39,6 +44,22 @@ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 1.5rem; + + >* { + max-width: 356px; + } +} + +.task-panel, +.projects-panel { + margin-bottom: 1.5rem; +} + +.tasks-grid, +.projects-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 356px)); + gap: 1rem; } .stats-card { @@ -54,13 +75,13 @@ display: flex; align-items: center; gap: 0.5rem; - font-weight: 600; + font-weight: var(--font-weight-semibold); margin-bottom: 0.75rem; color: var(--p-text-muted-color); - font-size: 0.875rem; + font-size: var(--font-size-sm); i { - font-size: 1.125rem; + font-size: var(--font-size-lg); } } @@ -77,8 +98,8 @@ } &__worked { - font-size: 1.5rem; - font-weight: 700; + font-size: var(--font-size-2xl); + font-weight: var(--font-weight-bold); color: var(--p-primary-color); } @@ -88,18 +109,18 @@ } &__target { - font-size: 1rem; + font-size: var(--font-size-base); color: var(--p-text-muted-color); } &__remaining { - font-size: 0.75rem; + font-size: var(--font-size-xs); color: var(--p-text-muted-color); } &__big-number { - font-size: 2.5rem; - font-weight: 700; + font-size: var(--font-size-4xl); + font-weight: var(--font-weight-bold); color: var(--p-primary-color); text-align: center; line-height: 1; @@ -139,47 +160,18 @@ text-align: center; i { - font-size: 3rem; - opacity: 0.5; + font-size: var(--font-size-4xl); + color: var(--p-text-muted-color); } p { margin: 0; - opacity: 0.7; + font-size: var(--font-size-base); + color: var(--p-text-muted-color); } } } -.tasks-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - grid-auto-rows: 1fr; - gap: 1rem; - list-style: none; - padding: 0; - margin: 0 0 1.5rem; - - > [role="listitem"] { - display: flex; - min-height: 150px; - } -} - -/* ==================== PROJECTS GRID ==================== */ -.projects-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - grid-auto-rows: 1fr; - gap: 1rem; - list-style: none; - padding: 0; - margin: 0; - - > [role="listitem"] { - display: flex; - } -} - .project-card { display: flex; flex-direction: column; @@ -286,4 +278,4 @@ background: var(--p-surface-100); border-radius: var(--p-border-radius-sm); } -} +} \ No newline at end of file diff --git a/src/app/pages/open-home/open-home.spec.ts b/src/app/pages/open-home/open-home.spec.ts index 1307d5d..6e0da81 100644 --- a/src/app/pages/open-home/open-home.spec.ts +++ b/src/app/pages/open-home/open-home.spec.ts @@ -554,4 +554,62 @@ describe('OpenHome', () => { ).toBeUndefined(); }); }); + + describe('status and tags helpers', () => { + it('should translate pending status when status is undefined', () => { + expect(component.getStatusDisplayName(undefined)).toBe('status.pending'); + }); + + it('should translate provided status key', () => { + expect(component.getStatusDisplayName('status.completed')).toBe( + 'status.completed', + ); + }); + + it('should return success severity', () => { + expect(component.getStatusSeverity('Completed')).toBe('success'); + expect(component.getStatusSeverity('Completada')).toBe('success'); + expect(component.getStatusSeverity('Done')).toBe('success'); + }); + + it('should return info severity', () => { + expect(component.getStatusSeverity('In Progress')).toBe('info'); + expect(component.getStatusSeverity('En curso')).toBe('info'); + expect(component.getStatusSeverity('Working')).toBe('info'); + }); + + it('should return danger severity', () => { + expect(component.getStatusSeverity('Blocked')).toBe('danger'); + expect(component.getStatusSeverity('Bloqueada')).toBe('danger'); + expect(component.getStatusSeverity('Error')).toBe('danger'); + }); + + it('should return warn severity', () => { + expect(component.getStatusSeverity('Pending')).toBe('warn'); + expect(component.getStatusSeverity('Pendiente')).toBe('warn'); + expect(component.getStatusSeverity('Todo')).toBe('warn'); + }); + + it('should return secondary for unknown severity', () => { + expect(component.getStatusSeverity('Unknown')).toBe('secondary'); + expect(component.getStatusSeverity(undefined)).toBe('secondary'); + }); + + it('should extract task tags', () => { + const taskWithTags = { + ...mockTasks[0], + tags: [ + { tag: { id: 't1', name: 'Bug' } }, + { tag: { id: 't2', name: 'UI' } }, + ], + } as Task; + + expect(component.getTaskTags(taskWithTags)).toEqual(['Bug', 'UI']); + }); + + it('should return empty array when task has no tags', () => { + const taskWithoutTags = { ...mockTasks[0], tags: [] } as Task; + expect(component.getTaskTags(taskWithoutTags)).toEqual([]); + }); + }); }); diff --git a/src/app/pages/open-home/open-home.ts b/src/app/pages/open-home/open-home.ts index 59def33..0fb2999 100644 --- a/src/app/pages/open-home/open-home.ts +++ b/src/app/pages/open-home/open-home.ts @@ -3,12 +3,10 @@ import { Component, OnInit, inject, signal, computed } from '@angular/core'; import { CardModule } from 'primeng/card'; import { ButtonModule } from 'primeng/button'; import { Router } from '@angular/router'; -import { TranslateModule } from '@ngx-translate/core'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { OpenLayoutComponent } from '../../components/open-layout/open-layout'; import { DatabaseService } from '../../services'; -import { StatsCard } from './components/stats-card/stats-card'; -import { TaskCard } from './components/task-card/task-card'; -import { ProjectCard } from './components/project-card/project-card'; +import { OpenCard } from '../../components/open-card/open-card'; import { Task, Project, @@ -46,9 +44,7 @@ interface TimeStats { ButtonModule, OpenLayoutComponent, TranslateModule, - StatsCard, - TaskCard, - ProjectCard, + OpenCard, ], templateUrl: './open-home.html', styleUrl: './open-home.scss', @@ -56,6 +52,7 @@ interface TimeStats { export class OpenHome implements OnInit { private readonly router = inject(Router); private readonly dbService = inject(DatabaseService); + private readonly translate = inject(TranslateService); pendingTasks = signal([]); openProjects = signal([]); @@ -209,28 +206,26 @@ export class OpenHome implements OnInit { * Counts work days in the week from config */ private getWorkDaysCount(monthConfig: MonthConfig): number { - try { - const workDays = JSON.parse(monthConfig.workDays); - return Array.isArray(workDays) ? workDays.length : 5; - } catch { - return 5; - } + const workDays = monthConfig.workDays + ?.split(',') + .map((d) => parseInt(d, 10)) + .filter((d) => !isNaN(d)); + return workDays?.length ?? 5; } /** * Checks if a day of week is a work day */ private isWorkDay(dayOfWeek: number, monthConfig: MonthConfig): boolean { - try { - const workDays = JSON.parse(monthConfig.workDays); - return Array.isArray(workDays) && workDays.includes(dayOfWeek); - } catch { - return dayOfWeek >= 1 && dayOfWeek <= 5; - } + const workDays = monthConfig.workDays + ?.split(',') + .map((d) => parseInt(d, 10)) + .filter((d) => !isNaN(d)) || [1, 2, 3, 4, 5]; + return workDays.includes(dayOfWeek); } /** - * Gets target minutes for a specific day + * Gets target minutes for a specific day using daySchedule (same logic as calendar) */ private getDayTarget( date: Date, @@ -241,18 +236,26 @@ export class OpenHome implements OnInit { const override = dayOverrides.find((o) => o.date === dateStr); if (override) { + if (override.dayTypeId) { + return 0; + } return override.minutes ?? 0; } - const dayOfWeek = date.getDay(); + const dayOfWeek = date.getDay() === 0 ? 7 : date.getDay(); if (!this.isWorkDay(dayOfWeek, monthConfig)) { return 0; } - const workDaysCount = this.getWorkDaysCount(monthConfig); - return workDaysCount > 0 - ? Math.round(monthConfig.weeklyMinutes / workDaysCount) - : 0; + // Use daySchedule for per-day minutes (consistent with calendar) + try { + const daySchedule: Record = monthConfig.daySchedule + ? JSON.parse(monthConfig.daySchedule) + : { '1': 480, '2': 480, '3': 480, '4': 480, '5': 480 }; + return daySchedule[String(dayOfWeek)] ?? 480; + } catch { + return 480; + } } /** @@ -290,6 +293,59 @@ export class OpenHome implements OnInit { } } + /** + * Gets translated status display name + */ + getStatusDisplayName(statusName?: string): string { + if (!statusName) return this.translate.instant('status.pending'); + return this.translate.instant(statusName); + } + + /** + * Gets status severity for PrimeNG tag + */ + getStatusSeverity( + statusName?: string, + ): 'success' | 'info' | 'warn' | 'danger' | 'secondary' { + const name = statusName?.toLowerCase() ?? ''; + if ( + name.includes('completed') || + name.includes('completada') || + name.includes('done') + ) { + return 'success'; + } + if ( + name.includes('progress') || + name.includes('curso') || + name.includes('working') + ) { + return 'info'; + } + if ( + name.includes('blocked') || + name.includes('bloqueada') || + name.includes('error') + ) { + return 'danger'; + } + if ( + name.includes('pending') || + name.includes('pendiente') || + name.includes('todo') + ) { + return 'warn'; + } + return 'secondary'; + } + + /** + * Gets task tags as string array + */ + getTaskTags(task: Task): string[] { + return task.tags?.map((t) => t.tag.name) ?? []; + } + goToTasks(): void { this.router.navigate(['/tasks']); } diff --git a/src/app/pages/open-settings-statuses/open-settings-statuses.spec.ts b/src/app/pages/open-settings-statuses/open-settings-statuses.spec.ts index 1f0a250..0d4ef42 100644 --- a/src/app/pages/open-settings-statuses/open-settings-statuses.spec.ts +++ b/src/app/pages/open-settings-statuses/open-settings-statuses.spec.ts @@ -137,7 +137,7 @@ describe('OpenSettingsStatusesComponent', () => { '#ff0000', ); expect(component.newStatusName()).toBe(''); - expect(component.newStatusColor()).toBe('#6b7280'); + expect(component.newStatusColor()).toBe('#475569'); expect(mockMessageService.add).toHaveBeenCalled(); })); diff --git a/src/app/pages/open-settings-statuses/open-settings-statuses.ts b/src/app/pages/open-settings-statuses/open-settings-statuses.ts index 56e8070..e74b413 100644 --- a/src/app/pages/open-settings-statuses/open-settings-statuses.ts +++ b/src/app/pages/open-settings-statuses/open-settings-statuses.ts @@ -49,7 +49,7 @@ export class OpenSettingsStatusesComponent implements OnInit { readonly statuses = signal([]); readonly loading = signal(false); readonly newStatusName = signal(''); - readonly newStatusColor = signal('#6b7280'); + readonly newStatusColor = signal('#475569'); readonly editingId = signal(null); readonly editingName = signal(''); readonly editingColor = signal(''); @@ -87,7 +87,7 @@ export class OpenSettingsStatusesComponent implements OnInit { try { await this.db.createTaskStatus(name, this.newStatusColor()); this.newStatusName.set(''); - this.newStatusColor.set('#6b7280'); + this.newStatusColor.set('#475569'); await this.loadStatuses(); this.messageService.add({ severity: 'success', diff --git a/src/app/themes/_dialog-polish.scss b/src/app/themes/_dialog-polish.scss new file mode 100644 index 0000000..fd199fd --- /dev/null +++ b/src/app/themes/_dialog-polish.scss @@ -0,0 +1,45 @@ +/** + * Dialog Polish Mixin + * + * Provides consistent, professional styling for all dialog components. + * Includes elevation, typography hierarchy, spacing, and visual separation. + */ + +@mixin dialog-polish { + ::ng-deep .p-dialog { + box-shadow: var(--elevation-modal); + border-radius: var(--radius-lg); + + .p-dialog-header { + font-size: var(--font-size-xl); + font-weight: var(--font-weight-semibold); + padding-block: 1.25rem; + border-bottom: 1px solid var(--p-surface-border); + } + + .p-dialog-content { + padding-block: 1.5rem; + } + + .p-dialog-footer { + padding-block: 1rem; + border-top: 1px solid var(--p-surface-border); + gap: 0.75rem; + display: flex; + justify-content: flex-end; + } + + /* Input field labels in dialogs */ + label { + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + color: var(--p-text-color); + } + + /* Form field spacing */ + .p-field, + .p-fluid { + margin-bottom: 1rem; + } + } +} diff --git a/src/app/themes/aura-black.preset.ts b/src/app/themes/aura-black.preset.ts deleted file mode 100644 index ddc5e93..0000000 --- a/src/app/themes/aura-black.preset.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { definePreset } from '@primeuix/themes'; -import Aura from '@primeuix/themes/aura'; - -// Custom Aura theme with black colors -export const AuraBlack = definePreset(Aura, { - semantic: { - primary: { - 50: '{zinc.50}', - 100: '{zinc.100}', - 200: '{zinc.200}', - 300: '{zinc.300}', - 400: '{zinc.400}', - 500: '{zinc.500}', - 600: '{zinc.600}', - 700: '{zinc.700}', - 800: '{zinc.800}', - 900: '{zinc.900}', - 950: '{zinc.950}', - }, - colorScheme: { - light: { - primary: { - color: '{zinc.950}', - inverseColor: '#ffffff', - hoverColor: '{zinc.900}', - activeColor: '{zinc.800}', - }, - highlight: { - background: '{zinc.950}', - focusBackground: '{zinc.700}', - color: '#ffffff', - focusColor: '#ffffff', - }, - surface: { - 0: '#ffffff', - 50: '{slate.50}', - 100: '{slate.100}', - 200: '{slate.200}', - 300: '{slate.300}', - 400: '{slate.400}', - 500: '{slate.500}', - 600: '{slate.600}', - 700: '{slate.700}', - 800: '{slate.800}', - 900: '{slate.900}', - 950: '{slate.950}', - }, - }, - dark: { - primary: { - color: '{zinc.50}', - inverseColor: '{zinc.950}', - hoverColor: '{zinc.100}', - activeColor: '{zinc.200}', - }, - highlight: { - background: 'rgba(250, 250, 250, .16)', - focusBackground: 'rgba(250, 250, 250, .24)', - color: 'rgba(255,255,255,.87)', - focusColor: 'rgba(255,255,255,.87)', - }, - surface: { - 0: '#ffffff', - 50: '{gray.50}', - 100: '{gray.100}', - 200: '{gray.200}', - 300: '{gray.300}', - 400: '{gray.400}', - 500: '{gray.500}', - 600: '{gray.600}', - 700: '{gray.700}', - 800: '{gray.800}', - 900: '{gray.900}', - 950: '{gray.950}', - }, - }, - }, - focusRing: { - width: '2px', - style: 'solid', - color: '{primary.color}', - offset: '2px', - }, - }, -}); diff --git a/src/app/themes/aura-open.preset.ts b/src/app/themes/aura-open.preset.ts new file mode 100644 index 0000000..68ccd6e --- /dev/null +++ b/src/app/themes/aura-open.preset.ts @@ -0,0 +1,116 @@ +import { definePreset } from '@primeuix/themes'; +import Aura from '@primeuix/themes/aura'; + +/** + * Custom PrimeNG Aura preset for OpenTimeTracker with teal/cyan primary palette. + * + * Color Palette: + * - Light Mode: Teal primary (#0D9488), Slate surface, Dark text + * - Dark Mode: Teal primary (#14B8A6), Slate surface, Light text + * + * This preset uses ONLY the specified color palette without introducing + * additional semantic colors. Green, red, blue, etc. are inherited from + * the Aura base theme. + */ +export const AuraOpen = definePreset(Aura, { + semantic: { + // Primary palette based on teal/cyan colors + primary: { + 50: '{teal.50}', + 100: '{teal.100}', + 200: '{teal.200}', + 300: '{teal.300}', + 400: '{teal.400}', + 500: '{teal.500}', + 600: '{teal.600}', + 700: '{teal.700}', + 800: '{teal.800}', + 900: '{teal.900}', + 950: '{teal.950}', + }, + colorScheme: { + light: { + primary: { + color: '{teal.600}', // #0D9488 + contrastColor: '#ffffff', + hoverColor: '{teal.700}', // #0F766E + activeColor: '{teal.800}', + }, + highlight: { + background: '{teal.600}', + focusBackground: '{teal.700}', + color: '#ffffff', + focusColor: '#ffffff', + }, + surface: { + 0: '#ffffff', + 50: '{slate.50}', // #f8fafc - Background + 100: '{slate.100}', + 200: '{slate.200}', // #e2e8f0 - Surface + 300: '{slate.300}', + 400: '{slate.400}', + 500: '{slate.500}', + 600: '{slate.600}', + 700: '{slate.700}', + 800: '{slate.800}', + 900: '{slate.900}', // #0f172a - Text Primary + 950: '{slate.950}', + ground: '{slate.50}', // #F8FAFC - Background + card: '{surface.0}', + border: '{slate.300}', + hover: '{slate.100}', + }, + text: { + color: '{slate.900}', // #0F172A - Text Primary + hoverColor: '{slate.950}', + mutedColor: '{slate.600}', // #475569 - Text Secondary + hoverMutedColor: '{slate.700}', + }, + }, + dark: { + primary: { + color: '{teal.500}', // #14B8A6 + contrastColor: '{slate.900}', + hoverColor: '{teal.600}', // #0D9488 + activeColor: '{teal.700}', + }, + highlight: { + background: 'color-mix(in srgb, {teal.500} 16%, transparent)', + focusBackground: 'color-mix(in srgb, {teal.500} 24%, transparent)', + color: '{teal.200}', + focusColor: '{teal.100}', + }, + surface: { + 0: '#ffffff', + 50: '{slate.50}', + 100: '{slate.100}', + 200: '{slate.200}', + 300: '{slate.300}', + 400: '{slate.400}', + 500: '{slate.500}', + 600: '{slate.600}', + 700: '{slate.700}', + 800: '{slate.800}', // #1E293B - Surface + 900: '{slate.900}', // #0F172A - Background + 950: '{slate.950}', + ground: '{slate.900}', // #0F172A - Background + card: '{slate.800}', + border: '{slate.700}', + hover: '{slate.800}', + }, + text: { + color: '{slate.200}', // #E2E8F0 - Text Primary + hoverColor: '{slate.100}', + mutedColor: '{slate.400}', // #94A3B8 - Text Secondary + hoverMutedColor: '{slate.300}', + }, + }, + }, + focusRing: { + width: '2px', + style: 'solid', + color: '{primary.color}', + offset: '2px', + }, + }, +}); diff --git a/src/main.ts b/src/main.ts index 5df75f9..00ff9b7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,11 @@ +import 'zone.js'; + +import './styles.scss'; +import 'primeflex/primeflex.css'; +import 'primeicons/primeicons.css'; + import { bootstrapApplication } from '@angular/platform-browser'; import { appConfig } from './app/app.config'; import { App } from './app/app'; -bootstrapApplication(App, appConfig) - .catch((err) => console.error(err)); +bootstrapApplication(App, appConfig).catch((err) => console.error(err)); diff --git a/src/stories/Configure.mdx b/src/stories/Configure.mdx deleted file mode 100644 index 384fafa..0000000 --- a/src/stories/Configure.mdx +++ /dev/null @@ -1,364 +0,0 @@ -import { Meta } from "@storybook/addon-docs/blocks"; - -import Github from "./assets/github.svg"; -import Discord from "./assets/discord.svg"; -import Youtube from "./assets/youtube.svg"; -import Tutorials from "./assets/tutorials.svg"; -import Styling from "./assets/styling.png"; -import Context from "./assets/context.png"; -import Assets from "./assets/assets.png"; -import Docs from "./assets/docs.png"; -import Share from "./assets/share.png"; -import FigmaPlugin from "./assets/figma-plugin.png"; -import Testing from "./assets/testing.png"; -import Accessibility from "./assets/accessibility.png"; -import Theming from "./assets/theming.png"; -import AddonLibrary from "./assets/addon-library.png"; - -export const RightArrow = () => - - - - - -
-
- # Configure your project - - Because Storybook works separately from your app, you'll need to configure it for your specific stack and setup. Below, explore guides for configuring Storybook with popular frameworks and tools. If you get stuck, learn how you can ask for help from our community. -
-
-
- A wall of logos representing different styling technologies -

Add styling and CSS

-

Like with web applications, there are many ways to include CSS within Storybook. Learn more about setting up styling within Storybook.

- Learn more -
-
- An abstraction representing the composition of data for a component -

Provide context and mocking

-

Often when a story doesn't render, it's because your component is expecting a specific environment or context (like a theme provider) to be available.

- Learn more -
-
- A representation of typography and image assets -
-

Load assets and resources

-

To link static files (like fonts) to your projects and stories, use the - `staticDirs` configuration option to specify folders to load when - starting Storybook.

- Learn more -
-
-
-
-
-
- # Do more with Storybook - - Now that you know the basics, let's explore other parts of Storybook that will improve your experience. This list is just to get you started. You can customise Storybook in many ways to fit your needs. -
- -
-
-
- A screenshot showing the autodocs tag being set, pointing a docs page being generated -

Autodocs

-

Auto-generate living, - interactive reference documentation from your components and stories.

- Learn more -
-
- A browser window showing a Storybook being published to a chromatic.com URL -

Publish to Chromatic

-

Publish your Storybook to review and collaborate with your entire team.

- Learn more -
-
- Windows showing the Storybook plugin in Figma -

Figma Plugin

-

Embed your stories into Figma to cross-reference the design and live - implementation in one place.

- Learn more -
-
- Screenshot of tests passing and failing -

Testing

-

Use stories to test a component in all its variations, no matter how - complex.

- Learn more -
-
- Screenshot of accessibility tests passing and failing -

Accessibility

-

Automatically test your components for a11y issues as you develop.

- Learn more -
-
- Screenshot of Storybook in light and dark mode -

Theming

-

Theme Storybook's UI to personalize it to your project.

- Learn more -
-
-
-
-
-
-

Addons

-

Integrate your tools with Storybook to connect workflows.

- Discover all addons -
-
- Integrate your tools with Storybook to connect workflows. -
-
- -
-
- Github logo - Join our contributors building the future of UI development. - - Star on GitHub -
-
- Discord logo -
- Get support and chat with frontend developers. - - Join Discord server -
-
-
- Youtube logo -
- Watch tutorials, feature previews and interviews. - - Watch on YouTube -
-
-
- A book -

Follow guided walkthroughs on for key workflows.

- - Discover tutorials -
-
- - diff --git a/src/stories/assets/accessibility.png b/src/stories/assets/accessibility.png deleted file mode 100644 index 6ffe6fe..0000000 Binary files a/src/stories/assets/accessibility.png and /dev/null differ diff --git a/src/stories/assets/accessibility.svg b/src/stories/assets/accessibility.svg deleted file mode 100644 index 107e93f..0000000 --- a/src/stories/assets/accessibility.svg +++ /dev/null @@ -1 +0,0 @@ -Accessibility \ No newline at end of file diff --git a/src/stories/assets/addon-library.png b/src/stories/assets/addon-library.png deleted file mode 100644 index 95deb38..0000000 Binary files a/src/stories/assets/addon-library.png and /dev/null differ diff --git a/src/stories/assets/assets.png b/src/stories/assets/assets.png deleted file mode 100644 index cfba681..0000000 Binary files a/src/stories/assets/assets.png and /dev/null differ diff --git a/src/stories/assets/avif-test-image.avif b/src/stories/assets/avif-test-image.avif deleted file mode 100644 index 530709b..0000000 Binary files a/src/stories/assets/avif-test-image.avif and /dev/null differ diff --git a/src/stories/assets/context.png b/src/stories/assets/context.png deleted file mode 100644 index e5cd249..0000000 Binary files a/src/stories/assets/context.png and /dev/null differ diff --git a/src/stories/assets/discord.svg b/src/stories/assets/discord.svg deleted file mode 100644 index d638958..0000000 --- a/src/stories/assets/discord.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/stories/assets/docs.png b/src/stories/assets/docs.png deleted file mode 100644 index a749629..0000000 Binary files a/src/stories/assets/docs.png and /dev/null differ diff --git a/src/stories/assets/figma-plugin.png b/src/stories/assets/figma-plugin.png deleted file mode 100644 index 8f79b08..0000000 Binary files a/src/stories/assets/figma-plugin.png and /dev/null differ diff --git a/src/stories/assets/github.svg b/src/stories/assets/github.svg deleted file mode 100644 index dc51352..0000000 --- a/src/stories/assets/github.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/stories/assets/share.png b/src/stories/assets/share.png deleted file mode 100644 index 8097a37..0000000 Binary files a/src/stories/assets/share.png and /dev/null differ diff --git a/src/stories/assets/styling.png b/src/stories/assets/styling.png deleted file mode 100644 index d341e82..0000000 Binary files a/src/stories/assets/styling.png and /dev/null differ diff --git a/src/stories/assets/testing.png b/src/stories/assets/testing.png deleted file mode 100644 index d4ac39a..0000000 Binary files a/src/stories/assets/testing.png and /dev/null differ diff --git a/src/stories/assets/theming.png b/src/stories/assets/theming.png deleted file mode 100644 index 1535eb9..0000000 Binary files a/src/stories/assets/theming.png and /dev/null differ diff --git a/src/stories/assets/tutorials.svg b/src/stories/assets/tutorials.svg deleted file mode 100644 index b492a9c..0000000 --- a/src/stories/assets/tutorials.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/stories/assets/youtube.svg b/src/stories/assets/youtube.svg deleted file mode 100644 index a7515d7..0000000 --- a/src/stories/assets/youtube.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/stories/button.component.ts b/src/stories/button.component.ts deleted file mode 100644 index 1e63617..0000000 --- a/src/stories/button.component.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { Component, Input, Output, EventEmitter } from '@angular/core'; - -@Component({ - selector: 'app-storybook-button', - standalone: true, - imports: [CommonModule], - template: ` `, - styleUrls: ['./button.css'], -}) -export class ButtonComponent { - /** Is this the principal call to action on the page? */ - @Input() - primary = false; - - /** What background color to use */ - @Input() - backgroundColor?: string; - - /** How large should the button be? */ - @Input() - size: 'small' | 'medium' | 'large' = 'medium'; - - /** - * Button contents - * - * @required - */ - @Input() - label = 'Button'; - - /** Optional click handler */ - @Output() - buttonClick = new EventEmitter(); - - public get classes(): string[] { - const mode = this.primary - ? 'storybook-button--primary' - : 'storybook-button--secondary'; - - return ['storybook-button', `storybook-button--${this.size}`, mode]; - } -} diff --git a/src/stories/button.css b/src/stories/button.css deleted file mode 100644 index 4e3620b..0000000 --- a/src/stories/button.css +++ /dev/null @@ -1,30 +0,0 @@ -.storybook-button { - display: inline-block; - cursor: pointer; - border: 0; - border-radius: 3em; - font-weight: 700; - line-height: 1; - font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; -} -.storybook-button--primary { - background-color: #555ab9; - color: white; -} -.storybook-button--secondary { - box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset; - background-color: transparent; - color: #333; -} -.storybook-button--small { - padding: 10px 16px; - font-size: 12px; -} -.storybook-button--medium { - padding: 11px 20px; - font-size: 14px; -} -.storybook-button--large { - padding: 12px 24px; - font-size: 16px; -} diff --git a/src/stories/button.stories.ts b/src/stories/button.stories.ts deleted file mode 100644 index 1fa6c9e..0000000 --- a/src/stories/button.stories.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/angular'; -import { fn } from 'storybook/test'; - -import { ButtonComponent } from './button.component'; - -// More on how to set up stories at: https://storybook.js.org/docs/writing-stories -const meta: Meta = { - title: 'Example/Button', - component: ButtonComponent, - tags: ['autodocs'], - argTypes: { - backgroundColor: { - control: 'color', - }, - }, - // Use `fn` to spy on the buttonClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#story-args - args: { buttonClick: fn() }, -}; - -export default meta; -type Story = StoryObj; - -// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args -export const Primary: Story = { - args: { - primary: true, - label: 'Button', - }, -}; - -export const Secondary: Story = { - args: { - label: 'Button', - }, -}; - -export const Large: Story = { - args: { - size: 'large', - label: 'Button', - }, -}; - -export const Small: Story = { - args: { - size: 'small', - label: 'Button', - }, -}; diff --git a/src/stories/header.component.ts b/src/stories/header.component.ts deleted file mode 100644 index 8ebe43c..0000000 --- a/src/stories/header.component.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { Component, Input, Output, EventEmitter } from '@angular/core'; -import { CommonModule } from '@angular/common'; - -import { ButtonComponent } from './button.component'; -import type { User } from './user'; - -@Component({ - selector: 'app-storybook-header', - standalone: true, - imports: [CommonModule, ButtonComponent], - template: `
-
-
- - - - - - - -

Acme

-
-
- @if (user) { - - Welcome, {{ user.name }}! - - - } - @if (!user) { - - - } -
-
-
`, - styleUrls: ['./header.css'], -}) -export class HeaderComponent { - @Input() - user: User | null = null; - - @Output() - login = new EventEmitter(); - - @Output() - logout = new EventEmitter(); - - @Output() - createAccount = new EventEmitter(); -} diff --git a/src/stories/header.css b/src/stories/header.css deleted file mode 100644 index 5efd46c..0000000 --- a/src/stories/header.css +++ /dev/null @@ -1,32 +0,0 @@ -.storybook-header { - display: flex; - justify-content: space-between; - align-items: center; - border-bottom: 1px solid rgba(0, 0, 0, 0.1); - padding: 15px 20px; - font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; -} - -.storybook-header svg { - display: inline-block; - vertical-align: top; -} - -.storybook-header h1 { - display: inline-block; - vertical-align: top; - margin: 6px 0 6px 10px; - font-weight: 700; - font-size: 20px; - line-height: 1; -} - -.storybook-header button + button { - margin-left: 10px; -} - -.storybook-header .welcome { - margin-right: 10px; - color: #333; - font-size: 14px; -} diff --git a/src/stories/header.stories.ts b/src/stories/header.stories.ts deleted file mode 100644 index 925d792..0000000 --- a/src/stories/header.stories.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/angular'; -import { fn } from 'storybook/test'; - -import { HeaderComponent } from './header.component'; - -const meta: Meta = { - title: 'Example/Header', - component: HeaderComponent, - // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs - tags: ['autodocs'], - parameters: { - // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout - layout: 'fullscreen', - }, - args: { - login: fn(), - logout: fn(), - createAccount: fn(), - }, -}; - -export default meta; -type Story = StoryObj; - -export const LoggedIn: Story = { - args: { - user: { - name: 'Jane Doe', - }, - }, -}; - -export const LoggedOut: Story = {}; diff --git a/src/stories/page.component.ts b/src/stories/page.component.ts deleted file mode 100644 index 2a31e54..0000000 --- a/src/stories/page.component.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { Component } from '@angular/core'; -import { CommonModule } from '@angular/common'; - -import { HeaderComponent } from './header.component'; -import type { User } from './user'; - -@Component({ - selector: 'app-storybook-page', - standalone: true, - imports: [CommonModule, HeaderComponent], - template: `
- -
-

Pages in Storybook

-

- We recommend building UIs with a - - component-driven - - process starting with atomic components and ending with pages. -

-

- Render pages with mock data. This makes it easy to build and review page - states without needing to navigate to them in your app. Here are some - handy patterns for managing page data in Storybook: -

-
    -
  • - Use a higher-level connected component. Storybook helps you compose - such data from the "args" of child component stories -
  • -
  • - Assemble data in the page component from your services. You can mock - these services out using Storybook. -
  • -
-

- Get a guided tutorial on component-driven development at - - Storybook tutorials - - . Read more in the - - docs - - . -

-
- Tip Adjust the width of the canvas with the - - - - - - Viewports addon in the toolbar -
-
-
`, - styleUrls: ['./page.css'], -}) -export class PageComponent { - user: User | null = null; - - doLogout() { - this.user = null; - } - - doLogin() { - this.user = { name: 'Jane Doe' }; - } - - doCreateAccount() { - this.user = { name: 'Jane Doe' }; - } -} diff --git a/src/stories/page.css b/src/stories/page.css deleted file mode 100644 index 77c81d2..0000000 --- a/src/stories/page.css +++ /dev/null @@ -1,68 +0,0 @@ -.storybook-page { - margin: 0 auto; - padding: 48px 20px; - max-width: 600px; - color: #333; - font-size: 14px; - line-height: 24px; - font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; -} - -.storybook-page h2 { - display: inline-block; - vertical-align: top; - margin: 0 0 4px; - font-weight: 700; - font-size: 32px; - line-height: 1; -} - -.storybook-page p { - margin: 1em 0; -} - -.storybook-page a { - color: inherit; -} - -.storybook-page ul { - margin: 1em 0; - padding-left: 30px; -} - -.storybook-page li { - margin-bottom: 8px; -} - -.storybook-page .tip { - display: inline-block; - vertical-align: top; - margin-right: 10px; - border-radius: 1em; - background: #e7fdd8; - padding: 4px 12px; - color: #357a14; - font-weight: 700; - font-size: 11px; - line-height: 12px; -} - -.storybook-page .tip-wrapper { - margin-top: 40px; - margin-bottom: 40px; - font-size: 13px; - line-height: 20px; -} - -.storybook-page .tip-wrapper svg { - display: inline-block; - vertical-align: top; - margin-top: 3px; - margin-right: 4px; - width: 12px; - height: 12px; -} - -.storybook-page .tip-wrapper svg path { - fill: #1ea7fd; -} diff --git a/src/stories/page.stories.ts b/src/stories/page.stories.ts deleted file mode 100644 index 659a14c..0000000 --- a/src/stories/page.stories.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/angular'; -import { expect, userEvent, within } from 'storybook/test'; - -import { PageComponent } from './page.component'; - -const meta: Meta = { - title: 'Example/Page', - component: PageComponent, - parameters: { - // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout - layout: 'fullscreen', - }, -}; - -export default meta; -type Story = StoryObj; - -export const LoggedOut: Story = {}; - -// More on component testing: https://storybook.js.org/docs/writing-tests/interaction-testing -export const LoggedIn: Story = { - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - const loginButton = canvas.getByRole('button', { name: /Log in/i }); - await expect(loginButton).toBeInTheDocument(); - await userEvent.click(loginButton); - await expect(loginButton).not.toBeInTheDocument(); - - const logoutButton = canvas.getByRole('button', { name: /Log out/i }); - await expect(logoutButton).toBeInTheDocument(); - }, -}; diff --git a/src/stories/user.ts b/src/stories/user.ts deleted file mode 100644 index c664619..0000000 --- a/src/stories/user.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface User { - name: string; -} diff --git a/src/styles.scss b/src/styles.scss index c96edd5..122bae3 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -10,6 +10,43 @@ html { font-size: 14px; + /* Design System Tokens - Elevation */ + --elevation-none: none; + --elevation-xs: 0 1px 2px rgba(15, 23, 42, 0.05); + --elevation-sm: 0 2px 4px rgba(15, 23, 42, 0.06); + --elevation-md: 0 4px 8px rgba(15, 23, 42, 0.08); + --elevation-lg: 0 8px 16px rgba(15, 23, 42, 0.1); + --elevation-xl: 0 12px 24px rgba(15, 23, 42, 0.12); + --elevation-hover: 0 8px 16px rgba(15, 23, 42, 0.12); + --elevation-modal: 0 24px 48px rgba(15, 23, 42, 0.2); + + /* Design System Tokens - Typography */ + --font-size-xs: 0.75rem; + --font-size-sm: 0.875rem; + --font-size-base: 1rem; + --font-size-lg: 1.125rem; + --font-size-xl: 1.25rem; + --font-size-2xl: 1.5rem; + --font-size-3xl: 2rem; + --font-size-4xl: 2.5rem; + + --font-weight-normal: 400; + --font-weight-medium: 500; + --font-weight-semibold: 600; + --font-weight-bold: 700; + + /* Design System Tokens - Border Radius */ + --radius-sm: 4px; + --radius-md: 6px; + --radius-lg: 8px; + --radius-xl: 12px; + --radius-full: 9999px; + + /* Design System Tokens - Transitions */ + --transition-fast: 150ms; + --transition-base: 200ms; + --transition-slow: 300ms; + /* Custom semantic colors - Light mode */ --c-positive-bg: var(--p-green-50); --c-positive-color: var(--p-green-600); @@ -19,16 +56,25 @@ html { --c-over-color: var(--p-blue-600); --c-info-bg: var(--p-blue-50); --c-info-color: var(--p-blue-700); - --c-primary-bg: var(--p-primary-50); - --c-primary-color: var(--p-primary-600); - --c-primary-border: var(--p-primary-200); - --c-surface-subtle: var(--p-surface-50); - --c-surface-muted: var(--p-surface-100); + --c-primary-bg: var(--p-teal-50); + --c-primary-color: var(--p-teal-600); + --c-primary-border: var(--p-teal-200); + --c-surface-subtle: var(--p-slate-50); + --c-surface-muted: var(--p-slate-100); } html.my-app-dark { - background-color: var(--p-surface-900); - color: var(--p-surface-0); + background-color: var(--p-slate-900); + color: var(--p-slate-200); + + /* Design System Tokens - Elevation (Dark mode) */ + --elevation-xs: 0 1px 2px rgba(0, 0, 0, 0.15); + --elevation-sm: 0 2px 4px rgba(0, 0, 0, 0.2); + --elevation-md: 0 4px 8px rgba(0, 0, 0, 0.25); + --elevation-lg: 0 8px 16px rgba(0, 0, 0, 0.3); + --elevation-xl: 0 12px 24px rgba(0, 0, 0, 0.35); + --elevation-hover: 0 8px 16px rgba(0, 0, 0, 0.35); + --elevation-modal: 0 24px 48px rgba(0, 0, 0, 0.5); /* Custom semantic colors - Dark mode */ --c-positive-bg: color-mix(in srgb, var(--p-green-500) 15%, transparent); @@ -39,11 +85,11 @@ html.my-app-dark { --c-over-color: var(--p-blue-400); --c-info-bg: color-mix(in srgb, var(--p-blue-500) 15%, transparent); --c-info-color: var(--p-blue-400); - --c-primary-bg: color-mix(in srgb, var(--p-primary-500) 15%, transparent); - --c-primary-color: var(--p-primary-400); - --c-primary-border: color-mix(in srgb, var(--p-primary-500) 30%, transparent); - --c-surface-subtle: color-mix(in srgb, var(--p-surface-0) 3%, transparent); - --c-surface-muted: color-mix(in srgb, var(--p-surface-0) 8%, transparent); + --c-primary-bg: color-mix(in srgb, var(--p-teal-500) 15%, transparent); + --c-primary-color: var(--p-teal-400); + --c-primary-border: color-mix(in srgb, var(--p-teal-500) 30%, transparent); + --c-surface-subtle: color-mix(in srgb, var(--p-slate-200) 3%, transparent); + --c-surface-muted: color-mix(in srgb, var(--p-slate-200) 8%, transparent); } body { @@ -54,7 +100,66 @@ body { color 0.3s; } -.p-button:hover { - transform: translateY(-1px); - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +/* Enhanced input states */ +.p-inputtext, +.p-dropdown, +.p-multiselect, +.p-calendar .p-inputtext { + transition: + border-color var(--transition-base), + box-shadow var(--transition-base); + + &:enabled:focus { + border-color: var(--p-primary-color); + box-shadow: 0 0 0 3px + color-mix(in srgb, var(--p-primary-color) 15%, transparent); + } + + &.p-invalid { + border-color: var(--p-red-500); + + &:focus { + box-shadow: 0 0 0 3px + color-mix(in srgb, var(--p-red-500) 15%, transparent); + } + } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } +} + +/* Enhanced button states */ +.p-button { + transition: + transform var(--transition-base), + box-shadow var(--transition-base); + + &:enabled:hover { + transform: translateY(-1px); + box-shadow: var(--elevation-hover); + } + + &:enabled:active { + transform: translateY(0); + box-shadow: var(--elevation-sm); + } + + &:focus-visible { + outline: 2px solid var(--p-primary-color); + outline-offset: 2px; + } +} + +/* Enhanced interactive states for clickable elements */ +.p-checkbox, +.p-radiobutton { + &:focus-within { + .p-checkbox-box, + .p-radiobutton-box { + box-shadow: 0 0 0 3px + color-mix(in srgb, var(--p-primary-color) 15%, transparent); + } + } }