From 8da48370bcd1108a22148c7e2f58b2f9e6f16369 Mon Sep 17 00:00:00 2001 From: melvinchia3636 Date: Sun, 31 May 2026 08:34:01 +0800 Subject: [PATCH] feat: client codebase refactoring completed --- .agent/workflows/create-dashboard-widget.md | 149 +++++++-- apps/lifeforge--wallet | 1 + apps/package.json | 5 +- bun.lock | 80 ++++- client/src/core/dashboard/widgets/Date.tsx | 4 +- client/src/core/dashboard/widgets/Quotes.tsx | 2 +- .../moduleManager/pages/Modules/index.tsx | 5 +- .../components/BgImageSelector/index.tsx | 31 +- .../components/AdjustmentColumn.tsx | 7 +- .../components/ResultShowcase.tsx | 122 +++++--- .../modals/AdjustBgImageModal/index.tsx | 12 +- .../components/DefaultBgTempSelector.tsx | 18 +- .../components/BgTempSelector/index.tsx | 7 +- .../components/BorderRadiusSelector/index.tsx | 10 +- .../components/CustomFontUploadModal.tsx | 7 +- .../components/FontListItem.tsx | 104 ------- .../FontFamilySelectorModal/index.tsx | 37 +-- .../tabs/CustomFontSelector.tsx | 203 ------------ .../tabs/GoogleFontSelector.tsx | 288 ------------------ .../tabs/custom/index.tsx | 232 ++++++++++++++ .../tabs/google/components/FontListItem.tsx | 129 ++++++++ .../google/components/GoogleFontFilter.tsx | 60 ++++ .../tabs/google/components/GoogleFontList.tsx | 106 +++++++ .../google/contexts/GoogleFontContext.tsx | 143 +++++++++ .../tabs/google/index.tsx | 80 +++++ .../tabs/google/utils/stylesheet.ts} | 2 +- .../components/FontFamilySelector/index.tsx | 45 ++- .../utils/detectFontMetadata.ts | 6 +- .../components/LanguageSelector.tsx | 10 +- .../components/DefaultThemeColorSelector.tsx | 12 +- .../components/ThemeColorSelector/index.tsx | 2 +- .../components/ThemeSelector.tsx | 2 +- .../src/routes/components/Sidebar/Sidebar.tsx | 2 +- .../components/Sidebar/SidebarBottomBar.tsx | 14 +- .../components/Sidebar/SidebarHeader.tsx | 19 +- package.json | 2 + packages/ui/UI_GUIDE.md | 286 ++++++++++++++++- .../data-display/OptionsColumn/index.tsx | 15 +- .../Pixabay/components/SearchResults.tsx | 3 + .../src/components/inputs/FileInput/index.tsx | 26 +- .../IconPickerModal/components/IconEntry.tsx | 2 +- .../components/inputs/SliderInput/index.tsx | 11 +- .../ui/src/components/layout/Card/index.tsx | 13 +- .../Pagination/components/NavButton.tsx | 5 +- packages/ui/src/index.ts | 3 +- packages/ui/src/system/colors/index.ts | 2 + packages/ui/src/system/colors/surfaces.ts | 24 ++ server/package.json | 8 +- server/src/lib/apiKeys/routes/entries.ts | 2 - server/src/lib/user/routes/personalization.ts | 30 +- shared/src/api/core/forgeEndpoint.ts | 2 - 51 files changed, 1529 insertions(+), 861 deletions(-) create mode 160000 apps/lifeforge--wallet delete mode 100644 client/src/core/personalization/components/FontFamilySelector/components/FontFamilySelectorModal/components/FontListItem.tsx delete mode 100644 client/src/core/personalization/components/FontFamilySelector/components/FontFamilySelectorModal/tabs/CustomFontSelector.tsx delete mode 100644 client/src/core/personalization/components/FontFamilySelector/components/FontFamilySelectorModal/tabs/GoogleFontSelector.tsx create mode 100644 client/src/core/personalization/components/FontFamilySelector/components/FontFamilySelectorModal/tabs/custom/index.tsx create mode 100644 client/src/core/personalization/components/FontFamilySelector/components/FontFamilySelectorModal/tabs/google/components/FontListItem.tsx create mode 100644 client/src/core/personalization/components/FontFamilySelector/components/FontFamilySelectorModal/tabs/google/components/GoogleFontFilter.tsx create mode 100644 client/src/core/personalization/components/FontFamilySelector/components/FontFamilySelectorModal/tabs/google/components/GoogleFontList.tsx create mode 100644 client/src/core/personalization/components/FontFamilySelector/components/FontFamilySelectorModal/tabs/google/contexts/GoogleFontContext.tsx create mode 100644 client/src/core/personalization/components/FontFamilySelector/components/FontFamilySelectorModal/tabs/google/index.tsx rename client/src/core/personalization/components/FontFamilySelector/{utils/fontFamily.ts => components/FontFamilySelectorModal/tabs/google/utils/stylesheet.ts} (95%) create mode 100644 packages/ui/src/system/colors/surfaces.ts diff --git a/.agent/workflows/create-dashboard-widget.md b/.agent/workflows/create-dashboard-widget.md index dee02b147..161b32364 100644 --- a/.agent/workflows/create-dashboard-widget.md +++ b/.agent/workflows/create-dashboard-widget.md @@ -59,8 +59,14 @@ export const config: WidgetConfig = { } ``` +> [!IMPORTANT] +> **Import paths for module widgets require special handling.** Module widgets live under `apps//client/src/` and use different import paths than core: +> - `@lifeforge/ui` → `lifeforge-ui` (no `@` prefix) +> - `@lifeforge/shared` → `shared` +> - `@/forgeAPI` → `@/utils/forgeAPI` (or `@/forgeAPI` depending on module setup) + > [!TIP] -> For small/compact widgets where space is limited, the `title` prop can be omitted from the `Widget` wrapper. The widget will still display properly without a header, giving more room for content. See `apps/momentVault/client/src/widgets/QuickAudioCapture.tsx` for an example. +> For small/compact widgets where space is limited, the `title` prop can be omitted from the `Widget` wrapper. The widget will still display properly without a header, giving more room for content. ### Widget with Dimension Constraints @@ -80,9 +86,19 @@ export const config: WidgetConfig = { ```tsx import { useQuery } from '@tanstack/react-query' -import { Button, EmptyStateScreen, Widget, WithQuery } from '@lifeforge/ui' -import { Link } from '@lifeforge/shared' -import type { WidgetConfig } from '@lifeforge/shared' +import { + Button, + Card, + EmptyStateScreen, + Flex, + Icon, + Text, + Widget, + WithQuery, + surface +} from 'lifeforge-ui' +import { Link } from 'shared' +import type { WidgetConfig } from 'shared' import forgeAPI from '@/utils/forgeAPI' @@ -93,8 +109,6 @@ export default function MyDataWidget() { {data => ( data.length > 0 ? ( -
    + {data.map(item => ( -
  • {item.name}
  • + + + + + + + {item.name} + {item.description} + + + ))} -
+ ) : ( - {/* Content that adapts to dimensions */} - + Widget Content + + Adapts to {w}x{h} + + ) } + +export const config: WidgetConfig = { + id: '', + icon: 'tabler:icon-name', + minW: 2, + minH: 1 +} +``` + +> [!NOTE] +> When using `Card` directly as a widget root (no `Widget` wrapper), `Card` already provides the correct default background (`surface.default`), shadow, padding, and rounded corners — no inline styles or Tailwind classes needed. + +### Legacy Pattern (Do NOT Use) + +```tsx +// ❌ OLD — uses removed component-bg classes, clsx, inline styles, and @iconify/react +import { Icon } from '@iconify/react' +import clsx from 'clsx' + +
+``` + +```tsx +// ✅ NEW — use Card with direction/align/justify props + ``` --- @@ -241,31 +320,42 @@ interface WidgetConfig { ## Widget Component Props Reference -The `Widget` component from `lifeforge-ui` accepts: +The `Widget` component from `@lifeforge/ui` accepts: | Prop | Type | Description | | ----------------- | ----------------- | ---------------------------------------------------- | | `icon` | `string` | Iconify icon name | -| `title` | `string` | Widget title (auto-translated if namespace provided) | +| `iconColor` | `TokenizedColor` | Optional custom icon color | +| `title` | `ReactNode` | Widget title (auto-translated if namespace provided) | +| `description` | `ReactNode` | Widget description | | `namespace` | `string \| false` | Translation namespace, `false` to disable | -| `className` | `string` | Additional CSS classes | | `actionComponent` | `ReactNode` | Component beside title (e.g., navigation button) | +| `variant` | `'default' \| 'large-icon'` | Visual variant | | `children` | `ReactNode` | Widget content | --- ## Common UI Components for Widgets -Import from `lifeforge-ui`: +Import from `lifeforge-ui` (modules) or `@lifeforge/ui` (core): | Component | Usage | | ------------------ | -------------------------------------------- | | `Widget` | Container wrapper with title and icon | +| `Card` | Card wrapper for dimension-responsive widgets| +| `Flex` | Flexbox layout container | +| `Box` | Generic layout primitive | +| `Text` | Typography (with spacing and style props) | +| `Icon` | Icon display (uses `size` prop, not `width`/`height`) | | `WithQuery` | Handles loading/error states for API queries | | `EmptyStateScreen` | Shows when no data exists | | `Scrollbar` | Scrollable content wrapper | | `Button` | Action buttons | | `LoadingScreen` | Loading indicator | +| `surface` | Pre-built bg presets (`surface.light`, `surface.lightInteractive`, `surface.default`, `surface.defaultInteractive`) | + +> [!WARNING] +> Never import `Icon` from `@iconify/react` directly. Always use the `Icon` primitive from the UI library. Use the `size` prop (not `width`/`height`) for sizing, and the `color` prop (not `className="text-..."`) for colors. --- @@ -275,14 +365,14 @@ Examine these existing widgets for patterns: ### Simple Display Widgets (No API) -- `client/src/apps/dashboard/widgets/Clock.tsx` - Time display with dimensions -- `client/src/apps/dashboard/widgets/Date.tsx` - Date display with theming -- `client/src/apps/dashboard/widgets/Quotes.tsx` - External API fetch +- `client/src/core/dashboard/widgets/Clock.tsx` - Time display with dimensions +- `client/src/core/dashboard/widgets/Date.tsx` - Date display with theming +- `client/src/core/dashboard/widgets/Quotes.tsx` - External API fetch ### Data-Driven Widgets - `apps/calendar/client/src/widgets/TodaysEvent.tsx` - List with items -- `apps/wallet/client/src/widgets/AssetsBalance.tsx` - Grid layout with toggle +- `apps/wallet/client/src/widgets/AssetsBalance.tsx` - Grid layout with toggle (not yet migrated — see as legacy reference) - `apps/todoList/client/src/widgets/TodoList.tsx` - List with context provider - `apps/codeTime/client/src/widgets/CodeTime.tsx` - Chart visualization @@ -298,7 +388,12 @@ Examine these existing widgets for patterns: - [ ] Create `apps//client/src/widgets/.tsx` - [ ] Export default React component - [ ] Export named `config: WidgetConfig` -- [ ] Use `Widget` wrapper component +- [ ] Use `Widget` wrapper component (or `Card` for dimension-responsive widgets without header) +- [ ] Import `Icon` from UI library, not `@iconify/react` +- [ ] Use `size` prop for icon sizing, `color` prop for icon colors +- [ ] No `className` with Tailwind classes +- [ ] No `component-bg-*` classes — use `surface` presets or `Card` instead +- [ ] Use `@lifeforge/ui` / `@lifeforge/shared` for core, `lifeforge-ui` / `shared` for modules - [ ] Add `namespace` for translations - [ ] Set appropriate dimension constraints - [ ] Add locale entries in all language files diff --git a/apps/lifeforge--wallet b/apps/lifeforge--wallet new file mode 160000 index 000000000..1a8dc9953 --- /dev/null +++ b/apps/lifeforge--wallet @@ -0,0 +1 @@ +Subproject commit 1a8dc9953dac1aa9bee2bf2a4410a7997be4afbd diff --git a/apps/package.json b/apps/package.json index 811049837..747729fb3 100644 --- a/apps/package.json +++ b/apps/package.json @@ -2,8 +2,5 @@ "name": "@lifeforge/apps", "private": true, "description": "LifeForge modules", - "dependencies": { - "@lifeforge/TedMeadow--lang-ru": "workspace:*", - "@lifeforge/lifeforge--achievements": "workspace:*" - } + "dependencies": {} } diff --git a/bun.lock b/bun.lock index 7641f5198..3d2c9c7e3 100644 --- a/bun.lock +++ b/bun.lock @@ -14,6 +14,7 @@ "dotenv": "^17.2.3", "i18next": "^25.7.4", "lodash": "^4.17.21", + "opentype.js": "^2.0.0", "react": "^19.2.0", "react-dom": "^19.2.0", "react-i18next": "^16.5.1", @@ -27,6 +28,7 @@ "@tailwindcss/vite": "^4.1.18", "@trivago/prettier-plugin-sort-imports": "^5.2.2", "@types/lodash": "^4.17.21", + "@types/opentype.js": "^1.3.10", "@types/prettier": "^3.0.0", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", @@ -64,6 +66,28 @@ "@lifeforge/ui": "workspace:*", }, }, + "apps/lifeforge--wallet": { + "name": "@lifeforge/lifeforge--wallet", + "version": "0.0.5", + "dependencies": { + "@vis.gl/react-google-maps": "^1.5.5", + "chart.js": "^4.5.0", + "moment-range": "^4.0.2", + "react-chartjs-2": "^5.3.0", + "react-to-print": "^3.1.1", + "react-virtualized": "^9.22.6", + "recharts": "^2.15.0", + "zustand": "^5.0.8", + }, + "devDependencies": { + "@types/react-virtualized": "^9.22.3", + }, + "peerDependencies": { + "@lifeforge/server-utils": "workspace:*", + "@lifeforge/shared": "workspace:*", + "@lifeforge/ui": "workspace:*", + }, + }, "client": { "name": "@lifeforge/client", "version": "0.0.0", @@ -106,8 +130,6 @@ "react-virtualized": "^9.22.6", "recharts": "^2.15.0", "socket.io-client": "^4.8.1", - "tailwindcss": "^4.1.14", - "tailwindcss-animate": "^1.0.7", "tinycolor2": "^1.6.0", "typescript": "^5.9.3", "uuid": "^13.0.0", @@ -332,6 +354,7 @@ "lodash": "^4.17.21", "morgan": "^1.10.0", "multer": "^1.4.5-lts.1", + "node-cache": "^5.1.2", "openai": "^6.7.0", "pdf2pic": "^3.2.0", "pocketbase": "^0.26.2", @@ -353,6 +376,7 @@ "@types/lodash": "^4.17.21", "@types/morgan": "^1.9.9", "@types/multer": "^1.4.12", + "@types/node-cache": "^4.2.5", "@types/request": "^2.48.12", "@types/speakeasy": "^2.0.10", "@types/uuid": "^10.0.0", @@ -607,6 +631,8 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], + "@googlemaps/js-api-loader": ["@googlemaps/js-api-loader@2.0.2", "", { "dependencies": { "@types/google.maps": "^3.53.1" } }, "sha512-bKVuTqatS8Jven5aFqVB7rCHF1VFEzpzyi0ruzO0GUR+A7m9oMqMgtnmpANj7kMYEvvhty8Fk7TnJ1MKjWHu+Q=="], + "@headlessui/react": ["@headlessui/react@2.2.10", "", { "dependencies": { "@floating-ui/react": "^0.26.16", "@react-aria/focus": "^3.20.2", "@react-aria/interactions": "^3.25.0", "@tanstack/react-virtual": "^3.13.9", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-5pVLNK9wlpxTUTy9GpgbX/SdcRh+HBnPktjM2wbiLTH4p+2EPHBO1aoSryUCuKUIItdDWO9ITlhUL8UnUN/oIA=="], "@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="], @@ -661,6 +687,8 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="], + "@lifeforge/TedMeadow--lang-ru": ["@lifeforge/TedMeadow--lang-ru@workspace:locales/TedMeadow--lang-ru"], "@lifeforge/client": ["@lifeforge/client@workspace:client"], @@ -669,6 +697,8 @@ "@lifeforge/lifeforge--lang-en": ["@lifeforge/lifeforge--lang-en@workspace:locales/lifeforge--lang-en"], + "@lifeforge/lifeforge--wallet": ["@lifeforge/lifeforge--wallet@workspace:apps/lifeforge--wallet"], + "@lifeforge/log": ["@lifeforge/log@workspace:packages/lifeforge-log"], "@lifeforge/server": ["@lifeforge/server@workspace:server"], @@ -1121,6 +1151,8 @@ "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], + "@types/google.maps": ["@types/google.maps@3.65.0", "", {}, "sha512-u4SHiRC3m27lPa4vDBxh2AI7mDcHcheX6GSHn1Mwi0Gap8/uhM2kFppiFTnWASXLHZO+1ahHciLeEIV+Sjqk/A=="], + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], @@ -1149,9 +1181,11 @@ "@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="], + "@types/node-cache": ["@types/node-cache@4.2.5", "", { "dependencies": { "node-cache": "*" } }, "sha512-faK2Owokboz53g8ooq2dw3iDJ6/HMTCIa2RvMte5WMTiABy+wA558K+iuyRtlR67Un5q9gEKysSDtqZYbSa0Pg=="], + "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="], - "@types/opentype.js": ["@types/opentype.js@1.3.9", "", {}, "sha512-KOGywvDPncA4/tTWV5xKNhjpsoSSAHIx3mHOhL5l3XX+c6Xu2dQnHvGs7mRNQsQRte1EqmQ0cPQQ8Z14lkv+yw=="], + "@types/opentype.js": ["@types/opentype.js@1.3.10", "", {}, "sha512-F67EFyk6j02okHz5JCgata3ZRAcZi9GLnzmkHw/rzJq3OCc8/ZVdoKrxMTYjcQP6IYHGBz2cav1cpzkOkPiPCQ=="], "@types/prettier": ["@types/prettier@3.0.0", "", { "dependencies": { "prettier": "*" } }, "sha512-mFMBfMOz8QxhYVbuINtswBp9VL2b4Y0QqYHwqLz3YbgtfAcat2Dl6Y1o4e22S/OVE6Ebl9m7wWiMT2lSbAs1wA=="], @@ -1309,6 +1343,8 @@ "@vanilla-extract/vite-plugin": ["@vanilla-extract/vite-plugin@5.2.2", "", { "dependencies": { "@vanilla-extract/compiler": "^0.7.0", "@vanilla-extract/integration": "^8.0.9" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-AUyB4fDR2b/Mo0lcXhhlf6RxnDPYwFMyKKopalJ4BwQNKYzZSoTwHJ1PLPO9SKhpz7lzXc0Z18GHQZOewzl3YA=="], + "@vis.gl/react-google-maps": ["@vis.gl/react-google-maps@1.8.3", "", { "dependencies": { "@googlemaps/js-api-loader": "^2.0.2", "@types/google.maps": "^3.54.10", "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "react": ">=16.8.0 || ^19.0 || ^19.0.0-rc", "react-dom": ">=16.8.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-DW7nEuvOJ299DmdBnvGiUARrgS/+sTEO1iJgG9J8YaErZqLoq7S4TJ22f3EjJvR4dti4L4gft43JEK77nnKXDw=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], "@vitest/browser": ["@vitest/browser@1.6.1", "", { "dependencies": { "@vitest/utils": "1.6.1", "magic-string": "^0.30.5", "sirv": "^2.0.4" }, "peerDependencies": { "playwright": "*", "safaridriver": "*", "vitest": "1.6.1", "webdriverio": "*" }, "optionalPeers": ["playwright", "safaridriver", "webdriverio"] }, "sha512-9ZYW6KQ30hJ+rIfJoGH4wAub/KAb4YrFzX0kVLASvTm7nJWVC5EAv5SlzlXVl3h3DaUq5aqHlZl77nmOPnALUQ=="], @@ -1563,6 +1599,8 @@ "character-reference-invalid": ["character-reference-invalid@1.1.4", "", {}, "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg=="], + "chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="], + "check-error": ["check-error@1.0.3", "", { "dependencies": { "get-func-name": "^2.0.2" } }, "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg=="], "cheerio": ["cheerio@1.2.0", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", "htmlparser2": "^10.1.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", "undici": "^7.19.0", "whatwg-mimetype": "^4.0.0" } }, "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg=="], @@ -1693,6 +1731,8 @@ "cytoscape-fcose": ["cytoscape-fcose@2.2.0", "", { "dependencies": { "cose-base": "^2.2.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ=="], + "d": ["d@1.0.2", "", { "dependencies": { "es5-ext": "^0.10.64", "type": "^2.7.2" } }, "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw=="], + "d3": ["d3@7.9.0", "", { "dependencies": { "d3-array": "3", "d3-axis": "3", "d3-brush": "3", "d3-chord": "3", "d3-color": "3", "d3-contour": "4", "d3-delaunay": "6", "d3-dispatch": "3", "d3-drag": "3", "d3-dsv": "3", "d3-ease": "3", "d3-fetch": "3", "d3-force": "3", "d3-format": "3", "d3-geo": "3", "d3-hierarchy": "3", "d3-interpolate": "3", "d3-path": "3", "d3-polygon": "3", "d3-quadtree": "3", "d3-random": "3", "d3-scale": "4", "d3-scale-chromatic": "3", "d3-selection": "3", "d3-shape": "3", "d3-time": "3", "d3-time-format": "4", "d3-timer": "3", "d3-transition": "3", "d3-zoom": "3" } }, "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA=="], "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], @@ -1913,6 +1953,12 @@ "es-toolkit": ["es-toolkit@1.47.0", "", {}, "sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw=="], + "es5-ext": ["es5-ext@0.10.64", "", { "dependencies": { "es6-iterator": "^2.0.3", "es6-symbol": "^3.1.3", "esniff": "^2.0.1", "next-tick": "^1.1.0" } }, "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg=="], + + "es6-iterator": ["es6-iterator@2.0.3", "", { "dependencies": { "d": "1", "es5-ext": "^0.10.35", "es6-symbol": "^3.1.1" } }, "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g=="], + + "es6-symbol": ["es6-symbol@3.1.4", "", { "dependencies": { "d": "^1.0.2", "ext": "^1.7.0" } }, "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg=="], + "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], "esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="], @@ -2001,6 +2047,8 @@ "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + "esniff": ["esniff@2.0.1", "", { "dependencies": { "d": "^1.0.1", "es5-ext": "^0.10.62", "event-emitter": "^0.3.5", "type": "^2.7.2" } }, "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg=="], + "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], @@ -2031,6 +2079,8 @@ "eval": ["eval@0.1.8", "", { "dependencies": { "@types/node": "*", "require-like": ">= 0.1.1" } }, "sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw=="], + "event-emitter": ["event-emitter@0.3.5", "", { "dependencies": { "d": "1", "es5-ext": "~0.10.14" } }, "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA=="], + "event-stream": ["event-stream@3.3.4", "", { "dependencies": { "duplexer": "~0.1.1", "from": "~0", "map-stream": "~0.1.0", "pause-stream": "0.0.11", "split": "0.3", "stream-combiner": "~0.0.4", "through": "~2.3.1" } }, "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g=="], "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], @@ -2051,6 +2101,8 @@ "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], + "ext": ["ext@1.7.0", "", { "dependencies": { "type": "^2.7.2" } }, "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw=="], + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], "extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="], @@ -2739,6 +2791,10 @@ "modern-ahocorasick": ["modern-ahocorasick@1.1.0", "", {}, "sha512-sEKPVl2rM+MNVkGQt3ChdmD8YsigmXdn5NifZn6jiwn9LRJpWm8F3guhaqrJT/JOat6pwpbXEk6kv+b9DMIjsQ=="], + "moment": ["moment@2.30.1", "", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="], + + "moment-range": ["moment-range@4.0.2", "", { "dependencies": { "es6-symbol": "^3.1.0" }, "peerDependencies": { "moment": ">= 2" } }, "sha512-n8sceWwSTjmz++nFHzeNEUsYtDqjgXgcOBzsHi+BoXQU2FW+eU92LUaK8gqOiSu5PG57Q9sYj1Fz4LRDj4FtKA=="], + "morgan": ["morgan@1.10.1", "", { "dependencies": { "basic-auth": "~2.0.1", "debug": "2.6.9", "depd": "~2.0.0", "on-finished": "~2.3.0", "on-headers": "~1.1.0" } }, "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A=="], "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], @@ -2773,6 +2829,10 @@ "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], + "next-tick": ["next-tick@1.1.0", "", {}, "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="], + + "node-cache": ["node-cache@5.1.2", "", { "dependencies": { "clone": "2.x" } }, "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg=="], + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], "node-exports-info": ["node-exports-info@1.6.0", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="], @@ -2837,7 +2897,7 @@ "opencollective-postinstall": ["opencollective-postinstall@2.0.3", "", { "bin": { "opencollective-postinstall": "index.js" } }, "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q=="], - "opentype.js": ["opentype.js@1.3.5", "", { "bin": { "ot": "bin/ot" } }, "sha512-thKDiELidAApOvXlncrpwDZKJCa9fXLEKM4+FoEWI+qTLDeNb+h7EkN+8a7KQODsB1GZ+Exz9KknkoPrEdXZDw=="], + "opentype.js": ["opentype.js@2.0.0", "", { "bin": { "ot": "bin/ot" } }, "sha512-kCyjv6xdDY1W/jLWZ/L3QhhTlKUqDZMQ5+Jdlw12b3dXkKNpYBqqlMMj0YDQPShWFTMwgZI1hG14kN3XUDSg/A=="], "optimist": ["optimist@0.6.0", "", { "dependencies": { "minimist": "~0.0.1", "wordwrap": "~0.0.2" } }, "sha512-ubrZPyOU0AHpXkmwqfWolap+eHMwQ484AKivkf0ZGyysd6fUJZl7ow9iu5UNV1vCZv46HQ7EM83IC3NGJ820hg=="], @@ -3015,6 +3075,8 @@ "react-aria": ["react-aria@3.48.0", "", { "dependencies": { "@internationalized/date": "^3.12.1", "@internationalized/number": "^3.6.6", "@internationalized/string": "^3.2.8", "@react-types/shared": "^3.34.0", "@swc/helpers": "^0.5.0", "aria-hidden": "^1.2.3", "clsx": "^2.0.0", "react-stately": "3.46.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-jQjd4rBEIMqecBaAKYJbVGK6EqIHLa5znVQ7jwFyK5vCyljoj6KhgtiahmcIPsG5vG5vEDLw+ba+bEWn6A2P4w=="], + "react-chartjs-2": ["react-chartjs-2@5.3.1", "", { "peerDependencies": { "chart.js": "^4.1.1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A=="], + "react-currency-input-field": ["react-currency-input-field@3.10.0", "", { "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-GRmZogHh1e1LrmgXg/fKHSuRLYUnj/c/AumfvfuDMA0UX1mDR6u2NR0fzDemRdq4tNHNLucJeJ2OKCr3ehqyDA=="], "react-custom-scrollbars": ["react-custom-scrollbars@4.2.1", "", { "dependencies": { "dom-css": "^2.0.0", "prop-types": "^15.5.10", "raf": "^3.1.0" }, "peerDependencies": { "react": "^0.14.0 || ^15.0.0 || ^16.0.0", "react-dom": "^0.14.0 || ^15.0.0 || ^16.0.0" } }, "sha512-VtJTUvZ7kPh/auZWIbBRceGPkE30XBYe+HktFxuMWBR2eVQQ+Ur6yFJMoaYcNpyGq22uYJ9Wx4UAEcC0K+LNPQ=="], @@ -3067,6 +3129,8 @@ "react-syntax-highlighter": ["react-syntax-highlighter@15.6.6", "", { "dependencies": { "@babel/runtime": "^7.3.1", "highlight.js": "^10.4.1", "highlightjs-vue": "^1.0.0", "lowlight": "^1.17.0", "prismjs": "^1.30.0", "refractor": "^3.6.0" }, "peerDependencies": { "react": ">= 0.14.0" } }, "sha512-DgXrc+AZF47+HvAPEmn7Ua/1p10jNoVZVI/LoPiYdtY+OM+/nG5yefLHKJwdKqY1adMuHFbeyBaG9j64ML7vTw=="], + "react-to-print": ["react-to-print@3.3.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ~19" } }, "sha512-7j9GIeNZA9glZlbv9mIbIHDOOx+WYfRMbJzh04NiSKjdaeGkxJuKjJQrtRuNKtt5AvEVVjrLCPokZ9yJX51Fvg=="], + "react-toastify": ["react-toastify@11.1.0", "", { "dependencies": { "clsx": "^2.1.1" }, "peerDependencies": { "react": "^18 || ^19", "react-dom": "^18 || ^19" } }, "sha512-e9h23x3phN0wbFeB6yovmWp7lobzV4CaCH0LO8nVP6H7Y+3GbcLpIzMm9dJhcp1RXbpyfvjgpfXqO80QAmn7sg=="], "react-tooltip": ["react-tooltip@5.30.1", "", { "dependencies": { "@floating-ui/dom": "^1.6.1", "classnames": "^2.3.0" }, "peerDependencies": { "react": ">=16.14.0", "react-dom": ">=16.14.0" } }, "sha512-1lSPLQXuVooePxadUpmcwLgOsF1mIty7UZTJ9XnyfX4drOzStYs4JMXnazcDLguQr41W5OUZddOp9kfvArdpEQ=="], @@ -3375,8 +3439,6 @@ "tailwindcss": ["tailwindcss@4.3.0", "", {}, "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="], - "tailwindcss-animate": ["tailwindcss-animate@1.0.7", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="], - "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], "tar": ["tar@7.5.15", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ=="], @@ -3465,6 +3527,8 @@ "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], + "type": ["type@2.7.3", "", {}, "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ=="], + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], "type-detect": ["type-detect@4.1.0", "", {}, "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw=="], @@ -3687,6 +3751,10 @@ "@joshwooding/vite-plugin-react-docgen-typescript/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "@lifeforge/client/@types/opentype.js": ["@types/opentype.js@1.3.9", "", {}, "sha512-KOGywvDPncA4/tTWV5xKNhjpsoSSAHIx3mHOhL5l3XX+c6Xu2dQnHvGs7mRNQsQRte1EqmQ0cPQQ8Z14lkv+yw=="], + + "@lifeforge/client/opentype.js": ["opentype.js@1.3.5", "", { "bin": { "ot": "bin/ot" } }, "sha512-thKDiELidAApOvXlncrpwDZKJCa9fXLEKM4+FoEWI+qTLDeNb+h7EkN+8a7KQODsB1GZ+Exz9KknkoPrEdXZDw=="], + "@lifeforge/client/react-i18next": ["react-i18next@15.7.4", "", { "dependencies": { "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 23.4.0", "react": ">= 16.8.0", "react-dom": "*", "react-native": "*", "typescript": "^5" }, "optionalPeers": ["react-dom", "react-native", "typescript"] }, "sha512-nyU8iKNrI5uDJch0z9+Y5XEr34b0wkyYj3Rp+tfbahxtlswxSCjcUL9H0nqXo9IR3/t5Y5PKIA3fx3MfUyR9Xw=="], "@lifeforge/log/@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="], diff --git a/client/src/core/dashboard/widgets/Date.tsx b/client/src/core/dashboard/widgets/Date.tsx index ee0641cdc..e08da98bd 100644 --- a/client/src/core/dashboard/widgets/Date.tsx +++ b/client/src/core/dashboard/widgets/Date.tsx @@ -29,7 +29,7 @@ export default function DateWidget({ align={ h === 2 ? 'start' : h === 1 ? { base: 'center', sm: 'end' } : 'start' } - bg="custom-500" + bg="primary" direction={h === 2 ? 'column' : h === 1 ? 'row' : 'column'} gap="md" height="100%" @@ -41,7 +41,7 @@ export default function DateWidget({ > diff --git a/client/src/core/dashboard/widgets/Quotes.tsx b/client/src/core/dashboard/widgets/Quotes.tsx index cb86b3019..7e1804db6 100644 --- a/client/src/core/dashboard/widgets/Quotes.tsx +++ b/client/src/core/dashboard/widgets/Quotes.tsx @@ -28,7 +28,7 @@ export default function Quotes() { return ( {modules => modules.length > 0 ? ( - + {Object.entries(groupedModules).map(([category, mods]) => ( @@ -124,7 +123,7 @@ function Modules() { ))} - + ) : ( {bgImage !== '' ? ( - <> + - + ) : ( - <> - - + )} diff --git a/client/src/core/personalization/components/BgImageSelector/modals/AdjustBgImageModal/components/AdjustmentColumn.tsx b/client/src/core/personalization/components/BgImageSelector/modals/AdjustBgImageModal/components/AdjustmentColumn.tsx index 2e9b32ebc..6c26c68bd 100644 --- a/client/src/core/personalization/components/BgImageSelector/modals/AdjustBgImageModal/components/AdjustmentColumn.tsx +++ b/client/src/core/personalization/components/BgImageSelector/modals/AdjustBgImageModal/components/AdjustmentColumn.tsx @@ -1,7 +1,7 @@ import _ from 'lodash' import { useTranslation } from 'react-i18next' -import { OptionsColumn, SliderInput } from '@lifeforge/ui' +import { OptionsColumn, SliderInput, surface } from '@lifeforge/ui' function AdjustmentColumn({ icon, @@ -20,18 +20,17 @@ function AdjustmentColumn({ return ( -
-
-
-

- - Lorem ipsum dolor sit amet -

-

+ + + + + Lorem ipsum dolor sit amet + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam ac. -

-
-
+ + - - - -
-

Lorem ipsum dolor

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. - Nullam ac. -

-
-
-
-
-
-
+ + + + + Lorem ipsum dolor + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam + ac. + + + + + + ) } diff --git a/client/src/core/personalization/components/BgImageSelector/modals/AdjustBgImageModal/index.tsx b/client/src/core/personalization/components/BgImageSelector/modals/AdjustBgImageModal/index.tsx index cf9a21f7f..0b6c4a7e7 100644 --- a/client/src/core/personalization/components/BgImageSelector/modals/AdjustBgImageModal/index.tsx +++ b/client/src/core/personalization/components/BgImageSelector/modals/AdjustBgImageModal/index.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { usePersonalization } from '@lifeforge/shared' -import { Button, ModalHeader } from '@lifeforge/ui' +import { Button, ModalHeader, Stack } from '@lifeforge/ui' import { useUserPersonalization } from '@/providers/features/UserPersonalizationProvider' @@ -92,7 +92,7 @@ function AdjustBgImageModal({ onClose }: { onClose: () => void }) { }, []) return ( -
+ void }) { bgSaturation={bgSaturation} overlayOpacity={overlayOpacity} /> -
+ {ADJUSTMENTS_COLUMNS.map(({ title, ...props }) => ( ))} - -
-
+ + ) } diff --git a/client/src/core/personalization/components/BgTempSelector/components/DefaultBgTempSelector.tsx b/client/src/core/personalization/components/BgTempSelector/components/DefaultBgTempSelector.tsx index 2c101c295..ecef01529 100644 --- a/client/src/core/personalization/components/BgTempSelector/components/DefaultBgTempSelector.tsx +++ b/client/src/core/personalization/components/BgTempSelector/components/DefaultBgTempSelector.tsx @@ -6,7 +6,8 @@ import { Flex, Listbox, ListboxOption, - Text + Text, + surface } from '@lifeforge/ui' import { useUserPersonalization } from '@/providers/features/UserPersonalizationProvider' @@ -34,15 +35,15 @@ function DefaultBgTempSelector({ return ( ( - + )} value={bgTemp.startsWith('#') ? 'bg-custom' : bgTemp} + width="100%" onChange={color => { changeBgTemp(color === 'bg-custom' ? customBgTemp : color) }} diff --git a/client/src/core/personalization/components/BgTempSelector/index.tsx b/client/src/core/personalization/components/BgTempSelector/index.tsx index 5788b9a95..9ceb2acd0 100644 --- a/client/src/core/personalization/components/BgTempSelector/index.tsx +++ b/client/src/core/personalization/components/BgTempSelector/index.tsx @@ -33,7 +33,12 @@ function BgTempSelector() { icon="tabler:temperature" title={t('bgTempSelector.title')} > - + {bgTemp.startsWith('#') && ( <> diff --git a/client/src/core/personalization/components/BorderRadiusSelector/index.tsx b/client/src/core/personalization/components/BorderRadiusSelector/index.tsx index c748e7408..de2ca5f1d 100644 --- a/client/src/core/personalization/components/BorderRadiusSelector/index.tsx +++ b/client/src/core/personalization/components/BorderRadiusSelector/index.tsx @@ -6,7 +6,8 @@ import { Listbox, ListboxOption, OptionsColumn, - Text + Text, + surface } from '@lifeforge/ui' import { useUserPersonalization } from '@/providers/features/UserPersonalizationProvider' @@ -37,12 +38,7 @@ function BorderRadiusSelector() { title={t('borderRadiusSelector.title')} > ( diff --git a/client/src/core/personalization/components/FontFamilySelector/components/CustomFontUploadModal.tsx b/client/src/core/personalization/components/FontFamilySelector/components/CustomFontUploadModal.tsx index 7ca8fae03..751b9f65d 100644 --- a/client/src/core/personalization/components/FontFamilySelector/components/CustomFontUploadModal.tsx +++ b/client/src/core/personalization/components/FontFamilySelector/components/CustomFontUploadModal.tsx @@ -7,7 +7,7 @@ import { FormModal, defineForm } from '@lifeforge/ui' import forgeAPI from '@/forgeAPI' import { detectFontMetadata } from '../utils/detectFontMetadata' -import type { CustomFont } from './FontFamilySelectorModal/tabs/CustomFontSelector' +import type { CustomFont } from './FontFamilySelectorModal/tabs/custom' function CustomFontUploadModal({ onClose, @@ -62,7 +62,7 @@ function CustomFontUploadModal({ label: 'fontFamily.inputs.fontFile', required: true, acceptedMimeTypes: { - 'font/*': ['ttf', 'otf', 'woff', 'woff2'] + font: ['ttf', 'otf', 'woff', 'woff2'] } }, displayName: { @@ -89,6 +89,9 @@ function CustomFontUploadModal({ (currentState.file as any).file ) + //TODO + console.log(currentState.file.file, metadata) + formStateStore.setState({ family: metadata.family, weight: metadata.weight, diff --git a/client/src/core/personalization/components/FontFamilySelector/components/FontFamilySelectorModal/components/FontListItem.tsx b/client/src/core/personalization/components/FontFamilySelector/components/FontFamilySelectorModal/components/FontListItem.tsx deleted file mode 100644 index 099211ffe..000000000 --- a/client/src/core/personalization/components/FontFamilySelector/components/FontFamilySelectorModal/components/FontListItem.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { Icon } from '@iconify/react' -import { useMutation, useQueryClient } from '@tanstack/react-query' -import clsx from 'clsx' -import { useEffect } from 'react' -import { toast } from 'react-toastify' - -import { usePromiseLoading } from '@lifeforge/shared' -import { Button } from '@lifeforge/ui' - -import forgeAPI from '@/forgeAPI' - -import { - addFontToStylesheet, - removeFontFromStylesheet -} from '../../../utils/fontFamily' -import type { FontFamily } from '../tabs/GoogleFontSelector' - -function FontListItem({ - font, - selectedFont, - isPinned, - setSelectedFont -}: { - font: FontFamily - selectedFont: string | null - isPinned: boolean - setSelectedFont: (font: string) => void -}) { - const queryClient = useQueryClient() - - const togglePinMutation = useMutation( - forgeAPI.user.personalization.toggleGoogleFontsPin.mutationOptions({ - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ['user', 'personalization', 'listGoogleFontsPin'] - }) - }, - onError: () => { - toast.error('Failed to toggle font pin') - } - }) - ) - - const [loadingPin, handleTogglePin] = usePromiseLoading(async () => { - await togglePinMutation.mutateAsync({ family: font.family }) - }) - - useEffect(() => { - addFontToStylesheet(font) - - return () => { - removeFontFromStylesheet(font.family) - } - }, [font]) - - return ( - - ) -} - -export default FontListItem diff --git a/client/src/core/personalization/components/FontFamilySelector/components/FontFamilySelectorModal/index.tsx b/client/src/core/personalization/components/FontFamilySelector/components/FontFamilySelectorModal/index.tsx index b71d3e84d..e7712fe58 100644 --- a/client/src/core/personalization/components/FontFamilySelector/components/FontFamilySelectorModal/index.tsx +++ b/client/src/core/personalization/components/FontFamilySelector/components/FontFamilySelectorModal/index.tsx @@ -3,12 +3,12 @@ import { useTranslation } from 'react-i18next' import { toast } from 'react-toastify' import { usePersonalization } from '@lifeforge/shared' -import { Button, ModalHeader, Tabs } from '@lifeforge/ui' +import { Button, ModalHeader, Stack, Tabs } from '@lifeforge/ui' import { useUserPersonalization } from '@/providers/features/UserPersonalizationProvider' -import CustomFontSelector from './tabs/CustomFontSelector' -import GoogleFontSelector from './tabs/GoogleFontSelector' +import CustomFontSelector from './tabs/custom' +import GoogleFontSelector from './tabs/google' type TabType = 'google' | 'custom' @@ -26,7 +26,7 @@ function FontFamilySelectorModal({ onClose }: { onClose: () => void }) { const [selectedFont, setSelectedFont] = useState(fontFamily) return ( -
+ void }) { onClose={onClose} /> void }) { } onTabChange={setActiveTab} /> - {activeTab === 'google' ? ( - - ) : ( - - )} + + {activeTab === 'google' ? ( + + ) : ( + + )} + {selectedFont && selectedFont !== fontFamily && ( )} -
+ ) } diff --git a/client/src/core/personalization/components/FontFamilySelector/components/FontFamilySelectorModal/tabs/CustomFontSelector.tsx b/client/src/core/personalization/components/FontFamilySelector/components/FontFamilySelectorModal/tabs/CustomFontSelector.tsx deleted file mode 100644 index 7adc69baa..000000000 --- a/client/src/core/personalization/components/FontFamilySelector/components/FontFamilySelectorModal/tabs/CustomFontSelector.tsx +++ /dev/null @@ -1,203 +0,0 @@ -import { Icon } from '@iconify/react' -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { useTranslation } from 'react-i18next' -import { toast } from 'react-toastify' - -import { usePersonalization } from '@lifeforge/shared' -import { - Button, - Card, - ConfirmationModal, - ContextMenu, - ContextMenuItem, - EmptyStateScreen, - Scrollbar, - WithQuery, - useModalStore -} from '@lifeforge/ui' - -import forgeAPI from '@/forgeAPI' - -import CustomFontUploadModal from '../../CustomFontUploadModal' - -export type CustomFont = { - id: string - displayName: string - family: string - weight: number - file: string - collectionId: string -} - -function CustomFontSelector({ - selectedFont, - setSelectedFont -}: { - selectedFont: string | null - setSelectedFont: (font: string | null) => void -}) { - const { t } = useTranslation('common.personalization') - - const { fontFamily } = usePersonalization() - - const { open } = useModalStore() - - const queryClient = useQueryClient() - - const customFontsQuery = useQuery( - forgeAPI.user.customFonts.list.queryOptions() - ) - - const deleteMutation = useMutation( - forgeAPI.user.customFonts.remove.mutationOptions({ - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ['user', 'customFonts', 'list'] - }) - toast.success('Custom font deleted successfully!') - }, - onError: () => { - toast.error('Failed to delete custom font') - } - }) - ) - - const handleUploadClick = () => { - open(CustomFontUploadModal, { - openType: 'create' - }) - } - - const handleDeleteClick = (font: CustomFont) => { - open(ConfirmationModal, { - title: t('fontFamily.customFonts.delete.title'), - description: t('fontFamily.customFonts.delete.description'), - confirmationButton: 'delete', - onConfirm: async () => { - await deleteMutation.mutateAsync({ id: font.id }) - - // If the deleted font was selected, clear selection - if (selectedFont === `custom:${font.id}`) { - setSelectedFont(null) - } - } - }) - } - - const isCustomFontSelected = (fontId: string) => { - return selectedFont === `custom:${fontId}` - } - - return ( -
- - - {fonts => - fonts.length > 0 ? ( - -
- {fonts.map(font => ( - - setSelectedFont( - selectedFont === `custom:${font.id}` - ? null - : `custom:${font.id}` - ) - } - onKeyDown={e => { - if (e.key === 'Enter' || e.key === ' ') { - setSelectedFont( - selectedFont === `custom:${font.id}` - ? null - : `custom:${font.id}` - ) - } - }} - > -
-
- -
-
-
-

{font.displayName}

-

- {font.family} • Weight {font.weight} -

-
- {isCustomFontSelected(font.id) && ( - - )} -
-
- -
- - { - open(CustomFontUploadModal, { - openType: 'edit', - initialData: font - }) - }} - /> - handleDeleteClick(font)} - /> - -
-
- ))} -
-
- ) : ( -
- -
- ) - } -
-
- ) -} - -export default CustomFontSelector diff --git a/client/src/core/personalization/components/FontFamilySelector/components/FontFamilySelectorModal/tabs/GoogleFontSelector.tsx b/client/src/core/personalization/components/FontFamilySelector/components/FontFamilySelectorModal/tabs/GoogleFontSelector.tsx deleted file mode 100644 index 73d257ded..000000000 --- a/client/src/core/personalization/components/FontFamilySelector/components/FontFamilySelectorModal/tabs/GoogleFontSelector.tsx +++ /dev/null @@ -1,288 +0,0 @@ -import { useQuery } from '@tanstack/react-query' -import _ from 'lodash' -import { useEffect, useMemo, useRef, useState } from 'react' -import { AutoSizer } from 'react-virtualized' - -import { usePersonalization } from '@lifeforge/shared' -import { - EmptyStateScreen, - Listbox, - ListboxOption, - Pagination, - Scrollbar, - SearchInput, - WithQuery -} from '@lifeforge/ui' - -import forgeAPI from '@/forgeAPI' - -import FontListItem from '../components/FontListItem' - -export interface FontFamily { - family: string - variants: string[] - subsets: string[] - version: string - lastModified: Date - files: Files - category: Category - kind: Kind - menu: string - colorCapabilities?: ColorCapability[] -} - -enum Category { - Display = 'display', - Handwriting = 'handwriting', - Monospace = 'monospace', - SansSerif = 'sans-serif', - Serif = 'serif' -} - -enum ColorCapability { - COLRv0 = 'COLRv0', - COLRv1 = 'COLRv1', - SVG = 'SVG' -} - -interface Files { - regular?: string - italic?: string - '500'?: string - '600'?: string - '700'?: string - '800'?: string - '100'?: string - '200'?: string - '300'?: string - '900'?: string - '100italic'?: string - '200italic'?: string - '300italic'?: string - '500italic'?: string - '600italic'?: string - '700italic'?: string - '800italic'?: string - '900italic'?: string -} - -enum Kind { - WebfontsWebfont = 'webfonts#webfont' -} - -function GoogleFontSelector({ - selectedFont, - setSelectedFont -}: { - selectedFont: string | null - setSelectedFont: (font: string | null) => void -}) { - const { fontFamily } = usePersonalization() - - const apiKeyAvailable = useQuery( - forgeAPI - .checkAPIKeys({ - keys: 'gcloud' - }) - .queryOptions() - ) - - const fontsQuery = useQuery<{ - enabled: boolean - items: FontFamily[] - }>( - forgeAPI.user.personalization.listGoogleFonts.queryOptions({ - enabled: apiKeyAvailable.data === true - }) - ) - - const pinnedFontsQuery = useQuery( - forgeAPI.user.personalization.listGoogleFontsPin.queryOptions({ - enabled: apiKeyAvailable.data === true - }) - ) - - const [selectedCategory, setSelectedCategory] = useState(null) - - const [searchQuery, setSearchQuery] = useState('') - - const [page, setPage] = useState(1) - - const scrollableRef = useRef(null) - - const filteredFonts = useMemo( - () => - fontsQuery.data?.items - .filter(font => { - return ( - (font.category === selectedCategory || !selectedCategory) && - font.family.toLowerCase().includes(searchQuery.toLowerCase()) - ) - }) - .sort((a, b) => { - const aPinned = pinnedFontsQuery.data?.includes(a.family) ? 1 : 0 - - const bPinned = pinnedFontsQuery.data?.includes(b.family) ? 1 : 0 - - if (aPinned !== bPinned) { - return bPinned - aPinned // Pinned fonts first - } - - return a.family.localeCompare(b.family) // Then sort alphabetically - }), - [fontsQuery.data, selectedCategory, searchQuery, pinnedFontsQuery.data] - ) - - useEffect(() => { - setPage(1) - scrollableRef.current?.scrollToTop() - }, [selectedCategory, searchQuery]) - - useEffect(() => { - if (!fontsQuery.data) return - - const indexOfCurrentFontFamily = fontsQuery.data.items.findIndex( - font => font.family === fontFamily - ) - - setPage( - indexOfCurrentFontFamily !== -1 - ? Math.ceil((indexOfCurrentFontFamily + 1) / 10) - : 1 - ) - }, [fontsQuery.data]) - - return ( - - {apiKeyAvailable => - apiKeyAvailable ? ( - <> -
- ( - - {_.startCase(selectedCategory || '') || 'All category'} - - )} - value={selectedCategory} - onChange={setSelectedCategory} - > - - {[ - ...new Set(fontsQuery.data?.items.map(font => font.category)) - ].map(category => ( - - ))} - - -
- - {pinnedFontsData => ( - - {data => - !data.enabled ? ( - - ) : filteredFonts!.length > 0 ? ( -
- - {({ height, width }) => ( - -
- { - setPage(page) - scrollableRef.current?.scrollToTop() - }} - /> - {filteredFonts - ?.slice((page - 1) * 10, page * 10) - .map(font => ( - pinnedFont === font.family - )} - selectedFont={selectedFont} - setSelectedFont={setSelectedFont} - /> - ))} - { - setPage(page) - scrollableRef.current?.scrollToTop() - }} - /> -
-
- )} -
-
- ) : ( -
- -
- ) - } -
- )} -
- - ) : ( -
- -
- ) - } -
- ) -} - -export default GoogleFontSelector diff --git a/client/src/core/personalization/components/FontFamilySelector/components/FontFamilySelectorModal/tabs/custom/index.tsx b/client/src/core/personalization/components/FontFamilySelector/components/FontFamilySelectorModal/tabs/custom/index.tsx new file mode 100644 index 000000000..8bf5c7eac --- /dev/null +++ b/client/src/core/personalization/components/FontFamilySelector/components/FontFamilySelectorModal/tabs/custom/index.tsx @@ -0,0 +1,232 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { useTranslation } from 'react-i18next' +import { toast } from 'react-toastify' +import { AutoSizer } from 'react-virtualized' + +import { usePersonalization } from '@lifeforge/shared' +import { + Box, + Button, + Card, + ConfirmationModal, + ContextMenu, + ContextMenuItem, + EmptyStateScreen, + Flex, + Icon, + Ring, + Scrollbar, + Stack, + Text, + WithQuery, + colorWithOpacity, + surface, + useModalStore +} from '@lifeforge/ui' + +import forgeAPI from '@/forgeAPI' + +import CustomFontUploadModal from '../../../CustomFontUploadModal' + +export type CustomFont = { + id: string + displayName: string + family: string + weight: number + file: string + collectionId: string +} + +function CustomFontSelector({ + selectedFont, + setSelectedFont +}: { + selectedFont: string | null + setSelectedFont: (font: string | null) => void +}) { + const { t } = useTranslation('common.personalization') + + const { fontFamily } = usePersonalization() + + const { open } = useModalStore() + + const queryClient = useQueryClient() + + const customFontsQuery = useQuery( + forgeAPI.user.customFonts.list.queryOptions() + ) + + const deleteMutation = useMutation( + forgeAPI.user.customFonts.remove.mutationOptions({ + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ['user', 'customFonts', 'list'] + }) + toast.success('Custom font deleted successfully!') + }, + onError: () => { + toast.error('Failed to delete custom font') + } + }) + ) + + const handleUploadClick = () => { + open(CustomFontUploadModal, { + openType: 'create' + }) + } + + const handleDeleteClick = (font: CustomFont) => { + open(ConfirmationModal, { + title: t('fontFamily.customFonts.delete.title'), + description: t('fontFamily.customFonts.delete.description'), + confirmationButton: 'delete', + onConfirm: async () => { + await deleteMutation.mutateAsync({ id: font.id }) + + if (selectedFont === `custom:${font.id}`) { + setSelectedFont(null) + } + } + }) + } + + const isCustomFontSelected = (fontId: string) => { + return selectedFont === `custom:${fontId}` + } + + return ( + + + + {fonts => + fonts.length > 0 ? ( + + + {({ width, height }) => ( + + + {fonts.map(font => ( + + setSelectedFont(`custom:${font.id}`)} + onKeyDown={e => { + if (e.key === 'Enter' || e.key === ' ') { + setSelectedFont( + selectedFont === `custom:${font.id}` + ? null + : `custom:${font.id}` + ) + } + }} + > + + + + + + + {font.displayName} + + + {font.family} • Weight {font.weight} + + + + + + {isCustomFontSelected(font.id) && ( + + )} + + { + open(CustomFontUploadModal, { + openType: 'edit', + initialData: font + }) + }} + /> + handleDeleteClick(font)} + /> + + + + + ))} + + + )} + + + ) : ( + + + + ) + } + + + ) +} + +export default CustomFontSelector diff --git a/client/src/core/personalization/components/FontFamilySelector/components/FontFamilySelectorModal/tabs/google/components/FontListItem.tsx b/client/src/core/personalization/components/FontFamilySelector/components/FontFamilySelectorModal/tabs/google/components/FontListItem.tsx new file mode 100644 index 000000000..2c564d8ff --- /dev/null +++ b/client/src/core/personalization/components/FontFamilySelector/components/FontFamilySelectorModal/tabs/google/components/FontListItem.tsx @@ -0,0 +1,129 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { useEffect } from 'react' +import { toast } from 'react-toastify' + +import { usePromiseLoading } from '@lifeforge/shared' +import { + Box, + Button, + Card, + Flex, + Icon, + Ring, + Text, + Transition, + surface +} from '@lifeforge/ui' + +import { + addFontToStylesheet, + removeFontFromStylesheet +} from '@/core/personalization/components/FontFamilySelector/components/FontFamilySelectorModal/tabs/google/utils/stylesheet' +import forgeAPI from '@/forgeAPI' + +import type { FontFamily } from '..' + +function FontListItem({ + font, + selectedFont, + isPinned, + setSelectedFont +}: { + font: FontFamily + selectedFont: string | null + isPinned: boolean + setSelectedFont: (font: string) => void +}) { + const queryClient = useQueryClient() + + const togglePinMutation = useMutation( + forgeAPI.user.personalization.toggleGoogleFontsPin.mutationOptions({ + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ['user', 'personalization', 'listGoogleFontsPin'] + }) + }, + onError: () => { + toast.error('Failed to toggle font pin') + } + }) + ) + + const [loadingPin, handleTogglePin] = usePromiseLoading(async () => { + await togglePinMutation.mutateAsync({ family: font.family }) + }) + + useEffect(() => { + addFontToStylesheet(font) + + return () => { + removeFontFromStylesheet(font.family) + } + }, [font]) + + return ( + + + setSelectedFont(font.family)} + > + + + {font.family} + + + ({font.variants.length} variants) + + + +``` + +After (using UI primitives with `surface` preset): +```tsx +import { Box, Text, surface } from '@lifeforge/ui' + + + Item + +``` + +Most legacy classes map directly to a `surface` preset (see table above). Prefer using the preset over writing the object inline. + +### Selected/Active State Borders + +Legacy code often combined `component-bg-lighter-with-hover` with Tailwind border utilities to indicate selection: +```tsx +className="component-bg-lighter-with-hover border-2 border-custom-500" +``` + +Replace this with `Card` (which already handles interactive backgrounds and shadows), or use `Box` with `bg` state conditions directly: + +```tsx + + Item + +``` + +The `Card` component with `isInteractive` applies the correct background mapping automatically: +```tsx +bg={ + isInteractive + ? { base: 'bg-50', dark: 'bg-900', hover: 'bg-100', darkHover: 'bg-800' } + : { base: 'bg-50', dark: 'bg-900' } +} +``` + +If you need custom background shades, use `Box` directly: + +```tsx + + Item + +``` + +For selected-state borders, use the `Ring` primitive to wrap the card instead of absolute-position hacks: + +```tsx + + + ... + + +``` + +> [!NOTE] +> `Ring` applies the outline to its child using `outline`, which does not affect layout — no absolute positioning needed. + +### Check Icon Overlay + +When an item is selected, a check icon overlay can be placed with absolute positioning: + +Before: +```tsx + +``` + +After: +```tsx + + + +``` + +### Running a Migration + +1. Search for `component-bg` in your component files. +2. Map each class to the `bg` prop using the table above. +3. Remove `className` entirely — use primitive props (`p`, `py`, `r`, `width`, etc.) for all layout. +4. Replace selected-state borders with `Bordered` overlay pattern. +5. Replace all `text-custom-500`, `text-bg-500` etc. Tailwind color classes with the `color` prop on `Text` or `Icon`. + +--- + +## 9. Migration Guide: `@iconify/react` → `@lifeforge/ui` Icon Primitive + +The standalone `Icon` from `@iconify/react` must never be imported directly. Always use the `Icon` primitive exported by `@lifeforge/ui`. + +### Key Differences + +| Aspect | `@iconify/react` Icon | `@lifeforge/ui` Icon | +|---|---|---| +| Import | `import { Icon } from '@iconify/react'` | `import { Icon } from '@lifeforge/ui'` | +| Sizing | `width` / `height` props (e.g. `width="1.5em"`) | **`size`** prop (e.g. `size="1.5em"`) | +| Color | `className="text-custom-500"` (Tailwind) | `color="custom-500"` (design token) | +| Layout | `className="absolute right-1.5 bottom-2"` (Tailwind) | `Box asChild position="absolute" bottom="sm" right="sm"` | +| Inherits | — | All `Text` props (`ml`, `mr`, `display`, `truncate`, etc.) | + +> `Icon` is built on top of the `Text` primitive, so any prop that `Text` accepts (e.g. `ml`, `mr`, `display`, `truncate`, `wrap`, `className`, `style`) works directly on `Icon` without needing a `Box` wrapper. + +### Migration Pattern + +Before: +```tsx +import { Icon } from '@iconify/react' + + +``` + +After (positioned): +```tsx +import { Box, Icon } from '@lifeforge/ui' + + + + +``` + +After (inline — no positioning needed): +```tsx +import { Icon } from '@lifeforge/ui' + + +``` + +> `Box asChild` is only needed when the Icon needs layout props it doesn't inherit (`position`, `top`, `left`, etc.). For spacing (`ml`, `mr`) and display properties, pass them directly to `Icon` since it inherits all `Text` props. + +### Migration Checklist + +1. Replace `import { Icon } from '@iconify/react'` with `import { Icon } from '@lifeforge/ui'` (or add `Icon` to the existing destructured import from `@lifeforge/ui`). +2. Replace `width="..." height="..."` with a single `size="..."`. +3. Replace `className="text-custom-500"` / `className="text-bg-500"` with `color="custom-500"` / `color="muted"`. +4. For positioned icons (`position`, `top`, `right`, `bottom`), wrap with `Box asChild`. For spacing and display, pass props directly to `Icon` (it inherits all `Text` props). +5. Remove all remaining `className` props from `Icon` — they are not used anywhere in the codebase. + +--- + +## 10. Developer Quick-Reference Checklist Before submitting a pull request, verify that you have adhered to all core design patterns: diff --git a/packages/ui/src/components/data-display/OptionsColumn/index.tsx b/packages/ui/src/components/data-display/OptionsColumn/index.tsx index ac7207ac2..4bc9ab4f4 100644 --- a/packages/ui/src/components/data-display/OptionsColumn/index.tsx +++ b/packages/ui/src/components/data-display/OptionsColumn/index.tsx @@ -1,11 +1,11 @@ -import { Card } from '@/components/layout' +import { Card, type CardProps } from '@/components/layout' import { Flex, Icon, Text } from '@/components/primitives' import { Tooltip } from '@/components/utilities' import type { ResponsiveProp, SpaceToken } from '@/system' type DirectionValue = 'row' | 'column' | 'row-reverse' | 'column-reverse' -interface OptionsColumnProps { +interface OptionsColumnProps extends Omit { /** The title of the configuration column */ title: string | React.ReactNode /** A brief description of the configuration column */ @@ -32,7 +32,8 @@ export function OptionsColumn({ orientation = 'horizontal', tooltip, children, - breakpoint = 'md' + breakpoint = 'md', + ...rest }: OptionsColumnProps) { const getDirection = (): ResponsiveProp => { if (orientation === 'vertical') return 'column' @@ -56,7 +57,13 @@ export function OptionsColumn({ } return ( - + diff --git a/packages/ui/src/components/inputs/FileInput/FilePickerModal/components/Pixabay/components/SearchResults.tsx b/packages/ui/src/components/inputs/FileInput/FilePickerModal/components/Pixabay/components/SearchResults.tsx index afb9d5488..07fde9ea1 100644 --- a/packages/ui/src/components/inputs/FileInput/FilePickerModal/components/Pixabay/components/SearchResults.tsx +++ b/packages/ui/src/components/inputs/FileInput/FilePickerModal/components/Pixabay/components/SearchResults.tsx @@ -58,10 +58,12 @@ export function SearchResults({ renderPhoto={({ photo, imageProps: { src, alt, style } }) => ( { setFile(photo.fullResURL) setPreview(photo.src) diff --git a/packages/ui/src/components/inputs/FileInput/index.tsx b/packages/ui/src/components/inputs/FileInput/index.tsx index 97748b3aa..de254ba3b 100644 --- a/packages/ui/src/components/inputs/FileInput/index.tsx +++ b/packages/ui/src/components/inputs/FileInput/index.tsx @@ -154,18 +154,22 @@ export function FileInput({ + color="muted" + icon={ + FILE_ICONS[ + (file instanceof File + ? file.name.split('.').pop() + : '') as keyof typeof FILE_ICONS + ] || 'tabler:file' + } + size="1.5rem" + /> - {file instanceof File ? file.name : file} + {file instanceof File + ? file.name + : file === 'keep' + ? (preview ?? '') + : (preview ?? file)} null) - console.log(await pb.getFullList.collection('entries').execute()) - if (!entry) { throw new ClientError('API Key not found', 404) } diff --git a/server/src/lib/user/routes/personalization.ts b/server/src/lib/user/routes/personalization.ts index 9db67afbb..28bb8bcbc 100644 --- a/server/src/lib/user/routes/personalization.ts +++ b/server/src/lib/user/routes/personalization.ts @@ -1,9 +1,12 @@ +import NodeCache from 'node-cache' import z from 'zod' import { ClientError } from '@lifeforge/server-utils' import forge from '../forge' +const fontCache = new NodeCache({ stdTTL: 86400, checkperiod: 3600 }) + interface FontFamily { family: string variants: string[] @@ -67,6 +70,13 @@ export const listGoogleFonts = forge api: { getAPIKey } } }) => { + const cached = fontCache.get<{ enabled: boolean; items: FontFamily[] }>( + 'listGoogleFonts' + ) + if (cached) { + return cached + } + const key = await getAPIKey('gcloud', pb) if (!key) { @@ -82,10 +92,14 @@ export const listGoogleFonts = forge const data = await response.json() - return { + const result = { enabled: true, items: data.items as FontFamily[] } + + fontCache.set('listGoogleFonts', result) + + return result } ) @@ -105,6 +119,14 @@ export const getGoogleFont = forge api: { getAPIKey } } }) => { + const cacheKey = `getGoogleFont:${family}` + const cached = fontCache.get<{ enabled: boolean; items?: unknown }>( + cacheKey + ) + if (cached) { + return cached + } + const key = await getAPIKey('gcloud', pb).catch(() => null) if (!key) { @@ -119,10 +141,14 @@ export const getGoogleFont = forge const data = await response.json() - return { + const result = { enabled: true, items: data.items } + + fontCache.set(cacheKey, result) + + return result } ) diff --git a/shared/src/api/core/forgeEndpoint.ts b/shared/src/api/core/forgeEndpoint.ts index 7db623422..904832c44 100644 --- a/shared/src/api/core/forgeEndpoint.ts +++ b/shared/src/api/core/forgeEndpoint.ts @@ -312,8 +312,6 @@ export default class ForgeEndpoint< return this.mutateRaw(data) } - console.log(data) - // Encrypt the request const { payload, session } = await encryptRequest(data)