mirror of
https://github.com/aljazceru/goose.git
synced 2026-01-31 12:14:32 +01:00
Removed ui-v2 directory and updated project to use node in hermit and readme (#2831)
This commit is contained in:
@@ -9,13 +9,3 @@ if git diff --cached --name-only | grep -q "^ui/desktop/"; then
|
||||
echo "Warning: ui/desktop directory does not exist, skipping lint-staged"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Only auto-format ui-v2 TS code if relevant files are modified
|
||||
if git diff --cached --name-only | grep -q "^ui-v2/"; then
|
||||
if [ -d "ui-v2" ]; then
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
cd ui-v2 && npx lint-staged
|
||||
else
|
||||
echo "Warning: ui-v2 directory does not exist, skipping lint-staged"
|
||||
fi
|
||||
fi
|
||||
|
||||
28
ui-v2/.gitignore
vendored
28
ui-v2/.gitignore
vendored
@@ -1,28 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
dist-electron
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode
|
||||
.idea
|
||||
.DS_Store
|
||||
|
||||
# Build output
|
||||
.vite/
|
||||
out/
|
||||
|
||||
# Generated JavaScript files in source
|
||||
electron/**/*.js
|
||||
!electron/**/*.config.js
|
||||
|
||||
# Playwright
|
||||
test-results/
|
||||
playwright-report/
|
||||
playwright/.cache/
|
||||
@@ -1 +0,0 @@
|
||||
registry=https://registry.npmjs.org/
|
||||
@@ -1 +0,0 @@
|
||||
v23
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"semi": true,
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
node_modules
|
||||
dist
|
||||
.vite
|
||||
coverage
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"extends": ["stylelint-config-standard"],
|
||||
"rules": {
|
||||
"at-rule-no-unknown": [
|
||||
true,
|
||||
{
|
||||
"ignoreAtRules": ["tailwind", "apply", "variants", "responsive", "screen", "theme", "custom-variant"]
|
||||
}
|
||||
],
|
||||
"at-rule-no-deprecated": null,
|
||||
"custom-property-pattern": null,
|
||||
"no-descending-specificity": null
|
||||
}
|
||||
}
|
||||
156
ui-v2/README.md
156
ui-v2/README.md
@@ -1,156 +0,0 @@
|
||||
# codename goose ui v2
|
||||
|
||||
Your on-machine AI agent, automating tasks seamlessly.
|
||||
|
||||
## Development
|
||||
|
||||
### Getting Started
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Start electron development server
|
||||
npm start
|
||||
```
|
||||
|
||||
### Building and Packaging
|
||||
|
||||
```bash
|
||||
# Build electron application
|
||||
npm run build
|
||||
|
||||
# Package electron app
|
||||
npm run package
|
||||
|
||||
# Create distributable
|
||||
npm run make
|
||||
```
|
||||
|
||||
### Quality and Testing
|
||||
|
||||
```bash
|
||||
# Run tests
|
||||
npm test
|
||||
|
||||
# Run tests with UI
|
||||
npm run test:ui
|
||||
|
||||
# Generate test coverage
|
||||
npm run test:coverage
|
||||
|
||||
# End-to-End Testing
|
||||
npm run test:e2e # Run all e2e tests headlessly
|
||||
npm run test:e2e:ui # Run e2e tests with UI mode
|
||||
|
||||
# Type checking
|
||||
npm run typecheck # Check all TypeScript files
|
||||
npm run tsc:electron # Check electron TypeScript files
|
||||
|
||||
# Linting
|
||||
npm run lint # Run all linting
|
||||
npm run lint:fix # Fix linting issues
|
||||
npm run lint:style # Check CSS
|
||||
npm run lint:style:fix # Fix CSS issues
|
||||
|
||||
# Code formatting
|
||||
npm run prettier # Check formatting
|
||||
npm run prettier:fix # Fix formatting
|
||||
npm run format # Fix all formatting (prettier + style)
|
||||
|
||||
# Run all checks (types, lint, format)
|
||||
npm run check-all
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
├── electron/ # Electron main process files
|
||||
│ ├── main.ts # Main process entry
|
||||
│ └── preload.ts # Preload script
|
||||
├── src/
|
||||
│ ├── components/ # React components
|
||||
│ ├── services/ # Application services
|
||||
│ │ └── platform/ # Platform services
|
||||
│ ├── test/ # Test setup and configurations
|
||||
│ │ ├── e2e/ # End-to-end test files
|
||||
│ │ ├── setup.ts
|
||||
│ │ └── types.d.ts
|
||||
│ └── App.tsx # Main application component
|
||||
├── index.html # HTML template
|
||||
├── playwright.config.ts # Playwright e2e test configuration
|
||||
├── vite.config.ts # Base Vite configuration
|
||||
├── vite.main.config.ts # Vite config for electron main
|
||||
├── vite.preload.config.ts # Vite config for preload script
|
||||
├── vite.renderer.config.ts # Vite config for electron renderer
|
||||
├── tsconfig.json # Base TypeScript configuration
|
||||
├── tsconfig.electron.json # TypeScript config for electron
|
||||
├── tsconfig.node.json # TypeScript config for Node.js
|
||||
└── forge.config.ts # Electron Forge configuration
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
The application is built as an Electron desktop application with a modern React frontend. Here's a detailed breakdown of the key architectural components:
|
||||
|
||||
### Platform Services
|
||||
|
||||
The application uses a service-based architecture to handle platform-specific functionality:
|
||||
|
||||
```typescript
|
||||
// Platform Service Interface
|
||||
export interface IPlatformService {
|
||||
copyToClipboard(text: string): Promise<void>;
|
||||
// Additional platform-specific operations
|
||||
}
|
||||
```
|
||||
|
||||
### Electron Integration
|
||||
|
||||
The architecture includes several key Electron-specific components:
|
||||
|
||||
1. **Preload Script**: Safely exposes Electron APIs to the renderer process
|
||||
|
||||
```typescript
|
||||
// Type definitions for Electron APIs
|
||||
declare global {
|
||||
interface Window {
|
||||
electronAPI: {
|
||||
copyToClipboard: (text: string) => Promise<void>;
|
||||
// Other API methods
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **IPC Communication**: Typed handlers for main process communication
|
||||
|
||||
```typescript
|
||||
// Electron platform service implementation
|
||||
export class PlatformService implements IPlatformService {
|
||||
async copyToClipboard(text: string): Promise<void> {
|
||||
return window.electronAPI.copyToClipboard(text);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Build System
|
||||
|
||||
The project uses a multi-configuration build system:
|
||||
|
||||
1. **Main Process**: Built using `vite.main.config.ts`
|
||||
- Handles core Electron functionality
|
||||
- Manages window creation and system integration
|
||||
|
||||
2. **Renderer Process**: Built using `vite.renderer.config.ts`
|
||||
- React application
|
||||
- UI components and application logic
|
||||
|
||||
3. **Preload Scripts**: Built using `vite.preload.config.ts`
|
||||
- Secure bridge between main and renderer processes
|
||||
- Exposes limited API surface to frontend
|
||||
|
||||
The build process is managed by Electron Forge, which handles:
|
||||
- Development environment setup
|
||||
- Application packaging
|
||||
- Distribution creation for various platforms
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"$schema": "http://localhost:3000/r/registry.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/styles/main.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
|
||||
import { app, BrowserWindow, ipcMain, clipboard, dialog, session } from 'electron';
|
||||
import type {
|
||||
IpcMainInvokeEvent,
|
||||
OnHeadersReceivedListenerDetails,
|
||||
HeadersReceivedResponse,
|
||||
} from 'electron';
|
||||
|
||||
const isDevelopment = !app.isPackaged;
|
||||
const isHeadless = process.env.HEADLESS === 'true';
|
||||
|
||||
// Enable sandbox before app is ready
|
||||
app.enableSandbox();
|
||||
|
||||
if (isHeadless) {
|
||||
app.disableHardwareAcceleration();
|
||||
}
|
||||
|
||||
async function createWindow() {
|
||||
// Handle different preload paths for dev and prod
|
||||
const preloadPath = isDevelopment
|
||||
? path.join(app.getAppPath(), '.vite/build/preload/preload.js')
|
||||
: path.join(__dirname, 'preload.js');
|
||||
|
||||
// Create the browser window with headless options when needed
|
||||
const mainWindow = new BrowserWindow({
|
||||
titleBarStyle: process.platform === 'darwin' ? 'hidden' : 'default',
|
||||
...(process.platform === 'darwin' ? { trafficLightPosition: { x: 16, y: 10 } } : {}),
|
||||
frame: false,
|
||||
width: 1200,
|
||||
height: 800,
|
||||
minWidth: 800,
|
||||
...(isHeadless
|
||||
? {
|
||||
show: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
webSecurity: true,
|
||||
preload: preloadPath,
|
||||
sandbox: true,
|
||||
offscreen: true,
|
||||
},
|
||||
}
|
||||
: {
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
webSecurity: true,
|
||||
preload: preloadPath,
|
||||
sandbox: true,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
// Set up CSP
|
||||
session.defaultSession.webRequest.onHeadersReceived(
|
||||
(
|
||||
details: OnHeadersReceivedListenerDetails,
|
||||
callback: (response: HeadersReceivedResponse) => void
|
||||
) => {
|
||||
callback({
|
||||
responseHeaders: {
|
||||
...details.responseHeaders,
|
||||
'Content-Security-Policy': [
|
||||
isDevelopment
|
||||
? `
|
||||
default-src 'self';
|
||||
script-src 'self' 'unsafe-inline';
|
||||
style-src 'self' 'unsafe-inline';
|
||||
connect-src 'self' ws://localhost:3001 http://localhost:3001;
|
||||
img-src 'self' data: https:;
|
||||
font-src 'self' data: https://cash-f.squarecdn.com;
|
||||
`
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
: `
|
||||
default-src 'self';
|
||||
script-src 'self';
|
||||
style-src 'self' 'unsafe-inline';
|
||||
img-src 'self' data: https:;
|
||||
font-src 'self' data: https://cash-f.squarecdn.com;
|
||||
`
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim(),
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// Set up IPC handlers
|
||||
ipcMain.handle('clipboard-copy', async (_: IpcMainInvokeEvent, text: string) => {
|
||||
clipboard.writeText(text);
|
||||
});
|
||||
|
||||
ipcMain.handle('clipboard-read', async () => {
|
||||
return clipboard.readText();
|
||||
});
|
||||
|
||||
interface SaveFileParams {
|
||||
content: string;
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
interface SaveFileResult {
|
||||
success: boolean;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
ipcMain.handle(
|
||||
'save-file',
|
||||
async (
|
||||
_: IpcMainInvokeEvent,
|
||||
{ content, fileName }: SaveFileParams
|
||||
): Promise<SaveFileResult> => {
|
||||
const { filePath } = await dialog.showSaveDialog({
|
||||
defaultPath: fileName,
|
||||
});
|
||||
|
||||
if (filePath) {
|
||||
await fs.writeFile(filePath, content, 'utf8');
|
||||
return { success: true, path: filePath };
|
||||
}
|
||||
return { success: false };
|
||||
}
|
||||
);
|
||||
|
||||
// Load the app
|
||||
if (isDevelopment) {
|
||||
mainWindow.loadURL('http://localhost:3001/');
|
||||
if (!isHeadless) {
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
} else {
|
||||
// In production, load from the asar archive
|
||||
mainWindow.loadFile(path.join(__dirname, 'renderer/index.html'));
|
||||
}
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow().catch(console.error);
|
||||
|
||||
app.on('activate', function () {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow().catch(console.error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
|
||||
// Define the API interface
|
||||
interface ElectronAPI {
|
||||
copyToClipboard(text: string): Promise<void>;
|
||||
}
|
||||
|
||||
// Expose protected methods that allow the renderer process to use
|
||||
// the ipcRenderer without exposing the entire object
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
copyToClipboard: (text: string) => ipcRenderer.invoke('clipboard-copy', text),
|
||||
} as ElectronAPI);
|
||||
@@ -1,143 +0,0 @@
|
||||
const eslint = require('@eslint/js');
|
||||
const typescript = require('@typescript-eslint/eslint-plugin');
|
||||
const typescriptParser = require('@typescript-eslint/parser');
|
||||
const importPlugin = require('eslint-plugin-import');
|
||||
const prettier = require('eslint-plugin-prettier');
|
||||
const react = require('eslint-plugin-react');
|
||||
const reactHooks = require('eslint-plugin-react-hooks');
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
ignores: ['**/node_modules/**', '**/dist/**', '**/out/**', '**/coverage/**', '**/.vite/**'],
|
||||
},
|
||||
// Configuration for Node.js files
|
||||
{
|
||||
files: ['**/*.{js,cjs,ts,mts}'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
module: 'readonly',
|
||||
require: 'readonly',
|
||||
process: 'readonly',
|
||||
__dirname: 'readonly',
|
||||
__filename: 'readonly',
|
||||
},
|
||||
},
|
||||
},
|
||||
// Base configuration for all files
|
||||
{
|
||||
files: ['**/*.{js,jsx,ts,tsx}'],
|
||||
plugins: {
|
||||
'@typescript-eslint': typescript,
|
||||
react,
|
||||
'react-hooks': reactHooks,
|
||||
import: importPlugin,
|
||||
prettier,
|
||||
},
|
||||
languageOptions: {
|
||||
parser: typescriptParser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
...eslint.configs.recommended.rules,
|
||||
...typescript.configs.recommended.rules,
|
||||
...react.configs.recommended.rules,
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'prettier/prettier': 'error',
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'error',
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||
'import/order': [
|
||||
'error',
|
||||
{
|
||||
groups: ['builtin', 'external', 'internal', ['parent', 'sibling']],
|
||||
pathGroups: [
|
||||
{
|
||||
pattern: 'react',
|
||||
group: 'external',
|
||||
position: 'before',
|
||||
},
|
||||
],
|
||||
pathGroupsExcludedImportTypes: ['react'],
|
||||
'newlines-between': 'always',
|
||||
alphabetize: {
|
||||
order: 'asc',
|
||||
caseInsensitive: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
// Browser-specific configuration
|
||||
{
|
||||
files: ['src/**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
window: 'readonly',
|
||||
document: 'readonly',
|
||||
navigator: 'readonly',
|
||||
console: 'readonly',
|
||||
setTimeout: 'readonly',
|
||||
setInterval: 'readonly',
|
||||
clearTimeout: 'readonly',
|
||||
clearInterval: 'readonly',
|
||||
requestAnimationFrame: 'readonly',
|
||||
localStorage: 'readonly',
|
||||
HTMLDivElement: 'readonly',
|
||||
HTMLTextAreaElement: 'readonly',
|
||||
HTMLFormElement: 'readonly',
|
||||
HTMLInputElement: 'readonly',
|
||||
MutationObserver: 'readonly',
|
||||
IntersectionObserver: 'readonly',
|
||||
Blob: 'readonly',
|
||||
SVGSVGElement: 'readonly',
|
||||
},
|
||||
},
|
||||
},
|
||||
// Electron main process configuration
|
||||
{
|
||||
files: ['electron/**/*.ts'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
__dirname: 'readonly',
|
||||
process: 'readonly',
|
||||
console: 'readonly',
|
||||
},
|
||||
},
|
||||
},
|
||||
// UI components (shadcn/ui) - more relaxed rules
|
||||
{
|
||||
files: ['src/components/ui/**/*.{ts,tsx}'],
|
||||
rules: {
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'react/prop-types': 'off',
|
||||
},
|
||||
},
|
||||
// Test configuration
|
||||
{
|
||||
files: ['**/*.test.{ts,tsx}', 'src/test/**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
console: 'readonly',
|
||||
jest: 'readonly',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-namespace': 'off',
|
||||
'@typescript-eslint/no-empty-interface': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,50 +0,0 @@
|
||||
import { VitePlugin } from '@electron-forge/plugin-vite';
|
||||
import type { ForgeConfig } from '@electron-forge/shared-types';
|
||||
|
||||
const config: ForgeConfig = {
|
||||
packagerConfig: {
|
||||
asar: true,
|
||||
},
|
||||
rebuildConfig: {},
|
||||
makers: [
|
||||
{
|
||||
name: '@electron-forge/maker-squirrel',
|
||||
config: {},
|
||||
},
|
||||
{
|
||||
name: '@electron-forge/maker-zip',
|
||||
config: {},
|
||||
platforms: ['darwin'],
|
||||
},
|
||||
{
|
||||
name: '@electron-forge/maker-deb',
|
||||
config: {},
|
||||
},
|
||||
{
|
||||
name: '@electron-forge/maker-rpm',
|
||||
config: {},
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
new VitePlugin({
|
||||
build: [
|
||||
{
|
||||
entry: 'electron/main.ts',
|
||||
config: 'vite.main.config.ts',
|
||||
},
|
||||
{
|
||||
entry: 'electron/preload.ts',
|
||||
config: 'vite.preload.config.ts',
|
||||
},
|
||||
],
|
||||
renderer: [
|
||||
{
|
||||
name: 'main_window',
|
||||
config: 'vite.renderer.config.ts',
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,27 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; font-src 'self' data: https://cash-f.squarecdn.com"
|
||||
/>
|
||||
<title>Goose v2</title>
|
||||
<script>
|
||||
// Immediately check dark mode preference to avoid flash
|
||||
(function() {
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
if (savedTheme === 'dark' || (!savedTheme && systemPrefersDark)) {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-background-default dark:bg-zinc-800 transition-colors duration-200">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,6 +0,0 @@
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]): string {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
16111
ui-v2/package-lock.json
generated
16111
ui-v2/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,108 +0,0 @@
|
||||
{
|
||||
"name": "goose-v2",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"description": "Goose v2",
|
||||
"main": ".vite/build/main.js",
|
||||
"scripts": {
|
||||
"start": "electron-forge start",
|
||||
"build": "electron-forge make",
|
||||
"package": "electron-forge package",
|
||||
"make": "electron-forge make",
|
||||
"test": "vitest",
|
||||
"test:ui": "vitest --ui",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:e2e": "playwright test --project=electron",
|
||||
"test:e2e:ui": "playwright test --ui --project=electron --workers=1",
|
||||
"test:e2e:electron": "playwright test --project=electron --headed",
|
||||
"test:e2e:electron:headless": "HEADLESS=true playwright test --project=electron",
|
||||
"test:e2e:electron:ui": "playwright test --ui --project=electron",
|
||||
"tsc": "tsc --noEmit",
|
||||
"tsc:web": "tsc --project tsconfig.json --noEmit",
|
||||
"tsc:electron": "tsc --project tsconfig.electron.json --noEmit",
|
||||
"typecheck": "npm run tsc:web && npm run tsc:electron",
|
||||
"lint": "eslint . && npm run lint:style",
|
||||
"lint:fix": "eslint . --fix && npm run lint:style:fix",
|
||||
"lint:style": "stylelint src/**/*.css",
|
||||
"lint:style:fix": "stylelint src/**/*.css --fix",
|
||||
"prettier": "prettier --check \"src/**/*.{ts,tsx,js,jsx,css}\" \"electron/**/*.{ts,tsx,js,jsx,css}\"",
|
||||
"prettier:fix": "prettier --write \"src/**/*.{ts,tsx,js,jsx,css}\" \"electron/**/*.{ts,tsx,js,jsx,css}\"",
|
||||
"format": "npm run prettier:fix && npm run lint:style:fix",
|
||||
"check-all": "npm run typecheck && npm run lint && npm run prettier",
|
||||
"prepare": "cd .. && npx husky"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@tanstack/react-router": "^1.120.5",
|
||||
"@tanstack/react-router-devtools": "^1.120.11",
|
||||
"@tanstack/router": "^0.0.1-beta.53",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^12.12.1",
|
||||
"lucide-react": "^0.511.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"recharts": "^2.15.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-forge/cli": "^7.8.1",
|
||||
"@electron-forge/maker-deb": "^7.8.1",
|
||||
"@electron-forge/maker-rpm": "^7.8.1",
|
||||
"@electron-forge/maker-squirrel": "^7.8.1",
|
||||
"@electron-forge/maker-zip": "^7.8.1",
|
||||
"@electron-forge/plugin-vite": "^7.8.1",
|
||||
"@electron-forge/shared-types": "^7.8.1",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@tailwindcss/postcss": "^4.1.7",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/react": "^19.1.4",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
"@typescript-eslint/eslint-plugin": "^8.32.1",
|
||||
"@typescript-eslint/parser": "^8.32.1",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"@vitest/coverage-v8": "^3.1.3",
|
||||
"@vitest/ui": "^3.1.3",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"concurrently": "^9.1.2",
|
||||
"electron": "^36.2.1",
|
||||
"eslint": "^9.27.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-prettier": "^5.4.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^26.1.0",
|
||||
"lint-staged": "^16.0.0",
|
||||
"postcss": "^8.5.3",
|
||||
"prettier": "^3.5.3",
|
||||
"stylelint": "^16.19.1",
|
||||
"stylelint-config-standard": "^38.0.0",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"tailwindcss": "^4.1.7",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"ts-node": "^10.9.2",
|
||||
"tw-animate-css": "^1.3.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.5",
|
||||
"vitest": "^3.1.3"
|
||||
},
|
||||
"lint-staged": {
|
||||
"src/**/*.{ts,tsx}": [
|
||||
"eslint --fix --max-warnings 0 --no-warn-ignored",
|
||||
"prettier --write",
|
||||
"bash -c 'tsc --pretty --noEmit --project tsconfig.json'"
|
||||
],
|
||||
"src/**/*.{css,json}": [
|
||||
"prettier --write",
|
||||
"stylelint --fix"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"forge": "./forge.config.ts"
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './src/test/e2e',
|
||||
workers: 1,
|
||||
use: {
|
||||
trace: 'on-first-retry',
|
||||
// Use headless mode in CI, non-headless locally unless specified
|
||||
headless: process.env.CI === 'true' || process.env.HEADLESS === 'true',
|
||||
// Add longer timeouts for CI
|
||||
navigationTimeout: 30000,
|
||||
actionTimeout: 15000,
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'electron',
|
||||
testMatch: ['**/electron/*.spec.ts'],
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
},
|
||||
},
|
||||
],
|
||||
timeout: 60000, // Increase overall timeout
|
||||
expect: {
|
||||
timeout: 15000, // Increase expect timeout
|
||||
},
|
||||
reporter: [['html'], ['list']],
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
/* eslint-env node */
|
||||
|
||||
/** @type {import('postcss').Config} */
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.3 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.3 MiB |
Binary file not shown.
Binary file not shown.
@@ -1,13 +0,0 @@
|
||||
<svg width="24" height="23" viewBox="0 0 24 23" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.5 10.5733C0.5 8.19817 2.41385 6.27272 4.77471 6.27272H6.67984C9.04069 6.27272 10.9545 8.19817 10.9545 10.5733V18.6994C10.9545 21.0745 9.04069 23 6.67983 23H4.77471C2.41385 23 0.5 21.0745 0.5 18.6994V10.5733Z" fill="white"/>
|
||||
<path d="M6.67977 22.6416V23H4.77477V22.6416H6.67977ZM10.5983 18.6994V10.5734C10.5983 8.39611 8.84392 6.63111 6.67977 6.63111H4.77477C2.61062 6.63111 0.856231 8.39611 0.856231 10.5734V18.6994C0.856231 20.8766 2.61062 22.6416 4.77477 22.6416V23L4.66449 22.9986C2.39119 22.9407 0.558919 21.0974 0.501392 18.8103L0.5 18.6994V10.5734C0.5 8.23528 2.35457 6.33297 4.66449 6.27412L4.77477 6.27272H6.67977L6.7904 6.27412C9.10023 6.33307 10.9545 8.23534 10.9545 10.5734V18.6994L10.9532 18.8103C10.8956 21.0973 9.06361 22.9406 6.7904 22.9986L6.67977 23V22.6416C8.84392 22.6416 10.5983 20.8766 10.5983 18.6994Z" fill="white"/>
|
||||
<path d="M13.0453 4.27471C13.0453 1.91385 14.9592 0 17.3201 0H19.2252C21.586 0 23.4999 1.91385 23.4999 4.27471V6.17984C23.4999 8.54069 21.586 10.4545 19.2252 10.4545H17.3201C14.9592 10.4545 13.0453 8.54069 13.0453 6.17983V4.27471Z" fill="white"/>
|
||||
<path d="M19.2251 10.0983V10.4545H17.3201V10.0983H19.2251ZM23.1437 6.17977V4.27477C23.1437 2.11062 21.3893 0.356231 19.2251 0.356231H17.3201C15.156 0.356231 13.4016 2.11062 13.4016 4.27477V6.17977C13.4016 8.34392 15.156 10.0983 17.3201 10.0983V10.4545L17.2098 10.4532C14.9366 10.3956 13.1044 8.56358 13.0467 6.2904L13.0453 6.17977V4.27477C13.0453 1.95075 14.8999 0.0598847 17.2098 0.00139153L17.3201 0H19.2251L19.3357 0.00139153C21.6456 0.0599881 23.4999 1.95082 23.4999 4.27477V6.17977L23.4985 6.2904C23.4408 8.56351 21.6089 10.3955 19.3357 10.4532L19.2251 10.4545V10.0983C21.3893 10.0983 23.1437 8.34392 23.1437 6.17977Z" fill="white"/>
|
||||
<path d="M19.3182 14.6364C19.3182 13.4816 20.2543 12.5455 21.4091 12.5455V12.5455C22.5639 12.5455 23.5 13.4816 23.5 14.6364V14.6364C23.5 15.7911 22.5639 16.7273 21.4091 16.7273V16.7273C20.2543 16.7273 19.3182 15.7911 19.3182 14.6364V14.6364Z" fill="white"/>
|
||||
<path d="M23.1522 14.6362C23.1521 13.6736 22.3715 12.8933 21.4089 12.8933C20.4464 12.8933 19.6661 13.6736 19.666 14.6362C19.666 15.5988 20.4463 16.3794 21.4089 16.3794V16.7273L21.3016 16.7246C20.2324 16.6704 19.3751 15.813 19.3209 14.7439L19.3182 14.6362C19.3182 13.4815 20.2543 12.5455 21.4089 12.5455L21.5166 12.5482C22.6213 12.6042 23.4999 13.5176 23.5 14.6362L23.4973 14.7439C23.4413 15.8486 22.5276 16.7273 21.4089 16.7273V16.3794C22.3716 16.3794 23.1522 15.5988 23.1522 14.6362Z" fill="white"/>
|
||||
<path d="M13.0453 14.6364C13.0453 13.4816 13.9815 12.5455 15.1363 12.5455V12.5455C16.291 12.5455 17.2272 13.4816 17.2272 14.6364V14.6364C17.2272 15.7911 16.291 16.7273 15.1363 16.7273V16.7273C13.9815 16.7273 13.0453 15.7911 13.0453 14.6364V14.6364Z" fill="white"/>
|
||||
<path d="M16.8793 14.6362C16.8793 13.6736 16.0987 12.8933 15.1361 12.8933C14.1735 12.8933 13.3932 13.6736 13.3932 14.6362C13.3932 15.5988 14.1735 16.3794 15.1361 16.3794V16.7273L15.0287 16.7246C13.9596 16.6704 13.1023 15.813 13.0481 14.7439L13.0453 14.6362C13.0454 13.4815 13.9814 12.5455 15.1361 12.5455L15.2438 12.5482C16.3485 12.6042 17.2271 13.5176 17.2272 14.6362L17.2244 14.7439C17.1685 15.8486 16.2548 16.7273 15.1361 16.7273V16.3794C16.0987 16.3794 16.8793 15.5988 16.8793 14.6362Z" fill="white"/>
|
||||
<path d="M0.5 2.09091C0.5 0.936132 1.45869 0 2.64129 0H8.81325C9.99586 0 10.9545 0.936132 10.9545 2.09091V2.09091C10.9545 3.24569 9.99586 4.18182 8.81325 4.18182H2.64129C1.45869 4.18182 0.5 3.24569 0.5 2.09091V2.09091Z" fill="white"/>
|
||||
<path d="M8.81333 3.83398V4.18182H2.64121V3.83398H8.81333ZM10.5983 2.09074C10.5983 1.12815 9.79917 0.347834 8.81333 0.347834H2.64121C1.65542 0.347891 0.85629 1.12818 0.856231 2.09074C0.856231 3.05334 1.65538 3.83393 2.64121 3.83398V4.18182L2.53128 4.1791C1.43629 4.12498 0.558276 3.26758 0.502783 2.19842L0.5 2.09074C0.500057 0.972081 1.39984 0.0586407 2.53128 0.00271745L2.64121 0H8.81333L8.92361 0.00271745C10.055 0.0587386 10.9545 0.972147 10.9545 2.09074L10.9518 2.19842C10.8963 3.26751 10.0185 4.12489 8.92361 4.1791L8.81333 4.18182V3.83398C9.79921 3.83398 10.5983 3.05338 10.5983 2.09074Z" fill="white"/>
|
||||
<path d="M13.1735 19.8579C13.5338 19.0234 14.546 18.6107 15.4505 18.9215L15.4934 18.9368L16.8286 19.4316L16.9187 19.4639C17.854 19.7861 18.8876 19.7689 19.8114 19.4137L21.0304 18.9449L21.0731 18.9291C21.9732 18.6075 22.9911 19.0079 23.3631 19.8381C23.735 20.6682 23.3225 21.6186 22.4412 21.982L22.3989 21.9989L21.1798 22.4676C19.3933 23.1545 17.3909 23.1772 15.5885 22.5329L15.5029 22.5016L14.1678 22.007L14.1252 21.9906C13.2389 21.6378 12.8131 20.6925 13.1735 19.8579Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.6 KiB |
@@ -1,139 +0,0 @@
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
interface BrandCardProps {
|
||||
date?: Date;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// Array of congratulatory messages for past days
|
||||
const pastDayMessages = [
|
||||
{ title: 'Great work!', message: 'You accomplished so much' },
|
||||
{ title: 'Well done!', message: 'Another successful day' },
|
||||
{ title: 'Fantastic job!', message: 'Making progress every day' },
|
||||
{ title: 'Nice one!', message: 'Another day in the books' },
|
||||
{ title: 'Awesome work!', message: 'Keep up the momentum' },
|
||||
];
|
||||
|
||||
export default function BrandCard({ date, className = '' }: BrandCardProps): ReactElement {
|
||||
const isToday = date ? new Date().toDateString() === date.toDateString() : true;
|
||||
|
||||
// Get a consistent message for each date
|
||||
const getPastDayMessage = (date: Date) => {
|
||||
// Use the date's day as an index to select a message
|
||||
const index = date.getDate() % pastDayMessages.length;
|
||||
return pastDayMessages[index];
|
||||
};
|
||||
|
||||
// Get message for past days
|
||||
const pastMessage = date ? getPastDayMessage(date) : pastDayMessages[0];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
flex flex-col justify-between
|
||||
p-4
|
||||
w-[366px] h-[256px]
|
||||
${isToday ? 'bg-textStandard dark:bg-white' : 'bg-gray-400/40 dark:bg-gray-400/40'}
|
||||
rounded-[18px]
|
||||
relative
|
||||
overflow-hidden
|
||||
transition-all duration-200
|
||||
shadow-[0_0_13.7px_rgba(0,0,0,0.04)]
|
||||
dark:shadow-[0_0_24px_rgba(255,255,255,0.02)]
|
||||
${className}
|
||||
`}
|
||||
>
|
||||
{/* Content */}
|
||||
<div className="relative z-10 w-full">
|
||||
{/* Logo */}
|
||||
<div
|
||||
className={`
|
||||
w-6 h-6
|
||||
${
|
||||
isToday
|
||||
? '[&_path]:fill-current text-white dark:text-gray-900'
|
||||
: '[&_path]:fill-current text-white/60 dark:text-white/60'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<svg width="24" height="23" viewBox="0 0 24 23" xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<path d="M0.5 10.5733C0.5 8.19815 2.41385 6.27271 4.77471 6.27271H6.67984C9.04069 6.27271 10.9545 8.19816 10.9545 10.5733V18.6994C10.9545 21.0745 9.04069 23 6.67983 23H4.77471C2.41385 23 0.5 21.0745 0.5 18.6994V10.5733Z" />
|
||||
<path d="M6.67977 22.6416V23H4.77477V22.6416H6.67977ZM10.5983 18.6993V10.5733C10.5983 8.3961 8.84392 6.63109 6.67977 6.63109H4.77477C2.61062 6.63109 0.856231 8.3961 0.856231 10.5733V18.6993C0.856231 20.8766 2.61062 22.6416 4.77477 22.6416V23L4.66449 22.9986C2.39119 22.9407 0.558919 21.0974 0.501392 18.8103L0.5 18.6993V10.5733C0.5 8.23526 2.35457 6.33295 4.66449 6.27411L4.77477 6.27271H6.67977L6.7904 6.27411C9.10023 6.33306 10.9545 8.23533 10.9545 10.5733V18.6993L10.9532 18.8103C10.8956 21.0973 9.06361 22.9406 6.7904 22.9986L6.67977 23V22.6416C8.84392 22.6416 10.5983 20.8766 10.5983 18.6993Z" />
|
||||
<path d="M13.0453 4.27471C13.0453 1.91385 14.9592 0 17.3201 0H19.2252C21.586 0 23.4999 1.91385 23.4999 4.27471V6.17984C23.4999 8.54069 21.586 10.4545 19.2252 10.4545H17.3201C14.9592 10.4545 13.0453 8.54069 13.0453 6.17983V4.27471Z" />
|
||||
<path d="M19.2251 10.0983V10.4545H17.3201V10.0983H19.2251ZM23.1437 6.17977V4.27477C23.1437 2.11062 21.3893 0.356231 19.2251 0.356231H17.3201C15.156 0.356231 13.4016 2.11062 13.4016 4.27477V6.17977C13.4016 8.34392 15.156 10.0983 17.3201 10.0983V10.4545L17.2098 10.4532C14.9366 10.3956 13.1044 8.56358 13.0467 6.2904L13.0453 6.17977V4.27477C13.0453 1.95075 14.8999 0.0598847 17.2098 0.00139153L17.3201 0H19.2251L19.3357 0.00139153C21.6456 0.0599881 23.4999 1.95082 23.4999 4.27477V6.17977L23.4985 6.2904C23.4408 8.56351 21.6089 10.3955 19.3357 10.4532L19.2251 10.4545V10.0983C21.3893 10.0983 23.1437 8.34392 23.1437 6.17977Z" />
|
||||
<path d="M19.3182 14.6363C19.3182 13.4815 20.2543 12.5454 21.4091 12.5454V12.5454C22.5639 12.5454 23.5 13.4815 23.5 14.6363V14.6363C23.5 15.7911 22.5639 16.7272 21.4091 16.7272V16.7272C20.2543 16.7272 19.3182 15.7911 19.3182 14.6363V14.6363Z" />
|
||||
<path d="M23.1522 14.6361C23.1521 13.6736 22.3715 12.8932 21.4089 12.8932C20.4464 12.8933 19.6661 13.6736 19.666 14.6361C19.666 15.5988 20.4464 16.3793 21.4089 16.3794V16.7272L21.3016 16.7245C20.2324 16.6704 19.3751 15.813 19.3209 14.7438L19.3182 14.6361C19.3183 13.4815 20.2543 12.5455 21.4089 12.5454L21.5166 12.5481C22.6213 12.6041 23.5 13.5175 23.5 14.6361L23.4973 14.7438C23.4413 15.8486 22.5276 16.7272 21.4089 16.7272V16.3794C22.3716 16.3794 23.1522 15.5988 23.1522 14.6361Z" />
|
||||
<path d="M13.0453 14.6363C13.0453 13.4815 13.9815 12.5454 15.1363 12.5454V12.5454C16.291 12.5454 17.2272 13.4815 17.2272 14.6363V14.6363C17.2272 15.7911 16.291 16.7272 15.1363 16.7272V16.7272C13.9815 16.7272 13.0453 15.7911 13.0453 14.6363V14.6363Z" />
|
||||
<path d="M16.8793 14.6361C16.8793 13.6736 16.0987 12.8932 15.1361 12.8932C14.1735 12.8933 13.3932 13.6736 13.3932 14.6361C13.3932 15.5988 14.1735 16.3793 15.1361 16.3794V16.7272L15.0287 16.7245C13.9596 16.6704 13.1023 15.813 13.0481 14.7438L13.0453 14.6361C13.0454 13.4815 13.9814 12.5455 15.1361 12.5454L15.2438 12.5481C16.3485 12.6041 17.2271 13.5175 17.2272 14.6361L17.2244 14.7438C17.1685 15.8486 16.2548 16.7272 15.1361 16.7272V16.3794C16.0987 16.3794 16.8793 15.5988 16.8793 14.6361Z" />
|
||||
<path d="M0.5 2.09091C0.5 0.936132 1.45869 0 2.64129 0H8.81325C9.99586 0 10.9545 0.936132 10.9545 2.09091V2.09091C10.9545 3.24569 9.99586 4.18182 8.81325 4.18182H2.64129C1.45869 4.18182 0.5 3.24569 0.5 2.09091V2.09091Z" />
|
||||
<path d="M8.81333 3.83398V4.18182H2.64121V3.83398H8.81333ZM10.5983 2.09074C10.5983 1.12815 9.79917 0.347834 8.81333 0.347834H2.64121C1.65542 0.347891 0.85629 1.12818 0.856231 2.09074C0.856231 3.05334 1.65538 3.83393 2.64121 3.83398V4.18182L2.53128 4.1791C1.43629 4.12498 0.558276 3.26758 0.502783 2.19842L0.5 2.09074C0.500057 0.972081 1.39984 0.0586407 2.53128 0.00271745L2.64121 0H8.81333L8.92361 0.00271745C10.055 0.0587386 10.9545 0.972147 10.9545 2.09074L10.9518 2.19842C10.8963 3.26751 10.0185 4.12489 8.92361 4.1791L8.81333 4.18182V3.83398C9.79921 3.83398 10.5983 3.05338 10.5983 2.09074Z" />
|
||||
<path d="M13.1735 19.8579C13.5338 19.0233 14.546 18.6107 15.4505 18.9214L15.4934 18.9368L16.8286 19.4315L16.9187 19.4638C17.854 19.786 18.8876 19.7689 19.8114 19.4136L21.0304 18.9449L21.0731 18.9291C21.9732 18.6074 22.9911 19.0079 23.3631 19.838C23.735 20.6682 23.3225 21.6185 22.4412 21.9819L22.3989 21.9989L21.1798 22.4675C19.3933 23.1545 17.3909 23.1771 15.5885 22.5328L15.5029 22.5016L14.1678 22.0069L14.1252 21.9906C13.2389 21.6378 12.8131 20.6924 13.1735 19.8579Z" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Text content - bottom */}
|
||||
<div className="relative z-10 w-full flex flex-col">
|
||||
{isToday ? (
|
||||
<>
|
||||
{/* Today's content */}
|
||||
<h2
|
||||
className={`
|
||||
font-['Cash_Sans'] font-semibold text-base
|
||||
text-white dark:text-gray-600
|
||||
tracking-[0.08em] max-w-[565px]
|
||||
mb-2
|
||||
transition-colors
|
||||
`}
|
||||
>
|
||||
Good morning
|
||||
</h2>
|
||||
|
||||
<h1
|
||||
style={{ fontWeight: 200 }}
|
||||
className={`
|
||||
font-['Cash_Sans'] text-[32px]
|
||||
text-white dark:text-gray-600
|
||||
leading-tight max-w-[565px]
|
||||
tracking-normal
|
||||
transition-colors
|
||||
`}
|
||||
>
|
||||
You've got 3 major updates this morning
|
||||
</h1>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* Past/Future date content */}
|
||||
<h2
|
||||
className={`
|
||||
font-['Cash_Sans'] font-semibold text-base
|
||||
text-white/60 dark:text-white/60
|
||||
tracking-[0.08em] max-w-[565px]
|
||||
mb-2
|
||||
transition-colors
|
||||
`}
|
||||
>
|
||||
{pastMessage?.title || 'Hello'}
|
||||
</h2>
|
||||
|
||||
<h1
|
||||
style={{ fontWeight: 200 }}
|
||||
className={`
|
||||
font-['Cash_Sans'] text-[32px]
|
||||
text-white/60 dark:text-white/60
|
||||
leading-tight max-w-[565px]
|
||||
tracking-normal
|
||||
transition-colors
|
||||
`}
|
||||
>
|
||||
{pastMessage?.message || 'Great work'}
|
||||
</h1>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { useEffect, useState, ReactElement } from 'react';
|
||||
|
||||
import { Moon, Sun } from 'lucide-react';
|
||||
|
||||
export function DarkModeToggle(): ReactElement {
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Initialize from localStorage or system preference
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
const shouldBeDark = savedTheme === 'dark' || (!savedTheme && systemPrefersDark);
|
||||
setIsDark(shouldBeDark);
|
||||
|
||||
if (shouldBeDark) {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const toggleDarkMode = () => {
|
||||
const newIsDark = !isDark;
|
||||
setIsDark(newIsDark);
|
||||
|
||||
if (newIsDark) {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem('theme', 'dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem('theme', 'light');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={toggleDarkMode}
|
||||
className="
|
||||
fixed bottom-4 left-4 z-50
|
||||
w-10 h-10
|
||||
rounded-full
|
||||
cursor-pointer
|
||||
flex items-center justify-center
|
||||
transition-all duration-200
|
||||
bg-white/80 dark:bg-black/80
|
||||
backdrop-blur-sm
|
||||
shadow-[0_0_13.7px_rgba(0,0,0,0.04)]
|
||||
dark:shadow-[0_0_24px_rgba(255,255,255,0.08)]
|
||||
hover:bg-white dark:hover:bg-black
|
||||
text-black/80 dark:text-white/80
|
||||
hover:text-black dark:hover:text-white
|
||||
group
|
||||
"
|
||||
title={isDark ? 'Switch to Light Mode' : 'Switch to Dark Mode'}
|
||||
>
|
||||
<span className="block dark:hidden transform transition-transform group-hover:rotate-12">
|
||||
<Moon size={18} />
|
||||
</span>
|
||||
<span className="hidden dark:block transform transition-transform group-hover:rotate-12">
|
||||
<Sun size={18} />
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import { useEffect, useState, ReactElement } from 'react';
|
||||
|
||||
import { useTimeline } from '../contexts/TimelineContext';
|
||||
|
||||
export function DateDisplay(): ReactElement {
|
||||
const { currentDate } = useTimeline();
|
||||
const [displayDate, setDisplayDate] = useState(currentDate);
|
||||
const [isFlipping, setIsFlipping] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsFlipping(true);
|
||||
const timer = setTimeout(() => {
|
||||
setDisplayDate(currentDate);
|
||||
setIsFlipping(false);
|
||||
}, 50); // Reduced from 100ms to 50ms for faster flip
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [currentDate]);
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
const monthNames = [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December',
|
||||
];
|
||||
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||
|
||||
return {
|
||||
month: monthNames[date.getMonth()],
|
||||
day: date.getDate(),
|
||||
weekday: dayNames[date.getDay()],
|
||||
};
|
||||
};
|
||||
|
||||
const formattedDate = formatDate(displayDate);
|
||||
|
||||
return (
|
||||
<div className="fixed top-[2px] left-1/2 -translate-x-1/2 z-40">
|
||||
<div
|
||||
className={`
|
||||
flex items-center gap-2 px-4 py-2
|
||||
text-text-default dark:text-white/70
|
||||
transition-all duration-150
|
||||
${isFlipping ? 'transform -translate-y-1 opacity-0' : 'transform translate-y-0 opacity-100'}
|
||||
`}
|
||||
>
|
||||
{formattedDate.weekday} {formattedDate.month} {formattedDate.day}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { FC } from 'react';
|
||||
|
||||
import { Goose, Rain } from './icons/Goose';
|
||||
|
||||
interface GooseLogoProps {
|
||||
className?: string;
|
||||
size?: 'default' | 'small';
|
||||
hover?: boolean;
|
||||
}
|
||||
|
||||
const GooseLogo: FC<GooseLogoProps> = ({ className = '', size = 'default', hover = true }) => {
|
||||
const sizes = {
|
||||
default: {
|
||||
frame: 'w-16 h-16',
|
||||
rain: 'w-[275px] h-[275px]',
|
||||
goose: 'w-16 h-16',
|
||||
},
|
||||
small: {
|
||||
frame: 'w-8 h-8',
|
||||
rain: 'w-[150px] h-[150px]',
|
||||
goose: 'w-8 h-8',
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${className} ${sizes[size].frame} group relative`}>
|
||||
{/* Rain with enhanced visibility for testing */}
|
||||
<div
|
||||
className={`${sizes[size].rain} absolute left-0 bottom-0 ${hover ? 'opacity-0 group-hover:opacity-100' : 'opacity-100'} transition-all duration-500 z-10`}
|
||||
style={{
|
||||
filter: 'brightness(2) contrast(2) saturate(2)',
|
||||
mixBlendMode: 'multiply',
|
||||
}}
|
||||
>
|
||||
<Rain className="w-full h-full" />
|
||||
</div>
|
||||
<Goose className={`${sizes[size].goose} absolute left-0 bottom-0 z-20`} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GooseLogo;
|
||||
@@ -1,12 +0,0 @@
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
import GooseLogo from '../components/GooseLogo';
|
||||
|
||||
export default function Home(): ReactElement {
|
||||
return (
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<GooseLogo />
|
||||
<h1 className="text-2xl font-bold text-textProminent">Goose v2</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const SuspenseLoader: React.FC = (): React.ReactElement => {
|
||||
return <div>Loading...</div>;
|
||||
};
|
||||
|
||||
export default SuspenseLoader;
|
||||
@@ -1,536 +0,0 @@
|
||||
import { useRef, useMemo, useEffect, ReactElement } from 'react';
|
||||
|
||||
import {
|
||||
ChartLineIcon,
|
||||
ChartBarIcon,
|
||||
PieChartIcon,
|
||||
ListIcon,
|
||||
StarIcon,
|
||||
TrendingUpIcon,
|
||||
} from './icons';
|
||||
import { useTimeline } from '../contexts/TimelineContext';
|
||||
import ChartTile from './tiles/ChartTile.tsx';
|
||||
import ClockTile from './tiles/ClockTile.tsx';
|
||||
import HighlightTile from './tiles/HighlightTile.tsx';
|
||||
import ListTile from './tiles/ListTile.tsx';
|
||||
import PieChartTile from './tiles/PieChartTile.tsx';
|
||||
import TimelineDots from './TimelineDots';
|
||||
|
||||
const generateRandomData = (length: number) =>
|
||||
Array.from({ length }, () => Math.floor(Math.random() * 100));
|
||||
|
||||
const generateTileData = (date: Date) => {
|
||||
const isToday = new Date().toDateString() === date.toDateString();
|
||||
|
||||
return {
|
||||
left: [
|
||||
// Performance metrics
|
||||
{
|
||||
type: 'chart' as const,
|
||||
props: {
|
||||
title: 'Daily Activity',
|
||||
value: '487',
|
||||
trend: '↑ 12%',
|
||||
data: generateRandomData(7),
|
||||
icon: <ChartLineIcon />,
|
||||
variant: 'line' as const,
|
||||
date,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'highlight' as const,
|
||||
props: {
|
||||
title: 'Achievement',
|
||||
value: isToday ? 'New Record!' : 'Great Work',
|
||||
icon: <StarIcon />,
|
||||
subtitle: isToday ? 'Personal best today' : 'Keep it up',
|
||||
date,
|
||||
accentColor: '#FFB800',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'pie' as const,
|
||||
props: {
|
||||
title: 'Task Distribution',
|
||||
icon: <PieChartIcon />,
|
||||
segments: [
|
||||
{ value: 45, color: '#00CAF7', label: 'Completed' },
|
||||
{ value: 35, color: '#FFB800', label: 'In Progress' },
|
||||
{ value: 20, color: '#FF4444', label: 'Pending' },
|
||||
],
|
||||
date,
|
||||
},
|
||||
},
|
||||
// Additional metrics
|
||||
{
|
||||
type: 'chart' as const,
|
||||
props: {
|
||||
title: 'Response Time',
|
||||
value: '245ms',
|
||||
trend: '↓ 18%',
|
||||
data: generateRandomData(7),
|
||||
icon: <ChartBarIcon />,
|
||||
variant: 'bar' as const,
|
||||
date,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'highlight' as const,
|
||||
props: {
|
||||
title: 'User Satisfaction',
|
||||
value: '98%',
|
||||
icon: <StarIcon />,
|
||||
subtitle: 'Based on feedback',
|
||||
date,
|
||||
accentColor: '#4CAF50',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'list' as const,
|
||||
props: {
|
||||
title: 'Top Priorities',
|
||||
icon: <ListIcon />,
|
||||
items: [
|
||||
{ text: 'Project Alpha', value: '87%', color: '#00CAF7' },
|
||||
{ text: 'Team Meeting', value: '2:30 PM' },
|
||||
{ text: 'Review Code', value: '13', color: '#FFB800' },
|
||||
{ text: 'Deploy Update', value: 'Done', color: '#4CAF50' },
|
||||
],
|
||||
date,
|
||||
},
|
||||
},
|
||||
// System metrics
|
||||
{
|
||||
type: 'chart' as const,
|
||||
props: {
|
||||
title: 'System Load',
|
||||
value: '42%',
|
||||
trend: '↑ 5%',
|
||||
data: generateRandomData(7),
|
||||
icon: <ChartLineIcon />,
|
||||
variant: 'line' as const,
|
||||
date,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'pie' as const,
|
||||
props: {
|
||||
title: 'Storage Usage',
|
||||
icon: <PieChartIcon />,
|
||||
segments: [
|
||||
{ value: 60, color: '#4CAF50', label: 'Free' },
|
||||
{ value: 25, color: '#FFB800', label: 'Used' },
|
||||
{ value: 15, color: '#FF4444', label: 'System' },
|
||||
],
|
||||
date,
|
||||
},
|
||||
},
|
||||
],
|
||||
right: [
|
||||
// Performance metrics
|
||||
{
|
||||
type: 'chart' as const,
|
||||
props: {
|
||||
title: 'Performance',
|
||||
value: '92%',
|
||||
trend: '↑ 8%',
|
||||
data: generateRandomData(7),
|
||||
icon: <ChartBarIcon />,
|
||||
variant: 'bar' as const,
|
||||
date,
|
||||
},
|
||||
},
|
||||
// Clock tile
|
||||
{
|
||||
type: 'clock' as const,
|
||||
props: {
|
||||
title: 'Current Time',
|
||||
date,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'highlight' as const,
|
||||
props: {
|
||||
title: 'Efficiency',
|
||||
value: '+28%',
|
||||
icon: <TrendingUpIcon />,
|
||||
subtitle: 'Above target',
|
||||
date,
|
||||
accentColor: '#4CAF50',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'pie' as const,
|
||||
props: {
|
||||
title: 'Resource Usage',
|
||||
icon: <PieChartIcon />,
|
||||
segments: [
|
||||
{ value: 55, color: '#4CAF50', label: 'Available' },
|
||||
{ value: 30, color: '#FFB800', label: 'In Use' },
|
||||
{ value: 15, color: '#FF4444', label: 'Reserved' },
|
||||
],
|
||||
date,
|
||||
},
|
||||
},
|
||||
// Updates and notifications
|
||||
{
|
||||
type: 'list' as const,
|
||||
props: {
|
||||
title: 'Recent Updates',
|
||||
icon: <ListIcon />,
|
||||
items: [
|
||||
{ text: 'System Update', value: 'Complete', color: '#4CAF50' },
|
||||
{ text: 'New Features', value: '3', color: '#00CAF7' },
|
||||
{ text: 'Bug Fixes', value: '7', color: '#FFB800' },
|
||||
{ text: 'Performance', value: '+15%', color: '#4CAF50' },
|
||||
],
|
||||
date,
|
||||
},
|
||||
},
|
||||
// Additional metrics
|
||||
{
|
||||
type: 'chart' as const,
|
||||
props: {
|
||||
title: 'User Activity',
|
||||
value: '1,247',
|
||||
trend: '↑ 23%',
|
||||
data: generateRandomData(7),
|
||||
icon: <ChartLineIcon />,
|
||||
variant: 'line' as const,
|
||||
date,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'highlight' as const,
|
||||
props: {
|
||||
title: 'New Users',
|
||||
value: '+156',
|
||||
icon: <TrendingUpIcon />,
|
||||
subtitle: 'Last 24 hours',
|
||||
date,
|
||||
accentColor: '#00CAF7',
|
||||
},
|
||||
},
|
||||
// System health
|
||||
{
|
||||
type: 'pie' as const,
|
||||
props: {
|
||||
title: 'API Health',
|
||||
icon: <PieChartIcon />,
|
||||
segments: [
|
||||
{ value: 75, color: '#4CAF50', label: 'Healthy' },
|
||||
{ value: 20, color: '#FFB800', label: 'Warning' },
|
||||
{ value: 5, color: '#FF4444', label: 'Critical' },
|
||||
],
|
||||
date,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'list' as const,
|
||||
props: {
|
||||
title: 'System Status',
|
||||
icon: <ListIcon />,
|
||||
items: [
|
||||
{ text: 'Main API', value: 'Online', color: '#4CAF50' },
|
||||
{ text: 'Database', value: '98%', color: '#00CAF7' },
|
||||
{ text: 'Cache', value: 'Synced', color: '#4CAF50' },
|
||||
{ text: 'CDN', value: 'Active', color: '#4CAF50' },
|
||||
],
|
||||
date,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
export default function Timeline(): ReactElement {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const sectionRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
const { setCurrentDate } = useTimeline();
|
||||
|
||||
const sections = useMemo(() => {
|
||||
const result = [];
|
||||
const today = new Date();
|
||||
|
||||
for (let i = 0; i <= 29; i++) {
|
||||
const date = new Date(today);
|
||||
date.setDate(today.getDate() - i);
|
||||
|
||||
const tileData = generateTileData(date);
|
||||
|
||||
result.push({
|
||||
date,
|
||||
isToday: i === 0,
|
||||
leftTiles: tileData.left,
|
||||
rightTiles: tileData.right,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
// Function to center the timeline in a section
|
||||
const centerTimeline = (
|
||||
sectionElement: HTMLDivElement | null,
|
||||
animate: boolean = true
|
||||
): HTMLDivElement | null => {
|
||||
if (!sectionElement) return sectionElement;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const totalWidth = sectionElement.scrollWidth;
|
||||
const viewportWidth = sectionElement.clientWidth;
|
||||
const scrollToX = Math.max(0, (totalWidth - viewportWidth) / 2);
|
||||
|
||||
if (animate) {
|
||||
sectionElement.scrollTo({
|
||||
left: scrollToX,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
} else {
|
||||
sectionElement.scrollLeft = scrollToX;
|
||||
}
|
||||
});
|
||||
|
||||
return sectionElement;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Capture ref values at the start of the effect
|
||||
const currentContainer = containerRef.current;
|
||||
const currentSections = [...sectionRefs.current];
|
||||
|
||||
// Create the intersection observer
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
const section = entry.target as HTMLDivElement;
|
||||
|
||||
// When section comes into view
|
||||
if (entry.isIntersecting) {
|
||||
// Update current date
|
||||
const sectionIndex = sectionRefs.current.indexOf(section);
|
||||
if (sectionIndex !== -1 && sections[sectionIndex]) {
|
||||
const date = sections[sectionIndex].date;
|
||||
setCurrentDate(date);
|
||||
}
|
||||
}
|
||||
|
||||
// When section is fully visible and centered
|
||||
if (entry.intersectionRatio > 0.8) {
|
||||
centerTimeline(section, true);
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
threshold: [0, 0.8, 1], // Track when section is hidden, mostly visible, and fully visible
|
||||
rootMargin: '-10% 0px', // Slightly reduced margin for more natural triggering
|
||||
}
|
||||
);
|
||||
|
||||
// Add scroll handler for even faster updates
|
||||
const handleScroll = () => {
|
||||
if (!currentContainer) return;
|
||||
|
||||
// Find the section closest to the middle of the viewport
|
||||
const viewportMiddle = window.innerHeight / 2;
|
||||
let closestSection: HTMLDivElement | null = null;
|
||||
let closestDistance = Infinity;
|
||||
|
||||
sectionRefs.current.forEach((section) => {
|
||||
if (!section) return;
|
||||
const rect = section.getBoundingClientRect();
|
||||
const sectionMiddle = rect.top + rect.height / 2;
|
||||
const distance = Math.abs(sectionMiddle - viewportMiddle);
|
||||
|
||||
if (distance < closestDistance) {
|
||||
closestDistance = distance;
|
||||
closestSection = section;
|
||||
}
|
||||
});
|
||||
|
||||
if (closestSection) {
|
||||
const sectionIndex = sectionRefs.current.indexOf(closestSection);
|
||||
if (sectionIndex !== -1 && sections[sectionIndex]) {
|
||||
const date = sections[sectionIndex].date;
|
||||
setCurrentDate(date);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Add scroll event listener with throttling
|
||||
let lastScrollTime = 0;
|
||||
const throttledScrollHandler = () => {
|
||||
const now = Date.now();
|
||||
if (now - lastScrollTime >= 150) {
|
||||
// Throttle to ~6-7 times per second
|
||||
handleScroll();
|
||||
lastScrollTime = now;
|
||||
}
|
||||
};
|
||||
|
||||
currentContainer?.addEventListener('scroll', throttledScrollHandler, { passive: true });
|
||||
|
||||
// Add resize handler
|
||||
const handleResize = () => {
|
||||
// Find the currently visible section
|
||||
const visibleSection = sectionRefs.current.find((section) => {
|
||||
if (!section) return false;
|
||||
const rect = section.getBoundingClientRect();
|
||||
const viewportHeight = window.innerHeight;
|
||||
return rect.top >= -viewportHeight / 2 && rect.bottom <= viewportHeight * 1.5;
|
||||
});
|
||||
|
||||
if (visibleSection) {
|
||||
centerTimeline(visibleSection, true); // Animate on resize
|
||||
}
|
||||
};
|
||||
|
||||
// Add resize event listener
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
// Observe all sections
|
||||
sectionRefs.current.forEach((section) => {
|
||||
if (section) {
|
||||
observer.observe(section);
|
||||
centerTimeline(section, false); // No animation on initial load
|
||||
}
|
||||
});
|
||||
|
||||
// Cleanup function using captured values
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
currentContainer?.removeEventListener('scroll', throttledScrollHandler);
|
||||
currentSections.forEach((section) => {
|
||||
if (section) {
|
||||
observer.unobserve(section);
|
||||
}
|
||||
});
|
||||
};
|
||||
}, [sections, setCurrentDate]);
|
||||
|
||||
interface TileProps {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface Tile {
|
||||
type: string;
|
||||
props: TileProps;
|
||||
}
|
||||
|
||||
const renderTile = (tile: Tile, index: number): ReactElement | null => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const props = tile.props as any; // Use any for flexibility with different tile prop types
|
||||
switch (tile.type) {
|
||||
case 'chart':
|
||||
return <ChartTile key={index} {...props} />;
|
||||
case 'highlight':
|
||||
return <HighlightTile key={index} {...props} />;
|
||||
case 'pie':
|
||||
return <PieChartTile key={index} {...props} />;
|
||||
case 'list':
|
||||
return <ListTile key={index} {...props} />;
|
||||
case 'clock':
|
||||
return <ClockTile key={index} {...props} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="h-screen overflow-y-scroll overflow-x-hidden snap-y snap-mandatory relative scrollbar-hide"
|
||||
>
|
||||
{sections.map((section, index) => (
|
||||
<div
|
||||
key={index}
|
||||
ref={(el) => {
|
||||
sectionRefs.current[index] = el;
|
||||
}}
|
||||
className="h-screen relative snap-center snap-always overflow-y-hidden overflow-x-scroll snap-x snap-mandatory scrollbar-hide animate-[fadein_300ms_ease-in-out]"
|
||||
>
|
||||
<div className="relative min-w-[calc(200vw+100px)] h-full flex items-center">
|
||||
{/* Main flex container */}
|
||||
<div className="w-full h-full flex">
|
||||
{/* Left Grid */}
|
||||
<div className="w-screen p-4 mt-6 overflow-hidden">
|
||||
<div
|
||||
className="ml-auto mr-0 flex flex-wrap gap-4 content-start justify-end"
|
||||
style={{ width: 'min(720px, 90%)' }}
|
||||
>
|
||||
{section.leftTiles.map((tile, i) => (
|
||||
<div key={i} className="w-[calc(50%-8px)]">
|
||||
{renderTile(tile, i)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Center Timeline */}
|
||||
<div className="w-100px relative flex flex-col items-center h-screen">
|
||||
{/* Upper Timeline Dots */}
|
||||
<TimelineDots
|
||||
height="calc(50vh - 96px)"
|
||||
isUpper={true}
|
||||
isCurrentDay={section.isToday}
|
||||
/>
|
||||
|
||||
{/* Date Display */}
|
||||
<div className="bg-white dark:bg-black shadow-[0_0_13.7px_rgba(0,0,0,0.04)] dark:shadow-[0_0_24px_rgba(255,255,255,0.08)] p-4 rounded-xl z-[3] flex flex-col items-center transition-all">
|
||||
<div
|
||||
className={`font-['Cash_Sans'] text-3xl font-light transition-colors ${
|
||||
section.isToday
|
||||
? 'text-black dark:text-white'
|
||||
: 'text-black/40 dark:text-white/40'
|
||||
}`}
|
||||
>
|
||||
{section.date.toLocaleString('default', { month: 'short' })}
|
||||
</div>
|
||||
<div
|
||||
className={`font-['Cash_Sans'] text-[64px] font-light leading-none transition-colors ${
|
||||
section.isToday
|
||||
? 'text-black dark:text-white'
|
||||
: 'text-black/40 dark:text-white/40'
|
||||
}`}
|
||||
>
|
||||
{section.date.getDate()}
|
||||
</div>
|
||||
<div
|
||||
className={`font-['Cash_Sans'] text-sm font-light mt-1 transition-colors ${
|
||||
section.isToday
|
||||
? 'text-black dark:text-white'
|
||||
: 'text-black/40 dark:text-white/40'
|
||||
}`}
|
||||
>
|
||||
{section.date.toLocaleString('default', { weekday: 'long' })}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lower Timeline Dots */}
|
||||
<TimelineDots
|
||||
height="calc(50vh - 96px)"
|
||||
isUpper={false}
|
||||
isCurrentDay={section.isToday}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right Grid */}
|
||||
<div className="w-screen p-4 mt-6 overflow-hidden">
|
||||
<div
|
||||
className="flex flex-wrap gap-4 content-start"
|
||||
style={{ width: 'min(720px, 90%)' }}
|
||||
>
|
||||
{section.rightTiles.map((tile, i) => (
|
||||
<div key={i} className="w-[calc(50%-8px)]">
|
||||
{renderTile(tile, i)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { createContext, useContext, useState, useCallback, ReactElement, ReactNode } from 'react';
|
||||
|
||||
interface TimelineContextType {
|
||||
currentDate: Date;
|
||||
setCurrentDate: (date: Date) => void;
|
||||
isCurrentDate: (date: Date) => boolean;
|
||||
}
|
||||
|
||||
const TimelineContext = createContext<TimelineContextType | undefined>(undefined);
|
||||
|
||||
export function TimelineProvider({ children }: { children: ReactNode }): ReactElement {
|
||||
const [currentDate, setCurrentDate] = useState(new Date());
|
||||
|
||||
const isCurrentDate = useCallback((date: Date): boolean => {
|
||||
return date.toDateString() === new Date().toDateString();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<TimelineContext.Provider value={{ currentDate, setCurrentDate, isCurrentDate }}>
|
||||
{children}
|
||||
</TimelineContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTimeline(): TimelineContextType {
|
||||
const context = useContext(TimelineContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useTimeline must be used within a TimelineProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
import { useMemo, ReactElement } from 'react';
|
||||
|
||||
interface TimelineDotsProps {
|
||||
height: number | string;
|
||||
isUpper?: boolean;
|
||||
isCurrentDay?: boolean;
|
||||
}
|
||||
|
||||
interface Dot {
|
||||
top: string;
|
||||
size: number;
|
||||
opacity: number;
|
||||
}
|
||||
|
||||
export default function TimelineDots({
|
||||
height,
|
||||
isUpper = false,
|
||||
isCurrentDay = false,
|
||||
}: TimelineDotsProps): ReactElement {
|
||||
// Generate random dots with clusters
|
||||
const dots = useMemo(() => {
|
||||
const generateDots = () => {
|
||||
const dots: Dot[] = [];
|
||||
const numDots = Math.floor(Math.random() * 8) + 8; // 8-15 dots
|
||||
|
||||
// Create 2-3 cluster points
|
||||
const clusterPoints = Array.from(
|
||||
{ length: Math.floor(Math.random() * 2) + 2 },
|
||||
() => Math.random() * 100
|
||||
);
|
||||
|
||||
for (let i = 0; i < numDots; i++) {
|
||||
// Decide if this dot should be part of a cluster
|
||||
const isCluster = Math.random() < 0.7; // 70% chance of being in a cluster
|
||||
|
||||
let top;
|
||||
if (isCluster && clusterPoints.length > 0) {
|
||||
// Pick a random cluster point and add some variation
|
||||
const clusterPoint = clusterPoints[Math.floor(Math.random() * clusterPoints.length)];
|
||||
if (clusterPoint !== undefined) {
|
||||
top = clusterPoint + (Math.random() - 0.5) * 15; // ±7.5% variation
|
||||
} else {
|
||||
top = Math.random() * 100;
|
||||
}
|
||||
} else {
|
||||
top = Math.random() * 100;
|
||||
}
|
||||
|
||||
// Ensure dot is within bounds
|
||||
top = Math.max(5, Math.min(95, top));
|
||||
|
||||
dots.push({
|
||||
top: `${top}%`,
|
||||
size: Math.random() * 2 + 2, // 2-4px
|
||||
opacity: Math.random() * 0.5 + 0.2, // 0.2-0.7 opacity
|
||||
});
|
||||
}
|
||||
return dots;
|
||||
};
|
||||
|
||||
return generateDots();
|
||||
}, []); // Empty dependency array means this only runs once
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex h-full left-1/2 -translate-x-[0.375px] flex flex-col items-center"
|
||||
style={{
|
||||
height: height,
|
||||
bottom: isUpper ? 'calc(50% + 96px)' : '0',
|
||||
top: isUpper ? undefined : 'calc(50% + 96px)',
|
||||
}}
|
||||
>
|
||||
{/* Main line */}
|
||||
<div className="w-[0.75px] h-full bg-black/10 dark:bg-white/10 relative">
|
||||
{/* Top dot for current day */}
|
||||
{isUpper && isCurrentDay && (
|
||||
<div
|
||||
className="absolute rounded-full bg-black dark:bg-white"
|
||||
style={{
|
||||
width: '4px',
|
||||
height: '4px',
|
||||
left: '-1.625px', // Center 4px dot on 0.75px line
|
||||
top: '0',
|
||||
transform: 'translateY(-50%)',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Random dots */}
|
||||
{dots.map((dot, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="absolute rounded-full bg-black/40 dark:bg-white/40"
|
||||
style={{
|
||||
width: `${dot.size}px`,
|
||||
height: `${dot.size}px`,
|
||||
left: `${-(dot.size - 0.75) / 2}px`, // Center dot on the line
|
||||
top: dot.top,
|
||||
opacity: dot.opacity,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
interface BrandCardProps {
|
||||
date?: Date;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const pastDayMessages = [
|
||||
{ title: 'Great work!', message: 'You accomplished so much' },
|
||||
{ title: 'Well done!', message: 'Another successful day' },
|
||||
{ title: 'Fantastic job!', message: 'Making progress every day' },
|
||||
{ title: 'Nice one!', message: 'Another day in the books' },
|
||||
{ title: 'Awesome work!', message: 'Keep up the momentum' },
|
||||
];
|
||||
|
||||
export default function BrandCard({ date, className }: BrandCardProps): ReactElement {
|
||||
const isToday = date ? new Date().toDateString() === date.toDateString() : true;
|
||||
|
||||
// Get a consistent message for each date
|
||||
const getPastDayMessage = (date: Date) => {
|
||||
// Use the date's day as an index to select a message
|
||||
const index = date.getDate() % pastDayMessages.length;
|
||||
return pastDayMessages[index];
|
||||
};
|
||||
|
||||
// Get message for past days
|
||||
const pastMessage = date ? getPastDayMessage(date) : pastDayMessages[0];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
flex flex-col justify-between
|
||||
p-4
|
||||
w-[366px] h-[256px]
|
||||
${isToday ? 'bg-textStandard dark:bg-white' : 'bg-gray-400/40 dark:bg-gray-400/40'}
|
||||
rounded-[18px]
|
||||
relative
|
||||
overflow-hidden
|
||||
transition-all duration-200
|
||||
shadow-[0_0_13.7px_rgba(0,0,0,0.04)]
|
||||
dark:shadow-[0_0_24px_rgba(255,255,255,0.02)]
|
||||
${className || ''}
|
||||
`}
|
||||
>
|
||||
{/* Content */}
|
||||
<div className="relative z-10 w-full">
|
||||
{/* Logo */}
|
||||
<div
|
||||
className={`
|
||||
w-6 h-6
|
||||
${
|
||||
isToday
|
||||
? '[&_path]:fill-current text-white dark:text-gray-900'
|
||||
: '[&_path]:fill-current text-white/60 dark:text-white/60'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<svg width="24" height="23" viewBox="0 0 24 23" xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<path d="M0.5 10.5733C0.5 8.19815 2.41385 6.27271 4.77471 6.27271H6.67984C9.04069 6.27271 10.9545 8.19816 10.9545 10.5733V18.6994C10.9545 21.0745 9.04069 23 6.67983 23H4.77471C2.41385 23 0.5 21.0745 0.5 18.6994V10.5733Z" />
|
||||
<path d="M6.67977 22.6416V23H4.77477V22.6416H6.67977ZM10.5983 18.6993V10.5733C10.5983 8.3961 8.84392 6.63109 6.67977 6.63109H4.77477C2.61062 6.63109 0.856231 8.3961 0.856231 10.5733V18.6993C0.856231 20.8766 2.61062 22.6416 4.77477 22.6416V23L4.66449 22.9986C2.39119 22.9407 0.558919 21.0974 0.501392 18.8103L0.5 18.6993V10.5733C0.5 8.23526 2.35457 6.33295 4.66449 6.27411L4.77477 6.27271H6.67977L6.7904 6.27411C9.10023 6.33306 10.9545 8.23533 10.9545 10.5733V18.6993L10.9532 18.8103C10.8956 21.0973 9.06361 22.9406 6.7904 22.9986L6.67977 23V22.6416C8.84392 22.6416 10.5983 20.8766 10.5983 18.6993Z" />
|
||||
<path d="M13.0453 4.27471C13.0453 1.91385 14.9592 0 17.3201 0H19.2252C21.586 0 23.4999 1.91385 23.4999 4.27471V6.17984C23.4999 8.54069 21.586 10.4545 19.2252 10.4545H17.3201C14.9592 10.4545 13.0453 8.54069 13.0453 6.17983V4.27471Z" />
|
||||
<path d="M19.2251 10.0983V10.4545H17.3201V10.0983H19.2251ZM23.1437 6.17977V4.27477C23.1437 2.11062 21.3893 0.356231 19.2251 0.356231H17.3201C15.156 0.356231 13.4016 2.11062 13.4016 4.27477V6.17977C13.4016 8.34392 15.156 10.0983 17.3201 10.0983V10.4545L17.2098 10.4532C14.9366 10.3956 13.1044 8.56358 13.0467 6.2904L13.0453 6.17977V4.27477C13.0453 1.95075 14.8999 0.0598847 17.2098 0.00139153L17.3201 0H19.2251L19.3357 0.00139153C21.6456 0.0599881 23.4999 1.95082 23.4999 4.27477V6.17977L23.4985 6.2904C23.4408 8.56351 21.6089 10.3955 19.3357 10.4532L19.2251 10.4545V10.0983C21.3893 10.0983 23.1437 8.34392 23.1437 6.17977Z" />
|
||||
<path d="M19.3182 14.6363C19.3182 13.4815 20.2543 12.5454 21.4091 12.5454V12.5454C22.5639 12.5454 23.5 13.4815 23.5 14.6363V14.6363C23.5 15.7911 22.5639 16.7272 21.4091 16.7272V16.7272C20.2543 16.7272 19.3182 15.7911 19.3182 14.6363V14.6363Z" />
|
||||
<path d="M23.1522 14.6361C23.1521 13.6736 22.3715 12.8932 21.4089 12.8932C20.4464 12.8933 19.6661 13.6736 19.666 14.6361C19.666 15.5988 20.4464 16.3793 21.4089 16.3794V16.7272L21.3016 16.7245C20.2324 16.6704 19.3751 15.813 19.3209 14.7438L19.3182 14.6361C19.3183 13.4815 20.2543 12.5455 21.4089 12.5454L21.5166 12.5481C22.6213 12.6041 23.5 13.5175 23.5 14.6361L23.4973 14.7438C23.4413 15.8486 22.5276 16.7272 21.4089 16.7272V16.3794C22.3716 16.3794 23.1522 15.5988 23.1522 14.6361Z" />
|
||||
<path d="M13.0453 14.6363C13.0453 13.4815 13.9815 12.5454 15.1363 12.5454V12.5454C16.291 12.5454 17.2272 13.4815 17.2272 14.6363V14.6363C17.2272 15.7911 16.291 16.7272 15.1363 16.7272V16.7272C13.9815 16.7272 13.0453 15.7911 13.0453 14.6363V14.6363Z" />
|
||||
<path d="M16.8793 14.6361C16.8793 13.6736 16.0987 12.8932 15.1361 12.8932C14.1735 12.8933 13.3932 13.6736 13.3932 14.6361C13.3932 15.5988 14.1735 16.3793 15.1361 16.3794V16.7272L15.0287 16.7245C13.9596 16.6704 13.1023 15.813 13.0481 14.7438L13.0453 14.6361C13.0454 13.4815 13.9814 12.5455 15.1361 12.5454L15.2438 12.5481C16.3485 12.6041 17.2271 13.5175 17.2272 14.6361L17.2244 14.7438C17.1685 15.8486 16.2548 16.7272 15.1361 16.7272V16.3794C16.0987 16.3794 16.8793 15.5988 16.8793 14.6361Z" />
|
||||
<path d="M0.5 2.09091C0.5 0.936132 1.45869 0 2.64129 0H8.81325C9.99586 0 10.9545 0.936132 10.9545 2.09091V2.09091C10.9545 3.24569 9.99586 4.18182 8.81325 4.18182H2.64129C1.45869 4.18182 0.5 3.24569 0.5 2.09091V2.09091Z" />
|
||||
<path d="M8.81333 3.83398V4.18182H2.64121V3.83398H8.81333ZM10.5983 2.09074C10.5983 1.12815 9.79917 0.347834 8.81333 0.347834H2.64121C1.65542 0.347891 0.85629 1.12818 0.856231 2.09074C0.856231 3.05334 1.65538 3.83393 2.64121 3.83398V4.18182L2.53128 4.1791C1.43629 4.12498 0.558276 3.26758 0.502783 2.19842L0.5 2.09074C0.500057 0.972081 1.39984 0.0586407 2.53128 0.00271745L2.64121 0H8.81333L8.92361 0.00271745C10.055 0.0587386 10.9545 0.972147 10.9545 2.09074L10.9518 2.19842C10.8963 3.26751 10.0185 4.12489 8.92361 4.1791L8.81333 4.18182V3.83398C9.79921 3.83398 10.5983 3.05338 10.5983 2.09074Z" />
|
||||
<path d="M13.1735 19.8579C13.5338 19.0233 14.546 18.6107 15.4505 18.9214L15.4934 18.9368L16.8286 19.4315L16.9187 19.4638C17.854 19.786 18.8876 19.7689 19.8114 19.4136L21.0304 18.9449L21.0731 18.9291C21.9732 18.6074 22.9911 19.0079 23.3631 19.838C23.735 20.6682 23.3225 21.6185 22.4412 21.9819L22.3989 21.9989L21.1798 22.4675C19.3933 23.1545 17.3909 23.1771 15.5885 22.5328L15.5029 22.5016L14.1678 22.0069L14.1252 21.9906C13.2389 21.6378 12.8131 20.6924 13.1735 19.8579Z" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Text content - bottom */}
|
||||
<div className="relative z-10 w-full flex flex-col">
|
||||
{isToday ? (
|
||||
<>
|
||||
{/* Today's content */}
|
||||
<h2
|
||||
className={`
|
||||
font-['Cash_Sans'] font-semibold text-base
|
||||
text-white dark:text-gray-600
|
||||
tracking-[0.08em] max-w-[565px]
|
||||
mb-2
|
||||
transition-colors
|
||||
`}
|
||||
>
|
||||
Good morning
|
||||
</h2>
|
||||
|
||||
<h1
|
||||
style={{ fontWeight: 200 }}
|
||||
className={`
|
||||
font-['Cash_Sans'] text-[32px]
|
||||
text-white dark:text-gray-600
|
||||
leading-tight max-w-[565px]
|
||||
tracking-normal
|
||||
transition-colors
|
||||
`}
|
||||
>
|
||||
You've got 3 major updates this morning
|
||||
</h1>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* Past/Future date content */}
|
||||
<h2
|
||||
className={`
|
||||
font-['Cash_Sans'] font-semibold text-base
|
||||
text-white/60 dark:text-white/60
|
||||
tracking-[0.08em] max-w-[565px]
|
||||
mb-2
|
||||
transition-colors
|
||||
`}
|
||||
>
|
||||
{pastMessage?.title || 'Hello'}
|
||||
</h2>
|
||||
|
||||
<h1
|
||||
style={{ fontWeight: 200 }}
|
||||
className={`
|
||||
font-['Cash_Sans'] text-[32px]
|
||||
text-white/60 dark:text-white/60
|
||||
leading-tight max-w-[565px]
|
||||
tracking-normal
|
||||
transition-colors
|
||||
`}
|
||||
>
|
||||
{pastMessage?.message || 'Great work'}
|
||||
</h1>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
interface ChatDockProps {
|
||||
onTileCreatorToggle: () => void;
|
||||
}
|
||||
|
||||
export const ChatDock: React.FC<ChatDockProps> = ({ onTileCreatorToggle }) => {
|
||||
return (
|
||||
<motion.div
|
||||
className="flex items-center gap-2 mb-2 px-2"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
delay: 0.2,
|
||||
type: 'spring',
|
||||
stiffness: 300,
|
||||
damping: 30,
|
||||
}}
|
||||
>
|
||||
<motion.button
|
||||
onClick={onTileCreatorToggle}
|
||||
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-zinc-700/50 transition-colors"
|
||||
title="Toggle Tile Creator"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<rect x="3" y="3" width="7" height="7" />
|
||||
<rect x="14" y="3" width="7" height="7" />
|
||||
<rect x="14" y="14" width="7" height="7" />
|
||||
<rect x="3" y="14" width="7" height="7" />
|
||||
</svg>
|
||||
</motion.button>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
@@ -1,111 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
|
||||
|
||||
// Define the tool items
|
||||
const CHAT_TOOLS = [
|
||||
{
|
||||
icon: (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="white" stroke="none">
|
||||
<path d="M4 6h16v2H4zm0 5h16v2H4zm0 5h16v2H4z" />
|
||||
</svg>
|
||||
),
|
||||
label: 'Make a Tile',
|
||||
color: 'bg-[#4F6BFF] hover:bg-[#4F6BFF]/90',
|
||||
rotation: -3,
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="white" stroke="none">
|
||||
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7 14l-5-5 1.41-1.41L12 14.17l4.59-4.58L18 11l-6 6z" />
|
||||
</svg>
|
||||
),
|
||||
label: 'Tasks',
|
||||
color: 'bg-[#E042A5] hover:bg-[#E042A5]/90',
|
||||
rotation: 2,
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="white" stroke="none">
|
||||
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
|
||||
</svg>
|
||||
),
|
||||
label: 'Add',
|
||||
color: 'bg-[#05C168] hover:bg-[#05C168]/90',
|
||||
rotation: -2,
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="white" stroke="none">
|
||||
<path d="M12 2L1 21h22L12 2zm0 3.83L19.17 19H4.83L12 5.83zM11 16h2v2h-2zm0-6h2v4h-2z" />
|
||||
</svg>
|
||||
),
|
||||
label: 'Issues',
|
||||
color: 'bg-[#FF9900] hover:bg-[#FF9900]/90',
|
||||
rotation: 3,
|
||||
},
|
||||
];
|
||||
|
||||
interface ChatIconsProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ChatIcons: React.FC<ChatIconsProps> = ({ className }) => {
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||
|
||||
return (
|
||||
<div className={`flex mb-4 items-start ${className}`}>
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="flex -space-x-6 relative">
|
||||
{CHAT_TOOLS.map((tool, index) => {
|
||||
const getX = () => {
|
||||
if (hoveredIndex === null) return 0;
|
||||
const spread = 16;
|
||||
const centerOffset = hoveredIndex * -spread;
|
||||
return index * spread + centerOffset;
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={tool.label}
|
||||
className="relative"
|
||||
animate={{
|
||||
x: getX(),
|
||||
rotate: hoveredIndex !== null ? 0 : tool.rotation,
|
||||
scale: hoveredIndex === index ? 1.1 : 1,
|
||||
zIndex: hoveredIndex === index ? 10 : CHAT_TOOLS.length - index,
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.2,
|
||||
ease: 'easeOut',
|
||||
scale: { duration: 0.1 },
|
||||
}}
|
||||
onHoverStart={() => setHoveredIndex(index)}
|
||||
onHoverEnd={() => setHoveredIndex(null)}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<motion.button
|
||||
aria-label={tool.label}
|
||||
className={`
|
||||
flex h-12 w-12 items-center justify-center rounded-xl
|
||||
transition-all duration-200 shadow-sm
|
||||
${tool.color}
|
||||
${hoveredIndex !== null && hoveredIndex !== index ? 'opacity-50' : ''}
|
||||
`}
|
||||
>
|
||||
{tool.icon}
|
||||
</motion.button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent sideOffset={5}>{tool.label}</TooltipContent>
|
||||
</Tooltip>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,135 +0,0 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
interface ChatInputProps {
|
||||
handleSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
|
||||
isLoading?: boolean;
|
||||
onStop?: () => void;
|
||||
initialValue?: string;
|
||||
}
|
||||
|
||||
export const ChatInput: React.FC<ChatInputProps> = ({
|
||||
handleSubmit,
|
||||
isLoading = false,
|
||||
onStop: _onStop,
|
||||
initialValue = '',
|
||||
}) => {
|
||||
const [input, setInput] = useState(initialValue);
|
||||
const [key, setKey] = useState(0); // Add a key to force re-render
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const adjustTextareaHeight = () => {
|
||||
const textarea = textareaRef.current;
|
||||
if (textarea) {
|
||||
textarea.style.height = 'auto';
|
||||
textarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px';
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
adjustTextareaHeight();
|
||||
}, [input]);
|
||||
|
||||
// Watch for class changes on html element (theme changes)
|
||||
useEffect(() => {
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.attributeName === 'class') {
|
||||
setKey((prev) => prev + 1); // Force textarea to re-render
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const htmlElement = document.documentElement;
|
||||
observer.observe(htmlElement, { attributes: true });
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const handleFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
if (!input.trim() || isLoading) return;
|
||||
|
||||
handleSubmit(e);
|
||||
setInput('');
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = 'auto';
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
const form = (e.target as HTMLTextAreaElement).form;
|
||||
if (form) form.requestSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={containerRef}
|
||||
className="w-full bg-black dark:bg-white rounded-xl shadow-lg"
|
||||
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 300,
|
||||
damping: 30,
|
||||
}}
|
||||
>
|
||||
<form onSubmit={handleFormSubmit} className="relative px-4 py-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<textarea
|
||||
key={key} // Force re-render when theme changes
|
||||
ref={textareaRef}
|
||||
name="message"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="What can goose help with? ⌘↑/⌘↓"
|
||||
className="flex-1 resize-none bg-transparent text-white dark:text-black
|
||||
focus:outline-none focus:ring-0 rounded-lg
|
||||
min-h-[40px] max-h-[200px] transition-all duration-200
|
||||
placeholder:text-zinc-500"
|
||||
style={{ overflow: input.split('\n').length > 1 ? 'auto' : 'hidden' }}
|
||||
/>
|
||||
|
||||
<motion.button
|
||||
type="submit"
|
||||
disabled={!input.trim()}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className={`
|
||||
p-2 rounded-lg w-10 h-10 flex items-center justify-center
|
||||
transition-colors duration-200
|
||||
${
|
||||
input.trim()
|
||||
? 'hover:bg-zinc-800 active:bg-zinc-700 dark:hover:bg-zinc-100 dark:active:bg-zinc-200'
|
||||
: 'cursor-not-allowed'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={input.trim() ? 'currentColor' : '#666'}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={`
|
||||
transition-colors duration-200
|
||||
${input.trim() ? 'text-white dark:text-black' : 'text-zinc-600 dark:text-zinc-400'}
|
||||
`}
|
||||
>
|
||||
<path d="M22 2L11 13M22 2L15 22L11 13L2 9L22 2Z" />
|
||||
</svg>
|
||||
</motion.button>
|
||||
</div>
|
||||
</form>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
@@ -1,64 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
import { ChatIcons } from './ChatIcons';
|
||||
|
||||
interface FloatingChatProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const FloatingChat: React.FC<FloatingChatProps> = ({ children }) => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
|
||||
// Create a debounced version of setIsVisible
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(
|
||||
() => {
|
||||
setIsVisible(isHovering);
|
||||
},
|
||||
isHovering ? 0 : 200
|
||||
);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [isHovering]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed bottom-0 left-0 right-0 z-50"
|
||||
onMouseEnter={() => setIsHovering(true)}
|
||||
onMouseLeave={() => setIsHovering(false)}
|
||||
>
|
||||
{/* Hover trigger area with black bar indicator */}
|
||||
<div className="absolute bottom-0 left-0 right-0 h-20 bg-transparent flex justify-center">
|
||||
<div
|
||||
className={`
|
||||
w-[600px] h-[15px]
|
||||
bg-black dark:bg-white
|
||||
rounded-t-[24px]
|
||||
transition-all duration-300
|
||||
absolute bottom-0
|
||||
${isVisible ? 'opacity-0 transform translate-y-2' : 'opacity-100 transform translate-y-0'}
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Chat container with transition */}
|
||||
<div
|
||||
className={`
|
||||
transform transition-all duration-300 ease-out
|
||||
${isVisible ? 'translate-y-0 opacity-100' : '-translate-y-4 opacity-0'}
|
||||
`}
|
||||
style={{
|
||||
paddingBottom: 'env(safe-area-inset-bottom, 16px)',
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-center w-full px-4 pb-4">
|
||||
<div className="w-[600px]">
|
||||
<ChatIcons className="mb-1" />
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,100 +0,0 @@
|
||||
import React, { useState, useEffect, ReactElement } from 'react';
|
||||
|
||||
import { FilterOption } from './types';
|
||||
|
||||
interface FloatingFiltersProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const getBarColor = (filters: FilterOption[], isDarkMode: boolean): string => {
|
||||
const activeFilter = filters.find((f) => f.isActive);
|
||||
switch (activeFilter?.id) {
|
||||
case 'tasks':
|
||||
return '#05C168';
|
||||
case 'projects':
|
||||
return '#0066FF';
|
||||
case 'automations':
|
||||
return '#B18CFF';
|
||||
case 'problems':
|
||||
return '#FF2E6C';
|
||||
default:
|
||||
return isDarkMode ? '#FFFFFF' : '#000000';
|
||||
}
|
||||
};
|
||||
|
||||
export function FloatingFilters({ children }: FloatingFiltersProps): ReactElement {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [activeFilters, setActiveFilters] = useState<FilterOption[]>([]);
|
||||
const [isDarkMode, setIsDarkMode] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const filterPills = React.Children.toArray(children)[0] as React.ReactElement<{
|
||||
filters?: FilterOption[];
|
||||
}>;
|
||||
if (filterPills?.props?.filters) {
|
||||
setActiveFilters(filterPills.props.filters);
|
||||
}
|
||||
}, [children]);
|
||||
|
||||
useEffect(() => {
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
setIsDarkMode(isDark);
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.attributeName === 'class') {
|
||||
setIsDarkMode(document.documentElement.classList.contains('dark'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class'],
|
||||
});
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const barColor = getBarColor(activeFilters, isDarkMode);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed left-0 right-0 z-40"
|
||||
style={{ top: 0 }}
|
||||
onMouseEnter={() => setIsVisible(true)}
|
||||
onMouseLeave={() => setIsVisible(false)}
|
||||
>
|
||||
{/* Spacer for the titlebar area */}
|
||||
<div className="h-[56px]" />
|
||||
|
||||
{/* Indicator bar */}
|
||||
<div className="absolute left-0 right-0 h-16 bg-transparent flex justify-center">
|
||||
<div
|
||||
className={`
|
||||
w-[200px] h-[6px]
|
||||
rounded-b-[24px]
|
||||
transition-all duration-300
|
||||
absolute top-0
|
||||
${isVisible ? 'opacity-0 transform -translate-y-1' : 'opacity-100 transform translate-y-0'}
|
||||
`}
|
||||
style={{ backgroundColor: barColor }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Filters container */}
|
||||
<div
|
||||
className={`
|
||||
transform transition-all duration-300 ease-out w-full
|
||||
${
|
||||
isVisible
|
||||
? 'translate-y-0 opacity-100 scale-y-100 origin-top'
|
||||
: 'translate-y-[calc(-100%+6px)] opacity-0 scale-y-95 origin-top'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export interface FilterOption {
|
||||
id: string;
|
||||
label: string;
|
||||
isActive: boolean;
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
export const ChartLineIcon = (): ReactElement => (
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M3 16L8 11L13 16L21 8"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const ChartBarIcon = (): ReactElement => (
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M20 20V4H16V20H20ZM14 20V10H10V20H14ZM8 20V14H4V20H8Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const PieChartIcon = (): ReactElement => (
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12 2V12H22C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const ListIcon = (): ReactElement => (
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M8 6H21M8 12H21M8 18H21M3 6H3.01M3 12H3.01M3 18H3.01"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const StarIcon = (): ReactElement => (
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const TrendingUpIcon = (): ReactElement => (
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M23 6L13.5 15.5L8.5 10.5L1 18M23 6H17M23 6V12"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -1,396 +0,0 @@
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
interface GooseProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Goose({ className = '' }: GooseProps): ReactElement {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<g clipPath="url(#clip0_2096_5193)">
|
||||
<path
|
||||
d="M20.9093 19.3861L19.5185 18.2413C18.7624 17.619 18.1189 16.8713 17.6157 16.0313C16.9205 14.8706 15.9599 13.8912 14.8133 13.1735L14.2533 12.8475C14.0614 12.7141 13.9276 12.5062 13.9086 12.2716C13.8963 12.1204 13.9326 11.9852 14.0171 11.8662C14.3087 11.4553 15.896 9.74698 16.1722 9.51845C16.528 9.22442 16.9243 8.97987 17.2921 8.69986C17.3443 8.66 17.3968 8.62035 17.4485 8.57989C17.4503 8.57808 17.4529 8.57668 17.4545 8.57508C17.5725 8.48195 17.6838 8.383 17.7724 8.26563C18.2036 7.76631 18.195 7.3443 18.195 7.3443C18.195 7.3443 18.1954 7.3439 18.1956 7.3437C18.1497 7.23133 17.9847 6.88163 17.6492 6.71759C17.9458 6.71178 18.2805 6.82294 18.4323 6.97156C18.6148 6.68534 18.7328 6.49967 18.9162 6.18762C18.9599 6.11352 18.9831 5.97652 18.8996 5.89981C18.8996 5.89981 18.8992 5.89981 18.8988 5.89981C18.8988 5.89981 18.8988 5.8994 18.8988 5.899C18.8972 5.8974 18.8952 5.8962 18.8936 5.8946C18.892 5.893 18.891 5.89119 18.8892 5.88939C18.8892 5.88939 18.8888 5.88939 18.8884 5.88939C18.8884 5.88939 18.8884 5.88899 18.8884 5.88859C18.885 5.88518 18.8812 5.88258 18.8776 5.87938C18.8754 5.87717 18.8736 5.87457 18.8716 5.87217C18.8692 5.87016 18.8665 5.86836 18.8643 5.86616C18.8609 5.86275 18.8587 5.85855 18.8551 5.85534C18.8551 5.85534 18.8545 5.85514 18.8543 5.85534C18.8543 5.85534 18.8543 5.85494 18.8543 5.85454C18.8527 5.85294 18.8507 5.85174 18.8491 5.85013C18.8475 5.84853 18.8463 5.84653 18.8447 5.84493C18.8447 5.84493 18.8441 5.84473 18.8439 5.84493C18.8439 5.84493 18.8439 5.84453 18.8439 5.84413C18.7672 5.7606 18.6302 5.78384 18.5561 5.8275C18.1503 6.06625 17.7555 6.32322 17.3996 6.54855C17.3996 6.54855 16.9778 6.53973 16.4783 6.97116C16.3607 7.05989 16.2618 7.17125 16.1688 7.28902C16.167 7.29082 16.1654 7.29322 16.164 7.29503C16.1234 7.3465 16.0837 7.39898 16.0441 7.45145C15.7639 7.81939 15.5195 8.21556 15.2255 8.57128C14.9971 8.84768 13.2887 10.4348 12.8777 10.7264C12.7587 10.8109 12.6237 10.8474 12.4723 10.835C12.2379 10.8161 12.0298 10.6821 11.8965 10.4903L11.5704 9.93024C10.8527 8.78318 9.87332 7.82299 8.71264 7.12778C7.87262 6.62466 7.12514 5.98092 6.50264 5.22503L5.35778 3.83421C5.3013 3.76571 5.19314 3.77693 5.15268 3.85585C5.02249 4.10941 4.77393 4.64479 4.58346 5.36483C4.57885 5.38186 4.58286 5.39988 4.59407 5.4135C4.83082 5.69952 5.37901 6.32983 6.03196 6.863C6.07742 6.90005 6.04017 6.97336 5.98369 6.95774C5.42047 6.80432 4.87288 6.55796 4.46308 6.34805C4.42964 6.33103 4.38918 6.35226 4.38437 6.38951C4.32068 6.89985 4.30425 7.46027 4.37155 8.05112C4.37355 8.07035 4.38577 8.08697 4.4036 8.09479C4.87088 8.29808 5.61816 8.59311 6.40269 8.78078C6.45958 8.7944 6.45777 8.87632 6.40029 8.88733C5.78941 9.0023 5.14968 9.02794 4.62973 9.02113C4.59327 9.02073 4.56643 9.05518 4.57625 9.09023C4.6806 9.45896 4.822 9.8339 5.00847 10.2115C5.08559 10.3811 5.16951 10.5475 5.25944 10.7104C5.27486 10.7382 5.3047 10.7548 5.33655 10.7534C5.76577 10.7324 6.28452 10.6871 6.80608 10.595C6.89501 10.5794 6.94268 10.6964 6.86757 10.7466C6.51345 10.9834 6.13571 11.1873 5.7844 11.3551C5.73733 11.3777 5.72211 11.4378 5.75315 11.4797C5.96186 11.7625 6.19139 12.0301 6.44075 12.2794C6.44075 12.2794 7.66853 13.5441 7.70198 13.6432C8.41841 12.9096 9.59612 12.0964 10.8966 11.3864C9.15488 12.8036 8.18387 13.8499 7.69517 14.4444L7.35447 14.9225C7.17742 15.1708 7.02379 15.4346 6.89541 15.7112C6.46579 16.6356 5.75756 18.5051 5.75756 18.5051C5.70328 18.6515 5.74754 18.7959 5.84168 18.89C5.84388 18.8922 5.84609 18.8944 5.84849 18.8964C5.85069 18.8986 5.8527 18.901 5.8549 18.9032C5.94924 18.9976 6.09345 19.0416 6.23986 18.9874C6.23986 18.9874 8.10897 18.2791 9.03371 17.8495C9.31031 17.7211 9.57429 17.5673 9.82245 17.3905L10.349 17.0153C10.6278 16.8166 11.0096 16.8483 11.2517 17.0904L12.4655 18.3042C12.7148 18.5535 12.9824 18.7831 13.2652 18.9918C13.3073 19.0226 13.3672 19.0076 13.3898 18.9605C13.5579 18.6094 13.7618 18.2313 13.9983 17.8774C14.0486 17.8022 14.1657 17.8501 14.1499 17.9388C14.0576 18.4606 14.0127 18.9794 13.9915 19.4084C13.9899 19.44 14.0067 19.4701 14.0345 19.4855C14.1972 19.5756 14.3636 19.6595 14.5335 19.7364C14.911 19.9229 15.2862 20.0645 15.6547 20.1687C15.6897 20.1785 15.7242 20.1516 15.7238 20.1152C15.7168 19.595 15.7424 18.9553 15.8576 18.3446C15.8684 18.2869 15.9503 18.2851 15.9641 18.3422C16.1516 19.127 16.4466 19.8742 16.6501 20.3413C16.6579 20.3591 16.6744 20.3712 16.6938 20.3734C17.2847 20.4407 17.8451 20.4242 18.3554 20.3606C18.3929 20.3559 18.4141 20.3155 18.3969 20.2818C18.187 19.872 17.9406 19.3241 17.7872 18.7612C17.7718 18.7046 17.8449 18.6675 17.8819 18.713C18.4151 19.3659 19.0454 19.9141 19.3314 20.1508C19.345 20.1621 19.3633 20.1659 19.3801 20.1615C20.1003 19.9712 20.6357 19.7226 20.8891 19.5922C20.968 19.5518 20.9792 19.4436 20.9107 19.3871L20.9093 19.3861Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2096_5193">
|
||||
<rect width="24" height="24" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
interface RainProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Rain({ className = '' }: RainProps): ReactElement {
|
||||
return (
|
||||
<svg
|
||||
width="103"
|
||||
height="103"
|
||||
viewBox="0 0 103 103"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<g id="wind">
|
||||
<g id="1" className="animate-[wind_2s_linear_infinite]">
|
||||
<path
|
||||
id="Vector 42_2"
|
||||
d="M66 33L62 37"
|
||||
stroke="url(#paint10_linear_2096_5587)"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
id="Vector 41_2"
|
||||
d="M70 43L68 45"
|
||||
stroke="url(#paint11_linear_2096_5587)"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
id="Vector 51_2"
|
||||
d="M60 34L59 35"
|
||||
stroke="url(#paint12_linear_2096_5587)"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
id="Vector 43_2"
|
||||
d="M56 47L52 51"
|
||||
stroke="url(#paint13_linear_2096_5587)"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
id="Vector 46_2"
|
||||
d="M98 1L94 5"
|
||||
stroke="url(#paint14_linear_2096_5587)"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
id="Vector 47_2"
|
||||
d="M102 11L100 13"
|
||||
stroke="url(#paint15_linear_2096_5587)"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
id="Vector 48_2"
|
||||
d="M88 15L84 19"
|
||||
stroke="url(#paint16_linear_2096_5587)"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
id="Vector 45_2"
|
||||
d="M76 26L72 30"
|
||||
stroke="url(#paint17_linear_2096_5587)"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
id="Vector 49_2"
|
||||
d="M87 28L83 32"
|
||||
stroke="url(#paint18_linear_2096_5587)"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
id="Vector 50_2"
|
||||
d="M76 34L74 36"
|
||||
stroke="url(#paint19_linear_2096_5587)"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</g>
|
||||
<g id="2" className="animate-[wind_2s_linear_1s_infinite]">
|
||||
<path
|
||||
id="Vector 42"
|
||||
d="M66 33L62 37"
|
||||
stroke="url(#paint0_linear_2096_5587)"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
id="Vector 41"
|
||||
d="M70 43L68 45"
|
||||
stroke="url(#paint1_linear_2096_5587)"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
id="Vector 51"
|
||||
d="M60 34L59 35"
|
||||
stroke="url(#paint2_linear_2096_5587)"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
id="Vector 43"
|
||||
d="M56 47L52 51"
|
||||
stroke="url(#paint3_linear_2096_5587)"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
id="Vector 46"
|
||||
d="M98 1L94 5"
|
||||
stroke="url(#paint4_linear_2096_5587)"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
id="Vector 47"
|
||||
d="M102 11L100 13"
|
||||
stroke="url(#paint5_linear_2096_5587)"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
id="Vector 48"
|
||||
d="M88 15L84 19"
|
||||
stroke="url(#paint6_linear_2096_5587)"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
id="Vector 45"
|
||||
d="M76 26L72 30"
|
||||
stroke="url(#paint7_linear_2096_5587)"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
id="Vector 49"
|
||||
d="M87 28L83 32"
|
||||
stroke="url(#paint8_linear_2096_5587)"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
id="Vector 50"
|
||||
d="M76 34L74 36"
|
||||
stroke="url(#paint9_linear_2096_5587)"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_2096_5587"
|
||||
x1="64"
|
||||
y1="32.9437"
|
||||
x2="64"
|
||||
y2="37.0563"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#EC5D2A" />
|
||||
<stop offset="1" stopColor="#57B9AF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_2096_5587"
|
||||
x1="69"
|
||||
y1="42.9719"
|
||||
x2="69"
|
||||
y2="45.0281"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#EC5D2A" />
|
||||
<stop offset="1" stopColor="#57B9AF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint2_linear_2096_5587"
|
||||
x1="59.5"
|
||||
y1="33.9859"
|
||||
x2="59.5"
|
||||
y2="35.0141"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#EC5D2A" />
|
||||
<stop offset="1" stopColor="#57B9AF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint3_linear_2096_5587"
|
||||
x1="54"
|
||||
y1="46.9437"
|
||||
x2="54"
|
||||
y2="51.0563"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#EC5D2A" />
|
||||
<stop offset="1" stopColor="#57B9AF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint4_linear_2096_5587"
|
||||
x1="96"
|
||||
y1="0.943728"
|
||||
x2="96"
|
||||
y2="5.05625"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#EC5D2A" />
|
||||
<stop offset="1" stopColor="#57B9AF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint5_linear_2096_5587"
|
||||
x1="101"
|
||||
y1="10.9719"
|
||||
x2="101"
|
||||
y2="13.0281"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#EC5D2A" />
|
||||
<stop offset="1" stopColor="#57B9AF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint6_linear_2096_5587"
|
||||
x1="86"
|
||||
y1="14.9437"
|
||||
x2="86"
|
||||
y2="19.0563"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#EC5D2A" />
|
||||
<stop offset="1" stopColor="#57B9AF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint7_linear_2096_5587"
|
||||
x1="74"
|
||||
y1="25.9437"
|
||||
x2="74"
|
||||
y2="30.0563"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#EC5D2A" />
|
||||
<stop offset="1" stopColor="#57B9AF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint8_linear_2096_5587"
|
||||
x1="85"
|
||||
y1="27.9437"
|
||||
x2="85"
|
||||
y2="32.0563"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#EC5D2A" />
|
||||
<stop offset="1" stopColor="#57B9AF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint9_linear_2096_5587"
|
||||
x1="75"
|
||||
y1="33.9719"
|
||||
x2="75"
|
||||
y2="36.0281"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#EC5D2A" />
|
||||
<stop offset="1" stopColor="#57B9AF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint10_linear_2096_5587"
|
||||
x1="64"
|
||||
y1="32.9437"
|
||||
x2="64"
|
||||
y2="37.0563"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#EC5D2A" />
|
||||
<stop offset="1" stopColor="#57B9AF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint11_linear_2096_5587"
|
||||
x1="69"
|
||||
y1="42.9719"
|
||||
x2="69"
|
||||
y2="45.0281"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#EC5D2A" />
|
||||
<stop offset="1" stopColor="#57B9AF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint12_linear_2096_5587"
|
||||
x1="59.5"
|
||||
y1="33.9859"
|
||||
x2="59.5"
|
||||
y2="35.0141"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#EC5D2A" />
|
||||
<stop offset="1" stopColor="#57B9AF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint13_linear_2096_5587"
|
||||
x1="54"
|
||||
y1="46.9437"
|
||||
x2="54"
|
||||
y2="51.0563"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#EC5D2A" />
|
||||
<stop offset="1" stopColor="#57B9AF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint14_linear_2096_5587"
|
||||
x1="96"
|
||||
y1="0.943728"
|
||||
x2="96"
|
||||
y2="5.05625"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#EC5D2A" />
|
||||
<stop offset="1" stopColor="#57B9AF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint15_linear_2096_5587"
|
||||
x1="101"
|
||||
y1="10.9719"
|
||||
x2="101"
|
||||
y2="13.0281"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#EC5D2A" />
|
||||
<stop offset="1" stopColor="#57B9AF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint16_linear_2096_5587"
|
||||
x1="86"
|
||||
y1="14.9437"
|
||||
x2="86"
|
||||
y2="19.0563"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#EC5D2A" />
|
||||
<stop offset="1" stopColor="#57B9AF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint17_linear_2096_5587"
|
||||
x1="74"
|
||||
y1="25.9437"
|
||||
x2="74"
|
||||
y2="30.0563"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#EC5D2A" />
|
||||
<stop offset="1" stopColor="#57B9AF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint18_linear_2096_5587"
|
||||
x1="85"
|
||||
y1="27.9437"
|
||||
x2="85"
|
||||
y2="32.0563"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#EC5D2A" />
|
||||
<stop offset="1" stopColor="#57B9AF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint19_linear_2096_5587"
|
||||
x1="75"
|
||||
y1="33.9719"
|
||||
x2="75"
|
||||
y2="36.0281"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#EC5D2A" />
|
||||
<stop offset="1" stopColor="#57B9AF" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
import { ReactElement, ReactNode } from 'react';
|
||||
|
||||
import { BarChart, Bar, LineChart, Line, CartesianGrid, XAxis } from 'recharts';
|
||||
|
||||
import { useTimelineStyles } from '../../hooks/useTimelineStyles.ts';
|
||||
|
||||
import {
|
||||
ChartConfig,
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from '@/components/ui/chart';
|
||||
|
||||
interface ChartTileProps {
|
||||
title: string;
|
||||
value: string;
|
||||
trend?: string;
|
||||
data: number[];
|
||||
icon: ReactNode;
|
||||
variant?: 'line' | 'bar';
|
||||
date?: Date;
|
||||
}
|
||||
|
||||
export default function ChartTile({
|
||||
title,
|
||||
value,
|
||||
trend,
|
||||
data,
|
||||
icon,
|
||||
variant = 'line',
|
||||
date,
|
||||
}: ChartTileProps): ReactElement {
|
||||
const { contentCardStyle } = useTimelineStyles(date);
|
||||
|
||||
// Convert the data array to the format expected by recharts
|
||||
const chartData = data.map((value, index) => ({
|
||||
value,
|
||||
point: `P${index + 1}`,
|
||||
}));
|
||||
|
||||
// Chart configuration with proper color variables
|
||||
const chartConfig = {
|
||||
value: {
|
||||
label: title,
|
||||
color: variant === 'line' ? 'var(--chart-2)' : 'var(--chart-1)',
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
flex flex-col justify-between
|
||||
w-[320px] min-h-[380px]
|
||||
${contentCardStyle}
|
||||
rounded-[18px]
|
||||
relative
|
||||
overflow-hidden
|
||||
transition-all duration-200
|
||||
hover:scale-[1.02]
|
||||
bg-background-default text-text-default
|
||||
`}
|
||||
>
|
||||
{/* Header section with icon */}
|
||||
<div className="p-4 space-y-4">
|
||||
<div className="w-6 h-6 text-text-default dark:text-white">{icon}</div>
|
||||
|
||||
<div>
|
||||
<div className="text-text-muted dark:text-white/60 text-sm mb-1">{title}</div>
|
||||
<div className="text-text-default dark:text-white text-2xl font-semibold">
|
||||
{value}
|
||||
{trend && (
|
||||
<span className="ml-1 text-sm text-text-muted dark:text-white/60">{trend}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chart Container */}
|
||||
<div className="w-full h-[200px] px-4 pb-6">
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-tooltip-wrapper]:!pointer-events-none"
|
||||
>
|
||||
{variant === 'line' ? (
|
||||
<LineChart
|
||||
width={288}
|
||||
height={162}
|
||||
data={chartData}
|
||||
margin={{ top: 10, right: 10, bottom: 0, left: -20 }}
|
||||
>
|
||||
<CartesianGrid vertical={false} className="stroke-border/50" />
|
||||
<XAxis
|
||||
dataKey="point"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
height={40}
|
||||
tick={{ fill: 'var(--text-muted)' }}
|
||||
/>
|
||||
<ChartTooltip
|
||||
content={
|
||||
<ChartTooltipContent className="border-border/50 bg-background-default text-text-default min-w-[180px] [&_.flex.flex-1]:gap-4 [&_.flex.flex-1>span]:whitespace-nowrap" />
|
||||
}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke="var(--chart-2)"
|
||||
strokeWidth={2}
|
||||
dot={{ fill: 'var(--chart-2)', r: 4 }}
|
||||
/>
|
||||
</LineChart>
|
||||
) : (
|
||||
<BarChart
|
||||
width={288}
|
||||
height={162}
|
||||
data={chartData}
|
||||
margin={{ top: 10, right: 10, bottom: 0, left: 10 }}
|
||||
>
|
||||
<CartesianGrid vertical={false} className="stroke-border/50" />
|
||||
<XAxis
|
||||
dataKey="point"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
height={40}
|
||||
tick={{ fill: 'var(--text-muted)' }}
|
||||
interval={0}
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
indicator="dashed"
|
||||
className="border-border/50 bg-background-default text-text-default min-w-[180px] [&_.flex.flex-1]:gap-4 [&_.flex.flex-1>span]:whitespace-nowrap"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Bar dataKey="value" fill="var(--chart-1)" radius={4} maxBarSize={32} />
|
||||
</BarChart>
|
||||
)}
|
||||
</ChartContainer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import { useState, useEffect, ReactElement } from 'react';
|
||||
|
||||
import { useTimelineStyles } from '../../hooks/useTimelineStyles.ts';
|
||||
|
||||
// Use string path for the background image
|
||||
const waveBgUrl = '/src/assets/backgrounds/wave-bg.png';
|
||||
|
||||
interface ClockCardProps {
|
||||
date?: Date;
|
||||
}
|
||||
|
||||
export default function ClockTile({ date }: ClockCardProps): ReactElement | null {
|
||||
const { contentCardStyle, isPastDate } = useTimelineStyles(date);
|
||||
const [currentTime, setCurrentTime] = useState(new Date());
|
||||
|
||||
// Update time every second for current day
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setCurrentTime(new Date());
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
// Don't render for past dates
|
||||
if (isPastDate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Format hours (12-hour format)
|
||||
const hours = currentTime.getHours() % 12 || 12;
|
||||
const minutes = currentTime.getMinutes().toString().padStart(2, '0');
|
||||
const period = currentTime.getHours() >= 12 ? 'PM' : 'AM';
|
||||
|
||||
// Format day name
|
||||
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||
const dayName = dayNames[currentTime.getDay()];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
flex flex-col justify-between
|
||||
p-4
|
||||
w-[213px] h-[213px]
|
||||
${contentCardStyle}
|
||||
rounded-[18px]
|
||||
relative
|
||||
overflow-hidden
|
||||
group
|
||||
`}
|
||||
>
|
||||
{/* Background Image with Gradient Overlay */}
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat transition-opacity duration-500"
|
||||
style={{
|
||||
backgroundImage: `url(${waveBgUrl})`,
|
||||
opacity: 0.8,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Gradient Overlay */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent" />
|
||||
|
||||
{/* Time Display */}
|
||||
<div className="flex flex-col items-start mt-auto relative z-10">
|
||||
<div className="flex items-baseline">
|
||||
<span className="font-['Cash_Sans'] text-[48px] font-light text-white leading-none">
|
||||
{hours}:{minutes}
|
||||
</span>
|
||||
<span className="ml-1 font-['Cash_Sans'] text-xl font-light text-white">{period}</span>
|
||||
</div>
|
||||
<span className="text-sm text-white/80 mt-1">{dayName}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import { ReactElement, ReactNode } from 'react';
|
||||
|
||||
import { useTimelineStyles } from '../../hooks/useTimelineStyles.ts';
|
||||
|
||||
interface HighlightTileProps {
|
||||
title: string;
|
||||
value: string;
|
||||
icon: ReactNode;
|
||||
subtitle?: string;
|
||||
date?: Date;
|
||||
accentColor?: string;
|
||||
}
|
||||
|
||||
export default function HighlightTile({
|
||||
title,
|
||||
value,
|
||||
icon,
|
||||
subtitle,
|
||||
date,
|
||||
accentColor = '#00CAF7',
|
||||
}: HighlightTileProps): ReactElement {
|
||||
const { contentCardStyle } = useTimelineStyles(date);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
flex flex-col justify-between
|
||||
w-[320px] h-[280px]
|
||||
${contentCardStyle}
|
||||
rounded-[18px]
|
||||
relative
|
||||
overflow-hidden
|
||||
transition-all duration-200
|
||||
hover:scale-[1.02]
|
||||
`}
|
||||
>
|
||||
{/* Background accent */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-5"
|
||||
style={{
|
||||
background: `radial-gradient(circle at top right, ${accentColor}, transparent 70%)`,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-4 h-full flex flex-col justify-between relative z-10">
|
||||
<div className="w-6 h-6 text-text-default dark:text-white">{icon}</div>
|
||||
|
||||
<div>
|
||||
<div className="text-gray-600 dark:text-white/60 text-sm mb-1">{title}</div>
|
||||
<div
|
||||
className="text-gray-900 dark:text-white text-2xl font-semibold"
|
||||
style={{ color: accentColor }}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
{subtitle && (
|
||||
<div className="text-gray-500 dark:text-white/60 text-sm mt-1">{subtitle}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import { ReactElement, ReactNode } from 'react';
|
||||
|
||||
import { useTimelineStyles } from '../../hooks/useTimelineStyles.ts';
|
||||
|
||||
interface ListItem {
|
||||
text: string;
|
||||
value?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
interface ListTileProps {
|
||||
title: string;
|
||||
icon: ReactNode;
|
||||
items: ListItem[];
|
||||
date?: Date;
|
||||
}
|
||||
|
||||
export default function ListTile({ title, icon, items, date }: ListTileProps): ReactElement {
|
||||
const { contentCardStyle } = useTimelineStyles(date);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
flex flex-col
|
||||
w-[320px] h-[420px]
|
||||
${contentCardStyle}
|
||||
rounded-[18px]
|
||||
relative
|
||||
overflow-hidden
|
||||
transition-all duration-200
|
||||
hover:scale-[1.02]
|
||||
`}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="p-4">
|
||||
<div className="w-6 h-6 mb-4 text-text-default dark:text-white">{icon}</div>
|
||||
<div className="text-gray-600 dark:text-white/60 text-sm mb-4">{title}</div>
|
||||
</div>
|
||||
|
||||
{/* List */}
|
||||
<div className="flex-1 overflow-y-auto px-4 pb-4">
|
||||
<div className="space-y-3">
|
||||
{items.map((item, index) => (
|
||||
<div key={index} className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full ${
|
||||
item.color ? '' : 'bg-gray-400 dark:bg-white/40'
|
||||
}`}
|
||||
style={item.color ? { backgroundColor: item.color } : {}}
|
||||
/>
|
||||
<span className="text-sm text-gray-600 dark:text-white/80">{item.text}</span>
|
||||
</div>
|
||||
{item.value && (
|
||||
<span
|
||||
className="text-sm font-medium"
|
||||
style={item.color ? { color: item.color } : {}}
|
||||
>
|
||||
{item.value}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
import { useState, ReactElement, ReactNode } from 'react';
|
||||
|
||||
import { PieChart, Pie, Cell, Sector } from 'recharts';
|
||||
|
||||
import { useTimelineStyles } from '../../hooks/useTimelineStyles';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface PieChartSegment {
|
||||
value: number;
|
||||
color: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface PieChartTileProps {
|
||||
title: string;
|
||||
icon: ReactNode;
|
||||
segments: PieChartSegment[];
|
||||
date?: Date;
|
||||
}
|
||||
|
||||
interface LabelProps {
|
||||
cx: number;
|
||||
cy: number;
|
||||
midAngle: number;
|
||||
innerRadius: number;
|
||||
outerRadius: number;
|
||||
percent: number;
|
||||
payload: { name: string };
|
||||
fill: string;
|
||||
}
|
||||
|
||||
// Custom label renderer with connecting lines
|
||||
const renderCustomizedLabel = ({
|
||||
cx,
|
||||
cy,
|
||||
midAngle,
|
||||
innerRadius: _innerRadius,
|
||||
outerRadius,
|
||||
percent,
|
||||
payload,
|
||||
fill,
|
||||
}: LabelProps) => {
|
||||
const RADIAN = Math.PI / 180;
|
||||
const sin = Math.sin(-RADIAN * midAngle);
|
||||
const cos = Math.cos(-RADIAN * midAngle);
|
||||
|
||||
// Adjust these values to position labels closer to the pie
|
||||
const labelOffset = 12;
|
||||
const labelDistance = 18;
|
||||
|
||||
// Calculate positions with shorter distances
|
||||
const mx = cx + (outerRadius + labelOffset) * cos;
|
||||
const my = cy + (outerRadius + labelOffset) * sin;
|
||||
const ex = mx + (cos >= 0 ? 1 : -1) * labelDistance;
|
||||
const ey = my;
|
||||
|
||||
// Text anchor based on which side of the pie we're on
|
||||
const textAnchor = cos >= 0 ? 'start' : 'end';
|
||||
|
||||
// Calculate percentage
|
||||
const value = (percent * 100).toFixed(0);
|
||||
|
||||
// Determine if label should be on top or bottom half for potential y-offset
|
||||
const isTopHalf = my < cy;
|
||||
const yOffset = isTopHalf ? -2 : 2;
|
||||
|
||||
// Force specific adjustments for "In Progress" label if needed
|
||||
const isInProgress = payload.name === 'In Progress';
|
||||
const adjustedEx = isInProgress ? ex - 5 : ex;
|
||||
|
||||
return (
|
||||
<g>
|
||||
{/* Label line - using absolute coordinates for reliability */}
|
||||
<path
|
||||
d={`M${cx + outerRadius * cos},${cy + outerRadius * sin}L${mx},${my}L${adjustedEx},${ey}`}
|
||||
stroke={fill}
|
||||
strokeWidth={1}
|
||||
fill="none"
|
||||
style={{ opacity: 1 }}
|
||||
/>
|
||||
{/* Label text with adjusted position */}
|
||||
<text
|
||||
x={adjustedEx + (cos >= 0 ? 5 : -5)}
|
||||
y={ey + yOffset}
|
||||
textAnchor={textAnchor}
|
||||
fill="var(--text-default)"
|
||||
className="text-[10px]"
|
||||
style={{
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
{payload.name} ({value}%)
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
interface ActiveShapeProps {
|
||||
cx: number;
|
||||
cy: number;
|
||||
innerRadius: number;
|
||||
outerRadius: number;
|
||||
startAngle: number;
|
||||
endAngle: number;
|
||||
fill: string;
|
||||
}
|
||||
|
||||
// Active shape renderer for hover effect
|
||||
const renderActiveShape = (props: ActiveShapeProps) => {
|
||||
const { cx, cy, innerRadius, outerRadius, startAngle, endAngle, fill } = props;
|
||||
|
||||
return (
|
||||
<Sector
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
innerRadius={innerRadius}
|
||||
outerRadius={outerRadius + 4}
|
||||
startAngle={startAngle}
|
||||
endAngle={endAngle}
|
||||
fill={fill}
|
||||
cornerRadius={4}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default function PieChartTile({
|
||||
title,
|
||||
icon,
|
||||
segments,
|
||||
date,
|
||||
}: PieChartTileProps): ReactElement {
|
||||
const { contentCardStyle } = useTimelineStyles(date);
|
||||
const [activeIndex, setActiveIndex] = useState<number>(0);
|
||||
|
||||
// Convert segments to the format expected by recharts and assign chart colors
|
||||
const chartData = segments.map((segment, index) => ({
|
||||
name: segment.label,
|
||||
value: segment.value,
|
||||
chartColor: `var(--chart-${index + 1})`, // Use chart-1, chart-2, chart-3, etc.
|
||||
}));
|
||||
|
||||
const onPieEnter = (_: unknown, index: number): void => {
|
||||
setActiveIndex(index);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
flex flex-col
|
||||
w-[320px] min-h-[380px]
|
||||
${contentCardStyle}
|
||||
rounded-[18px]
|
||||
relative
|
||||
overflow-hidden
|
||||
transition-all duration-200
|
||||
hover:scale-[1.02]
|
||||
bg-background-default text-text-default
|
||||
`}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="p-4">
|
||||
<div className="w-6 h-6 mb-4 text-text-default dark:text-white">{icon}</div>
|
||||
<div className="text-text-muted dark:text-white/60 text-sm">{title}</div>
|
||||
</div>
|
||||
|
||||
{/* Pie Chart */}
|
||||
<div className="flex-1 flex items-center justify-center p-4">
|
||||
<div
|
||||
className={cn(
|
||||
'[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground',
|
||||
"[&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50",
|
||||
'[&_.recharts-curve.recharts-tooltip-cursor]:stroke-border',
|
||||
"[&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border",
|
||||
'[&_.recharts-radial-bar-background-sector]:fill-muted',
|
||||
'[&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted',
|
||||
"[&_.recharts-reference-line_[stroke='#ccc']]:stroke-border",
|
||||
'flex justify-center text-xs',
|
||||
"[&_.recharts-dot[stroke='#fff']]:stroke-transparent",
|
||||
'[&_.recharts-layer]:outline-hidden',
|
||||
'[&_.recharts-sector]:outline-hidden',
|
||||
"[&_.recharts-sector[stroke='#fff']]:stroke-transparent",
|
||||
'[&_.recharts-surface]:outline-hidden'
|
||||
)}
|
||||
>
|
||||
<PieChart width={288} height={162} margin={{ top: 30, right: 40, bottom: 10, left: 40 }}>
|
||||
<Pie
|
||||
activeIndex={activeIndex}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
activeShape={renderActiveShape as any}
|
||||
data={chartData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={45}
|
||||
outerRadius={65}
|
||||
paddingAngle={5}
|
||||
dataKey="value"
|
||||
onMouseEnter={onPieEnter}
|
||||
cornerRadius={4}
|
||||
label={renderCustomizedLabel}
|
||||
labelLine={false}
|
||||
startAngle={90}
|
||||
endAngle={-270}
|
||||
isAnimationActive={false}
|
||||
>
|
||||
{chartData.map((entry, index) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={entry.chartColor}
|
||||
stroke="var(--background-default)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,321 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import * as RechartsPrimitive from 'recharts';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||
const THEMES = { light: '', dark: '.dark' } as const;
|
||||
|
||||
export type ChartConfig = {
|
||||
[k in string]: {
|
||||
label?: React.ReactNode;
|
||||
icon?: React.ComponentType;
|
||||
} & (
|
||||
| { color?: string; theme?: never }
|
||||
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||
);
|
||||
};
|
||||
|
||||
type ChartContextProps = {
|
||||
config: ChartConfig;
|
||||
};
|
||||
|
||||
const ChartContext = React.createContext<ChartContextProps | null>(null);
|
||||
|
||||
function useChart() {
|
||||
const context = React.useContext(ChartContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useChart must be used within a <ChartContainer />');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
function ChartContainer({
|
||||
id,
|
||||
className,
|
||||
children,
|
||||
config,
|
||||
...props
|
||||
}: React.ComponentProps<'div'> & {
|
||||
config: ChartConfig;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const uniqueId = React.useId();
|
||||
const chartId = `chart-${id || uniqueId.replace(/:/g, '')}`;
|
||||
|
||||
return (
|
||||
<ChartContext.Provider value={{ config }}>
|
||||
<div
|
||||
data-slot="chart"
|
||||
data-chart={chartId}
|
||||
className={cn(
|
||||
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChartStyle id={chartId} config={config} />
|
||||
{children}
|
||||
</div>
|
||||
</ChartContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||
const colorConfig = Object.entries(config).filter(([, config]) => config.theme || config.color);
|
||||
|
||||
if (!colorConfig.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: Object.entries(THEMES)
|
||||
.map(
|
||||
([theme, prefix]) => `
|
||||
${prefix} [data-chart=${id}] {
|
||||
${colorConfig
|
||||
.map(([key, itemConfig]) => {
|
||||
const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color;
|
||||
return color ? ` --color-${key}: ${color};` : null;
|
||||
})
|
||||
.join('\n')}
|
||||
}
|
||||
`
|
||||
)
|
||||
.join('\n'),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ChartTooltip = RechartsPrimitive.Tooltip;
|
||||
|
||||
function ChartTooltipContent({
|
||||
active,
|
||||
payload,
|
||||
className,
|
||||
indicator = 'dot',
|
||||
hideLabel = false,
|
||||
hideIndicator = false,
|
||||
label,
|
||||
labelFormatter,
|
||||
labelClassName,
|
||||
formatter,
|
||||
color,
|
||||
nameKey,
|
||||
labelKey,
|
||||
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||
React.ComponentProps<'div'> & {
|
||||
hideLabel?: boolean;
|
||||
hideIndicator?: boolean;
|
||||
indicator?: 'line' | 'dot' | 'dashed';
|
||||
nameKey?: string;
|
||||
labelKey?: string;
|
||||
}) {
|
||||
const { config } = useChart();
|
||||
|
||||
const tooltipLabel = React.useMemo(() => {
|
||||
if (hideLabel || !payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [item] = payload;
|
||||
const key = `${labelKey || item?.dataKey || item?.name || 'value'}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const value =
|
||||
!labelKey && typeof label === 'string'
|
||||
? config[label as keyof typeof config]?.label || label
|
||||
: itemConfig?.label;
|
||||
|
||||
if (labelFormatter) {
|
||||
return (
|
||||
<div className={cn('font-medium', labelClassName)}>{labelFormatter(value, payload)}</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div className={cn('font-medium', labelClassName)}>{value}</div>;
|
||||
}, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey]);
|
||||
|
||||
if (!active || !payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nestLabel = payload.length === 1 && indicator !== 'dot';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'border-border/50 bg-background-default grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!nestLabel ? tooltipLabel : null}
|
||||
<div className="grid gap-1.5">
|
||||
{payload.map((item, index) => {
|
||||
const key = `${nameKey || item.name || item.dataKey || 'value'}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const indicatorColor = color || item.payload.fill || item.color;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.dataKey}
|
||||
className={cn(
|
||||
'[&>svg]:text-text-muted flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5',
|
||||
indicator === 'dot' && 'items-center'
|
||||
)}
|
||||
>
|
||||
{formatter && item?.value !== undefined && item.name ? (
|
||||
formatter(item.value, item.name, item, index, item.payload)
|
||||
) : (
|
||||
<>
|
||||
{itemConfig?.icon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
!hideIndicator && (
|
||||
<div
|
||||
className={cn(
|
||||
'shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)',
|
||||
{
|
||||
'h-2.5 w-2.5': indicator === 'dot',
|
||||
'w-1': indicator === 'line',
|
||||
'w-0 border-[1.5px] border-dashed bg-transparent':
|
||||
indicator === 'dashed',
|
||||
'my-0.5': nestLabel && indicator === 'dashed',
|
||||
}
|
||||
)}
|
||||
style={
|
||||
{
|
||||
'--color-bg': indicatorColor,
|
||||
'--color-border': indicatorColor,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-1 justify-between leading-none',
|
||||
nestLabel ? 'items-end' : 'items-center'
|
||||
)}
|
||||
>
|
||||
<div className="grid gap-1.5">
|
||||
{nestLabel ? tooltipLabel : null}
|
||||
<span className="text-text-muted">{itemConfig?.label || item.name}</span>
|
||||
</div>
|
||||
{item.value && (
|
||||
<span className="text-text-default font-mono tabular-nums">
|
||||
{item.value.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const ChartLegend = RechartsPrimitive.Legend;
|
||||
|
||||
function ChartLegendContent({
|
||||
className,
|
||||
hideIcon = false,
|
||||
payload,
|
||||
verticalAlign = 'bottom',
|
||||
nameKey,
|
||||
}: React.ComponentProps<'div'> &
|
||||
Pick<RechartsPrimitive.LegendProps, 'payload' | 'verticalAlign'> & {
|
||||
hideIcon?: boolean;
|
||||
nameKey?: string;
|
||||
}) {
|
||||
const { config } = useChart();
|
||||
|
||||
if (!payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-center gap-4',
|
||||
verticalAlign === 'top' ? 'pb-3' : 'pt-3',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{payload.map((item) => {
|
||||
const key = `${nameKey || item.dataKey || 'value'}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.value}
|
||||
className={cn(
|
||||
'[&>svg]:text-text-muted flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3'
|
||||
)}
|
||||
>
|
||||
{itemConfig?.icon && !hideIcon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
<div
|
||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||
style={{
|
||||
backgroundColor: item.color,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{itemConfig?.label}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Helper to extract item config from a payload.
|
||||
function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
|
||||
if (typeof payload !== 'object' || payload === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const payloadPayload =
|
||||
'payload' in payload && typeof payload.payload === 'object' && payload.payload !== null
|
||||
? payload.payload
|
||||
: undefined;
|
||||
|
||||
let configLabelKey: string = key;
|
||||
|
||||
if (key in payload && typeof payload[key as keyof typeof payload] === 'string') {
|
||||
configLabelKey = payload[key as keyof typeof payload] as string;
|
||||
} else if (
|
||||
payloadPayload &&
|
||||
key in payloadPayload &&
|
||||
typeof payloadPayload[key as keyof typeof payloadPayload] === 'string'
|
||||
) {
|
||||
configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;
|
||||
}
|
||||
|
||||
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config];
|
||||
}
|
||||
|
||||
export {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartStyle,
|
||||
};
|
||||
@@ -1,4 +0,0 @@
|
||||
Files in this directory are automatically generated/pulled in from the shadcn registry.
|
||||
|
||||
Add new components like this:
|
||||
```npx shadcn@canary add http://localhost:3000/r/chart.json```
|
||||
@@ -1,58 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function TooltipProvider({
|
||||
delayDuration = 0,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||
return (
|
||||
<TooltipPrimitive.Provider
|
||||
data-slot="tooltip-provider"
|
||||
delayDuration={delayDuration}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function TooltipContent({
|
||||
className,
|
||||
sideOffset = 0,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'bg-[#2E2E2E] text-[#FFFFFF] animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="fill-[#2E2E2E]" />
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
@@ -1,31 +0,0 @@
|
||||
import { createContext, useContext, useState, useCallback, ReactElement, ReactNode } from 'react';
|
||||
|
||||
interface TimelineContextType {
|
||||
currentDate: Date;
|
||||
setCurrentDate: (date: Date) => void;
|
||||
isCurrentDate: (date: Date) => boolean;
|
||||
}
|
||||
|
||||
const TimelineContext = createContext<TimelineContextType | undefined>(undefined);
|
||||
|
||||
export function TimelineProvider({ children }: { children: ReactNode }): ReactElement {
|
||||
const [currentDate, setCurrentDate] = useState(new Date());
|
||||
|
||||
const isCurrentDate = useCallback((date: Date): boolean => {
|
||||
return date.toDateString() === new Date().toDateString();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<TimelineContext.Provider value={{ currentDate, setCurrentDate, isCurrentDate }}>
|
||||
{children}
|
||||
</TimelineContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTimeline(): TimelineContextType {
|
||||
const context = useContext(TimelineContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useTimeline must be used within a TimelineProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { useTimeline } from '../components/TimelineContext';
|
||||
|
||||
interface TimelineStyles {
|
||||
isPastDate: boolean;
|
||||
greetingCardStyle: {
|
||||
background: string;
|
||||
text: string;
|
||||
};
|
||||
contentCardStyle: string;
|
||||
}
|
||||
|
||||
export function useTimelineStyles(date?: Date): TimelineStyles {
|
||||
const { isCurrentDate } = useTimeline();
|
||||
const isPastDate = date ? date < new Date() && !isCurrentDate(date) : false;
|
||||
|
||||
// Content cards match the Tasks Completed tile styling
|
||||
const contentCardStyle =
|
||||
'bg-white dark:bg-[#121212] shadow-[0_0_13.7px_rgba(0,0,0,0.04)] dark:shadow-[0_0_24px_rgba(255,255,255,0.02)]';
|
||||
|
||||
// Greeting card styles based on date
|
||||
const greetingCardStyle = !isPastDate
|
||||
? {
|
||||
background: 'bg-textStandard', // Black background
|
||||
text: 'text-white', // White text
|
||||
}
|
||||
: {
|
||||
background: 'bg-gray-100', // Light grey background
|
||||
text: 'text-gray-600', // Darker grey text
|
||||
};
|
||||
|
||||
return {
|
||||
isPastDate,
|
||||
greetingCardStyle,
|
||||
contentCardStyle,
|
||||
};
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
interface MainLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
|
||||
return (
|
||||
<div className="min-h-screen bg-background-default dark:bg-zinc-800 transition-colors duration-200">
|
||||
<div className="titlebar-drag-region" />
|
||||
<div className="p-5 pt-10 max-w-3xl mx-auto">{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]): string {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export const copyToClipboard = (text: string): void => {
|
||||
if (window === undefined) return;
|
||||
window.navigator.clipboard.writeText(text);
|
||||
};
|
||||
|
||||
export function getComponentName(name: string): string {
|
||||
// convert kebab-case to title case
|
||||
return name.replace(/-/g, ' ');
|
||||
}
|
||||
|
||||
export function getRandomIndex<T>(array: T[]): number {
|
||||
return Math.floor(Math.random() * array.length);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { RouterProvider } from '@tanstack/react-router';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
import { router } from './routeTree';
|
||||
|
||||
import './styles/main.css';
|
||||
|
||||
// Initialize the router
|
||||
await router.load();
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<RouterProvider router={router} />
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -1,88 +0,0 @@
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
|
||||
// This file was automatically generated by TanStack Router.
|
||||
// You should NOT make any changes in this file as it will be overwritten.
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
// Import Routes
|
||||
|
||||
import { Route as rootRoute } from './routes/__root';
|
||||
import { Route as IndexImport } from './routes/index';
|
||||
|
||||
// Create/Update Routes
|
||||
|
||||
const IndexRoute = IndexImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any);
|
||||
|
||||
// Populate the FileRoutesByPath interface
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/': {
|
||||
id: '/';
|
||||
path: '/';
|
||||
fullPath: '/';
|
||||
preLoaderRoute: typeof IndexImport;
|
||||
parentRoute: typeof rootRoute;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Create and export the route tree
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute;
|
||||
}
|
||||
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute;
|
||||
}
|
||||
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRoute;
|
||||
'/': typeof IndexRoute;
|
||||
}
|
||||
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath;
|
||||
fullPaths: '/';
|
||||
fileRoutesByTo: FileRoutesByTo;
|
||||
to: '/';
|
||||
id: '__root__' | '/';
|
||||
fileRoutesById: FileRoutesById;
|
||||
}
|
||||
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute;
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
};
|
||||
|
||||
export const routeTree = rootRoute
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>();
|
||||
|
||||
/* ROUTE_MANIFEST_START
|
||||
{
|
||||
"routes": {
|
||||
"__root__": {
|
||||
"filePath": "__root.tsx",
|
||||
"children": [
|
||||
"/"
|
||||
]
|
||||
},
|
||||
"/": {
|
||||
"filePath": "index.tsx"
|
||||
}
|
||||
}
|
||||
}
|
||||
ROUTE_MANIFEST_END */
|
||||
@@ -1,12 +0,0 @@
|
||||
import { createRouter } from '@tanstack/react-router';
|
||||
|
||||
import { routeTree } from './routeTree.gen';
|
||||
|
||||
export const router = createRouter({ routeTree });
|
||||
|
||||
// Register the router instance for type safety
|
||||
declare module '@tanstack/react-router' {
|
||||
interface Register {
|
||||
router: typeof router;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { createRootRoute, Outlet } from '@tanstack/react-router';
|
||||
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools';
|
||||
|
||||
import { MainLayout } from '../layout/MainLayout';
|
||||
|
||||
export const Route = createRootRoute({
|
||||
component: () => (
|
||||
<MainLayout>
|
||||
<Outlet />
|
||||
<TanStackRouterDevtools />
|
||||
</MainLayout>
|
||||
),
|
||||
});
|
||||
@@ -1,7 +0,0 @@
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
|
||||
import Home from '../components/Home';
|
||||
|
||||
export const Route = createFileRoute('/')({
|
||||
component: Home,
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
declare global {
|
||||
interface Window {
|
||||
electronAPI: {
|
||||
copyToClipboard: (text: string) => Promise<void>;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class ElectronService {
|
||||
async copyToClipboard(text: string): Promise<void> {
|
||||
return window.electronAPI.copyToClipboard(text);
|
||||
}
|
||||
}
|
||||
|
||||
export const electronService = new ElectronService();
|
||||
@@ -1,250 +0,0 @@
|
||||
@import url('tailwindcss');
|
||||
@import url('tw-animate-css');
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme {
|
||||
/* reset */
|
||||
--color-*: initial;
|
||||
|
||||
/* constants */
|
||||
--color-white: #fff;
|
||||
--color-black: #000;
|
||||
|
||||
/* slate */
|
||||
--color-slate-100: #f0f2f8;
|
||||
--color-slate-200: #c8cdd6;
|
||||
--color-slate-300: #a1a7b0;
|
||||
--color-slate-400: #6e747e;
|
||||
--color-slate-500: #4a4e54;
|
||||
--color-slate-600: #2e2e2e;
|
||||
|
||||
/* utility */
|
||||
--color-red-100: #ff6b6b;
|
||||
--color-red-200: #f94b4b;
|
||||
--color-blue-100: #7cacff;
|
||||
--color-blue-200: #5c98f9;
|
||||
--color-green-100: #a3d795;
|
||||
--color-green-200: #91cb80;
|
||||
--color-yellow-100: #ffd966;
|
||||
--color-yellow-200: #fbcd44;
|
||||
}
|
||||
|
||||
:root {
|
||||
overflow: hidden;
|
||||
|
||||
/* shape */
|
||||
--radius: 0.5rem;
|
||||
|
||||
/* theming accents */
|
||||
--background-accent: var(--color-black);
|
||||
--border-accent: var(--color-black);
|
||||
--text-accent: var(--color-black);
|
||||
|
||||
/* Semantic */
|
||||
--background-default: var(--color-white);
|
||||
--background-medium: var(--color-slate-200);
|
||||
--background-muted: var(--color-slate-100);
|
||||
--background-inverse: var(--color-black);
|
||||
--background-danger: var(--color-red-200);
|
||||
--background-success: var(--color-green-200);
|
||||
--background-info: var(--color-blue-200);
|
||||
--background-warning: var(--color-yellow-200);
|
||||
--border-default: var(--color-slate-200);
|
||||
--border-input: var(--color-slate-200);
|
||||
--border-strong: var(--color-slate-300);
|
||||
--border-inverse: var(--color-black);
|
||||
--border-danger: var(--color-red-200);
|
||||
--border-success: var(--color-green-200);
|
||||
--border-warning: var(--color-yellow-200);
|
||||
--border-info: var(--color-blue-200);
|
||||
--text-default: var(--color-slate-600);
|
||||
--text-muted: var(--color-slate-400);
|
||||
--text-inverse: var(--color-white);
|
||||
--text-danger: var(--color-red-200);
|
||||
--text-success: var(--color-green-200);
|
||||
--text-warning: var(--color-yellow-200);
|
||||
--text-info: var(--color-blue-200);
|
||||
--ring: var(--border-strong);
|
||||
--chart-1: #f6b44a;
|
||||
--chart-2: #7585ff;
|
||||
--chart-3: #d76a6a;
|
||||
--chart-4: #d185e0;
|
||||
--chart-5: #91cb80;
|
||||
--sidebar: var(--background-default);
|
||||
--sidebar-foreground: var(--text-default);
|
||||
--sidebar-primary: var(--background-accent);
|
||||
--sidebar-primary-foreground: var(--text-inverse);
|
||||
--sidebar-accent: var(--background-muted);
|
||||
--sidebar-accent-foreground: var(--text-default);
|
||||
--sidebar-border: var(--border-default);
|
||||
--sidebar-ring: var(--border-default);
|
||||
}
|
||||
|
||||
.dark {
|
||||
/* theming accents */
|
||||
--background-accent: var(--color-white);
|
||||
--border-accent: var(--color-white);
|
||||
--text-accent: var(--color-white);
|
||||
|
||||
/* semantic */
|
||||
--background-default: var(--color-black);
|
||||
--background-medium: var(--color-slate-500);
|
||||
--background-muted: var(--color-slate-600);
|
||||
--background-inverse: var(--color-white);
|
||||
--background-danger: var(--color-red-100);
|
||||
--background-success: var(--color-green-100);
|
||||
--background-info: var(--color-blue-100);
|
||||
--background-warning: var(--color-yellow-100);
|
||||
--border-default: var(--color-slate-600);
|
||||
--border-input: var(--color-slate-600);
|
||||
--border-strong: var(--color-slate-200);
|
||||
--border-inverse: var(--color-white);
|
||||
--border-danger: var(--color-red-200);
|
||||
--border-success: var(--color-green-200);
|
||||
--border-warning: var(--color-yellow-200);
|
||||
--border-info: var(--color-blue-200);
|
||||
--text-default: var(--color-white);
|
||||
--text-muted: var(--color-slate-300);
|
||||
--text-inverse: var(--color-black);
|
||||
--text-danger: var(--color-red-100);
|
||||
--text-success: var(--color-green-100);
|
||||
--text-warning: var(--color-yellow-100);
|
||||
--text-info: var(--color-blue-100);
|
||||
--ring: var(--border-strong);
|
||||
--chart-1: #f6b44a;
|
||||
--chart-2: #7585ff;
|
||||
--chart-3: #d76a6a;
|
||||
--chart-4: #d185e0;
|
||||
--chart-5: #91cb80;
|
||||
--sidebar: var(--background-default);
|
||||
--sidebar-foreground: var(--text-default);
|
||||
--sidebar-primary: var(--background-accent);
|
||||
--sidebar-primary-foreground: var(--text-inverse);
|
||||
--sidebar-accent: var(--background-muted);
|
||||
--sidebar-accent-foreground: var(--text-default);
|
||||
--sidebar-border: var(--border-default);
|
||||
--sidebar-ring: var(--border-default);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
/* semantic */
|
||||
--color-background-default: var(--background-default);
|
||||
--color-background-medium: var(--background-medium);
|
||||
--color-background-inverse: var(--background-inverse);
|
||||
--color-background-muted: var(--background-muted);
|
||||
--color-background-danger: var(--background-danger);
|
||||
--color-background-success: var(--background-success);
|
||||
--color-background-info: var(--background-info);
|
||||
--color-background-warning: var(--background-warning);
|
||||
--color-background-accent: var(--background-accent);
|
||||
--color-border-accent: var(--border-accent);
|
||||
--color-text-accent: var(--text-accent);
|
||||
--color-border-default: var(--border-default);
|
||||
--color-border-input: var(--border-input);
|
||||
--color-border-strong: var(--border-strong);
|
||||
--color-border-inverse: var(--border-inverse);
|
||||
--color-border-danger: var(--border-danger);
|
||||
--color-border-success: var(--border-success);
|
||||
--color-border-warning: var(--border-warning);
|
||||
--color-border-info: var(--border-info);
|
||||
--color-text-default: var(--text);
|
||||
--color-text-muted: var(--text-muted);
|
||||
--color-text-inverse: var(--text-inverse);
|
||||
--color-text-danger: var(--text-danger);
|
||||
--color-text-success: var(--text-success);
|
||||
--color-text-warning: var(--text-warning);
|
||||
--color-text-info: var(--text-info);
|
||||
|
||||
/* fonts */
|
||||
--font-sans: 'Cash Sans', sans-serif;
|
||||
--font-mono: 'Cash Sans Mono', monospace;
|
||||
--font-serif: serif;
|
||||
|
||||
/* shape */
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Cash Sans';
|
||||
src:
|
||||
url('https://cash-f.squarecdn.com/static/fonts/cashsans/woff2/CashSans-Light.woff2')
|
||||
format('woff2'),
|
||||
url('https://cash-f.squarecdn.com/static/fonts/cashsans/woff/CashSans-Light.woff')
|
||||
format('woff');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Cash Sans';
|
||||
src:
|
||||
url('https://cash-f.squarecdn.com/static/fonts/cashsans/woff2/CashSans-Regular.woff2')
|
||||
format('woff2'),
|
||||
url('https://cash-f.squarecdn.com/static/fonts/cashsans/woff/CashSans-Regular.woff')
|
||||
format('woff');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Cash Sans';
|
||||
src:
|
||||
url('https://cash-f.squarecdn.com/static/fonts/cashsans/woff2/CashSans-Medium.woff2')
|
||||
format('woff2'),
|
||||
url('https://cash-f.squarecdn.com/static/fonts/cashsans/woff/CashSans-Medium.woff')
|
||||
format('woff');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Cash Sans Mono';
|
||||
src: url('../assets/fonts/CashSansMono-Light.woff2') format('woff2');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Cash Sans Mono';
|
||||
src: url('../assets/fonts/CashSansMono-Regular.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border-default;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background-default text-text-default;
|
||||
}
|
||||
}
|
||||
|
||||
.titlebar-drag-region {
|
||||
-webkit-app-region: drag;
|
||||
height: 32px;
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 50;
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
import { spawn } from 'child_process';
|
||||
import { Buffer } from 'node:buffer';
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
let electronProcess: ReturnType<typeof spawn> | undefined;
|
||||
|
||||
// Helper function to safely kill a process and its children
|
||||
async function killProcess(pid: number): Promise<void> {
|
||||
try {
|
||||
// Try to kill the process group first
|
||||
try {
|
||||
process.kill(-pid, 'SIGTERM');
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.log('Failed to kill process group:', message);
|
||||
}
|
||||
|
||||
// Wait a bit and then try to kill the process directly
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
try {
|
||||
process.kill(pid, 'SIGTERM');
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.log('Failed to kill process:', message);
|
||||
}
|
||||
|
||||
// Final cleanup with SIGKILL if needed
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
try {
|
||||
process.kill(pid, 'SIGKILL');
|
||||
} catch (error: unknown) {
|
||||
// Process is probably already dead
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.log('Process already terminated:', message);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.log('Error during process cleanup:', message);
|
||||
}
|
||||
}
|
||||
|
||||
test.describe('electron app', () => {
|
||||
test.beforeAll(async () => {
|
||||
console.log('Starting Electron app...');
|
||||
console.log('Environment:', process.env.NODE_ENV);
|
||||
console.log('HEADLESS:', process.env.HEADLESS);
|
||||
|
||||
// Start electron with minimal memory settings
|
||||
electronProcess = spawn('npm', ['run', 'start:electron'], {
|
||||
stdio: 'pipe',
|
||||
shell: true,
|
||||
env: {
|
||||
...process.env,
|
||||
ELECTRON_IS_DEV: '1',
|
||||
NODE_ENV: 'development',
|
||||
HEADLESS: process.env.HEADLESS || 'false',
|
||||
ELECTRON_START_URL: 'http://localhost:3001',
|
||||
// Add memory limits for Electron
|
||||
ELECTRON_EXTRA_LAUNCH_ARGS: '--js-flags="--max-old-space-size=512" --disable-gpu',
|
||||
},
|
||||
// Set detached to false and create a new process group
|
||||
detached: false,
|
||||
});
|
||||
|
||||
// Store the PID for cleanup
|
||||
const pid = electronProcess.pid;
|
||||
console.log('Started Electron app with PID:', pid);
|
||||
|
||||
// Capture stdout and stderr for debugging
|
||||
electronProcess.stdout?.on('data', (data: Buffer) => {
|
||||
console.log(`Electron stdout: ${data.toString()}`);
|
||||
});
|
||||
|
||||
electronProcess.stderr?.on('data', (data: Buffer) => {
|
||||
console.log(`Electron stderr: ${data.toString()}`);
|
||||
});
|
||||
|
||||
// Wait for the app to be ready
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
console.log('Stopping Electron app...');
|
||||
|
||||
if (electronProcess?.pid) {
|
||||
console.log('Killing Electron process:', electronProcess.pid);
|
||||
await killProcess(electronProcess.pid);
|
||||
}
|
||||
|
||||
// Give processes time to fully terminate
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
});
|
||||
|
||||
test('shows correct runtime', async ({ page }) => {
|
||||
console.log('Navigating to http://localhost:3001');
|
||||
const response = await page.goto('http://localhost:3001');
|
||||
console.log('Navigation status:', response?.status());
|
||||
|
||||
// Wait for and check the text with more detailed logging
|
||||
console.log('Looking for runtime text...');
|
||||
const runtimeText = page.locator('text=Running in: Electron');
|
||||
|
||||
// Wait for the text to be visible
|
||||
await expect(runtimeText).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
// Add custom matchers
|
||||
declare global {
|
||||
namespace Vi {
|
||||
interface Assertion {
|
||||
// Define the custom matcher without depending on jest types
|
||||
toHaveBeenCalledExactlyOnceWith(...args: unknown[]): void;
|
||||
}
|
||||
interface AsymmetricMatchersContaining {
|
||||
// Define the custom matcher without depending on jest types
|
||||
toHaveBeenCalledExactlyOnceWith(...args: unknown[]): void;
|
||||
}
|
||||
}
|
||||
}
|
||||
36
ui-v2/src/test/types.d.ts
vendored
36
ui-v2/src/test/types.d.ts
vendored
@@ -1,36 +0,0 @@
|
||||
/// <reference types="vitest" />
|
||||
/// <reference types="@testing-library/jest-dom" />
|
||||
|
||||
declare module '*.png' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.jpg' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.jpeg' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.gif' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.svg' {
|
||||
import * as React from 'react';
|
||||
export const ReactComponent: React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module '*.json' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -1,92 +0,0 @@
|
||||
import generated from '@tailwindcss/typography';
|
||||
import tailwindcss_animate from 'tailwindcss-animate';
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
darkMode: 'class', // Change to class-based dark mode
|
||||
plugins: [tailwindcss_animate, generated],
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Cash Sans', 'sans-serif'],
|
||||
mono: ['Cash Sans Mono', 'monospace'],
|
||||
},
|
||||
keyframes: {
|
||||
shimmer: {
|
||||
'0%': { backgroundPosition: '200% 0' },
|
||||
'100%': { backgroundPosition: '-200% 0' },
|
||||
},
|
||||
loader: {
|
||||
'0%': { left: 0, width: 0 },
|
||||
'50%': { left: 0, width: '100%' },
|
||||
'100%': { left: '100%', width: 0 },
|
||||
},
|
||||
popin: {
|
||||
from: { opacity: 0, transform: 'scale(0.95)' },
|
||||
to: { opacity: 1, transform: 'scale(1)' },
|
||||
},
|
||||
fadein: {
|
||||
'0%': { opacity: 0 },
|
||||
'100%': { opacity: 1 },
|
||||
},
|
||||
appear: {
|
||||
'0%': { opacity: 0, transform: 'translateY(12px)' },
|
||||
'100%': { opacity: 1, transform: 'translateY(0)' },
|
||||
},
|
||||
flyin: {
|
||||
'0%': { opacity: 0, transform: 'translate(-300%, 300%)' },
|
||||
'100%': { opacity: 1, transform: 'translate(0, 0)' },
|
||||
},
|
||||
wind: {
|
||||
'0%': { transform: 'translate(0, 0)' },
|
||||
'99.99%': { transform: 'translate(-100%, 100%)' },
|
||||
'100%': { transform: 'translate(0, 0)' },
|
||||
},
|
||||
rotate: {
|
||||
'0%': { transform: 'rotate(0deg)' },
|
||||
'100%': { transform: 'rotate(360deg)' },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
'shimmer-pulse': 'shimmer 4s ease-in-out infinite',
|
||||
'gradient-loader': 'loader 750ms ease-in-out infinite',
|
||||
},
|
||||
colors: {
|
||||
bgApp: 'var(--background-app)',
|
||||
bgSubtle: 'var(--background-subtle)',
|
||||
bgStandard: 'var(--background-standard)',
|
||||
bgProminent: 'var(--background-prominent)',
|
||||
bgAppInverse: 'var(--background-app-inverse)',
|
||||
bgSubtleInverse: 'var(--background-subtle-inverse)',
|
||||
bgStandardInverse: 'var(--background-standard-inverse)',
|
||||
bgProminentInverse: 'var(--background-prominent-inverse)',
|
||||
|
||||
borderSubtle: 'var(--border-subtle)',
|
||||
borderStandard: 'var(--border-standard)',
|
||||
borderProminent: 'var(--border-prominent)',
|
||||
|
||||
textProminent: 'var(--text-prominent)',
|
||||
textStandard: 'var(--text-standard)',
|
||||
textSubtle: 'var(--text-subtle)',
|
||||
textPlaceholder: 'var(--text-placeholder)',
|
||||
textProminentInverse: 'var(--text-prominent-inverse)',
|
||||
|
||||
iconProminent: 'var(--icon-prominent)',
|
||||
iconStandard: 'var(--icon-standard)',
|
||||
iconSubtle: 'var(--icon-subtle)',
|
||||
iconExtraSubtle: 'var(--icon-extra-subtle)',
|
||||
slate: 'var(--slate)',
|
||||
blockTeal: 'var(--block-teal)',
|
||||
blockOrange: 'var(--block-orange)',
|
||||
},
|
||||
typography: {
|
||||
DEFAULT: {
|
||||
css: {
|
||||
color: 'var(--text-standard)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitThis": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"noImplicitReturns": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@platform": ["src/services/platform/electron"],
|
||||
"@platform/*": ["src/services/platform/electron/*"]
|
||||
},
|
||||
"types": ["vitest/globals", "@testing-library/jest-dom", "electron"]
|
||||
},
|
||||
"include": ["src", "electron", "src/test/types.d.ts"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitThis": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"noImplicitReturns": true,
|
||||
"types": ["vitest/globals", "@testing-library/jest-dom"]
|
||||
},
|
||||
"include": ["src", "src/test/types.d.ts"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts", "vite.main.config.ts", "vite.renderer.config.ts"]
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
'@shared': path.resolve(__dirname, './shared'),
|
||||
},
|
||||
},
|
||||
define: {
|
||||
'process.env.IS_ELECTRON': JSON.stringify(true),
|
||||
},
|
||||
build: {
|
||||
outDir: '.vite/build',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
import { builtinModules } from 'module';
|
||||
import path from 'path';
|
||||
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
build: {
|
||||
outDir: '.vite/build',
|
||||
lib: {
|
||||
entry: {
|
||||
main: path.join(__dirname, 'electron/main.ts'),
|
||||
preload: path.join(__dirname, 'electron/preload.ts'),
|
||||
},
|
||||
formats: ['cjs'],
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['electron', ...builtinModules],
|
||||
output: {
|
||||
format: 'cjs',
|
||||
entryFileNames: '[name].js',
|
||||
},
|
||||
},
|
||||
emptyOutDir: false,
|
||||
sourcemap: true,
|
||||
minify: false,
|
||||
},
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
import { builtinModules } from 'module';
|
||||
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
root: process.cwd(),
|
||||
build: {
|
||||
outDir: '.vite/build/preload',
|
||||
lib: {
|
||||
entry: 'electron/preload.ts',
|
||||
formats: ['cjs'],
|
||||
fileName: () => 'preload.js',
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['electron', ...builtinModules],
|
||||
output: {
|
||||
format: 'cjs',
|
||||
entryFileNames: 'preload.js',
|
||||
sourcemap: false,
|
||||
},
|
||||
},
|
||||
emptyOutDir: true,
|
||||
sourcemap: false,
|
||||
minify: false,
|
||||
},
|
||||
});
|
||||
@@ -1,42 +0,0 @@
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
'@shared': path.resolve(__dirname, './shared'),
|
||||
'@platform': path.resolve(__dirname, './src/services/platform/electron'),
|
||||
},
|
||||
},
|
||||
base: './',
|
||||
define: {
|
||||
'process.env.IS_ELECTRON': JSON.stringify(true),
|
||||
},
|
||||
build: {
|
||||
outDir: '.vite/build/renderer',
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: path.join(__dirname, 'index.html'),
|
||||
},
|
||||
},
|
||||
// Ensure production builds don't need unsafe-eval
|
||||
target: 'esnext',
|
||||
minify: 'esbuild',
|
||||
},
|
||||
root: path.join(__dirname, ''),
|
||||
publicDir: 'public',
|
||||
clearScreen: false,
|
||||
server: {
|
||||
port: 3001,
|
||||
strictPort: true,
|
||||
},
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
/// <reference types="vitest" />
|
||||
import path from 'path';
|
||||
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
setupFiles: ['./src/test/setup.ts'],
|
||||
css: true,
|
||||
exclude: ['**/test/e2e/**', '**/node_modules/**'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'html'],
|
||||
exclude: ['node_modules/', 'src/test/'],
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@platform': path.resolve(__dirname, './src/services/platform/web'),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
v23
|
||||
@@ -2,15 +2,13 @@
|
||||
|
||||
Native desktop app for Goose built with [Electron](https://www.electronjs.org/) and [ReactJS](https://react.dev/).
|
||||
|
||||
# Prerequisites
|
||||
- [nvm](https://github.com/nvm-sh/nvm) (recommended for managing node versions easier but not required)
|
||||
- node major version equal to or greater than specified in .nvmrc and package.json
|
||||
- [rust](https://www.rust-lang.org/tools/install) (for building the server)
|
||||
# Building and running
|
||||
Goose uses [Hermit](https://github.com/cashapp/hermit) to manage dependencies, so you will need to have it installed and activated.
|
||||
|
||||
```
|
||||
git clone git@github.com:block/goose.git
|
||||
source ./bin/activate-hermit
|
||||
cd goose/ui/desktop
|
||||
nvm use
|
||||
npm install
|
||||
npm run start
|
||||
```
|
||||
|
||||
676
ui/desktop/package-lock.json
generated
676
ui/desktop/package-lock.json
generated
@@ -92,7 +92,7 @@
|
||||
"vite": "^6.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^23.0.0"
|
||||
"node": "^22.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/openai": {
|
||||
@@ -1660,74 +1660,6 @@
|
||||
"integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
|
||||
"integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz",
|
||||
"integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz",
|
||||
@@ -1745,346 +1677,6 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz",
|
||||
"integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz",
|
||||
"integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz",
|
||||
"integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz",
|
||||
"integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz",
|
||||
"integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz",
|
||||
"integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz",
|
||||
"integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz",
|
||||
"integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
|
||||
@@ -4380,34 +3972,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.41.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.1.tgz",
|
||||
"integrity": "sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.41.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.1.tgz",
|
||||
"integrity": "sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.41.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.1.tgz",
|
||||
@@ -4422,244 +3986,6 @@
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.41.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.1.tgz",
|
||||
"integrity": "sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.41.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.1.tgz",
|
||||
"integrity": "sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.41.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.1.tgz",
|
||||
"integrity": "sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.41.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.1.tgz",
|
||||
"integrity": "sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.41.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.1.tgz",
|
||||
"integrity": "sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.41.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.1.tgz",
|
||||
"integrity": "sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.41.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.1.tgz",
|
||||
"integrity": "sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
|
||||
"version": "4.41.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.1.tgz",
|
||||
"integrity": "sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||
"version": "4.41.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.1.tgz",
|
||||
"integrity": "sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.41.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.1.tgz",
|
||||
"integrity": "sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.41.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.1.tgz",
|
||||
"integrity": "sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.41.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.1.tgz",
|
||||
"integrity": "sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.41.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.1.tgz",
|
||||
"integrity": "sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.41.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.1.tgz",
|
||||
"integrity": "sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.41.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.1.tgz",
|
||||
"integrity": "sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.41.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.1.tgz",
|
||||
"integrity": "sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.41.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.1.tgz",
|
||||
"integrity": "sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@sindresorhus/is": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"version": "1.0.25",
|
||||
"description": "Goose App",
|
||||
"engines": {
|
||||
"node": "^23.0.0"
|
||||
"node": "^22.9.0"
|
||||
},
|
||||
"main": ".vite/build/main.js",
|
||||
"scripts": {
|
||||
|
||||
Reference in New Issue
Block a user