diff --git a/package.json b/package.json index 885e4476..d32310f5 100644 --- a/package.json +++ b/package.json @@ -1,67 +1,68 @@ -{ - "name": "@egdev6/design-system", - "private": false, - "version": "1.0.0", - "type": "module", - "scripts": { - "clean": "rm -rf dist node_modules pnpm-lock.yaml package-lock.json && npm cache clean --force", - "reinstall": "pnpm run clean && pnpm install", - "biome-all": "biome check --write .", - "biome-staged": "biome check . --write --staged --verbose", - "storybook": "storybook dev -p 6006", - "storybook-build": "storybook build && node scripts/inject-preview-head.js", - "storybook-clean-cache": "rm -rf node_modules/.cache/storybook", - "pre-commit": "pnpm run biome-staged && tsc && git add -A", - "postinstall": "lefthook install" - }, - "dependencies": { - "@radix-ui/react-avatar": "1.1.9", - "@radix-ui/react-dialog": "1.1.14", - "@radix-ui/react-dropdown-menu": "2.1.15", - "@radix-ui/react-select": "2.2.5", - "@storybook/addon-actions": "8.6.12", - "class-variance-authority": "0.7.1", - "clsx": "2.1.1", - "lucide-react": "0.507.0", - "postcss": "8.5.3", - "react": "19.1.0", - "react-dom": "19.1.0", - "react-spinners": "0.17.0", - "shadcn": "2.5.0", - "spinners-react": "1.0.11", - "tailwind-merge": "3.2.0", - "tailwind-variants": "1.0.0", - "tw-animate-css": "1.2.9", - "zustand": "5.0.4" - }, - "devDependencies": { - "@biomejs/biome": "1.9.4", - "@egdev6/compilot-cli": "1.0.9", - "@storybook/addon-a11y": "8.6.12", - "@storybook/addon-docs": "8.6.12", - "@storybook/addon-essentials": "8.6.12", - "@storybook/addon-interactions": "8.6.12", - "@storybook/addon-links": "8.6.12", - "@storybook/blocks": "8.6.12", - "@storybook/manager-api": "8.6.12", - "@storybook/react": "8.6.12", - "@storybook/react-vite": "8.6.12", - "@storybook/test": "8.6.12", - "@storybook/theming": "8.6.12", - "@tailwindcss/vite": "4.1.5", - "@types/node": "22.15.3", - "@types/react": "19.1.2", - "@types/react-dom": "19.1.3", - "@vitejs/plugin-react": "4.4.1", - "@vitejs/plugin-react-swc": "3.9.0", - "lefthook": "1.11.12", - "react-docgen-typescript": "2.2.2", - "storybook": "8.6.12", - "storybook-dark-mode": "4.0.2", - "storybook-watch": "1.0.6", - "tailwindcss": "4.1.5", - "typescript": "5.8.3", - "vite": "6.3.4", - "vite-tsconfig-paths": "5.1.4" - } -} +{ + "name": "@egdev6/design-system", + "private": false, + "version": "1.0.0", + "type": "module", + "scripts": { + "clean": "rm -rf dist node_modules pnpm-lock.yaml package-lock.json && npm cache clean --force", + "reinstall": "pnpm run clean && pnpm install", + "biome-all": "biome check --write .", + "biome-staged": "biome check . --write --staged --verbose", + "storybook": "storybook dev -p 6006", + "storybook-build": "storybook build && node scripts/inject-preview-head.js", + "storybook-clean-cache": "rm -rf node_modules/.cache/storybook", + "pre-commit": "pnpm run biome-staged && tsc && git add -A", + "postinstall": "lefthook install" + }, + "dependencies": { + "@internationalized/date": "^3.8.2", + "@radix-ui/react-avatar": "1.1.9", + "@radix-ui/react-dialog": "1.1.14", + "@radix-ui/react-dropdown-menu": "2.1.15", + "@radix-ui/react-select": "2.2.5", + "@storybook/addon-actions": "8.6.12", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", + "lucide-react": "0.507.0", + "postcss": "8.5.3", + "react": "19.1.0", + "react-dom": "19.1.0", + "react-spinners": "0.17.0", + "shadcn": "2.5.0", + "spinners-react": "1.0.11", + "tailwind-merge": "3.2.0", + "tailwind-variants": "1.0.0", + "tw-animate-css": "1.2.9", + "zustand": "5.0.4" + }, + "devDependencies": { + "@biomejs/biome": "1.9.4", + "@egdev6/compilot-cli": "1.0.9", + "@storybook/addon-a11y": "8.6.12", + "@storybook/addon-docs": "8.6.12", + "@storybook/addon-essentials": "8.6.12", + "@storybook/addon-interactions": "8.6.12", + "@storybook/addon-links": "8.6.12", + "@storybook/blocks": "8.6.12", + "@storybook/manager-api": "8.6.12", + "@storybook/react": "8.6.12", + "@storybook/react-vite": "8.6.12", + "@storybook/test": "8.6.12", + "@storybook/theming": "8.6.12", + "@tailwindcss/vite": "4.1.5", + "@types/node": "22.15.3", + "@types/react": "19.1.2", + "@types/react-dom": "19.1.3", + "@vitejs/plugin-react": "4.4.1", + "@vitejs/plugin-react-swc": "3.9.0", + "lefthook": "1.11.12", + "react-docgen-typescript": "2.2.2", + "storybook": "8.6.12", + "storybook-dark-mode": "4.0.2", + "storybook-watch": "1.0.6", + "tailwindcss": "4.1.5", + "typescript": "5.8.3", + "vite": "6.3.4", + "vite-tsconfig-paths": "5.1.4" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6293a803..e7c43a95 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@internationalized/date': + specifier: ^3.8.2 + version: 3.8.2 '@radix-ui/react-avatar': specifier: 1.1.9 version: 1.1.9(@types/react-dom@19.1.3(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -95,7 +98,7 @@ importers: version: 8.6.12(@storybook/test@8.6.12(storybook@8.6.12))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.12)(typescript@5.8.3) '@storybook/react-vite': specifier: 8.6.12 - version: 8.6.12(@storybook/test@8.6.12(storybook@8.6.12))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(rollup@4.45.1)(storybook@8.6.12)(typescript@5.8.3)(vite@6.3.4(@types/node@22.15.3)(jiti@2.4.2)(lightningcss@1.29.2)) + version: 8.6.12(@storybook/test@8.6.12(storybook@8.6.12))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(rollup@4.46.2)(storybook@8.6.12)(typescript@5.8.3)(vite@6.3.4(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2)) '@storybook/test': specifier: 8.6.12 version: 8.6.12(storybook@8.6.12) @@ -104,7 +107,7 @@ importers: version: 8.6.12(storybook@8.6.12) '@tailwindcss/vite': specifier: 4.1.5 - version: 4.1.5(vite@6.3.4(@types/node@22.15.3)(jiti@2.4.2)(lightningcss@1.29.2)) + version: 4.1.5(vite@6.3.4(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2)) '@types/node': specifier: 22.15.3 version: 22.15.3 @@ -116,10 +119,10 @@ importers: version: 19.1.3(@types/react@19.1.2) '@vitejs/plugin-react': specifier: 4.4.1 - version: 4.4.1(vite@6.3.4(@types/node@22.15.3)(jiti@2.4.2)(lightningcss@1.29.2)) + version: 4.4.1(vite@6.3.4(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2)) '@vitejs/plugin-react-swc': specifier: 3.9.0 - version: 3.9.0(vite@6.3.4(@types/node@22.15.3)(jiti@2.4.2)(lightningcss@1.29.2)) + version: 3.9.0(vite@6.3.4(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2)) lefthook: specifier: 1.11.12 version: 1.11.12 @@ -143,10 +146,10 @@ importers: version: 5.8.3 vite: specifier: 6.3.4 - version: 6.3.4(@types/node@22.15.3)(jiti@2.4.2)(lightningcss@1.29.2) + version: 6.3.4(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2) vite-tsconfig-paths: specifier: 5.1.4 - version: 5.1.4(typescript@5.8.3)(vite@6.3.4(@types/node@22.15.3)(jiti@2.4.2)(lightningcss@1.29.2)) + version: 5.1.4(typescript@5.8.3)(vite@6.3.4(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2)) packages: @@ -243,8 +246,8 @@ packages: resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.27.6': - resolution: {integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==} + '@babel/helpers@7.28.2': + resolution: {integrity: sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==} engines: {node: '>=6.9.0'} '@babel/parser@7.28.0': @@ -276,8 +279,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/runtime@7.27.6': - resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} + '@babel/runtime@7.28.2': + resolution: {integrity: sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==} engines: {node: '>=6.9.0'} '@babel/template@7.27.2': @@ -288,8 +291,8 @@ packages: resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==} engines: {node: '>=6.9.0'} - '@babel/types@7.28.1': - resolution: {integrity: sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==} + '@babel/types@7.28.2': + resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} engines: {node: '>=6.9.0'} '@biomejs/biome@1.9.4': @@ -515,14 +518,14 @@ packages: cpu: [x64] os: [win32] - '@floating-ui/core@1.7.2': - resolution: {integrity: sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==} + '@floating-ui/core@1.7.3': + resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} - '@floating-ui/dom@1.7.2': - resolution: {integrity: sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==} + '@floating-ui/dom@1.7.3': + resolution: {integrity: sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==} - '@floating-ui/react-dom@2.1.4': - resolution: {integrity: sha512-JbbpPhp38UmXDDAu60RJmbeme37Jbgsm7NrHGgzYYFKmblzRUh6Pa641dII6LsjwF4XlScDrde2UAzDo/b9KPw==} + '@floating-ui/react-dom@2.1.5': + resolution: {integrity: sha512-HDO/1/1oH9fjj4eLgegrlH3dklZpHtUYYFiVwMUwfGvk9jWDRWqkklA2/NFScknrcNSspbV868WjXORvreDX+Q==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' @@ -561,6 +564,9 @@ packages: '@types/node': optional: true + '@internationalized/date@3.8.2': + resolution: {integrity: sha512-/wENk7CbvLbkUvX1tu0mwq49CVkkWpkXubGel6birjRPyo6uQ4nQpnq5xZu823zRCwwn82zgHrvgF1vZyvmVgA==} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -593,8 +599,8 @@ packages: '@types/react': '>=16' react: '>=16' - '@mswjs/interceptors@0.39.3': - resolution: {integrity: sha512-9bw/wBL7pblsnOCIqvn1788S9o4h+cC5HWXg0Xhh0dOzsZ53IyfmBM+FYqpDDPbm0xjCqEqvCITloF3Dm4TXRQ==} + '@mswjs/interceptors@0.39.5': + resolution: {integrity: sha512-B9nHSJYtsv79uo7QdkZ/b/WoKm20IkVSmTc/WCKarmDtFwM0dRx2ouEniqwNkzCSLn3fydzKmnMzjtfdOWt3VQ==} engines: {node: '>=18'} '@nodelib/fs.scandir@2.1.5': @@ -992,103 +998,103 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.45.1': - resolution: {integrity: sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA==} + '@rollup/rollup-android-arm-eabi@4.46.2': + resolution: {integrity: sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.45.1': - resolution: {integrity: sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ==} + '@rollup/rollup-android-arm64@4.46.2': + resolution: {integrity: sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.45.1': - resolution: {integrity: sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA==} + '@rollup/rollup-darwin-arm64@4.46.2': + resolution: {integrity: sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.45.1': - resolution: {integrity: sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og==} + '@rollup/rollup-darwin-x64@4.46.2': + resolution: {integrity: sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.45.1': - resolution: {integrity: sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g==} + '@rollup/rollup-freebsd-arm64@4.46.2': + resolution: {integrity: sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.45.1': - resolution: {integrity: sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A==} + '@rollup/rollup-freebsd-x64@4.46.2': + resolution: {integrity: sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.45.1': - resolution: {integrity: sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q==} + '@rollup/rollup-linux-arm-gnueabihf@4.46.2': + resolution: {integrity: sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.45.1': - resolution: {integrity: sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q==} + '@rollup/rollup-linux-arm-musleabihf@4.46.2': + resolution: {integrity: sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.45.1': - resolution: {integrity: sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw==} + '@rollup/rollup-linux-arm64-gnu@4.46.2': + resolution: {integrity: sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.45.1': - resolution: {integrity: sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog==} + '@rollup/rollup-linux-arm64-musl@4.46.2': + resolution: {integrity: sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.45.1': - resolution: {integrity: sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg==} + '@rollup/rollup-linux-loongarch64-gnu@4.46.2': + resolution: {integrity: sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.45.1': - resolution: {integrity: sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg==} + '@rollup/rollup-linux-ppc64-gnu@4.46.2': + resolution: {integrity: sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.45.1': - resolution: {integrity: sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw==} + '@rollup/rollup-linux-riscv64-gnu@4.46.2': + resolution: {integrity: sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.45.1': - resolution: {integrity: sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA==} + '@rollup/rollup-linux-riscv64-musl@4.46.2': + resolution: {integrity: sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.45.1': - resolution: {integrity: sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw==} + '@rollup/rollup-linux-s390x-gnu@4.46.2': + resolution: {integrity: sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.45.1': - resolution: {integrity: sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==} + '@rollup/rollup-linux-x64-gnu@4.46.2': + resolution: {integrity: sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.45.1': - resolution: {integrity: sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw==} + '@rollup/rollup-linux-x64-musl@4.46.2': + resolution: {integrity: sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.45.1': - resolution: {integrity: sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg==} + '@rollup/rollup-win32-arm64-msvc@4.46.2': + resolution: {integrity: sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.45.1': - resolution: {integrity: sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw==} + '@rollup/rollup-win32-ia32-msvc@4.46.2': + resolution: {integrity: sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.45.1': - resolution: {integrity: sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA==} + '@rollup/rollup-win32-x64-msvc@4.46.2': + resolution: {integrity: sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==} cpu: [x64] os: [win32] @@ -1277,68 +1283,68 @@ packages: peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - '@swc/core-darwin-arm64@1.13.2': - resolution: {integrity: sha512-44p7ivuLSGFJ15Vly4ivLJjg3ARo4879LtEBAabcHhSZygpmkP8eyjyWxrH3OxkY1eRZSIJe8yRZPFw4kPXFPw==} + '@swc/core-darwin-arm64@1.13.3': + resolution: {integrity: sha512-ux0Ws4pSpBTqbDS9GlVP354MekB1DwYlbxXU3VhnDr4GBcCOimpocx62x7cFJkSpEBF8bmX8+/TTCGKh4PbyXw==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] - '@swc/core-darwin-x64@1.13.2': - resolution: {integrity: sha512-Lb9EZi7X2XDAVmuUlBm2UvVAgSCbD3qKqDCxSI4jEOddzVOpNCnyZ/xEampdngUIyDDhhJLYU9duC+Mcsv5Y+A==} + '@swc/core-darwin-x64@1.13.3': + resolution: {integrity: sha512-p0X6yhxmNUOMZrbeZ3ZNsPige8lSlSe1llllXvpCLkKKxN/k5vZt1sULoq6Nj4eQ7KeHQVm81/+AwKZyf/e0TA==} engines: {node: '>=10'} cpu: [x64] os: [darwin] - '@swc/core-linux-arm-gnueabihf@1.13.2': - resolution: {integrity: sha512-9TDe/92ee1x57x+0OqL1huG4BeljVx0nWW4QOOxp8CCK67Rpc/HHl2wciJ0Kl9Dxf2NvpNtkPvqj9+BUmM9WVA==} + '@swc/core-linux-arm-gnueabihf@1.13.3': + resolution: {integrity: sha512-OmDoiexL2fVWvQTCtoh0xHMyEkZweQAlh4dRyvl8ugqIPEVARSYtaj55TBMUJIP44mSUOJ5tytjzhn2KFxFcBA==} engines: {node: '>=10'} cpu: [arm] os: [linux] - '@swc/core-linux-arm64-gnu@1.13.2': - resolution: {integrity: sha512-KJUSl56DBk7AWMAIEcU83zl5mg3vlQYhLELhjwRFkGFMvghQvdqQ3zFOYa4TexKA7noBZa3C8fb24rI5sw9Exg==} + '@swc/core-linux-arm64-gnu@1.13.3': + resolution: {integrity: sha512-STfKku3QfnuUj6k3g9ld4vwhtgCGYIFQmsGPPgT9MK/dI3Lwnpe5Gs5t1inoUIoGNP8sIOLlBB4HV4MmBjQuhw==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-arm64-musl@1.13.2': - resolution: {integrity: sha512-teU27iG1oyWpNh9CzcGQ48ClDRt/RCem7mYO7ehd2FY102UeTws2+OzLESS1TS1tEZipq/5xwx3FzbVgiolCiQ==} + '@swc/core-linux-arm64-musl@1.13.3': + resolution: {integrity: sha512-bc+CXYlFc1t8pv9yZJGus372ldzOVscBl7encUBlU1m/Sig0+NDJLz6cXXRcFyl6ABNOApWeR4Yl7iUWx6C8og==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-x64-gnu@1.13.2': - resolution: {integrity: sha512-dRPsyPyqpLD0HMRCRpYALIh4kdOir8pPg4AhNQZLehKowigRd30RcLXGNVZcc31Ua8CiPI4QSgjOIxK+EQe4LQ==} + '@swc/core-linux-x64-gnu@1.13.3': + resolution: {integrity: sha512-dFXoa0TEhohrKcxn/54YKs1iwNeW6tUkHJgXW33H381SvjKFUV53WR231jh1sWVJETjA3vsAwxKwR23s7UCmUA==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-linux-x64-musl@1.13.2': - resolution: {integrity: sha512-CCxETW+KkYEQDqz1SYC15YIWYheqFC+PJVOW76Maa/8yu8Biw+HTAcblKf2isrlUtK8RvrQN94v3UXkC2NzCEw==} + '@swc/core-linux-x64-musl@1.13.3': + resolution: {integrity: sha512-ieyjisLB+ldexiE/yD8uomaZuZIbTc8tjquYln9Quh5ykOBY7LpJJYBWvWtm1g3pHv6AXlBI8Jay7Fffb6aLfA==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-win32-arm64-msvc@1.13.2': - resolution: {integrity: sha512-Wv/QTA6PjyRLlmKcN6AmSI4jwSMRl0VTLGs57PHTqYRwwfwd7y4s2fIPJVBNbAlXd795dOEP6d/bGSQSyhOX3A==} + '@swc/core-win32-arm64-msvc@1.13.3': + resolution: {integrity: sha512-elTQpnaX5vESSbhCEgcwXjpMsnUbqqHfEpB7ewpkAsLzKEXZaK67ihSRYAuAx6ewRQTo7DS5iTT6X5aQD3MzMw==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@swc/core-win32-ia32-msvc@1.13.2': - resolution: {integrity: sha512-PuCdtNynEkUNbUXX/wsyUC+t4mamIU5y00lT5vJcAvco3/r16Iaxl5UCzhXYaWZSNVZMzPp9qN8NlSL8M5pPxw==} + '@swc/core-win32-ia32-msvc@1.13.3': + resolution: {integrity: sha512-nvehQVEOdI1BleJpuUgPLrclJ0TzbEMc+MarXDmmiRFwEUGqj+pnfkTSb7RZyS1puU74IXdK/YhTirHurtbI9w==} engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@swc/core-win32-x64-msvc@1.13.2': - resolution: {integrity: sha512-qlmMkFZJus8cYuBURx1a3YAG2G7IW44i+FEYV5/32ylKkzGNAr9tDJSA53XNnNXkAB5EXSPsOz7bn5C3JlEtdQ==} + '@swc/core-win32-x64-msvc@1.13.3': + resolution: {integrity: sha512-A+JSKGkRbPLVV2Kwx8TaDAV0yXIXm/gc8m98hSkVDGlPBBmydgzNdWy3X7HTUBM7IDk7YlWE7w2+RUGjdgpTmg==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core@1.13.2': - resolution: {integrity: sha512-YWqn+0IKXDhqVLKoac4v2tV6hJqB/wOh8/Br8zjqeqBkKa77Qb0Kw2i7LOFzjFNZbZaPH6AlMGlBwNrxaauaAg==} + '@swc/core@1.13.3': + resolution: {integrity: sha512-ZaDETVWnm6FE0fc+c2UE8MHYVS3Fe91o5vkmGfgwGXFbxYvAjKSqxM/j4cRc9T7VZNSJjriXq58XkfCp3Y6f+w==} engines: {node: '>=10'} peerDependencies: '@swc/helpers': '>=0.5.17' @@ -1349,8 +1355,8 @@ packages: '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} - '@swc/types@0.1.23': - resolution: {integrity: sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw==} + '@swc/types@0.1.24': + resolution: {integrity: sha512-tjTMh3V4vAORHtdTprLlfoMptu1WfTZG9Rsca6yOKyNYsRr+MUXutKmliB17orgSZk5DpnDxs8GUdd/qwYxOng==} '@tailwindcss/node@4.1.5': resolution: {integrity: sha512-CBhSWo0vLnWhXIvpD0qsPephiaUYfHUX3U9anwDaHZAeuGpTiB3XmsxPAN6qX7bFhipyGBqOa1QYQVVhkOUGxg==} @@ -1471,8 +1477,8 @@ packages: '@types/babel__template@7.4.4': resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} - '@types/babel__traverse@7.20.7': - resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} @@ -1486,8 +1492,8 @@ packages: '@types/fined@1.1.5': resolution: {integrity: sha512-2N93vadEGDFhASTIRbizbl4bNqpMOId5zZfj6hHqYZfEzEfO9onnU4Im8xvzo8uudySDveDHBOOSlTWf38ErfQ==} - '@types/inquirer@9.0.8': - resolution: {integrity: sha512-CgPD5kFGWsb8HJ5K7rfWlifao87m4ph8uioU7OTncJevmE/VLIqAAjfQtko578JZg7/f69K4FgqYym3gNr7DeA==} + '@types/inquirer@9.0.9': + resolution: {integrity: sha512-/mWx5136gts2Z2e5izdoRCo46lPp5TMs9R15GTSsgg/XnZyxDWVqoVU3R9lWnccKpqwsJLvRoxbCjoJtZB7DSw==} '@types/liftoff@4.0.3': resolution: {integrity: sha512-UgbL2kR5pLrWICvr8+fuSg0u43LY250q7ZMkC+XKC3E+rs/YBDEnQIzsnhU5dYsLlwMi3R75UvCL87pObP1sxw==} @@ -1708,8 +1714,8 @@ packages: camel-case@4.1.2: resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} - caniuse-lite@1.0.30001727: - resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==} + caniuse-lite@1.0.30001731: + resolution: {integrity: sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==} capital-case@1.0.4: resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} @@ -1726,8 +1732,8 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - chalk@5.4.1: - resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} + chalk@5.5.0: + resolution: {integrity: sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} change-case@4.1.2: @@ -1939,8 +1945,8 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - electron-to-chromium@1.5.190: - resolution: {integrity: sha512-k4McmnB2091YIsdCgkS0fMVMPOJgxl93ltFzaryXqwip1AaxeDqKCGLxkXODDA5Ab/D+tV5EL5+aTx76RvLRxw==} + electron-to-chromium@1.5.197: + resolution: {integrity: sha512-m1xWB3g7vJ6asIFz+2pBUbq3uGmfmln1M9SSvBe4QIFWYrRHylP73zL/3nMjDmwz8V+1xAXQDfBd6+HPW0WvDQ==} emoji-regex@10.4.0: resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} @@ -1951,8 +1957,8 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - enhanced-resolve@5.18.2: - resolution: {integrity: sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==} + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} engines: {node: '>=10.13.0'} environment@1.1.0: @@ -1974,8 +1980,8 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} - es-toolkit@1.39.7: - resolution: {integrity: sha512-ek/wWryKouBrZIjkwW2BFf91CWOIMvoy2AE5YYgUrfWsJQM2Su1LoLtrw8uusEpN9RfqLlV/0FVNjT0WMv8Bxw==} + es-toolkit@1.39.8: + resolution: {integrity: sha512-A8QO9TfF+rltS8BXpdu8OS+rpGgEdnRhqIVxO/ZmNvnXBYgOdSsxukT55ELyP94gZIntWJ+Li9QRrT2u1Kitpg==} esbuild-register@3.6.0: resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} @@ -2103,8 +2109,8 @@ packages: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} - fs-extra@11.3.0: - resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} + fs-extra@11.3.1: + resolution: {integrity: sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==} engines: {node: '>=14.14'} fs.realpath@1.0.0: @@ -2475,8 +2481,8 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - jiti@2.4.2: - resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + jiti@2.5.1: + resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==} hasBin: true js-tokens@4.0.0: @@ -2678,8 +2684,8 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true - loupe@3.1.4: - resolution: {integrity: sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==} + loupe@3.2.0: + resolution: {integrity: sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==} lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} @@ -3185,8 +3191,8 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - rollup@4.45.1: - resolution: {integrity: sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==} + rollup@4.46.2: + resolution: {integrity: sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -3736,11 +3742,11 @@ snapshots: '@babel/generator': 7.28.0 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) - '@babel/helpers': 7.27.6 + '@babel/helpers': 7.28.2 '@babel/parser': 7.28.0 '@babel/template': 7.27.2 '@babel/traverse': 7.28.0 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 convert-source-map: 2.0.0 debug: 4.4.1 gensync: 1.0.0-beta.2 @@ -3752,14 +3758,14 @@ snapshots: '@babel/generator@7.28.0': dependencies: '@babel/parser': 7.28.0 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 '@jridgewell/gen-mapping': 0.3.12 '@jridgewell/trace-mapping': 0.3.29 jsesc: 3.1.0 '@babel/helper-annotate-as-pure@7.27.3': dependencies: - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 '@babel/helper-compilation-targets@7.27.2': dependencies: @@ -3787,14 +3793,14 @@ snapshots: '@babel/helper-member-expression-to-functions@7.27.1': dependencies: '@babel/traverse': 7.28.0 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 transitivePeerDependencies: - supports-color '@babel/helper-module-imports@7.27.1': dependencies: '@babel/traverse': 7.28.0 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 transitivePeerDependencies: - supports-color @@ -3809,7 +3815,7 @@ snapshots: '@babel/helper-optimise-call-expression@7.27.1': dependencies: - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 '@babel/helper-plugin-utils@7.27.1': {} @@ -3825,7 +3831,7 @@ snapshots: '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: '@babel/traverse': 7.28.0 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 transitivePeerDependencies: - supports-color @@ -3835,14 +3841,14 @@ snapshots: '@babel/helper-validator-option@7.27.1': {} - '@babel/helpers@7.27.6': + '@babel/helpers@7.28.2': dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 '@babel/parser@7.28.0': dependencies: - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.0)': dependencies: @@ -3870,13 +3876,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/runtime@7.27.6': {} + '@babel/runtime@7.28.2': {} '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 '@babel/parser': 7.28.0 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 '@babel/traverse@7.28.0': dependencies: @@ -3885,12 +3891,12 @@ snapshots: '@babel/helper-globals': 7.28.0 '@babel/parser': 7.28.0 '@babel/template': 7.27.2 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 debug: 4.4.1 transitivePeerDependencies: - supports-color - '@babel/types@7.28.1': + '@babel/types@7.28.2': dependencies: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 @@ -3949,9 +3955,9 @@ snapshots: ink: 5.2.0(@types/react@19.1.2)(react@18.3.1) ink-divider: 4.1.1(@types/react@19.1.2)(react@18.3.1) ink-multi-select: 2.0.0 - ink-select-input: 6.1.0(ink@5.2.0(@types/react@19.1.2)(react@19.1.0))(react@18.3.1) - ink-spinner: 5.0.0(ink@5.2.0(@types/react@19.1.2)(react@19.1.0))(react@18.3.1) - ink-text-input: 6.0.0(ink@5.2.0(@types/react@19.1.2)(react@19.1.0))(react@18.3.1) + ink-select-input: 6.1.0(ink@5.2.0(@types/react@19.1.2)(react@18.3.1))(react@18.3.1) + ink-spinner: 5.0.0(ink@5.2.0(@types/react@19.1.2)(react@18.3.1))(react@18.3.1) + ink-text-input: 6.0.0(ink@5.2.0(@types/react@19.1.2)(react@18.3.1))(react@18.3.1) module-alias: 2.2.3 nanoid: 5.1.5 node-plop: 0.32.0 @@ -4042,18 +4048,18 @@ snapshots: '@esbuild/win32-x64@0.25.8': optional: true - '@floating-ui/core@1.7.2': + '@floating-ui/core@1.7.3': dependencies: '@floating-ui/utils': 0.2.10 - '@floating-ui/dom@1.7.2': + '@floating-ui/dom@1.7.3': dependencies: - '@floating-ui/core': 1.7.2 + '@floating-ui/core': 1.7.3 '@floating-ui/utils': 0.2.10 - '@floating-ui/react-dom@2.1.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@floating-ui/react-dom@2.1.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@floating-ui/dom': 1.7.2 + '@floating-ui/dom': 1.7.3 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) @@ -4085,6 +4091,10 @@ snapshots: optionalDependencies: '@types/node': 22.15.3 + '@internationalized/date@3.8.2': + dependencies: + '@swc/helpers': 0.5.17 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -4094,12 +4104,12 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 - '@joshwooding/vite-plugin-react-docgen-typescript@0.5.0(typescript@5.8.3)(vite@6.3.4(@types/node@22.15.3)(jiti@2.4.2)(lightningcss@1.29.2))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.5.0(typescript@5.8.3)(vite@6.3.4(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2))': dependencies: glob: 10.4.5 magic-string: 0.27.0 react-docgen-typescript: 2.2.2(typescript@5.8.3) - vite: 6.3.4(@types/node@22.15.3)(jiti@2.4.2)(lightningcss@1.29.2) + vite: 6.3.4(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2) optionalDependencies: typescript: 5.8.3 @@ -4123,7 +4133,7 @@ snapshots: '@types/react': 19.1.2 react: 19.1.0 - '@mswjs/interceptors@0.39.3': + '@mswjs/interceptors@0.39.5': dependencies: '@open-draft/deferred-promise': 2.2.0 '@open-draft/logger': 0.3.0 @@ -4314,7 +4324,7 @@ snapshots: '@radix-ui/react-popper@1.2.7(@types/react-dom@19.1.3(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@floating-ui/react-dom': 2.1.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@floating-ui/react-dom': 2.1.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.1.3(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0) '@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.1.0) @@ -4500,72 +4510,72 @@ snapshots: '@radix-ui/rect@1.1.1': {} - '@rollup/pluginutils@5.2.0(rollup@4.45.1)': + '@rollup/pluginutils@5.2.0(rollup@4.46.2)': dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 picomatch: 4.0.3 optionalDependencies: - rollup: 4.45.1 + rollup: 4.46.2 - '@rollup/rollup-android-arm-eabi@4.45.1': + '@rollup/rollup-android-arm-eabi@4.46.2': optional: true - '@rollup/rollup-android-arm64@4.45.1': + '@rollup/rollup-android-arm64@4.46.2': optional: true - '@rollup/rollup-darwin-arm64@4.45.1': + '@rollup/rollup-darwin-arm64@4.46.2': optional: true - '@rollup/rollup-darwin-x64@4.45.1': + '@rollup/rollup-darwin-x64@4.46.2': optional: true - '@rollup/rollup-freebsd-arm64@4.45.1': + '@rollup/rollup-freebsd-arm64@4.46.2': optional: true - '@rollup/rollup-freebsd-x64@4.45.1': + '@rollup/rollup-freebsd-x64@4.46.2': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.45.1': + '@rollup/rollup-linux-arm-gnueabihf@4.46.2': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.45.1': + '@rollup/rollup-linux-arm-musleabihf@4.46.2': optional: true - '@rollup/rollup-linux-arm64-gnu@4.45.1': + '@rollup/rollup-linux-arm64-gnu@4.46.2': optional: true - '@rollup/rollup-linux-arm64-musl@4.45.1': + '@rollup/rollup-linux-arm64-musl@4.46.2': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.45.1': + '@rollup/rollup-linux-loongarch64-gnu@4.46.2': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.45.1': + '@rollup/rollup-linux-ppc64-gnu@4.46.2': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.45.1': + '@rollup/rollup-linux-riscv64-gnu@4.46.2': optional: true - '@rollup/rollup-linux-riscv64-musl@4.45.1': + '@rollup/rollup-linux-riscv64-musl@4.46.2': optional: true - '@rollup/rollup-linux-s390x-gnu@4.45.1': + '@rollup/rollup-linux-s390x-gnu@4.46.2': optional: true - '@rollup/rollup-linux-x64-gnu@4.45.1': + '@rollup/rollup-linux-x64-gnu@4.46.2': optional: true - '@rollup/rollup-linux-x64-musl@4.45.1': + '@rollup/rollup-linux-x64-musl@4.46.2': optional: true - '@rollup/rollup-win32-arm64-msvc@4.45.1': + '@rollup/rollup-win32-arm64-msvc@4.46.2': optional: true - '@rollup/rollup-win32-ia32-msvc@4.45.1': + '@rollup/rollup-win32-ia32-msvc@4.46.2': optional: true - '@rollup/rollup-win32-x64-msvc@4.45.1': + '@rollup/rollup-win32-x64-msvc@4.46.2': optional: true '@storybook/addon-a11y@8.6.12(storybook@8.6.12)': @@ -4680,13 +4690,13 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - '@storybook/builder-vite@8.6.12(storybook@8.6.12)(vite@6.3.4(@types/node@22.15.3)(jiti@2.4.2)(lightningcss@1.29.2))': + '@storybook/builder-vite@8.6.12(storybook@8.6.12)(vite@6.3.4(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2))': dependencies: '@storybook/csf-plugin': 8.6.12(storybook@8.6.12) browser-assert: 1.2.1 storybook: 8.6.12 ts-dedent: 2.2.0 - vite: 6.3.4(@types/node@22.15.3)(jiti@2.4.2)(lightningcss@1.29.2) + vite: 6.3.4(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2) '@storybook/components@8.6.12(storybook@8.6.12)': dependencies: @@ -4751,11 +4761,11 @@ snapshots: react-dom: 19.1.0(react@19.1.0) storybook: 8.6.12 - '@storybook/react-vite@8.6.12(@storybook/test@8.6.12(storybook@8.6.12))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(rollup@4.45.1)(storybook@8.6.12)(typescript@5.8.3)(vite@6.3.4(@types/node@22.15.3)(jiti@2.4.2)(lightningcss@1.29.2))': + '@storybook/react-vite@8.6.12(@storybook/test@8.6.12(storybook@8.6.12))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(rollup@4.46.2)(storybook@8.6.12)(typescript@5.8.3)(vite@6.3.4(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.5.0(typescript@5.8.3)(vite@6.3.4(@types/node@22.15.3)(jiti@2.4.2)(lightningcss@1.29.2)) - '@rollup/pluginutils': 5.2.0(rollup@4.45.1) - '@storybook/builder-vite': 8.6.12(storybook@8.6.12)(vite@6.3.4(@types/node@22.15.3)(jiti@2.4.2)(lightningcss@1.29.2)) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.5.0(typescript@5.8.3)(vite@6.3.4(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2)) + '@rollup/pluginutils': 5.2.0(rollup@4.46.2) + '@storybook/builder-vite': 8.6.12(storybook@8.6.12)(vite@6.3.4(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2)) '@storybook/react': 8.6.12(@storybook/test@8.6.12(storybook@8.6.12))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.12)(typescript@5.8.3) find-up: 5.0.0 magic-string: 0.30.17 @@ -4765,7 +4775,7 @@ snapshots: resolve: 1.22.10 storybook: 8.6.12 tsconfig-paths: 4.2.0 - vite: 6.3.4(@types/node@22.15.3)(jiti@2.4.2)(lightningcss@1.29.2) + vite: 6.3.4(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2) optionalDependencies: '@storybook/test': 8.6.12(storybook@8.6.12) transitivePeerDependencies: @@ -4803,62 +4813,62 @@ snapshots: dependencies: storybook: 8.6.12 - '@swc/core-darwin-arm64@1.13.2': + '@swc/core-darwin-arm64@1.13.3': optional: true - '@swc/core-darwin-x64@1.13.2': + '@swc/core-darwin-x64@1.13.3': optional: true - '@swc/core-linux-arm-gnueabihf@1.13.2': + '@swc/core-linux-arm-gnueabihf@1.13.3': optional: true - '@swc/core-linux-arm64-gnu@1.13.2': + '@swc/core-linux-arm64-gnu@1.13.3': optional: true - '@swc/core-linux-arm64-musl@1.13.2': + '@swc/core-linux-arm64-musl@1.13.3': optional: true - '@swc/core-linux-x64-gnu@1.13.2': + '@swc/core-linux-x64-gnu@1.13.3': optional: true - '@swc/core-linux-x64-musl@1.13.2': + '@swc/core-linux-x64-musl@1.13.3': optional: true - '@swc/core-win32-arm64-msvc@1.13.2': + '@swc/core-win32-arm64-msvc@1.13.3': optional: true - '@swc/core-win32-ia32-msvc@1.13.2': + '@swc/core-win32-ia32-msvc@1.13.3': optional: true - '@swc/core-win32-x64-msvc@1.13.2': + '@swc/core-win32-x64-msvc@1.13.3': optional: true - '@swc/core@1.13.2': + '@swc/core@1.13.3': dependencies: '@swc/counter': 0.1.3 - '@swc/types': 0.1.23 + '@swc/types': 0.1.24 optionalDependencies: - '@swc/core-darwin-arm64': 1.13.2 - '@swc/core-darwin-x64': 1.13.2 - '@swc/core-linux-arm-gnueabihf': 1.13.2 - '@swc/core-linux-arm64-gnu': 1.13.2 - '@swc/core-linux-arm64-musl': 1.13.2 - '@swc/core-linux-x64-gnu': 1.13.2 - '@swc/core-linux-x64-musl': 1.13.2 - '@swc/core-win32-arm64-msvc': 1.13.2 - '@swc/core-win32-ia32-msvc': 1.13.2 - '@swc/core-win32-x64-msvc': 1.13.2 + '@swc/core-darwin-arm64': 1.13.3 + '@swc/core-darwin-x64': 1.13.3 + '@swc/core-linux-arm-gnueabihf': 1.13.3 + '@swc/core-linux-arm64-gnu': 1.13.3 + '@swc/core-linux-arm64-musl': 1.13.3 + '@swc/core-linux-x64-gnu': 1.13.3 + '@swc/core-linux-x64-musl': 1.13.3 + '@swc/core-win32-arm64-msvc': 1.13.3 + '@swc/core-win32-ia32-msvc': 1.13.3 + '@swc/core-win32-x64-msvc': 1.13.3 '@swc/counter@0.1.3': {} - '@swc/types@0.1.23': + '@swc/types@0.1.24': dependencies: '@swc/counter': 0.1.3 '@tailwindcss/node@4.1.5': dependencies: - enhanced-resolve: 5.18.2 - jiti: 2.4.2 + enhanced-resolve: 5.18.3 + jiti: 2.5.1 lightningcss: 1.29.2 tailwindcss: 4.1.5 @@ -4913,17 +4923,17 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.5 '@tailwindcss/oxide-win32-x64-msvc': 4.1.5 - '@tailwindcss/vite@4.1.5(vite@6.3.4(@types/node@22.15.3)(jiti@2.4.2)(lightningcss@1.29.2))': + '@tailwindcss/vite@4.1.5(vite@6.3.4(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2))': dependencies: '@tailwindcss/node': 4.1.5 '@tailwindcss/oxide': 4.1.5 tailwindcss: 4.1.5 - vite: 6.3.4(@types/node@22.15.3)(jiti@2.4.2)(lightningcss@1.29.2) + vite: 6.3.4(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2) '@testing-library/dom@10.4.0': dependencies: '@babel/code-frame': 7.27.1 - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.2 '@types/aria-query': 5.0.4 aria-query: 5.3.0 chalk: 4.1.2 @@ -4957,23 +4967,23 @@ snapshots: '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.28.0 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.20.7 + '@types/babel__traverse': 7.28.0 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 '@types/babel__template@7.4.4': dependencies: '@babel/parser': 7.28.0 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 - '@types/babel__traverse@7.20.7': + '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 '@types/cookie@0.6.0': {} @@ -4983,7 +4993,7 @@ snapshots: '@types/fined@1.1.5': {} - '@types/inquirer@9.0.8': + '@types/inquirer@9.0.9': dependencies: '@types/through': 0.0.33 rxjs: 7.8.2 @@ -5019,21 +5029,21 @@ snapshots: '@types/uuid@9.0.8': {} - '@vitejs/plugin-react-swc@3.9.0(vite@6.3.4(@types/node@22.15.3)(jiti@2.4.2)(lightningcss@1.29.2))': + '@vitejs/plugin-react-swc@3.9.0(vite@6.3.4(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2))': dependencies: - '@swc/core': 1.13.2 - vite: 6.3.4(@types/node@22.15.3)(jiti@2.4.2)(lightningcss@1.29.2) + '@swc/core': 1.13.3 + vite: 6.3.4(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2) transitivePeerDependencies: - '@swc/helpers' - '@vitejs/plugin-react@4.4.1(vite@6.3.4(@types/node@22.15.3)(jiti@2.4.2)(lightningcss@1.29.2))': + '@vitejs/plugin-react@4.4.1(vite@6.3.4(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2))': dependencies: '@babel/core': 7.28.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0) '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.0) '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.3.4(@types/node@22.15.3)(jiti@2.4.2)(lightningcss@1.29.2) + vite: 6.3.4(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2) transitivePeerDependencies: - supports-color @@ -5060,13 +5070,13 @@ snapshots: dependencies: '@vitest/pretty-format': 2.0.5 estree-walker: 3.0.3 - loupe: 3.1.4 + loupe: 3.2.0 tinyrainbow: 1.2.0 '@vitest/utils@2.1.9': dependencies: '@vitest/pretty-format': 2.1.9 - loupe: 3.1.4 + loupe: 3.2.0 tinyrainbow: 1.2.0 acorn@8.15.0: {} @@ -5174,8 +5184,8 @@ snapshots: browserslist@4.25.1: dependencies: - caniuse-lite: 1.0.30001727 - electron-to-chromium: 1.5.190 + caniuse-lite: 1.0.30001731 + electron-to-chromium: 1.5.197 node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.25.1) @@ -5217,7 +5227,7 @@ snapshots: pascal-case: 3.1.2 tslib: 2.8.1 - caniuse-lite@1.0.30001727: {} + caniuse-lite@1.0.30001731: {} capital-case@1.0.4: dependencies: @@ -5230,7 +5240,7 @@ snapshots: assertion-error: 2.0.1 check-error: 2.1.1 deep-eql: 5.0.2 - loupe: 3.1.4 + loupe: 3.2.0 pathval: 2.0.1 chalk@3.0.0: @@ -5243,7 +5253,7 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 - chalk@5.4.1: {} + chalk@5.5.0: {} change-case@4.1.2: dependencies: @@ -5443,7 +5453,7 @@ snapshots: eastasianwidth@0.2.0: {} - electron-to-chromium@1.5.190: {} + electron-to-chromium@1.5.197: {} emoji-regex@10.4.0: {} @@ -5451,7 +5461,7 @@ snapshots: emoji-regex@9.2.2: {} - enhanced-resolve@5.18.2: + enhanced-resolve@5.18.3: dependencies: graceful-fs: 4.2.11 tapable: 2.2.2 @@ -5470,7 +5480,7 @@ snapshots: dependencies: es-errors: 1.3.0 - es-toolkit@1.39.7: {} + es-toolkit@1.39.8: {} esbuild-register@3.6.0(esbuild@0.25.8): dependencies: @@ -5626,7 +5636,7 @@ snapshots: dependencies: fetch-blob: 3.2.0 - fs-extra@11.3.0: + fs-extra@11.3.1: dependencies: graceful-fs: 4.2.11 jsonfile: 6.1.0 @@ -5811,22 +5821,22 @@ snapshots: lodash.isequal: 4.5.0 prop-types: 15.8.1 - ink-select-input@6.1.0(ink@5.2.0(@types/react@19.1.2)(react@19.1.0))(react@18.3.1): + ink-select-input@6.1.0(ink@5.2.0(@types/react@19.1.2)(react@18.3.1))(react@18.3.1): dependencies: figures: 6.1.0 ink: 5.2.0(@types/react@19.1.2)(react@18.3.1) react: 18.3.1 to-rotated: 1.0.0 - ink-spinner@5.0.0(ink@5.2.0(@types/react@19.1.2)(react@19.1.0))(react@18.3.1): + ink-spinner@5.0.0(ink@5.2.0(@types/react@19.1.2)(react@18.3.1))(react@18.3.1): dependencies: cli-spinners: 2.9.2 ink: 5.2.0(@types/react@19.1.2)(react@18.3.1) react: 18.3.1 - ink-text-input@6.0.0(ink@5.2.0(@types/react@19.1.2)(react@19.1.0))(react@18.3.1): + ink-text-input@6.0.0(ink@5.2.0(@types/react@19.1.2)(react@18.3.1))(react@18.3.1): dependencies: - chalk: 5.4.1 + chalk: 5.5.0 ink: 5.2.0(@types/react@19.1.2)(react@18.3.1) react: 18.3.1 type-fest: 4.41.0 @@ -5837,12 +5847,12 @@ snapshots: ansi-escapes: 7.0.0 ansi-styles: 6.2.1 auto-bind: 5.0.1 - chalk: 5.4.1 + chalk: 5.5.0 cli-boxes: 3.0.0 cli-cursor: 4.0.0 cli-truncate: 4.0.0 code-excerpt: 4.0.0 - es-toolkit: 1.39.7 + es-toolkit: 1.39.8 indent-string: 5.0.0 is-in-ci: 1.0.0 patch-console: 2.0.0 @@ -6001,7 +6011,7 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 - jiti@2.4.2: {} + jiti@2.5.1: {} js-tokens@4.0.0: {} @@ -6152,19 +6162,19 @@ snapshots: log-symbols@5.1.0: dependencies: - chalk: 5.4.1 + chalk: 5.5.0 is-unicode-supported: 1.3.0 log-symbols@6.0.0: dependencies: - chalk: 5.4.1 + chalk: 5.5.0 is-unicode-supported: 1.3.0 loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 - loupe@3.1.4: {} + loupe@3.2.0: {} lower-case@2.0.2: dependencies: @@ -6251,7 +6261,7 @@ snapshots: '@bundled-es-modules/statuses': 1.0.1 '@bundled-es-modules/tough-cookie': 0.1.6 '@inquirer/confirm': 5.1.14(@types/node@22.15.3) - '@mswjs/interceptors': 0.39.3 + '@mswjs/interceptors': 0.39.5 '@open-draft/deferred-promise': 2.2.0 '@open-draft/until': 2.1.0 '@types/cookie': 0.6.0 @@ -6295,7 +6305,7 @@ snapshots: node-plop@0.32.0: dependencies: - '@types/inquirer': 9.0.8 + '@types/inquirer': 9.0.9 change-case: 4.1.2 del: 7.1.0 globby: 13.2.2 @@ -6384,7 +6394,7 @@ snapshots: ora@6.3.1: dependencies: - chalk: 5.4.1 + chalk: 5.5.0 cli-cursor: 4.0.0 cli-spinners: 2.9.2 is-interactive: 2.0.0 @@ -6396,7 +6406,7 @@ snapshots: ora@8.2.0: dependencies: - chalk: 5.4.1 + chalk: 5.5.0 cli-cursor: 5.0.0 cli-spinners: 2.9.2 is-interactive: 2.0.0 @@ -6498,7 +6508,7 @@ snapshots: plop@4.0.1: dependencies: '@types/liftoff': 4.0.3 - chalk: 5.4.1 + chalk: 5.5.0 interpret: 3.1.1 liftoff: 4.0.0 minimist: 1.2.8 @@ -6508,7 +6518,7 @@ snapshots: polished@4.3.1: dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.2 possible-typed-array-names@1.1.0: {} @@ -6555,9 +6565,9 @@ snapshots: dependencies: '@babel/core': 7.28.0 '@babel/traverse': 7.28.0 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 '@types/babel__core': 7.20.5 - '@types/babel__traverse': 7.20.7 + '@types/babel__traverse': 7.28.0 '@types/doctrine': 0.0.9 '@types/resolve': 1.20.6 doctrine: 3.0.0 @@ -6686,30 +6696,30 @@ snapshots: dependencies: glob: 7.2.3 - rollup@4.45.1: + rollup@4.46.2: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.45.1 - '@rollup/rollup-android-arm64': 4.45.1 - '@rollup/rollup-darwin-arm64': 4.45.1 - '@rollup/rollup-darwin-x64': 4.45.1 - '@rollup/rollup-freebsd-arm64': 4.45.1 - '@rollup/rollup-freebsd-x64': 4.45.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.45.1 - '@rollup/rollup-linux-arm-musleabihf': 4.45.1 - '@rollup/rollup-linux-arm64-gnu': 4.45.1 - '@rollup/rollup-linux-arm64-musl': 4.45.1 - '@rollup/rollup-linux-loongarch64-gnu': 4.45.1 - '@rollup/rollup-linux-powerpc64le-gnu': 4.45.1 - '@rollup/rollup-linux-riscv64-gnu': 4.45.1 - '@rollup/rollup-linux-riscv64-musl': 4.45.1 - '@rollup/rollup-linux-s390x-gnu': 4.45.1 - '@rollup/rollup-linux-x64-gnu': 4.45.1 - '@rollup/rollup-linux-x64-musl': 4.45.1 - '@rollup/rollup-win32-arm64-msvc': 4.45.1 - '@rollup/rollup-win32-ia32-msvc': 4.45.1 - '@rollup/rollup-win32-x64-msvc': 4.45.1 + '@rollup/rollup-android-arm-eabi': 4.46.2 + '@rollup/rollup-android-arm64': 4.46.2 + '@rollup/rollup-darwin-arm64': 4.46.2 + '@rollup/rollup-darwin-x64': 4.46.2 + '@rollup/rollup-freebsd-arm64': 4.46.2 + '@rollup/rollup-freebsd-x64': 4.46.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.46.2 + '@rollup/rollup-linux-arm-musleabihf': 4.46.2 + '@rollup/rollup-linux-arm64-gnu': 4.46.2 + '@rollup/rollup-linux-arm64-musl': 4.46.2 + '@rollup/rollup-linux-loongarch64-gnu': 4.46.2 + '@rollup/rollup-linux-ppc64-gnu': 4.46.2 + '@rollup/rollup-linux-riscv64-gnu': 4.46.2 + '@rollup/rollup-linux-riscv64-musl': 4.46.2 + '@rollup/rollup-linux-s390x-gnu': 4.46.2 + '@rollup/rollup-linux-x64-gnu': 4.46.2 + '@rollup/rollup-linux-x64-musl': 4.46.2 + '@rollup/rollup-win32-arm64-msvc': 4.46.2 + '@rollup/rollup-win32-ia32-msvc': 4.46.2 + '@rollup/rollup-win32-x64-msvc': 4.46.2 fsevents: 2.3.3 run-applescript@7.0.0: {} @@ -6771,7 +6781,7 @@ snapshots: diff: 5.2.0 execa: 7.2.0 fast-glob: 3.3.3 - fs-extra: 11.3.0 + fs-extra: 11.3.1 https-proxy-agent: 6.2.1 kleur: 4.1.5 msw: 2.10.4(@types/node@22.15.3)(typescript@5.8.3) @@ -7068,29 +7078,29 @@ snapshots: v8flags@4.0.1: {} - vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@6.3.4(@types/node@22.15.3)(jiti@2.4.2)(lightningcss@1.29.2)): + vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@6.3.4(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2)): dependencies: debug: 4.4.1 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.8.3) optionalDependencies: - vite: 6.3.4(@types/node@22.15.3)(jiti@2.4.2)(lightningcss@1.29.2) + vite: 6.3.4(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2) transitivePeerDependencies: - supports-color - typescript - vite@6.3.4(@types/node@22.15.3)(jiti@2.4.2)(lightningcss@1.29.2): + vite@6.3.4(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2): dependencies: esbuild: 0.25.8 fdir: 6.4.6(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.3 - rollup: 4.45.1 + rollup: 4.46.2 tinyglobby: 0.2.14 optionalDependencies: '@types/node': 22.15.3 fsevents: 2.3.3 - jiti: 2.4.2 + jiti: 2.5.1 lightningcss: 1.29.2 wcwidth@1.0.1: diff --git a/src/components/atoms/calendar/Calendar.stories.tsx b/src/components/atoms/calendar/Calendar.stories.tsx new file mode 100644 index 00000000..a1b7a618 --- /dev/null +++ b/src/components/atoms/calendar/Calendar.stories.tsx @@ -0,0 +1,847 @@ +import { CalendarDate } from '@internationalized/date'; +import { startOfWeek } from '@internationalized/date'; +import type { Meta, StoryObj } from '@storybook/react'; +import { useEffect, useState } from 'react'; +import { Calendar } from './index'; +import type { CalendarRadius } from './types'; + +/** + ## DESCRIPTION + The Calendar component is a highly customizable and accessible date picker for React, supporting internationalized dates, advanced visual variants, multi-month views, and modern UX features. + + ## FEATURES + - **Date Selection**: Visual feedback for single and range selection, including drag & drop for ranges. + - **Multi-Month View**: Display 1, 2, or 3 months side-by-side with a unified header and perfectly centered navigation controls. + - **Month/Year Picker**: Two-column, HeroUI-style dropdown for fast navigation, with scrollbars hidden for a cleaner UI. + - **Dark Mode & Variants**: Multiple visual styles (`filled`, `outlined`, `soft`, `ghost`) and full dark mode support. + - **Customizable Size & Radius**: Choose from `sm`, `md`, `lg` sizes and border radius options. + - **Min/Max & Disabled Dates**: Restrict selectable dates and disable specific days. + - **Read-Only & Disabled Modes**: Prevent interaction when needed. + - **Keyboard & Screen Reader Accessibility**: Full ARIA support and keyboard navigation. + - **CVA Integration**: All variants, sizes, radius, disabled, and readOnly states managed with class-variance-authority (CVA). + - **Internationalized Date Support**: Uses `@internationalized/date` (`CalendarDate`) for locale-aware date logic. + - **Animations**: Smooth transitions for showing/hiding the calendar. + - **Custom Highlighted Dates**: Highlight any date with custom colors or styles using the `highlightedDates` prop (e.g., holidays, events). + - **Performance Optimizations**: Improved memoization and initialization order for faster rendering and reduced re-renders. + - **No Runtime CSS Injection**: All styles are managed via Tailwind/CVA or inline styles—no runtime CSS injection. + - **Maintainable Styling**: Compound variants for day styling are split into helper arrays for easier customization and maintainability. + - **Standardized Props**: Calendar component props are now standardized for clarity and maintainability. + - **Custom Color Prop**: The `color` prop allows you to select accessible color schemes for selected days and ranges. It supports a wide palette and works for all visual variants (`filled`, `outlined`, `soft`, `ghost`). + - **Dynamic Hover Fix**: Improved hover effect for non-selected days, using a subtle background for better visual integration and accessibility. The hover color is now correctly prioritized. + + ## ACCESSIBILITY + - Uses ARIA roles and keyboard navigation + - Ensures color contrast for all states + - Month/year picker and multi-month view are fully contained and accessible + - Custom color schemes for selected/range days are designed to meet WCAG 2 AA contrast requirements. +*/ + +const meta: Meta = { + title: 'Atoms/Calendar', + component: Calendar, + parameters: { + docs: { autodocs: true }, + layout: 'centered' + }, + tags: ['autodocs'], + argTypes: { + color: { + control: { type: 'select' }, + options: [ + 'default', + 'orange', + 'orange-light', + 'orange-dark', + 'yellow', + 'yellow-light', + 'yellow-dark', + 'green', + 'green-light', + 'green-dark', + 'teal', + 'teal-light', + 'teal-dark', + 'blue', + 'blue-light', + 'blue-dark', + 'indigo', + 'indigo-light', + 'indigo-dark', + 'purple', + 'purple-light', + 'purple-dark', + 'pink', + 'pink-light', + 'pink-dark' + ], + description: 'Color scheme for selected days and ranges.', + table: { + type: { summary: 'string' }, + defaultValue: { summary: 'default' } + } + }, + selectedDate: { + control: 'date', + description: 'The currently selected date in the calendar.' + }, + variant: { + control: { type: 'radio' }, + options: ['filled', 'outlined', 'soft', 'ghost'], + description: 'Visual variant of the calendar.', + table: { + type: { summary: '"filled" | "outlined" | "soft" | "ghost"' }, + defaultValue: { summary: 'filled' } + } + }, + size: { + control: { type: 'radio' }, + options: ['sm', 'md', 'lg'], + description: 'Size of the calendar (small, medium, large).', + table: { + type: { summary: '"sm" | "md" | "lg"' }, + defaultValue: { summary: 'md' } + } + }, + radius: { + control: { type: 'radio' }, + options: ['none', 'sm', 'md', 'lg'], + description: 'Border radius of the calendar. Options: none, sm, md, lg.', + table: { + type: { summary: '"none" | "sm" | "md" | "lg"' }, + defaultValue: { summary: 'md' } + } + }, + show: { + control: { type: 'boolean' }, + description: 'Show or hide the calendar with animation.', + table: { + type: { summary: 'boolean' }, + defaultValue: { summary: 'true' } + } + }, + minDate: { + control: 'date', + description: 'The minimum selectable date (time ignored; only date is used). Dates before this will be disabled.', + table: { + type: { summary: 'Date (time ignored)' }, + defaultValue: { summary: 'undefined' } + } + }, + maxDate: { + control: 'date', + description: 'The maximum selectable date (time ignored; only date is used). Dates after this will be disabled.', + table: { + type: { summary: 'Date (time ignored)' }, + defaultValue: { summary: 'undefined' } + } + }, + disabled: { + control: 'boolean', + description: 'Disables all interaction and selection in the calendar.', + table: { + type: { summary: 'boolean' }, + defaultValue: { summary: 'false' } + } + }, + readOnly: { + control: 'boolean', + description: 'Makes the calendar read-only (dates are visible but cannot be selected).', + table: { + type: { summary: 'boolean' }, + defaultValue: { summary: 'false' } + } + }, + firstDayOfWeek: { + control: { type: 'number', min: 0, max: 6 }, + description: 'The first day of the week (0 = Sunday, 1 = Monday, ... 6 = Saturday).', + table: { + type: { summary: 'number' }, + defaultValue: { summary: '0' } + } + }, + theme: { + table: { disable: true } + } + } +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + selectedDate: null, + onDateChange: () => { + /* noop */ + }, + size: 'md', + variant: 'filled', + radius: 'md', + show: true, + disabled: false, + readOnly: false, + firstDayOfWeek: 1, + color: 'default' + }, + parameters: { + docs: { + description: { + story: 'Default calendar. You can select a date and use the controls to see different states.' + } + } + } +}; + +export const ShowTransition: Story = { + render: () => { + const [show, setShow] = useState(false); + return ( +
+ +
+ { + /* noop */ + }} + size='md' + variant='filled' + radius='md' + disabled={false} + firstDayOfWeek={1} + /> +
+
+ ); + }, + parameters: { + docs: { + description: { + story: 'Single selection mode: Click a date to select it. Only one date can be selected at a time.' + } + } + } +}; + +export const RangeSelection: Story = { + args: { + selectedDate: [null, null], + onDateChange: () => { + /* noop */ + }, + size: 'md', + show: true, + disabled: false, + firstDayOfWeek: 1, + variant: 'filled', + color: 'default' + }, + parameters: { + docs: { + description: { + story: + 'Range selection mode: Select a range by clicking two dates. The calendar will highlight the selected range.' + } + } + } +}; + +export const Disabled: Story = { + args: { + selectedDate: null, + onDateChange: () => { + /* noop */ + }, + size: 'md', + show: true, + disabled: true, + firstDayOfWeek: 1, + color: 'default' + }, + parameters: { + docs: { + description: { + story: 'Disabled calendar: No interaction or selection is allowed.' + } + } + } +}; + +export const ReadOnly: Story = { + args: { + selectedDate: null, + onDateChange: () => { + /* noop */ + }, + size: 'md', + show: true, + disabled: false, + readOnly: true, + firstDayOfWeek: 1, + color: 'default' + }, + parameters: { + docs: { + description: { + story: 'Read-only calendar: Dates are visible but cannot be selected.' + } + } + } +}; + +export const WithMinMax: Story = { + args: { + selectedDate: null, + onDateChange: () => { + /* noop */ + }, + minDate: new Date(2025, 7, 10), + maxDate: new Date(2025, 7, 20), + size: 'md', + show: true, + disabled: false, + firstDayOfWeek: 1, + color: 'default' + }, + parameters: { + docs: { + description: { + story: 'Calendar with min and max dates: Only dates between August 10 and August 20, 2025 can be selected.' + } + } + } +}; + +export const Outlined: Story = { + args: { + selectedDate: null, + variant: 'outlined', + size: 'md', + show: true, + disabled: false, + firstDayOfWeek: 1, + onDateChange: () => { + /* noop */ + }, + color: 'default' + }, + parameters: { + docs: { + description: { + story: 'Calendar with outlined visual style.' + } + } + } +}; + +export const WithSelectedDate: Story = { + args: { + selectedDate: new Date(2025, 7, 19), + size: 'md', + show: true, + disabled: false, + firstDayOfWeek: 1, + onDateChange: () => { + /* noop */ + }, + color: 'default' + }, + parameters: { + docs: { + description: { + story: 'Calendar with a pre-selected date.' + } + } + } +}; + +export const WithDisabledDates: Story = { + args: { + selectedDate: null, + disabledDates: [new Date(2025, 7, 10), new Date(2025, 7, 12), new Date(2025, 7, 18)], + size: 'md', + show: true, + disabled: false, + firstDayOfWeek: 1, + onDateChange: () => { + /* noop */ + }, + color: 'default' + }, + parameters: { + docs: { + description: { + story: 'Calendar with specific disabled dates.' + } + } + } +}; + +export const WithCalendarDate: Story = { + render: () => { + const [locale, setLocale] = useState('en-US'); + const today = new Date(); + const [calendarDate, setCalendarDate] = useState(() => + startOfWeek(new CalendarDate(today.getFullYear(), today.getMonth() + 1, today.getDate()), locale) + ); + + // Update calendarDate when locale changes + useEffect(() => { + const newCalendarDate = startOfWeek( + new CalendarDate(today.getFullYear(), today.getMonth() + 1, today.getDate()), + locale + ); + setCalendarDate(newCalendarDate); + }, [locale]); + + return ( +
+ + + { + /* noop */ + }} + size='md' + show={true} + locale={locale.startsWith('es') ? 'es' : 'en'} + /> +
+ CalendarDate: + + {calendarDate.toString()} + +
+
+ ); + }, + parameters: { + docs: { + description: { + story: + 'Demonstrates how to use @internationalized/date to get the current date and pass it to the Calendar component.' + } + } + } +}; + +export const VisibleMonths: Story = { + render: () => { + const [visibleMonths, setVisibleMonths] = useState(2); + const [isDark, setIsDark] = useState(() => { + if (typeof document !== 'undefined') { + return document.body.classList.contains('dark') || document.body.getAttribute('data-theme') === 'dark'; + } + return false; + }); + useEffect(() => { + const handler = () => { + setIsDark(document.body.classList.contains('dark') || document.body.getAttribute('data-theme') === 'dark'); + }; + window.addEventListener('themechange', handler); + const observer = new MutationObserver(handler); + observer.observe(document.body, { attributes: true, attributeFilter: ['class', 'data-theme'] }); + return () => { + window.removeEventListener('themechange', handler); + observer.disconnect(); + }; + }, []); + const bgColor = isDark ? '#18191e' : '#d1d5db'; + const textColor = isDark ? '#fff' : '#222'; + return ( +
+ + + { + // No-op for Storybook + }} + size='md' + show={true} + /> +
+ ); + }, + parameters: { + docs: { + description: { + story: 'Interactively select the number of visible months (1, 2, or 3).' + } + } + } +}; + +export const CustomSelectedAndDisabledDates: Story = { + args: { + selectedDate: new Date(2025, 7, 1), + disabledDates: [new Date(2025, 7, 10), new Date(2025, 7, 12), new Date(2025, 7, 18), new Date(2025, 7, 31)], + size: 'md', + show: true, + disabled: false, + firstDayOfWeek: 1, + onDateChange: () => { + /* noop */ + } + }, + parameters: { + docs: { + description: { + story: 'Calendar with a selected date and custom disabled dates.' + } + } + } +}; + +export const HighlightedDates: Story = { + render: () => { + function getContrastText(bgColor: string): string { + if (!bgColor) { + return '#222'; + } + const hex = bgColor.replace('#', ''); + const r = parseInt(hex.substring(0, 2), 16); + const g = parseInt(hex.substring(2, 4), 16); + const b = parseInt(hex.substring(4, 6), 16); + const yiq = (r * 299 + g * 587 + b * 114) / 1000; + return yiq >= 128 ? '#222' : '#fff'; + } + + const today = new Date(); + const eventDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 2); + const holidayDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 5); + const highlightedDates = [ + { + date: eventDate, + className: 'font-bold', + style: { border: '2px solid #22c55e', background: '#22c55e', color: getContrastText('#22c55e') } + }, + { + date: holidayDate, + className: 'font-bold', + style: { border: '2px dashed #eab308', background: '#eab308', color: getContrastText('#eab308') } + } + ]; + + return ; + }, + parameters: { + docs: { + description: { + story: 'Demonstrates how to highlight custom dates (e.g., holidays, events) with custom colors and styles.' + } + } + } +}; + +export const Sizes: Story = { + render: () => { + const [size, setSize] = useState<'sm' | 'md' | 'lg'>('md'); + const [isDark, setIsDark] = useState(() => { + if (typeof document !== 'undefined') { + return document.body.classList.contains('dark') || document.body.getAttribute('data-theme') === 'dark'; + } + return false; + }); + useEffect(() => { + const handler = () => { + setIsDark(document.body.classList.contains('dark') || document.body.getAttribute('data-theme') === 'dark'); + }; + window.addEventListener('themechange', handler); + + const observer = new MutationObserver(handler); + observer.observe(document.body, { attributes: true, attributeFilter: ['class', 'data-theme'] }); + return () => { + window.removeEventListener('themechange', handler); + observer.disconnect(); + }; + }, []); + const bgColor = isDark ? '#18191e' : '#d1d5db'; + const textColor = isDark ? '#fff' : '#222'; + return ( +
+ + + { + /* noop */ + }} + show={true} + /> +
+ ); + }, + parameters: { + docs: { + description: { + story: 'Interactively select the calendar size (sm, md, lg).' + } + } + } +}; + +const selectStyle = document.createElement('style'); +selectStyle.innerHTML = ` + .calendar-radius-bg select:focus option:checked, + .calendar-radius-bg select option:checked { + color: #e00; + background: #fff; + } +`; +document.head.appendChild(selectStyle); +// Add theme-aware background for WithRadius story +const style = document.createElement('style'); +style.innerHTML = ` + .calendar-radius-bg { + background: white; + color: black; + } + @media (prefers-color-scheme: dark) { + .calendar-radius-bg { + background: black; + color: white; + } + } +`; +document.head.appendChild(style); + +export const WithRadius: Story = { + render: () => { + const [radius, setRadius] = useState('md'); + const [isDark, setIsDark] = useState(() => { + if (typeof document !== 'undefined') { + return document.body.classList.contains('dark') || document.body.getAttribute('data-theme') === 'dark'; + } + return false; + }); + useEffect(() => { + const handler = () => { + setIsDark(document.body.classList.contains('dark') || document.body.getAttribute('data-theme') === 'dark'); + }; + window.addEventListener('themechange', handler); + const observer = new MutationObserver(handler); + observer.observe(document.body, { attributes: true, attributeFilter: ['class', 'data-theme'] }); + return () => { + window.removeEventListener('themechange', handler); + observer.disconnect(); + }; + }, []); + const bgColor = isDark ? '#18191e' : '#d1d5db'; + const textColor = isDark ? '#fff' : '#222'; + return ( +
+ + + { + /* noop */ + }} + size='md' + show={true} + /> +
+ ); + }, + parameters: { + docs: { + description: { + story: 'Interactively select the border radius for the calendar.' + } + } + } +}; diff --git a/src/components/atoms/calendar/Calendar.tsx b/src/components/atoms/calendar/Calendar.tsx new file mode 100644 index 00000000..6858f7a7 --- /dev/null +++ b/src/components/atoms/calendar/Calendar.tsx @@ -0,0 +1,357 @@ +import React from 'react'; +import { MonthYearPickerDropdown } from './MonthYearPickerDropdown'; +import { calendarCva } from './calendarCva'; +import { dayCva } from './dayCva'; +import type { CalendarProps } from './types'; +import { calendarColorPalette, lightenColor, useCalendar } from './useCalendar'; + +// Functional calendar component for date visualization and selection +export const Calendar: React.FC = ({ + firstDayOfWeek = 1, // Monday as default + variant = 'filled', + size = 'md', + radius = 'md', + show = true, + theme = 'light', + disabled = false, + readOnly = false, + minDate, + maxDate, + onDateChange, + selectedDate, + disabledDates, + highlightedDates, + locale, + visibleMonths = 1, + color = 'default' +}) => { + const { + monthDatas, + weekdayNames, + monthNames, + currentDate, + setCurrentDate, + handleDayClick, + goToPrevMonth, + goToNextMonth, + isSameDay + } = useCalendar({ + selectedDate, + onDateChange, + minDate, + maxDate, + disabled, + readOnly, + firstDayOfWeek, + disabledDates, + highlightedDates, + locale, + visibleMonths, + theme + }); + + // Drag state for range selection + const [isDragging, setIsDragging] = React.useState(false); + const [dragStart, setDragStart] = React.useState(null); + const [dragEnd, setDragEnd] = React.useState(null); + + // Effect to handle global mouseup for drag & drop range selection + React.useEffect(() => { + if (!isDragging) { + return; + } + const handleGlobalMouseUp = () => { + setIsDragging(false); + if (dragStart && dragEnd && !isSameDay(dragStart, dragEnd)) { + handleDayClick(dragEnd, false); + } + setDragStart(null); + setDragEnd(null); + }; + window.addEventListener('mouseup', handleGlobalMouseUp); + return () => { + window.removeEventListener('mouseup', handleGlobalMouseUp); + }; + }, [isDragging, dragStart, dragEnd, handleDayClick, isSameDay]); + + // State for picker mode (months/years) + const [pickerMode, setPickerMode] = React.useState<'calendar' | 'month' | 'year'>('calendar'); + + // Header: month and year selector (combined) + const years = Array.from({ length: 21 }, (_, i) => 2015 + i); // 2015-2035 for a wide range + + // Determine radius for CVA and style + const radiusCva = radius; + + if (!show) { + return null; + } + + // Compute header label for single or multi-month + let headerLabel = ''; + if (monthDatas.length === 1) { + headerLabel = monthDatas[0].label; + } else { + const first = monthDatas[0]; + const last = monthDatas[monthDatas.length - 1]; + const firstMonth = monthNames[first.monthDate.getMonth()]; + const lastMonth = monthNames[last.monthDate.getMonth()]; + const firstYear = first.monthDate.getFullYear(); + const lastYear = last.monthDate.getFullYear(); + headerLabel = + firstYear === lastYear + ? `${firstMonth} - ${lastMonth} ${firstYear}` + : `${firstMonth} ${firstYear} - ${lastMonth} ${lastYear}`; + } + + // Generate dynamic hover class + const bgColor = calendarColorPalette[color] || calendarColorPalette['default']; + const hoverColor = lightenColor(bgColor, 20); // Lighten by 20% for hover + const dynamicHoverClass = `custom-hover-${color.replace(/[^a-zA-Z0-9]/g, '')}`; + const dynamicStyles = ` + .${dynamicHoverClass}:hover { + background-color: ${hoverColor} !important; + color: #fff !important; + } + `; + return ( +
+ +
1 ? 'w-auto' : ''}`} + role='application' + style={{ + animation: 'fadeIn 0.3s', + ...(readOnly ? { pointerEvents: 'none' } : {}), + ...(disabled ? { pointerEvents: 'none' } : {}) + }} + > + {/* Header: month and year, click to open picker */} +
+ + + +
+ + {/* Fullscreen month/year picker view */} + {pickerMode === 'month' && ( + { + const newDate = new Date(year, month, 1); + setCurrentDate(newDate); + setPickerMode('calendar'); + }} + onCancel={() => setPickerMode('calendar')} + monthNames={monthNames} + years={years} + locale={locale} + /> + )} + + {/* Calendar grid */} + {pickerMode === 'calendar' && ( +
+ {monthDatas.map((monthData, index) => ( +
+ {/* Weekday headers */} +
+
+ {weekdayNames.map((day) => ( +
+ {day} +
+ ))} +
+
+ {/* Grid of days */} +
+ {monthData.weeks.map((week, weekIndex) => ( +
+ {week.map((day, dayIndex) => { + // Determine if day is in drag range + let isDragInRange = false; + let isDragRangeStart = false; + let isDragRangeEnd = false; + if (dragStart && dragEnd) { + const start = dragStart < dragEnd ? dragStart : dragEnd; + const end = dragStart > dragEnd ? dragStart : dragEnd; + isDragInRange = day.date >= start && day.date <= end; + isDragRangeStart = isSameDay(day.date, start); + isDragRangeEnd = isSameDay(day.date, end); + } + // Only apply highlight if not selected + const isSelectedDay = day.isSelected; + let highlightClass; + let highlightStyle; + if (day.isHighlighted) { + if (isSelectedDay) { + // Only apply border from highlightStyle if present + if (day.highlightStyle?.border) { + highlightStyle = { border: day.highlightStyle.border }; + } + // Optionally merge border class if present + if (day.highlightClassName?.includes('border')) { + highlightClass = day.highlightClassName + .split(' ') + .filter((c: string) => c.includes('border')) + .join(' '); + } + } else { + highlightClass = day.highlightClassName; + highlightStyle = day.highlightStyle; + } + } + // Determine color for selected/range days + let dayStyle = { userSelect: 'none', ...highlightStyle }; + if ((day.isSelected || day.isInRange || isDragInRange) && !day.isDisabled) { + if (variant === 'outlined') { + dayStyle = { + ...dayStyle, + border: `2px solid ${bgColor}`, + background: 'transparent' + }; + } else if (variant === 'soft') { + dayStyle = { + ...dayStyle, + background: bgColor + '22', // 13% opacity + color: bgColor + }; + } else if (variant === 'ghost') { + dayStyle = { + ...dayStyle, + background: 'transparent', + color: bgColor, + textDecoration: 'underline' + }; + } else { + // filled + dayStyle = { + ...dayStyle + }; + } + } + // Apply dynamic hover class for selected/range days + const hoverClass = + (day.isSelected || day.isInRange || isDragInRange) && !day.isDisabled + ? dynamicHoverClass + : ''; + return ( +
{ + setIsDragging(true); + setDragStart(day.date); + setDragEnd(day.date); + } + } + onMouseEnter={ + isDragging && !(day.isDisabled || readOnly) ? () => setDragEnd(day.date) : undefined + } + onMouseUp={ + isDragging && !(day.isDisabled || readOnly) + ? () => { + setIsDragging(false); + if (dragStart && dragEnd && !isSameDay(dragStart, dragEnd)) { + handleDayClick(dragEnd, false); + } + setDragStart(null); + setDragEnd(null); + } + : undefined + } + onClick={ + day.isDisabled || readOnly + ? undefined + : () => { + if (!isDragging) { + handleDayClick(day.date, day.isDisabled); + } + } + } + role='gridcell' + aria-selected={day.isSelected ? 'true' : 'false'} + aria-disabled={day.isDisabled || readOnly ? 'true' : 'false'} + tabIndex={day.isDisabled || readOnly ? -1 : 0} + style={dayStyle} + > + {day.date.getDate()} +
+ ); + })} +
+ ))} +
+
+ ))} +
+ )} +
+
+ ); +}; diff --git a/src/components/atoms/calendar/MonthYearPickerDropdown.tsx b/src/components/atoms/calendar/MonthYearPickerDropdown.tsx new file mode 100644 index 00000000..1b978714 --- /dev/null +++ b/src/components/atoms/calendar/MonthYearPickerDropdown.tsx @@ -0,0 +1,146 @@ +import type React from 'react'; +import { useEffect, useRef, useState } from 'react'; + +export interface MonthYearPickerDropdownProps { + currentYear: number; + currentMonth: number; + years: number[]; + monthNames: string[]; + minDate?: Date; + maxDate?: Date; + onChange: (year: number, month: number) => void; + onCancel: () => void; + locale?: string; +} + +export const MonthYearPickerDropdown: React.FC = ({ + currentYear, + currentMonth, + years, + monthNames, + minDate, + maxDate, + onChange, + onCancel, + locale = 'en' +}) => { + // Inline scrollbar hide styles + const scrollbarHideStyle: React.CSSProperties = { + scrollbarWidth: 'none', // Firefox + msOverflowStyle: 'none' // IE 10+ + }; + + const monthsContainerRef = useRef(null); + const yearsContainerRef = useRef(null); + const selectedMonthRef = useRef(null); + const selectedYearRef = useRef(null); + + const [selectedYear, setSelectedYear] = useState(currentYear); + const [selectedMonth, setSelectedMonth] = useState(currentMonth); + + useEffect(() => { + if (selectedMonthRef.current && monthsContainerRef.current) { + selectedMonthRef.current.scrollIntoView({ block: 'center', behavior: 'smooth' }); + } + if (selectedYearRef.current && yearsContainerRef.current) { + selectedYearRef.current.scrollIntoView({ block: 'center', behavior: 'smooth' }); + } + }, [selectedMonth, selectedYear]); + + // Disable months/years outside min/max + const isMonthDisabled = (year: number, month: number) => { + if (minDate && (year < minDate.getFullYear() || (year === minDate.getFullYear() && month < minDate.getMonth()))) { + return true; + } + if (maxDate && (year > maxDate.getFullYear() || (year === maxDate.getFullYear() && month > maxDate.getMonth()))) { + return true; + } + return false; + }; + + const handleSelect = () => { + onChange(selectedYear, selectedMonth); + }; + + // Buttons translate + const buttonLabels: Record = { + en: { cancel: 'Cancel', select: 'Select' }, + es: { cancel: 'Cancelar', select: 'Seleccionar' } + }; + const labels = buttonLabels[locale as keyof typeof buttonLabels] || buttonLabels['en']; + + return ( +
+
+
+ {/* Months */} +
+ {monthNames.map((month, idx) => ( + + ))} +
+ {/* Years */} +
+ {years.map((year) => ( + + ))} +
+
+
+ + +
+
+
+ ); +}; diff --git a/src/components/atoms/calendar/calendarCva.ts b/src/components/atoms/calendar/calendarCva.ts new file mode 100644 index 00000000..d346483e --- /dev/null +++ b/src/components/atoms/calendar/calendarCva.ts @@ -0,0 +1,51 @@ +import { cva } from 'class-variance-authority'; + +export const calendarCva = cva( + 'font-inter transition-all duration-300 ease-in-out opacity-100 scale-100 animate-fadeIn min-h-[300px]', + { + variants: { + variant: { + filled: 'bg-white text-gray-900 shadow-lg dark:bg-gray-900 dark:text-gray-100 dark:shadow-black', + outlined: 'border border-gray-500 dark:border-gray-600', + soft: 'bg-red-100 dark:bg-red-900', + ghost: 'bg-transparent' + }, + size: { + sm: 'p-2 w-64', + md: 'p-4 w-80', + lg: 'p-6 w-96' + }, + radius: { + none: 'rounded-none', + sm: 'rounded', + md: 'rounded-lg', + lg: 'rounded-2xl' + }, + theme: { + light: '', + dark: '' + }, + show: { + true: 'opacity-100 scale-100', + false: 'hidden opacity-0 scale-95' + }, + disabled: { + true: 'pointer-events-none', + false: '' + }, + readOnly: { + true: 'select-none', + false: '' + } + }, + defaultVariants: { + variant: 'filled', + size: 'md', + radius: 'md', + theme: 'light', + show: true, + disabled: false, + readOnly: false + } + } +); diff --git a/src/components/atoms/calendar/dayCva.ts b/src/components/atoms/calendar/dayCva.ts new file mode 100644 index 00000000..f18f587c --- /dev/null +++ b/src/components/atoms/calendar/dayCva.ts @@ -0,0 +1,247 @@ +import { cva } from 'class-variance-authority'; +// Helper arrays for compound variants +const filledRangeVariants = [ + { + isInRange: true, + isRangeStart: false, + isRangeEnd: false, + variant: 'filled' as const, + class: 'bg-red-600 text-white rounded-none dark:bg-red-600 dark:text-white' + }, + { + isInRange: true, + isRangeStart: true, + variant: 'filled' as const, + class: 'bg-red-600 text-white rounded-l-full dark:bg-red-600 dark:text-white' + }, + { + isInRange: true, + isRangeEnd: true, + variant: 'filled' as const, + class: 'bg-red-600 text-white rounded-r-full dark:bg-red-600 dark:text-white' + } +]; + +const filledDisabledRangeVariants = [ + { + isInRange: true, + isDisabled: true, + variant: 'filled' as const, + class: + 'bg-red-600 text-gray-900 dark:bg-red-600 dark:text-gray-100 rounded-none opacity-40 cursor-not-allowed pointer-events-none' + }, + { + isInRange: true, + isRangeStart: true, + isDisabled: true, + variant: 'filled' as const, + class: + 'bg-red-600 text-gray-900 dark:bg-red-600 dark:text-gray-100 rounded-l-full opacity-40 cursor-not-allowed pointer-events-none' + }, + { + isInRange: true, + isRangeEnd: true, + isDisabled: true, + variant: 'filled' as const, + class: + 'bg-red-600 text-gray-900 dark:bg-red-600 dark:text-gray-100 rounded-r-full opacity-40 cursor-not-allowed pointer-events-none' + } +]; + +const outlinedRangeVariants = [ + { + isInRange: true, + isRangeStart: false, + isRangeEnd: false, + variant: 'outlined' as const, + class: 'bg-red-900 text-red-100 border border-red-400 rounded-none' + }, + { + isInRange: true, + isRangeStart: true, + variant: 'outlined' as const, + class: 'bg-red-900 text-red-100 border-2 border-red-400 rounded-l-full' + }, + { + isInRange: true, + isRangeEnd: true, + variant: 'outlined' as const, + class: 'bg-red-900 text-red-100 border-2 border-red-400 rounded-r-full' + } +]; + +const outlinedDisabledRangeVariants = [ + { + isInRange: true, + isDisabled: true, + variant: 'outlined' as const, + class: + 'bg-red-900 text-red-100 border border-red-400 rounded-none opacity-40 cursor-not-allowed pointer-events-none' + }, + { + isInRange: true, + isRangeStart: true, + isDisabled: true, + variant: 'outlined' as const, + class: + 'bg-red-900 text-red-100 border-2 border-red-400 rounded-l-full opacity-40 cursor-not-allowed pointer-events-none' + }, + { + isInRange: true, + isRangeEnd: true, + isDisabled: true, + variant: 'outlined' as const, + class: + 'bg-red-900 text-red-100 border-2 border-red-400 rounded-r-full opacity-40 cursor-not-allowed pointer-events-none' + } +]; + +export const dayCva = cva( + 'flex items-center justify-center font-medium select-none transition-all duration-150 ease-in-out', + { + variants: { + readOnly: { + true: '', // No visual styles, only interaction is removed in the component + false: '' + }, + size: { + sm: 'w-8 h-8 text-xs', + md: 'w-10 h-10 text-sm', + lg: 'w-12 h-12 text-base' + }, + isCurrentMonth: { + true: 'text-gray-900 dark:text-gray-100', + false: 'text-gray-400 bg-transparent cursor-default pointer-events-none dark:text-gray-500 dark:bg-transparent' + }, + isSelected: { + true: '', + false: '' + }, + variant: { + outlined: '', + soft: '', + ghost: '', + filled: '' + }, + isToday: { + true: '', + false: '' + }, + isDisabled: { + true: 'text-gray-400 bg-transparent cursor-default pointer-events-none dark:text-gray-500 dark:bg-transparent', + false: 'cursor-pointer' + }, + isInRange: { + true: '', + false: '' + }, + isRangeStart: { + true: '', + false: '' + }, + isRangeEnd: { + true: '', + false: '' + }, + theme: { + light: '', + dark: '' + } + }, + compoundVariants: [ + // Disabled days of current month should always look like out-of-month days (highest priority) + { + isCurrentMonth: true, + isDisabled: true, + class: + 'text-gray-400 !text-gray-400 bg-transparent cursor-default pointer-events-none dark:text-gray-500 !dark:text-gray-500 dark:bg-transparent' + }, + ...filledRangeVariants, + ...filledDisabledRangeVariants, + ...outlinedRangeVariants, + ...outlinedDisabledRangeVariants, + // Selected variants + { + isSelected: true, + variant: 'outlined' as const, + class: 'border-2 border-red-400 bg-transparent rounded-full' + }, + { + isSelected: true, + variant: 'soft' as const, + class: 'bg-red-100 text-red-700 rounded-full dark:bg-red-700 dark:text-white' + }, + { + isSelected: true, + variant: 'ghost' as const, + class: 'text-red-300 font-bold underline rounded-full' + }, + { + isSelected: true, + variant: 'filled' as const, + class: 'bg-red-600 text-white rounded-full dark:bg-red-600 dark:text-white' + }, + // Today variant + { + isToday: true, + isSelected: false, + class: 'border-2 border-red-400 text-gray-900 dark:text-gray-100' + }, + // Override rounded for range start and end when selected - to fix overriding of rounded-full + { + isSelected: true, + isRangeStart: true, + isRangeEnd: false, + class: 'rounded-l-full !rounded-r-none' + }, + { + isSelected: true, + isRangeStart: false, + isRangeEnd: true, + class: 'rounded-r-full !rounded-l-none' + }, + { + isSelected: true, + isRangeStart: true, + isRangeEnd: true, + class: 'rounded-full' + }, + // Prevent hover on selected or range days + { + isSelected: true, + class: '!hover:bg-transparent !hover:text-inherit' + }, + { + isInRange: true, + class: '!hover:bg-transparent !hover:text-inherit' + }, + // Hover effect for non-selected current-month days in light theme + { + isCurrentMonth: true, + isSelected: false, + isInRange: false, + isDisabled: false, + theme: 'light', + class: 'hover:bg-gray-300 hover:text-gray-900' + }, + // Hover effect for non-selected current-month days in dark theme + { + isCurrentMonth: true, + isSelected: false, + isInRange: false, + isDisabled: false, + theme: 'dark', + class: 'hover:!bg-gray-100 hover:!text-gray-900' + } + ], + defaultVariants: { + size: 'md', + isCurrentMonth: true, + isSelected: false, + variant: 'filled', + isToday: false, + isDisabled: false, + theme: 'light' + } + } +); diff --git a/src/components/atoms/calendar/index.ts b/src/components/atoms/calendar/index.ts new file mode 100644 index 00000000..37f65793 --- /dev/null +++ b/src/components/atoms/calendar/index.ts @@ -0,0 +1,2 @@ +export { Calendar } from './Calendar'; +export type { CalendarProps } from './types'; diff --git a/src/components/atoms/calendar/types.ts b/src/components/atoms/calendar/types.ts new file mode 100644 index 00000000..9e72e0cf --- /dev/null +++ b/src/components/atoms/calendar/types.ts @@ -0,0 +1,52 @@ +import type React from 'react'; +export type CalendarVariant = 'filled' | 'outlined' | 'soft' | 'ghost'; +export type CalendarSize = 'sm' | 'md' | 'lg'; +export type CalendarRadius = 'none' | 'sm' | 'md' | 'lg'; + +export interface CalendarProps { + color?: + | 'default' + | 'orange' + | 'orange-light' + | 'orange-dark' + | 'yellow' + | 'yellow-light' + | 'yellow-dark' + | 'green' + | 'green-light' + | 'green-dark' + | 'teal' + | 'teal-light' + | 'teal-dark' + | 'blue' + | 'blue-light' + | 'blue-dark' + | 'indigo' + | 'indigo-light' + | 'indigo-dark' + | 'purple' + | 'purple-light' + | 'purple-dark' + | 'pink' + | 'pink-light' + | 'pink-dark'; + selectedDate?: Date | null | [Date | null, Date | null]; + onDateChange?: (date: Date | null | [Date | null, Date | null]) => void; + disabledDates?: Date[]; + variant?: CalendarVariant; + size?: CalendarSize; + radius?: CalendarRadius; + show?: boolean; + maxDate?: Date; + minDate?: Date; + disabled?: boolean; + readOnly?: boolean; + firstDayOfWeek?: number; + theme?: 'light' | 'dark'; + /** Array of highlighted dates with custom styles or classes. */ + highlightedDates?: { date: Date; className?: string; style?: React.CSSProperties }[]; + /** Locale code for weekday and month names (e.g., 'en', 'es'). */ + locale?: string; + /** Number of visible months (1 for single, 2+ for multi-month view). */ + visibleMonths?: number; +} diff --git a/src/components/atoms/calendar/useCalendar.ts b/src/components/atoms/calendar/useCalendar.ts new file mode 100644 index 00000000..d387526d --- /dev/null +++ b/src/components/atoms/calendar/useCalendar.ts @@ -0,0 +1,425 @@ +import { CalendarDate } from '@internationalized/date'; +import { useMemo, useState } from 'react'; +import type { CalendarProps } from './types'; + +// Utility to convert a native JS Date object to a CalendarDate +function dateToCalendarDate(date: Date): CalendarDate { + return new CalendarDate(date.getFullYear(), date.getMonth() + 1, date.getDate()); +} + +// Utility to lighten a hex color for hover effects +// Converts hex to RGB, increases brightness, and returns hex +export function lightenColor(hex: string, percent: number): string { + // Remove # if present + hex = hex.replace(/^#/, ''); + // Parse hex to RGB + const r = parseInt(hex.slice(0, 2), 16); + const g = parseInt(hex.slice(2, 4), 16); + const b = parseInt(hex.slice(4, 6), 16); + // Increase brightness by percent (0-100) + const factor = 1 + percent / 100; + const newR = Math.min(255, Math.round(r * factor)); + const newG = Math.min(255, Math.round(g * factor)); + const newB = Math.min(255, Math.round(b * factor)); + // Convert back to hex + return `#${newR.toString(16).padStart(2, '0')}${newG.toString(16).padStart(2, '0')}${newB.toString(16).padStart(2, '0')}`; +} + +// Month names by locale +// Color palette mapping for Calendar color prop +export const calendarColorPalette: Record = { + default: '#e11d48', // current color (red) + orange: '#f97316', + 'orange-light': '#fda65c', + 'orange-dark': '#d94e08', + yellow: '#eab308', + 'yellow-light': '#fde047', + 'yellow-dark': '#b58903', + green: '#22c55e', + 'green-light': '#5ee78b', + 'green-dark': '#138a3d', + teal: '#14b8a6', + 'teal-light': '#40dfcb', + 'teal-dark': '#0a7f74', + blue: '#3b82f6', + 'blue-light': '#7bb0fa', + 'blue-dark': '#1e4ed8', + indigo: '#6366f1', + 'indigo-light': '#9ca3fa', + 'indigo-dark': '#4338ca', + purple: '#8b5cf6', + 'purple-light': '#c4b5fd', + 'purple-dark': '#6d28d9', + pink: '#ec4899', + 'pink-light': '#fda4cf', + 'pink-dark': '#be185d' +}; + +const monthNamesByLocale: Record = { + en: [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December' + ], + es: [ + 'Enero', + 'Febrero', + 'Marzo', + 'Abril', + 'Mayo', + 'Junio', + 'Julio', + 'Agosto', + 'Septiembre', + 'Octubre', + 'Noviembre', + 'Diciembre' + ] +}; + +// Abbreviated weekday names (starting from Sunday) +const weekdayNamesByLocale: Record = { + en: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], + es: ['Dom', 'Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb'] +}; + +export const useCalendar = ({ + selectedDate: initialSelectedDate = null, + onDateChange, + disabledDates = [], // Make sure this has a default value + minDate, + maxDate, + disabled = false, + readOnly = false, + firstDayOfWeek = 1, + highlightedDates = [], + locale = 'en', + visibleMonths = 1 +}: CalendarProps & { firstDayOfWeek?: number; locale?: string; visibleMonths?: number }) => { + // Helper function to normalize date to local midnight (ignore time and timezone) + const normalizeDate = (date: Date): string => { + const year = date.getFullYear(); + const month = date.getMonth(); + const day = date.getDate(); + return `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`; + }; + + // Helper function to check if a date is disabled + const isDateDisabled = (date: Date): boolean => { + const normalizedDate = normalizeDate(date); + return disabledDates.some((disabledDate) => { + const normalizedDisabledDate = normalizeDate(disabledDate); + return normalizedDate === normalizedDisabledDate; + }); + }; + + // Groups days into weeks of 7 days + const groupDaysIntoWeeks = (days: any[]) => { + const weeks = []; + for (let i = 0; i < days.length; i += 7) { + weeks.push(days.slice(i, i + 7)); + } + return weeks; + }; + + // selectedDate can be Date|null or [Date|null, Date|null] + const [currentDate, setCurrentDate] = useState(() => { + if (Array.isArray(initialSelectedDate)) { + return initialSelectedDate[0] ?? new Date(); + } + return initialSelectedDate ?? new Date(); + }); + + const [selectedDate, setSelectedDate] = useState( + Array.isArray(initialSelectedDate) ? null : initialSelectedDate + ); + + const [selectedRange, setSelectedRange] = useState<[Date | null, Date | null]>( + Array.isArray(initialSelectedDate) ? initialSelectedDate : [null, null] + ); + + // Compare only year, month, day (ignore time) + const isSameDay = (d1: Date, d2: Date): boolean => { + return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth() && d1.getDate() === d2.getDate(); + }; + + const getStartDayOfMonth = (year: number, month: number): number => { + const firstDay = new Date(year, month, 1).getDay(); + return (firstDay - firstDayOfWeek + 7) % 7; + }; + + const baseWeekdayNames = weekdayNamesByLocale[locale] || weekdayNamesByLocale['en']; + const weekdayNames = [...baseWeekdayNames.slice(firstDayOfWeek), ...baseWeekdayNames.slice(0, firstDayOfWeek)]; + + // Memoized month names by locale + const monthNames = useMemo(() => { + return monthNamesByLocale[locale] || monthNamesByLocale['en']; + }, [locale]); + + // Helper function to check if date is in min/max range + const isDateInRange = (date: Date): boolean => { + const dateOnly = new Date(date.getFullYear(), date.getMonth(), date.getDate()); + + if (minDate) { + const minDateOnly = new Date(minDate.getFullYear(), minDate.getMonth(), minDate.getDate()); + if (dateOnly < minDateOnly) { + return false; + } + } + + if (maxDate) { + const maxDateOnly = new Date(maxDate.getFullYear(), maxDate.getMonth(), maxDate.getDate()); + if (dateOnly > maxDateOnly) { + return false; + } + } + + return true; + }; + + // Function to compute days for a specific month + const getMonthDays = (monthDate: Date) => { + const year = monthDate.getFullYear(); + const month = monthDate.getMonth(); + + // Prev month days + const startDayIndex = getStartDayOfMonth(year, month); + const daysInPrevMonth = new Date(year, month, 0).getDate(); + const prevDays = []; + for (let i = startDayIndex - 1; i >= 0; i--) { + const date = new Date(year, month - 1, daysInPrevMonth - i); + date.setHours(0, 0, 0, 0); + + const isDisabled = disabled || !isDateInRange(date) || isDateDisabled(date); + const isInRange = !!( + selectedRange[0] && + selectedRange[1] && + date >= selectedRange[0] && + date <= selectedRange[1] + ); + const isRangeStart = !!(selectedRange[0] && isSameDay(date, selectedRange[0])); + const isRangeEnd = !!(selectedRange[1] && isSameDay(date, selectedRange[1])); + const highlight = highlightedDates.find((h) => isSameDay(h.date, date)); + + prevDays.push({ + date, + isCurrentMonth: false, + isToday: false, + isSelected: false, + isDisabled, + isInRange, + isRangeStart, + isRangeEnd, + isHighlighted: !!highlight, + highlightClassName: highlight?.className, + highlightStyle: highlight?.style + }); + } + + // Current month days + const daysInMonth = new Date(year, month + 1, 0).getDate(); + const currDays = []; + const today = new Date(); + today.setHours(0, 0, 0, 0); + for (let i = 1; i <= daysInMonth; i++) { + const date = new Date(year, month, i); + date.setHours(0, 0, 0, 0); + + const isToday = isSameDay(date, today); + const isSelected = !!(selectedDate && isSameDay(date, selectedDate)); + const isDisabled = disabled || !isDateInRange(date) || isDateDisabled(date); + const isInRange = !!( + selectedRange[0] && + selectedRange[1] && + date >= selectedRange[0] && + date <= selectedRange[1] + ); + const isRangeStart = !!(selectedRange[0] && isSameDay(date, selectedRange[0])); + const isRangeEnd = !!(selectedRange[1] && isSameDay(date, selectedRange[1])); + const highlight = highlightedDates.find((h) => isSameDay(h.date, date)); + + currDays.push({ + date, + isCurrentMonth: true, + isToday, + isSelected, + isDisabled, + isInRange, + isRangeStart, + isRangeEnd, + isHighlighted: !!highlight, + highlightClassName: highlight?.className, + highlightStyle: highlight?.style + }); + } + + // Next month days + const totalDaysDisplayed = startDayIndex + daysInMonth; + const remainingCells = 42 - totalDaysDisplayed; // 6 weeks + const nextDays = []; + for (let i = 1; i <= remainingCells; i++) { + const date = new Date(year, month + 1, i); + date.setHours(0, 0, 0, 0); + + const isDisabled = disabled || !isDateInRange(date) || isDateDisabled(date); + const isInRange = !!( + selectedRange[0] && + selectedRange[1] && + date >= selectedRange[0] && + date <= selectedRange[1] + ); + const isRangeStart = !!(selectedRange[0] && isSameDay(date, selectedRange[0])); + const isRangeEnd = !!(selectedRange[1] && isSameDay(date, selectedRange[1])); + const highlight = highlightedDates.find((h) => isSameDay(h.date, date)); + + nextDays.push({ + date, + isCurrentMonth: false, + isToday: false, + isSelected: false, + isDisabled, + isInRange, + isRangeStart, + isRangeEnd, + isHighlighted: !!highlight, + highlightClassName: highlight?.className, + highlightStyle: highlight?.style + }); + } + + const daysInCalendar = [...prevDays, ...currDays, ...nextDays]; + const weeks = groupDaysIntoWeeks(daysInCalendar); + + return { weeks, daysInCalendar, label: `${monthNames[month]} ${year}` }; + }; + + // Compute month datas for visible months + const monthDatas = useMemo(() => { + const datas = []; + if (visibleMonths === 1) { + // Current month only + const monthDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1); + const monthDayData = getMonthDays(monthDate); + datas.push({ + monthDate, + weeks: monthDayData.weeks, + label: monthDayData.label + }); + } else if (visibleMonths === 2) { + // Current and next month + for (let i = 0; i < 2; i++) { + const monthDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + i, 1); + const monthDayData = getMonthDays(monthDate); + datas.push({ + monthDate, + weeks: monthDayData.weeks, + label: monthDayData.label + }); + } + } else if (visibleMonths === 3) { + // Previous, current, and next month + for (let i = -1; i <= 1; i++) { + const monthDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + i, 1); + const monthDayData = getMonthDays(monthDate); + datas.push({ + monthDate, + weeks: monthDayData.weeks, + label: monthDayData.label + }); + } + } + return datas; + }, [ + currentDate, + visibleMonths, + selectedDate, + selectedRange, + disabledDates, + minDate, + maxDate, + disabled, + readOnly, + highlightedDates, + firstDayOfWeek, + locale + ]); + + const handleDayClick = (date: Date, isDisabled: boolean) => { + // Check if date is actually disabled + const isActuallyDisabled = isDisabled || readOnly || !isDateInRange(date) || isDateDisabled(date); + + if (isActuallyDisabled) { + return; + } + + // If selectedDate is a range, handle range selection + if (Array.isArray(initialSelectedDate)) { + // First click: set start, visually mark as selected + if (!selectedRange[0] || (selectedRange[0] && selectedRange[1])) { + setSelectedRange([date, null]); + onDateChange?.([date, null]); + setSelectedDate(date); // Mark start date as selected for visual feedback + } else if (selectedRange[0] && !selectedRange[1]) { + if (date < selectedRange[0]) { + setSelectedRange([date, selectedRange[0]]); + onDateChange?.([date, selectedRange[0]]); + setSelectedDate(date); // Mark new start date + } else { + setSelectedRange([selectedRange[0], date]); + onDateChange?.([selectedRange[0], date]); + setSelectedDate(selectedRange[0]); // Keep start visually selected + } + } + } else { + setSelectedDate(date); + onDateChange?.(date); + } + }; + + const goToPrevMonth = () => { + setCurrentDate((prevDate) => { + const newDate = new Date(prevDate); + newDate.setMonth(newDate.getMonth() - 1); + return newDate; + }); + }; + + const goToNextMonth = () => { + setCurrentDate((prevDate) => { + const newDate = new Date(prevDate); + newDate.setMonth(newDate.getMonth() + 1); + return newDate; + }); + }; + + // Example usage: convert the current date to CalendarDate + const currentCalendarDate = dateToCalendarDate(currentDate); + + return { + monthDatas, + weekdayNames, + monthNames, + currentDate, + setCurrentDate, + currentCalendarDate, + selectedDate: Array.isArray(initialSelectedDate) ? null : selectedDate, + selectedRange: Array.isArray(initialSelectedDate) ? selectedRange : [null, null], + isSameDay, + getStartDayOfMonth, + groupDaysIntoWeeks, + handleDayClick, + goToPrevMonth, + goToNextMonth, + onDateChange, + disabledDates + }; +}; diff --git a/src/components/atoms/header/Header.stories.tsx b/src/components/atoms/header/Header.stories.tsx index 0cc42cf1..58f74e77 100644 --- a/src/components/atoms/header/Header.stories.tsx +++ b/src/components/atoms/header/Header.stories.tsx @@ -24,9 +24,8 @@ type Story = StoryObj; export const Default: Story = { args: { children: 'Lorem ipsum', - font: 'secondaryBold', - tag: 'h1', prominent: false, + className: '', srOnly: false } }; @@ -35,7 +34,7 @@ export const Default: Story = { * - Different header tags (h1, h2, h3, h4, h5, h6) with primary font. */ -export const PrimaryH1: Story = { +export const PrimaryHeader: Story = { render: () => (
@@ -64,7 +63,7 @@ export const PrimaryH1: Story = { * - Differnt header tags (h1, h2, h3, h4, h5, h6) with secondary font. */ -export const SecondaryH1: Story = { +export const SecondaryHeader: Story = { render: () => (
@@ -93,7 +92,7 @@ export const SecondaryH1: Story = { * - Differnt header tags (h1, h2, h3, h4, h5, h6) with secondaryBold font. */ -export const SecondaryH1Bold: Story = { +export const SecondaryHeaderBold: Story = { render: () => (
diff --git a/src/components/atoms/header/Header.tsx b/src/components/atoms/header/Header.tsx index e87758bc..778d6d80 100644 --- a/src/components/atoms/header/Header.tsx +++ b/src/components/atoms/header/Header.tsx @@ -4,7 +4,7 @@ import { useHeader } from './useHeader'; const Header: FC = ({ ...props }) => { const { tag, children, ...rest } = useHeader(props); - const Component = tag; + const Component = tag ?? 'h1'; return {children}; }; diff --git a/src/components/atoms/header/types.ts b/src/components/atoms/header/types.ts index 2feab910..df1dda46 100644 --- a/src/components/atoms/header/types.ts +++ b/src/components/atoms/header/types.ts @@ -1,8 +1,6 @@ -import type { VariantProps } from 'tailwind-variants'; -import { tv } from 'tailwind-variants'; +import { type VariantProps, cva } from 'class-variance-authority'; -export const headerVariants = tv({ - base: 'font-normal leading-[1.2] text-text-light dark:text-text-dark', +export const headerVariants = cva(['font-normal leading-[1.2] text-text-light dark:text-text-dark'], { variants: { font: { primary: 'font-primary', @@ -10,12 +8,12 @@ export const headerVariants = tv({ secondaryBold: 'font-secondary-bold' }, tag: { - h1: 'fs-h1', - h2: 'fs-h2', - h3: 'fs-h3', - h4: 'fs-h4', - h5: 'fs-h5', - h6: 'fs-h6' + h1: 'fs-h1 tablet:fs-tablet-h1', + h2: 'fs-h2 tablet:fs-tablet-h2', + h3: 'fs-h3 tablet:fs-tablet-h3', + h4: 'fs-h4 tablet:fs-tablet-h4', + h5: 'fs-h5 tablet:fs-tablet-h5', + h6: 'fs-h6 tablet:fs-tablet-h6' }, prominent: { true: 'font-bold', @@ -27,14 +25,15 @@ export const headerVariants = tv({ } }, defaultVariants: { - size: undefined, - color: 'default', - prominent: false + font: 'primary', + tag: 'h1', + prominent: false, + srOnly: false } }); -export type HeaderVariant = keyof typeof headerVariants.variants.tag; -export type HeaderFont = keyof typeof headerVariants.variants.font; +export type HeaderVariant = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; +export type HeaderFont = 'primary' | 'secondary' | 'secondaryBold'; export type HeaderProps = { /** @control text */ diff --git a/src/components/atoms/header/useHeader.ts b/src/components/atoms/header/useHeader.ts index 5a6ee4e7..3e25770f 100644 --- a/src/components/atoms/header/useHeader.ts +++ b/src/components/atoms/header/useHeader.ts @@ -2,7 +2,7 @@ import { cn } from '@/lib/utils'; import { type HeaderProps, headerVariants } from './types'; export const useHeader = ({ - font = 'secondaryBold', + font = 'primary', tag = 'h1', prominent = false, className, @@ -33,8 +33,8 @@ export const useHeader = ({ const prop = { className: cn( + fontSize ? fontByTag(fontSize) : headerVariants({ tag }), headerVariants({ font, prominent, srOnly }), - fontSize ? fontByTag(fontSize) : cn(headerVariants({ tag })), className ), id: id || undefined, diff --git a/src/components/atoms/text/Text.stories.tsx b/src/components/atoms/text/Text.stories.tsx index 2392fff7..80f7f9a3 100644 --- a/src/components/atoms/text/Text.stories.tsx +++ b/src/components/atoms/text/Text.stories.tsx @@ -128,6 +128,22 @@ export const CustomColors: Story = { className: 'text-yellow dark:text-pink' } }; +/** + * - This option allows you to customize the text size using Tailwind CSS classes. + * - You can specify any valid Tailwind CSS size class to change the text size. + */ + +export const CustomSize: Story = { + args: { + children: 'Lorem ipsum', + font: 'secondary', + tag: 'p', + prominent: false, + srOnly: false, + isHtml: false, + className: 'text-2xl' + } +}; /** * - This option allows you to set the `aria-live` attribute for accessibility. diff --git a/src/components/atoms/text/Text.tsx b/src/components/atoms/text/Text.tsx index ce4e2b6d..53f67063 100644 --- a/src/components/atoms/text/Text.tsx +++ b/src/components/atoms/text/Text.tsx @@ -4,7 +4,7 @@ import { useText } from './useText'; const Text: FC = ({ ...props }) => { const { tag, isHtml, sanitizedHtml, children, ...rest } = useText(props); - const Component = tag; + const Component = tag ?? 'p'; if (isHtml && typeof sanitizedHtml === 'string') { return ( // biome-ignore lint/security/noDangerouslySetInnerHtml: @@ -12,7 +12,7 @@ const Text: FC = ({ ...props }) => { ); } - return {children}; + return {children}; }; export default Text; diff --git a/src/components/atoms/text/types.ts b/src/components/atoms/text/types.ts index bff75ea9..6f065e89 100644 --- a/src/components/atoms/text/types.ts +++ b/src/components/atoms/text/types.ts @@ -1,9 +1,7 @@ +import { type VariantProps, cva } from 'class-variance-authority'; import type { ReactNode } from 'react'; -import type { VariantProps } from 'tailwind-variants'; -import { tv } from 'tailwind-variants'; -export const textVariants = tv({ - base: 'font-normal leading-[1.2] text-text-light dark:text-text-dark tracking-widest', +export const textVariants = cva(['font-normal leading-[1.2] text-text-light dark:text-text-dark tracking-widest'], { variants: { font: { primary: 'font-primary', @@ -25,14 +23,15 @@ export const textVariants = tv({ } }, defaultVariants: { - size: undefined, - color: 'default', - prominent: false + font: 'secondary', + tag: 'p', + prominent: false, + srOnly: false } }); -export type TextVariant = keyof typeof textVariants.variants.tag; -export type TextFont = keyof typeof textVariants.variants.font; +export type TextVariant = 'p' | 'small' | 'span'; +export type TextFont = 'primary' | 'secondary' | 'secondaryBold'; type BaseTextProps = { /** diff --git a/src/components/molecules/breadcrumb/Breadcrumb.css b/src/components/molecules/breadcrumb/Breadcrumb.css new file mode 100644 index 00000000..c580191c --- /dev/null +++ b/src/components/molecules/breadcrumb/Breadcrumb.css @@ -0,0 +1,5 @@ +nav > ol > li > a, +nav > ol > li > span { + padding: 0px; + text-decoration: none; +} diff --git a/src/components/molecules/breadcrumb/Breadcrumb.stories.tsx b/src/components/molecules/breadcrumb/Breadcrumb.stories.tsx new file mode 100644 index 00000000..51de8551 --- /dev/null +++ b/src/components/molecules/breadcrumb/Breadcrumb.stories.tsx @@ -0,0 +1,475 @@ +import Badge from '@/components/atoms/badge'; +import Dropdown from '@/components/atoms/dropdown'; +import Icon from '@/components/atoms/icon'; +import type { Meta, StoryObj } from '@storybook/react'; +import { DynamicIcon } from 'lucide-react/dynamic'; +import Breadcrumb from './Breadcrumb'; +import type { BreadcrumbItem } from './types'; +import { useBreadcrumb } from './useBreadcrumb'; + +/** + * ## DESCRIPTION + * Breadcrumbs display a hierarchy of links to the current page or resource in an application. + * + * ## SEARCH ICONS + * You can search for icons in the [Lucide Icons] library (https://lucide.dev/icons). Use the icon name as the `icon` prop value to apply it as a separator for the elements. If not set, default separators will be applied. + * + * ## DEPENDENCIES + * - Icon: Uses Icon component from `lucide-react` for icons. + * + */ + +const meta: Meta = { + title: 'Molecules/Breadcrumb', + component: Breadcrumb, + argTypes: { + maxItem: { + control: { type: 'number', min: 0 } + }, + itemsBeforeCollapse: { + control: { type: 'number', min: 1 } + }, + itemsAfterCollapse: { + control: { type: 'number', min: 1 } + } + }, + parameters: { + docs: { + autodocs: true + } + }, + tags: ['autodocs'] +}; +export default meta; + +type Story = StoryObj; + +const items: BreadcrumbItem[] = [ + { title: 'Home', href: '/' }, + { title: 'Library', href: '/library' }, + { title: 'Data', href: '#', target: '_blank' } +]; + +export const Default: Story = { + args: { + items, + variant: 'regular', + rounded: 'md', + size: 'md', + separator: '/', + iconCollapse: 'accessibility', + startContent: undefined, + endContent: undefined, + hideSeparator: false, + maxItem: 0, + itemsBeforeCollapse: 1, + itemsAfterCollapse: 1, + containerClassName: '', + linkClassName: 'dark:text-white dark:hover:text-white' + } +}; + +const textHoverStyleInDarkMode = 'dark:text-white dark:hover:text-white'; +/** + * - **Sizes**: Different size variants for the breadcrumb component (xs,sm, md, lg, xl). + */ +export const Sizes: Story = { + render: () => ( +
+ + + +
+ ) +}; + +/** + * - Text color variations using the textColor property. + - The format for applying color to text is with tailwind. + - The last element of the breadcrumb is a text-type atom component and will be disabled. + */ + +export const Colors: Story = { + render: () => ( +
+ + + +
+ ) +}; + +/** + * Different visual styles including regular, underlined, bordered, line variants. + * - ⚠️ Some variants use a transparent background by default. To ensure proper visibility, place it inside a container with a solid or plain background using the bgColor property. + * + */ +const newStylesLinkClassName = `hover:text-gray-700 ${textHoverStyleInDarkMode}`; +export const Variants: Story = { + render: () => ( +
+ + + + +
+ ) +}; + +/** + * - **Border Radius**: Allows you to apply different border radius to the container: md, xs, sm, lg, xl, full, none. + */ + +export const Rounded: Story = { + render: () => ( +
+ + + + +
+ ) +}; + +/** + * - Allows you to apply a separator between elements. You can use any Lucide React icon or the characters /, |, >. + - Simply enter the name of the Lucide React icon or the characters mentioned above in the “separator” property. + */ + +export const Separatators: Story = { + render: () => ( +
+ + + +
+ ) +}; + +/** + * - **Hidden Separators**: Toggle separator visibility with the hideSeparator prop. + * This is useful when you want to display breadcrumb items without visual separators between them. + */ + +export const HiddenSeparator: Story = { + render: () => ( +
+ +
+ ) +}; + +/** + * - **Start and End Content**: Add icons or content at the beginning and end of the breadcrumb. + * - Use the startContent and endContent props. + */ +export const BeforeAndAfterLinkIcon: Story = { + render: () => ( +
+ + + +
+ ) +}; + +/** + *- Controls the number of items visible with automatic collapse using icons. + - maxItem: A value of 0 shows all items, and a value less than the number of items (the minimum is 2) collapses the remaining items. + - itemsAfterCollapse: These are the number of items you want to display before collapse (left side). + - ItemsBeforeCollapse: These are the number of items you want to display after collapse (right side). + - In this example, I have 3 items and I want 2 to be displayed, so the remaining item is 1 and will be hidden. the items are not displayed, only the chosen icon. + - The icon can be applied with the + */ + +export const MaxItemsAndCollapse: Story = { + render: () => ( + + ) +}; + +// mapper from breadcrumb items to dropdown items +const breadcrumbItemsToDropdownSchema = (breadcrumbItems: BreadcrumbItem[], title = 'Rutas ocultas') => { + if (breadcrumbItems.length === 0) { + return []; + } + + return [ + { type: 'label' as const, label: title }, + { type: 'separator' as const }, + ...breadcrumbItems.map((item) => ({ + type: 'item' as const, + label: item.title, + onClick: () => { + if (item.href) { + window.location.href = item.href; + } + }, + startContent: + })) + ]; +}; + +function getDropdownForItems(items: BreadcrumbItem[], maxItem: number, before: number, after: number) { + const { getHiddenItems } = useBreadcrumb({ items, maxItem, itemsBeforeCollapse: before, itemsAfterCollapse: after }); + return breadcrumbItemsToDropdownSchema(getHiddenItems()); +} + +const maxItem = 2; +const itemsBeforeCollapse = 1; +const itemsAfterCollapse = 1; + +/** + * - Use custom JSX elements instead of icons for collapsed breadcrumb sections. + * This provides complete control over the visual representation of collapsed items, allowing for interactive buttons or styled elements. + * In the current example, the dropdown defined in the atoms section has been used. + */ + +export const WithJsxElmentCollapsed: Story = { + render: () => { + return ( +
+ + + + } + /> + + +
+ +
+ + } + /> +
+ ); + } +}; diff --git a/src/components/molecules/breadcrumb/Breadcrumb.tsx b/src/components/molecules/breadcrumb/Breadcrumb.tsx new file mode 100644 index 00000000..29a9bd12 --- /dev/null +++ b/src/components/molecules/breadcrumb/Breadcrumb.tsx @@ -0,0 +1,147 @@ +import { cn } from '@/lib/utils'; +import { DynamicIcon, type IconName } from 'lucide-react/dynamic'; +import type { FC } from 'react'; +import React, { useId } from 'react'; +import Link from '../../atoms/link'; +import Text from '../../atoms/text'; +import './breadcrumb.css'; +import { type BreadcrumbItem, type BreadcrumbProps, breadcrumbBase } from './types'; +import { useBreadcrumb } from './useBreadcrumb'; + +const Breadcrumb: FC = (props) => { + const { + maxItem = 0, + itemsBeforeCollapse = 1, + itemsAfterCollapse = 1, + separatorClassName, + linkClassName, + textClassName, + containerClassName, + state, + ...restProps + } = props; + + const uniqueId = useId(); + const ariaLabel = props['aria-label'] || `Breadcrumb navigation ${uniqueId}`; + + const { + processedItems, + renderSeparator, + isBreadcrumbItem, + endContent, + hideSeparator, + rounded, + separator, + size, + iconSizes, + startContent, + variant, + isHovered, + handleMouseEnter, + handleMouseLeave + } = useBreadcrumb({ + ...restProps, + maxItem, + itemsBeforeCollapse, + itemsAfterCollapse + }); + + const getCurrentState = () => { + if (isHovered) { + return 'hovered'; + } + return 'default'; + }; + + const renderBreadcrumbItem = (item: BreadcrumbItem | React.ReactNode, isLast: boolean) => { + if (!isBreadcrumbItem(item)) { + return {item}; + } + return ( + <> + {renderStarOrEndIcon(startContent)} + {renderTextOrLinkItem(item, isLast)} + {renderStarOrEndIcon(endContent)} + + ); + }; + + const renderStarOrEndIcon = (iconItem: React.ReactNode) => { + if (!iconItem) { + return null; + } + + return ( + + + ); + }; + + const renderTextOrLinkItem = (item: BreadcrumbItem, isLast: boolean) => { + if (isLast) { + return ( + + {item.title} + + ); + } + return ( + + {item.title} + + ); + }; + + const renderSeparatorElement = (isLast: boolean) => { + if (hideSeparator || isLast) { + return null; + } + + return ( + + ); + }; + + return ( + + ); +}; + +export default Breadcrumb; diff --git a/src/components/molecules/breadcrumb/index.ts b/src/components/molecules/breadcrumb/index.ts new file mode 100644 index 00000000..62c3baf2 --- /dev/null +++ b/src/components/molecules/breadcrumb/index.ts @@ -0,0 +1,3 @@ +import Breadcrumb from './Breadcrumb'; +export * from './types'; +export default Breadcrumb; diff --git a/src/components/molecules/breadcrumb/types.ts b/src/components/molecules/breadcrumb/types.ts new file mode 100644 index 00000000..e84b0ca5 --- /dev/null +++ b/src/components/molecules/breadcrumb/types.ts @@ -0,0 +1,147 @@ +import type { IconSizes } from '@/components/atoms/icon-button'; +import type { DynamicIconName } from '@/components/utils/types'; +import { type VariantProps, cva } from 'class-variance-authority'; +import type { ReactNode } from 'react'; + +/** + * CVA configuration for the Breadcrumb component. + * Controls base styles, variants, and compound variants. + */ +export const breadcrumbBase = cva( + 'flex flex-row items-center gap-2 overflow-hidden max-w-full font-sans whitespace-nowrap border-2 transition-all duration-200', + { + variants: { + size: { + xs: 'text-xs px-1 py-0.5', + sm: 'text-sm px-2 py-1', + md: 'text-md px-2 py-1.5', + lg: 'text-lg px-3 py-2', + xl: 'text-xl px-4 py-2.5' + }, + rounded: { + xs: 'rounded-xs', + sm: 'rounded-sm', + md: 'rounded-md', + lg: 'rounded-lg', + xl: 'rounded-xl', + full: 'rounded-full', + none: 'rounded-none' + }, + variant: { + regular: [ + 'bg-gray-light-500', + 'border-gray-light-500', + 'dark:bg-gray-dark-600', + 'dark:text-white', + 'dark:border-gray-dark-600' + ], + underlined: [ + 'bg-transparent', + 'border-gray-light-500', + 'dark:bg-transparent', + 'dark:text-white', + 'dark:border-gray-dark-600' + ], + line: [ + 'bg-transparent', + 'border-t-transparent', + 'border-l-transparent', + 'border-r-transparent', + '!rounded-none', + 'border-b-gray-light-500', + 'dark:text-white', + 'dark:border-b-gray-dark-600' + ], + bordered: [ + 'bg-gray-light-500', + 'border-gray-light-600', + 'dark:text-white', + 'dark:bg-gray-dark-600', + 'dark:border-gray-dark-500' + ] + }, + state: { + default: '', + hovered: '' + }, + hovered: { + true: '', + false: '' + } + }, + defaultVariants: { + size: 'md', + rounded: 'md', + variant: 'regular', + state: 'default', + hovered: false + }, + compoundVariants: [ + { + variant: 'regular', + hovered: true, + class: + 'bg-gray-light-400 border-gray-light-400 dark:text-white dark:bg-gray-dark-500 dark:border-gray-dark-500 dark:text-white dark:hover:text-white' + }, + { + variant: 'bordered', + hovered: true, + class: 'border-secondary dark:border-secondary dark:text-white dark:text-white dark:hover:text-white' + }, + { + variant: 'underlined', + hovered: true, + class: 'border-secondary dark:border-secondary dark:text-white dark:text-white dark:hover:text-white' + } + ] + } +); + +/** + * Breadcrumb component variants generated from CVA. + */ +export type BreadcrumbVariants = VariantProps; + +/** + * Individual breadcrumb item. + */ +export interface BreadcrumbItem { + title: string; + href?: string; + target?: '_blank' | '_self' | '_parent' | '_top'; + disabled?: boolean; + icon?: DynamicIconName; +} + +/** + * Props for the Breadcrumb component. + */ +export interface BreadcrumbProps extends BreadcrumbVariants { + 'aria-label'?: string; + containerClassName?: string; + collapsedElement?: ReactNode; + endContent?: DynamicIconName | ReactNode; + hideSeparator?: boolean; + iconCollapse?: DynamicIconName | ReactNode; + iconSizes?: IconSizes; + items: BreadcrumbItem[]; + itemsAfterCollapse?: number; + itemsBeforeCollapse?: number; + linkClassName?: string; + maxItem?: number; + onCollapsedClick?: (hiddenItems: BreadcrumbItem[]) => void; + onItemClick?: (item: BreadcrumbItem, index: number) => void; + separator?: ReactNode; + separatorClassName?: string; + showTooltip?: boolean; + startContent?: DynamicIconName | ReactNode; + textClassName?: string; +} + +/** + * Processed breadcrumb item for internal rendering. + */ +export interface ProcessedBreadcrumbItem { + item: BreadcrumbItem | ReactNode; + isLast: boolean; +} diff --git a/src/components/molecules/breadcrumb/useBreadcrumb.tsx b/src/components/molecules/breadcrumb/useBreadcrumb.tsx new file mode 100644 index 00000000..eb33b5bb --- /dev/null +++ b/src/components/molecules/breadcrumb/useBreadcrumb.tsx @@ -0,0 +1,133 @@ +import type { VariantProps } from 'class-variance-authority'; +import { DynamicIcon, type IconName } from 'lucide-react/dynamic'; +import { type ComponentProps, type MouseEvent, type ReactNode, useState } from 'react'; +import type { BreadcrumbItem, BreadcrumbProps, ProcessedBreadcrumbItem, breadcrumbBase } from './types'; + +type BreadcrumbItemCollapsed = BreadcrumbItem | ReactNode; + +export const useBreadcrumb = ({ + items, + variant = 'regular', + size = 'md', + iconSizes = 18, + rounded = 'md', + startContent, + endContent, + hideSeparator = false, + separator = '/', + maxItem = 0, + itemsBeforeCollapse = 1, + itemsAfterCollapse = 1, + iconCollapse = 'more-horizontal', + collapsedElement, + onCollapsedClick, + onMouseEnter, + onMouseLeave, + showTooltip = false +}: Omit, 'state' | 'focused'> & BreadcrumbProps & ComponentProps<'nav'>) => { + const [isHovered, setIsHovered] = useState(false); + + const handleMouseEnter = (e: MouseEvent) => { + setIsHovered(true); + onMouseEnter?.(e); + }; + + const handleMouseLeave = (e: MouseEvent) => { + setIsHovered(false); + onMouseLeave?.(e); + }; + + const controlString = /[->/|](?![a-zA-Z0-9])/; + const renderSeparator = (separator: ReactNode): ReactNode => { + if (typeof separator === 'string') { + return controlString.test(separator) ? ( + + ) : ( +