Compare commits
15 Commits
324d676599
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 94506aa349 | |||
| d364e5e39f | |||
| 38d2defeb1 | |||
| 324d8c0244 | |||
| 263b400860 | |||
| cbf5ee784f | |||
| 0d817537be | |||
| 1fe9c1021a | |||
| 8d4d389982 | |||
| d36028d67b | |||
| 8cf3a1f03a | |||
| 6af4d7b798 | |||
| ae1798f6f4 | |||
| 0768e8fa29 | |||
| 0d0aad1d47 |
+2
-2
@@ -16,8 +16,8 @@ dist-ssr
|
|||||||
# env files (can opt-in for committing if needed)
|
# env files (can opt-in for committing if needed)
|
||||||
.env*
|
.env*
|
||||||
|
|
||||||
# turbo
|
# generated i18n build schema
|
||||||
.turbo
|
.i18n
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"ignorePatterns": [
|
||||||
|
".i18n/**",
|
||||||
|
"**/.output/**",
|
||||||
|
"**/coverage/**",
|
||||||
|
"**/dist/**",
|
||||||
|
"apps/web/src/locales/**/messages.ts",
|
||||||
|
"apps/web/src/route-tree.gen.ts"
|
||||||
|
],
|
||||||
|
"printWidth": 80,
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": false,
|
||||||
|
"sortPackageJson": false,
|
||||||
|
"sortTailwindcss": {
|
||||||
|
"functions": ["cn", "cva"],
|
||||||
|
"stylesheet": "./packages/ui/src/styles/globals.css"
|
||||||
|
},
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5"
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||||
|
"categories": {
|
||||||
|
"correctness": "error",
|
||||||
|
"perf": "warn",
|
||||||
|
"suspicious": "warn"
|
||||||
|
},
|
||||||
|
"ignorePatterns": [
|
||||||
|
".i18n/**",
|
||||||
|
"**/.output/**",
|
||||||
|
"**/coverage/**",
|
||||||
|
"**/dist/**",
|
||||||
|
"apps/web/src/locales/**/messages.ts",
|
||||||
|
"apps/web/src/route-tree.gen.ts"
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"reportUnusedDisableDirectives": "warn"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"react/react-in-jsx-scope": "off",
|
||||||
|
"vitest/expect-expect": "off",
|
||||||
|
"vitest/require-mock-type-parameters": "off"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"eslint",
|
||||||
|
"typescript",
|
||||||
|
"unicorn",
|
||||||
|
"oxc",
|
||||||
|
"react",
|
||||||
|
"import",
|
||||||
|
"vitest"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
dist/
|
|
||||||
node_modules/
|
|
||||||
.turbo/
|
|
||||||
coverage/
|
|
||||||
pnpm-lock.yaml
|
|
||||||
.pnpm-store/
|
|
||||||
-11
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"endOfLine": "lf",
|
|
||||||
"semi": false,
|
|
||||||
"singleQuote": false,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"trailingComma": "es5",
|
|
||||||
"printWidth": 80,
|
|
||||||
"plugins": ["prettier-plugin-tailwindcss"],
|
|
||||||
"tailwindStylesheet": "packages/ui/src/styles/globals.css",
|
|
||||||
"tailwindFunctions": ["cn", "cva"]
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,17 @@
|
|||||||
"sourceLocale": "en",
|
"sourceLocale": "en",
|
||||||
"locales": ["en", "zh-Hans"],
|
"locales": ["en", "zh-Hans"],
|
||||||
"catalogPath": "src/locales/{locale}/messages",
|
"catalogPath": "src/locales/{locale}/messages",
|
||||||
|
"catalogSources": [
|
||||||
|
"@workspace/ui/locales/{locale}",
|
||||||
|
"@workspace/blocks/appearance/locales/{locale}",
|
||||||
|
"@workspace/blocks/chats/locales/{locale}",
|
||||||
|
"@workspace/blocks/layout/locales/{locale}",
|
||||||
|
"@workspace/blocks/media/locales/{locale}",
|
||||||
|
"@workspace/blocks/navigation/locales/{locale}",
|
||||||
|
"@workspace/blocks/notifications/locales/{locale}",
|
||||||
|
"@workspace/blocks/search/locales/{locale}",
|
||||||
|
"@workspace/lexical/locales/{locale}"
|
||||||
|
],
|
||||||
"include": ["src", "../../packages/blocks/src", "../../packages/ui/src"],
|
"include": ["src", "../../packages/blocks/src", "../../packages/ui/src"],
|
||||||
"exclude": ["**/*.test.{ts,tsx}", "**/node_modules/**"]
|
"exclude": ["**/*.test.{ts,tsx}", "**/node_modules/**"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
import project from "./i18n.config.json" with { type: "json" }
|
|
||||||
import { createLinguiConfig } from "@workspace/i18n/lingui-config"
|
|
||||||
|
|
||||||
export default createLinguiConfig(project)
|
|
||||||
@@ -6,11 +6,11 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"build": "vite build && tsc -b",
|
"build": "vite build && tsc -b",
|
||||||
"format": "prettier --write \"**/*.{ts,tsx}\"",
|
|
||||||
"i18n": "workspace-i18n",
|
"i18n": "workspace-i18n",
|
||||||
"i18n:compile": "workspace-i18n compile",
|
"i18n:compile": "workspace-i18n compile",
|
||||||
"i18n:extract": "workspace-i18n extract",
|
"i18n:extract": "workspace-i18n extract",
|
||||||
"i18n:ui": "workspace-i18n ui",
|
"i18n:ui": "workspace-i18n ui",
|
||||||
|
"test": "vitest run",
|
||||||
"typecheck": "tsc -b",
|
"typecheck": "tsc -b",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"start": "node .output/server/index.mjs"
|
"start": "node .output/server/index.mjs"
|
||||||
@@ -19,8 +19,6 @@
|
|||||||
"@base-ui/react": "^1.6.0",
|
"@base-ui/react": "^1.6.0",
|
||||||
"@base-ui/utils": "^0.3.1",
|
"@base-ui/utils": "^0.3.1",
|
||||||
"@floating-ui/utils": "^0.2.12",
|
"@floating-ui/utils": "^0.2.12",
|
||||||
"@hugeicons/core-free-icons": "^4.2.3",
|
|
||||||
"@hugeicons/react": "^1.1.9",
|
|
||||||
"@iconify/react": "^6.0.2",
|
"@iconify/react": "^6.0.2",
|
||||||
"@lingui/core": "^6",
|
"@lingui/core": "^6",
|
||||||
"@tanstack/react-form": "^1.33.2",
|
"@tanstack/react-form": "^1.33.2",
|
||||||
@@ -29,10 +27,14 @@
|
|||||||
"@tanstack/react-router": "^1.170.18",
|
"@tanstack/react-router": "^1.170.18",
|
||||||
"@tanstack/react-start": "^1.168.32",
|
"@tanstack/react-start": "^1.168.32",
|
||||||
"@workspace/blocks": "workspace:*",
|
"@workspace/blocks": "workspace:*",
|
||||||
|
"@workspace/icons": "workspace:*",
|
||||||
"@workspace/i18n": "workspace:*",
|
"@workspace/i18n": "workspace:*",
|
||||||
|
"@workspace/lexical": "workspace:*",
|
||||||
"@workspace/ui": "workspace:*",
|
"@workspace/ui": "workspace:*",
|
||||||
|
"country-flag-icons": "^1.6.20",
|
||||||
"lucide-react": "^1.27.0",
|
"lucide-react": "^1.27.0",
|
||||||
"react": "^19.2.6",
|
"react": "^19.2.6",
|
||||||
|
"react-day-picker": "^10.0.1",
|
||||||
"react-dom": "^19.2.6",
|
"react-dom": "^19.2.6",
|
||||||
"sonner": "^2.0.7"
|
"sonner": "^2.0.7"
|
||||||
},
|
},
|
||||||
@@ -43,6 +45,7 @@
|
|||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"@vitejs/plugin-react": "^6",
|
"@vitejs/plugin-react": "^6",
|
||||||
|
"jsdom": "^30.0.1",
|
||||||
"nitro": "^3.0.260610-beta",
|
"nitro": "^3.0.260610-beta",
|
||||||
"typescript": "~6",
|
"typescript": "~6",
|
||||||
"vite": "^8",
|
"vite": "^8",
|
||||||
|
|||||||
@@ -0,0 +1,336 @@
|
|||||||
|
import type { MessageDescriptor } from "@workspace/i18n"
|
||||||
|
|
||||||
|
export const navigationMessages = {
|
||||||
|
overview: /* i18n */ {
|
||||||
|
id: "app.navigation.overview",
|
||||||
|
message: "Overview",
|
||||||
|
},
|
||||||
|
"overview-dashboard": /* i18n */ {
|
||||||
|
id: "app.navigation.overview-dashboard",
|
||||||
|
message: "Business overview",
|
||||||
|
},
|
||||||
|
"overview-realtime": /* i18n */ {
|
||||||
|
id: "app.navigation.overview-realtime",
|
||||||
|
message: "Real-time monitoring",
|
||||||
|
},
|
||||||
|
"overview-tasks": /* i18n */ {
|
||||||
|
id: "app.navigation.overview-tasks",
|
||||||
|
message: "Tasks",
|
||||||
|
},
|
||||||
|
"catalog-and-inventory": /* i18n */ {
|
||||||
|
id: "app.navigation.catalog-and-inventory",
|
||||||
|
message: "Products and inventory",
|
||||||
|
},
|
||||||
|
"catalog-products": /* i18n */ {
|
||||||
|
id: "app.navigation.catalog-products",
|
||||||
|
message: "Products",
|
||||||
|
},
|
||||||
|
"catalog-products-list": /* i18n */ {
|
||||||
|
id: "app.navigation.catalog-products-list",
|
||||||
|
message: "Product list",
|
||||||
|
},
|
||||||
|
"catalog-products-drafts": /* i18n */ {
|
||||||
|
id: "app.navigation.catalog-products-drafts",
|
||||||
|
message: "Product drafts",
|
||||||
|
},
|
||||||
|
"catalog-products-bundles": /* i18n */ {
|
||||||
|
id: "app.navigation.catalog-products-bundles",
|
||||||
|
message: "Product bundles",
|
||||||
|
},
|
||||||
|
"catalog-products-attributes": /* i18n */ {
|
||||||
|
id: "app.navigation.catalog-products-attributes",
|
||||||
|
message: "Product attributes",
|
||||||
|
},
|
||||||
|
"catalog-product-specifications": /* i18n */ {
|
||||||
|
id: "app.navigation.catalog-product-specifications",
|
||||||
|
message: "Specification templates",
|
||||||
|
},
|
||||||
|
"catalog-product-brands": /* i18n */ {
|
||||||
|
id: "app.navigation.catalog-product-brands",
|
||||||
|
message: "Brands",
|
||||||
|
},
|
||||||
|
"catalog-categories": /* i18n */ {
|
||||||
|
id: "app.navigation.catalog-categories",
|
||||||
|
message: "Categories",
|
||||||
|
},
|
||||||
|
"catalog-category-tree": /* i18n */ {
|
||||||
|
id: "app.navigation.catalog-category-tree",
|
||||||
|
message: "Category tree",
|
||||||
|
},
|
||||||
|
"catalog-category-navigation": /* i18n */ {
|
||||||
|
id: "app.navigation.catalog-category-navigation",
|
||||||
|
message: "Navigation categories",
|
||||||
|
},
|
||||||
|
"inventory-center": /* i18n */ {
|
||||||
|
id: "app.navigation.inventory-center",
|
||||||
|
message: "Inventory",
|
||||||
|
},
|
||||||
|
"inventory-overview": /* i18n */ {
|
||||||
|
id: "app.navigation.inventory-overview",
|
||||||
|
message: "Inventory overview",
|
||||||
|
},
|
||||||
|
"inventory-inbound": /* i18n */ {
|
||||||
|
id: "app.navigation.inventory-inbound",
|
||||||
|
message: "Inbound orders",
|
||||||
|
},
|
||||||
|
"inventory-outbound": /* i18n */ {
|
||||||
|
id: "app.navigation.inventory-outbound",
|
||||||
|
message: "Outbound orders",
|
||||||
|
},
|
||||||
|
"inventory-transfers": /* i18n */ {
|
||||||
|
id: "app.navigation.inventory-transfers",
|
||||||
|
message: "Stock transfers",
|
||||||
|
},
|
||||||
|
"inventory-alerts": /* i18n */ {
|
||||||
|
id: "app.navigation.inventory-alerts",
|
||||||
|
message: "Inventory alerts",
|
||||||
|
},
|
||||||
|
"content-assets": /* i18n */ {
|
||||||
|
id: "app.navigation.content-assets",
|
||||||
|
message: "Content",
|
||||||
|
},
|
||||||
|
"content-articles": /* i18n */ {
|
||||||
|
id: "app.navigation.content-articles",
|
||||||
|
message: "Articles",
|
||||||
|
},
|
||||||
|
"content-media": /* i18n */ {
|
||||||
|
id: "app.navigation.content-media",
|
||||||
|
message: "Media",
|
||||||
|
},
|
||||||
|
"content-pages": /* i18n */ {
|
||||||
|
id: "app.navigation.content-pages",
|
||||||
|
message: "Custom pages",
|
||||||
|
},
|
||||||
|
"trade-and-fulfillment": /* i18n */ {
|
||||||
|
id: "app.navigation.trade-and-fulfillment",
|
||||||
|
message: "Orders and fulfillment",
|
||||||
|
},
|
||||||
|
"trade-orders": /* i18n */ {
|
||||||
|
id: "app.navigation.trade-orders",
|
||||||
|
message: "Orders",
|
||||||
|
},
|
||||||
|
"trade-orders-awaiting-payment": /* i18n */ {
|
||||||
|
id: "app.navigation.trade-orders-awaiting-payment",
|
||||||
|
message: "Awaiting payment",
|
||||||
|
},
|
||||||
|
"trade-orders-awaiting-shipment": /* i18n */ {
|
||||||
|
id: "app.navigation.trade-orders-awaiting-shipment",
|
||||||
|
message: "Awaiting shipment",
|
||||||
|
},
|
||||||
|
"trade-orders-shipped": /* i18n */ {
|
||||||
|
id: "app.navigation.trade-orders-shipped",
|
||||||
|
message: "In transit",
|
||||||
|
},
|
||||||
|
"trade-orders-completed": /* i18n */ {
|
||||||
|
id: "app.navigation.trade-orders-completed",
|
||||||
|
message: "Completed orders",
|
||||||
|
},
|
||||||
|
"trade-orders-canceled": /* i18n */ {
|
||||||
|
id: "app.navigation.trade-orders-canceled",
|
||||||
|
message: "Canceled orders",
|
||||||
|
},
|
||||||
|
"fulfillment-center": /* i18n */ {
|
||||||
|
id: "app.navigation.fulfillment-center",
|
||||||
|
message: "Fulfillment",
|
||||||
|
},
|
||||||
|
"fulfillment-deliveries": /* i18n */ {
|
||||||
|
id: "app.navigation.fulfillment-deliveries",
|
||||||
|
message: "Delivery tasks",
|
||||||
|
},
|
||||||
|
"fulfillment-pickups": /* i18n */ {
|
||||||
|
id: "app.navigation.fulfillment-pickups",
|
||||||
|
message: "Pickup verification",
|
||||||
|
},
|
||||||
|
"fulfillment-freight-rules": /* i18n */ {
|
||||||
|
id: "app.navigation.fulfillment-freight-rules",
|
||||||
|
message: "Shipping rules",
|
||||||
|
},
|
||||||
|
"after-sales-center": /* i18n */ {
|
||||||
|
id: "app.navigation.after-sales-center",
|
||||||
|
message: "After-sales",
|
||||||
|
},
|
||||||
|
"after-sales-returns": /* i18n */ {
|
||||||
|
id: "app.navigation.after-sales-returns",
|
||||||
|
message: "Return requests",
|
||||||
|
},
|
||||||
|
"after-sales-refunds": /* i18n */ {
|
||||||
|
id: "app.navigation.after-sales-refunds",
|
||||||
|
message: "Refund review",
|
||||||
|
},
|
||||||
|
"after-sales-disputes": /* i18n */ {
|
||||||
|
id: "app.navigation.after-sales-disputes",
|
||||||
|
message: "Transaction disputes",
|
||||||
|
},
|
||||||
|
"customers-and-marketing": /* i18n */ {
|
||||||
|
id: "app.navigation.customers-and-marketing",
|
||||||
|
message: "Customers and marketing",
|
||||||
|
},
|
||||||
|
"customer-center": /* i18n */ {
|
||||||
|
id: "app.navigation.customer-center",
|
||||||
|
message: "Customers",
|
||||||
|
},
|
||||||
|
"customer-profiles": /* i18n */ {
|
||||||
|
id: "app.navigation.customer-profiles",
|
||||||
|
message: "Customer profiles",
|
||||||
|
},
|
||||||
|
"customer-segments": /* i18n */ {
|
||||||
|
id: "app.navigation.customer-segments",
|
||||||
|
message: "Customer segments",
|
||||||
|
},
|
||||||
|
"customer-tiers": /* i18n */ {
|
||||||
|
id: "app.navigation.customer-tiers",
|
||||||
|
message: "Customer tiers",
|
||||||
|
},
|
||||||
|
"customer-risk-list": /* i18n */ {
|
||||||
|
id: "app.navigation.customer-risk-list",
|
||||||
|
message: "Risk list",
|
||||||
|
},
|
||||||
|
"marketing-campaigns": /* i18n */ {
|
||||||
|
id: "app.navigation.marketing-campaigns",
|
||||||
|
message: "Marketing campaigns",
|
||||||
|
},
|
||||||
|
"marketing-coupons": /* i18n */ {
|
||||||
|
id: "app.navigation.marketing-coupons",
|
||||||
|
message: "Coupons",
|
||||||
|
},
|
||||||
|
"marketing-flash-sales": /* i18n */ {
|
||||||
|
id: "app.navigation.marketing-flash-sales",
|
||||||
|
message: "Flash sales",
|
||||||
|
},
|
||||||
|
"marketing-gift-promotions": /* i18n */ {
|
||||||
|
id: "app.navigation.marketing-gift-promotions",
|
||||||
|
message: "Gift promotions",
|
||||||
|
},
|
||||||
|
"marketing-referrals": /* i18n */ {
|
||||||
|
id: "app.navigation.marketing-referrals",
|
||||||
|
message: "Referral rewards",
|
||||||
|
},
|
||||||
|
"loyalty-center": /* i18n */ {
|
||||||
|
id: "app.navigation.loyalty-center",
|
||||||
|
message: "Membership benefits",
|
||||||
|
},
|
||||||
|
"loyalty-points": /* i18n */ {
|
||||||
|
id: "app.navigation.loyalty-points",
|
||||||
|
message: "Points rules",
|
||||||
|
},
|
||||||
|
"loyalty-stored-value": /* i18n */ {
|
||||||
|
id: "app.navigation.loyalty-stored-value",
|
||||||
|
message: "Stored-value accounts",
|
||||||
|
},
|
||||||
|
"loyalty-benefits": /* i18n */ {
|
||||||
|
id: "app.navigation.loyalty-benefits",
|
||||||
|
message: "Benefits configuration",
|
||||||
|
},
|
||||||
|
"data-and-finance": /* i18n */ {
|
||||||
|
id: "app.navigation.data-and-finance",
|
||||||
|
message: "Data and finance",
|
||||||
|
},
|
||||||
|
"report-center": /* i18n */ {
|
||||||
|
id: "app.navigation.report-center",
|
||||||
|
message: "Reports",
|
||||||
|
},
|
||||||
|
"report-sales": /* i18n */ {
|
||||||
|
id: "app.navigation.report-sales",
|
||||||
|
message: "Sales analysis",
|
||||||
|
},
|
||||||
|
"report-products": /* i18n */ {
|
||||||
|
id: "app.navigation.report-products",
|
||||||
|
message: "Product analysis",
|
||||||
|
},
|
||||||
|
"report-customers": /* i18n */ {
|
||||||
|
id: "app.navigation.report-customers",
|
||||||
|
message: "Customer analysis",
|
||||||
|
},
|
||||||
|
"report-channels": /* i18n */ {
|
||||||
|
id: "app.navigation.report-channels",
|
||||||
|
message: "Channel analysis",
|
||||||
|
},
|
||||||
|
"finance-center": /* i18n */ {
|
||||||
|
id: "app.navigation.finance-center",
|
||||||
|
message: "Finance",
|
||||||
|
},
|
||||||
|
"finance-settlements": /* i18n */ {
|
||||||
|
id: "app.navigation.finance-settlements",
|
||||||
|
message: "Settlement statements",
|
||||||
|
},
|
||||||
|
"finance-reconciliation": /* i18n */ {
|
||||||
|
id: "app.navigation.finance-reconciliation",
|
||||||
|
message: "Reconciliation",
|
||||||
|
},
|
||||||
|
"finance-invoices": /* i18n */ {
|
||||||
|
id: "app.navigation.finance-invoices",
|
||||||
|
message: "Invoices",
|
||||||
|
},
|
||||||
|
"finance-payouts": /* i18n */ {
|
||||||
|
id: "app.navigation.finance-payouts",
|
||||||
|
message: "Payout history",
|
||||||
|
},
|
||||||
|
"system-management": /* i18n */ {
|
||||||
|
id: "app.navigation.system-management",
|
||||||
|
message: "System management",
|
||||||
|
},
|
||||||
|
"system-members": /* i18n */ {
|
||||||
|
id: "app.navigation.system-members",
|
||||||
|
message: "Organization members",
|
||||||
|
},
|
||||||
|
"system-members-admins": /* i18n */ {
|
||||||
|
id: "app.navigation.system-members-admins",
|
||||||
|
message: "Administrator accounts",
|
||||||
|
},
|
||||||
|
"system-members-roles": /* i18n */ {
|
||||||
|
id: "app.navigation.system-members-roles",
|
||||||
|
message: "Roles",
|
||||||
|
},
|
||||||
|
"system-members-permissions": /* i18n */ {
|
||||||
|
id: "app.navigation.system-members-permissions",
|
||||||
|
message: "Permission policies",
|
||||||
|
},
|
||||||
|
"system-notifications": /* i18n */ {
|
||||||
|
id: "app.navigation.system-notifications",
|
||||||
|
message: "Notifications",
|
||||||
|
},
|
||||||
|
"system-notification-templates": /* i18n */ {
|
||||||
|
id: "app.navigation.system-notification-templates",
|
||||||
|
message: "Notification templates",
|
||||||
|
},
|
||||||
|
"system-notification-logs": /* i18n */ {
|
||||||
|
id: "app.navigation.system-notification-logs",
|
||||||
|
message: "Delivery logs",
|
||||||
|
},
|
||||||
|
"system-audit": /* i18n */ {
|
||||||
|
id: "app.navigation.system-audit",
|
||||||
|
message: "Audit log",
|
||||||
|
},
|
||||||
|
"system-integrations": /* i18n */ {
|
||||||
|
id: "app.navigation.system-integrations",
|
||||||
|
message: "Integrations",
|
||||||
|
},
|
||||||
|
"system-integration-api-keys": /* i18n */ {
|
||||||
|
id: "app.navigation.system-integration-api-keys",
|
||||||
|
message: "API keys",
|
||||||
|
},
|
||||||
|
"system-integration-webhooks": /* i18n */ {
|
||||||
|
id: "app.navigation.system-integration-webhooks",
|
||||||
|
message: "Webhook configuration",
|
||||||
|
},
|
||||||
|
"system-settings": /* i18n */ {
|
||||||
|
id: "app.navigation.system-settings",
|
||||||
|
message: "System settings",
|
||||||
|
},
|
||||||
|
"system-settings-store": /* i18n */ {
|
||||||
|
id: "app.navigation.system-settings-store",
|
||||||
|
message: "Store information",
|
||||||
|
},
|
||||||
|
"system-settings-payments": /* i18n */ {
|
||||||
|
id: "app.navigation.system-settings-payments",
|
||||||
|
message: "Payment channels",
|
||||||
|
},
|
||||||
|
"system-settings-shipping": /* i18n */ {
|
||||||
|
id: "app.navigation.system-settings-shipping",
|
||||||
|
message: "Shipping settings",
|
||||||
|
},
|
||||||
|
"system-recycle-bin": /* i18n */ {
|
||||||
|
id: "app.navigation.system-recycle-bin",
|
||||||
|
message: "Recycle bin",
|
||||||
|
},
|
||||||
|
} as const satisfies Record<string, MessageDescriptor>
|
||||||
@@ -56,77 +56,79 @@ import {
|
|||||||
UserShield01Icon,
|
UserShield01Icon,
|
||||||
Wallet01Icon,
|
Wallet01Icon,
|
||||||
WalletCardsIcon,
|
WalletCardsIcon,
|
||||||
} from "@hugeicons/core-free-icons"
|
} from "@workspace/icons"
|
||||||
|
|
||||||
import type { NavigationGroup } from "@workspace/blocks/navigation"
|
import type { NavigationGroup } from "@workspace/blocks/navigation"
|
||||||
|
|
||||||
|
import { navigationMessages } from "./navigation-messages"
|
||||||
|
|
||||||
export const primaryNavigationGroups: readonly NavigationGroup[] = [
|
export const primaryNavigationGroups: readonly NavigationGroup[] = [
|
||||||
{
|
{
|
||||||
id: "overview",
|
id: "overview",
|
||||||
label: "概览",
|
label: navigationMessages["overview"],
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "overview-dashboard",
|
id: "overview-dashboard",
|
||||||
to: "/",
|
to: "/",
|
||||||
label: "经营总览",
|
label: navigationMessages["overview-dashboard"],
|
||||||
icon: Analytics01Icon,
|
icon: Analytics01Icon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "overview-realtime",
|
id: "overview-realtime",
|
||||||
to: "/analytics/realtime",
|
to: "/analytics/realtime",
|
||||||
label: "实时监控",
|
label: navigationMessages["overview-realtime"],
|
||||||
icon: AnalyticsUpIcon,
|
icon: AnalyticsUpIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "overview-tasks",
|
id: "overview-tasks",
|
||||||
to: "/workspace/tasks",
|
to: "/workspace/tasks",
|
||||||
label: "待办中心",
|
label: navigationMessages["overview-tasks"],
|
||||||
icon: Task01Icon,
|
icon: Task01Icon,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "catalog-and-inventory",
|
id: "catalog-and-inventory",
|
||||||
label: "商品与库存",
|
label: navigationMessages["catalog-and-inventory"],
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "catalog-products",
|
id: "catalog-products",
|
||||||
label: "商品中心",
|
label: navigationMessages["catalog-products"],
|
||||||
icon: ShoppingBag01Icon,
|
icon: ShoppingBag01Icon,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "catalog-products-list",
|
id: "catalog-products-list",
|
||||||
to: "/catalog/products/list",
|
to: "/catalog/products/list",
|
||||||
label: "商品列表",
|
label: navigationMessages["catalog-products-list"],
|
||||||
icon: ShoppingBag01Icon,
|
icon: ShoppingBag01Icon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "catalog-products-drafts",
|
id: "catalog-products-drafts",
|
||||||
to: "/catalog/products/drafts",
|
to: "/catalog/products/drafts",
|
||||||
label: "商品草稿",
|
label: navigationMessages["catalog-products-drafts"],
|
||||||
icon: File01Icon,
|
icon: File01Icon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "catalog-products-bundles",
|
id: "catalog-products-bundles",
|
||||||
to: "/catalog/products/bundles",
|
to: "/catalog/products/bundles",
|
||||||
label: "组合商品",
|
label: navigationMessages["catalog-products-bundles"],
|
||||||
icon: Package02Icon,
|
icon: Package02Icon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "catalog-products-attributes",
|
id: "catalog-products-attributes",
|
||||||
label: "商品属性",
|
label: navigationMessages["catalog-products-attributes"],
|
||||||
icon: Settings03Icon,
|
icon: Settings03Icon,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "catalog-product-specifications",
|
id: "catalog-product-specifications",
|
||||||
to: "/catalog/products/attributes/specifications",
|
to: "/catalog/products/attributes/specifications",
|
||||||
label: "规格模板",
|
label: navigationMessages["catalog-product-specifications"],
|
||||||
icon: ClipboardListIcon,
|
icon: ClipboardListIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "catalog-product-brands",
|
id: "catalog-product-brands",
|
||||||
to: "/catalog/products/attributes/brands",
|
to: "/catalog/products/attributes/brands",
|
||||||
label: "品牌资料",
|
label: navigationMessages["catalog-product-brands"],
|
||||||
icon: StarIcon,
|
icon: StarIcon,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -136,19 +138,19 @@ export const primaryNavigationGroups: readonly NavigationGroup[] = [
|
|||||||
{
|
{
|
||||||
id: "catalog-categories",
|
id: "catalog-categories",
|
||||||
to: "/catalog/categories",
|
to: "/catalog/categories",
|
||||||
label: "分类目录",
|
label: navigationMessages["catalog-categories"],
|
||||||
icon: ClipboardListIcon,
|
icon: ClipboardListIcon,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "catalog-category-tree",
|
id: "catalog-category-tree",
|
||||||
to: "/catalog/categories/tree",
|
to: "/catalog/categories/tree",
|
||||||
label: "分类树",
|
label: navigationMessages["catalog-category-tree"],
|
||||||
icon: Structure01Icon,
|
icon: Structure01Icon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "catalog-category-navigation",
|
id: "catalog-category-navigation",
|
||||||
to: "/catalog/categories/navigation",
|
to: "/catalog/categories/navigation",
|
||||||
label: "导航分类",
|
label: navigationMessages["catalog-category-navigation"],
|
||||||
icon: Compass01Icon,
|
icon: Compass01Icon,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -156,37 +158,37 @@ export const primaryNavigationGroups: readonly NavigationGroup[] = [
|
|||||||
{
|
{
|
||||||
id: "inventory-center",
|
id: "inventory-center",
|
||||||
to: "/inventory",
|
to: "/inventory",
|
||||||
label: "库存中心",
|
label: navigationMessages["inventory-center"],
|
||||||
icon: PackageIcon,
|
icon: PackageIcon,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "inventory-overview",
|
id: "inventory-overview",
|
||||||
to: "/inventory/overview",
|
to: "/inventory/overview",
|
||||||
label: "库存总览",
|
label: navigationMessages["inventory-overview"],
|
||||||
icon: Chart01Icon,
|
icon: Chart01Icon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "inventory-inbound",
|
id: "inventory-inbound",
|
||||||
to: "/inventory/inbound",
|
to: "/inventory/inbound",
|
||||||
label: "入库单",
|
label: navigationMessages["inventory-inbound"],
|
||||||
icon: InboxDownloadIcon,
|
icon: InboxDownloadIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "inventory-outbound",
|
id: "inventory-outbound",
|
||||||
to: "/inventory/outbound",
|
to: "/inventory/outbound",
|
||||||
label: "出库单",
|
label: navigationMessages["inventory-outbound"],
|
||||||
icon: InboxUploadIcon,
|
icon: InboxUploadIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "inventory-transfers",
|
id: "inventory-transfers",
|
||||||
to: "/inventory/transfers",
|
to: "/inventory/transfers",
|
||||||
label: "调拨单",
|
label: navigationMessages["inventory-transfers"],
|
||||||
icon: ArrowDataTransferHorizontalIcon,
|
icon: ArrowDataTransferHorizontalIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "inventory-alerts",
|
id: "inventory-alerts",
|
||||||
to: "/inventory/alerts",
|
to: "/inventory/alerts",
|
||||||
label: "库存预警",
|
label: navigationMessages["inventory-alerts"],
|
||||||
icon: Alert02Icon,
|
icon: Alert02Icon,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -194,25 +196,25 @@ export const primaryNavigationGroups: readonly NavigationGroup[] = [
|
|||||||
{
|
{
|
||||||
id: "content-assets",
|
id: "content-assets",
|
||||||
to: "/content",
|
to: "/content",
|
||||||
label: "内容素材",
|
label: navigationMessages["content-assets"],
|
||||||
icon: File01Icon,
|
icon: File01Icon,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "content-articles",
|
id: "content-articles",
|
||||||
to: "/content/articles",
|
to: "/content/articles",
|
||||||
label: "内容文章",
|
label: navigationMessages["content-articles"],
|
||||||
icon: File01Icon,
|
icon: File01Icon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "content-media",
|
id: "content-media",
|
||||||
to: "/content/media",
|
to: "/content/media",
|
||||||
label: "媒体资源",
|
label: navigationMessages["content-media"],
|
||||||
icon: GalleryHorizontalIcon,
|
icon: GalleryHorizontalIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "content-pages",
|
id: "content-pages",
|
||||||
to: "/content/pages",
|
to: "/content/pages",
|
||||||
label: "自定义页面",
|
label: navigationMessages["content-pages"],
|
||||||
icon: AppWindowIcon,
|
icon: AppWindowIcon,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -221,42 +223,42 @@ export const primaryNavigationGroups: readonly NavigationGroup[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "trade-and-fulfillment",
|
id: "trade-and-fulfillment",
|
||||||
label: "交易与履约",
|
label: navigationMessages["trade-and-fulfillment"],
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "trade-orders",
|
id: "trade-orders",
|
||||||
to: "/trade/orders",
|
to: "/trade/orders",
|
||||||
label: "订单中心",
|
label: navigationMessages["trade-orders"],
|
||||||
icon: ShoppingCart01Icon,
|
icon: ShoppingCart01Icon,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "trade-orders-awaiting-payment",
|
id: "trade-orders-awaiting-payment",
|
||||||
to: "/trade/orders/awaiting-payment",
|
to: "/trade/orders/awaiting-payment",
|
||||||
label: "待付款订单",
|
label: navigationMessages["trade-orders-awaiting-payment"],
|
||||||
icon: WalletCardsIcon,
|
icon: WalletCardsIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "trade-orders-awaiting-shipment",
|
id: "trade-orders-awaiting-shipment",
|
||||||
to: "/trade/orders/awaiting-shipment",
|
to: "/trade/orders/awaiting-shipment",
|
||||||
label: "待发货订单",
|
label: navigationMessages["trade-orders-awaiting-shipment"],
|
||||||
icon: PackageIcon,
|
icon: PackageIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "trade-orders-shipped",
|
id: "trade-orders-shipped",
|
||||||
to: "/trade/orders/shipped",
|
to: "/trade/orders/shipped",
|
||||||
label: "配送中订单",
|
label: navigationMessages["trade-orders-shipped"],
|
||||||
icon: DeliveryTruck01Icon,
|
icon: DeliveryTruck01Icon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "trade-orders-completed",
|
id: "trade-orders-completed",
|
||||||
to: "/trade/orders/completed",
|
to: "/trade/orders/completed",
|
||||||
label: "已完成订单",
|
label: navigationMessages["trade-orders-completed"],
|
||||||
icon: CheckmarkCircle01Icon,
|
icon: CheckmarkCircle01Icon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "trade-orders-canceled",
|
id: "trade-orders-canceled",
|
||||||
to: "/trade/orders/canceled",
|
to: "/trade/orders/canceled",
|
||||||
label: "已取消订单",
|
label: navigationMessages["trade-orders-canceled"],
|
||||||
icon: CancelCircleIcon,
|
icon: CancelCircleIcon,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -264,25 +266,25 @@ export const primaryNavigationGroups: readonly NavigationGroup[] = [
|
|||||||
{
|
{
|
||||||
id: "fulfillment-center",
|
id: "fulfillment-center",
|
||||||
to: "/fulfillment",
|
to: "/fulfillment",
|
||||||
label: "配送履约",
|
label: navigationMessages["fulfillment-center"],
|
||||||
icon: DeliveryTruck01Icon,
|
icon: DeliveryTruck01Icon,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "fulfillment-deliveries",
|
id: "fulfillment-deliveries",
|
||||||
to: "/fulfillment/deliveries",
|
to: "/fulfillment/deliveries",
|
||||||
label: "配送任务",
|
label: navigationMessages["fulfillment-deliveries"],
|
||||||
icon: Location01Icon,
|
icon: Location01Icon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "fulfillment-pickups",
|
id: "fulfillment-pickups",
|
||||||
to: "/fulfillment/pickups",
|
to: "/fulfillment/pickups",
|
||||||
label: "自提核销",
|
label: navigationMessages["fulfillment-pickups"],
|
||||||
icon: Store01Icon,
|
icon: Store01Icon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "fulfillment-freight-rules",
|
id: "fulfillment-freight-rules",
|
||||||
to: "/fulfillment/freight-rules",
|
to: "/fulfillment/freight-rules",
|
||||||
label: "运费规则",
|
label: navigationMessages["fulfillment-freight-rules"],
|
||||||
icon: RoadLocation01Icon,
|
icon: RoadLocation01Icon,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -290,25 +292,25 @@ export const primaryNavigationGroups: readonly NavigationGroup[] = [
|
|||||||
{
|
{
|
||||||
id: "after-sales-center",
|
id: "after-sales-center",
|
||||||
to: "/after-sales",
|
to: "/after-sales",
|
||||||
label: "售后中心",
|
label: navigationMessages["after-sales-center"],
|
||||||
icon: CallMissed01Icon,
|
icon: CallMissed01Icon,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "after-sales-returns",
|
id: "after-sales-returns",
|
||||||
to: "/after-sales/returns",
|
to: "/after-sales/returns",
|
||||||
label: "退货申请",
|
label: navigationMessages["after-sales-returns"],
|
||||||
icon: Refresh01Icon,
|
icon: Refresh01Icon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "after-sales-refunds",
|
id: "after-sales-refunds",
|
||||||
to: "/after-sales/refunds",
|
to: "/after-sales/refunds",
|
||||||
label: "退款审核",
|
label: navigationMessages["after-sales-refunds"],
|
||||||
icon: MoneySendSquareIcon,
|
icon: MoneySendSquareIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "after-sales-disputes",
|
id: "after-sales-disputes",
|
||||||
to: "/after-sales/disputes",
|
to: "/after-sales/disputes",
|
||||||
label: "交易纠纷",
|
label: navigationMessages["after-sales-disputes"],
|
||||||
icon: SecurityWarningIcon,
|
icon: SecurityWarningIcon,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -317,36 +319,36 @@ export const primaryNavigationGroups: readonly NavigationGroup[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "customers-and-marketing",
|
id: "customers-and-marketing",
|
||||||
label: "客户与营销",
|
label: navigationMessages["customers-and-marketing"],
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "customer-center",
|
id: "customer-center",
|
||||||
to: "/customers",
|
to: "/customers",
|
||||||
label: "客户中心",
|
label: navigationMessages["customer-center"],
|
||||||
icon: IdentityCardIcon,
|
icon: IdentityCardIcon,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "customer-profiles",
|
id: "customer-profiles",
|
||||||
to: "/customers/profiles",
|
to: "/customers/profiles",
|
||||||
label: "客户档案",
|
label: navigationMessages["customer-profiles"],
|
||||||
icon: IdentityCardIcon,
|
icon: IdentityCardIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "customer-segments",
|
id: "customer-segments",
|
||||||
to: "/customers/segments",
|
to: "/customers/segments",
|
||||||
label: "客户分群",
|
label: navigationMessages["customer-segments"],
|
||||||
icon: UserGroupIcon,
|
icon: UserGroupIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "customer-tiers",
|
id: "customer-tiers",
|
||||||
to: "/customers/tiers",
|
to: "/customers/tiers",
|
||||||
label: "客户等级",
|
label: navigationMessages["customer-tiers"],
|
||||||
icon: CrownIcon,
|
icon: CrownIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "customer-risk-list",
|
id: "customer-risk-list",
|
||||||
to: "/customers/risk-list",
|
to: "/customers/risk-list",
|
||||||
label: "风险名单",
|
label: navigationMessages["customer-risk-list"],
|
||||||
icon: SecurityWarningIcon,
|
icon: SecurityWarningIcon,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -354,31 +356,31 @@ export const primaryNavigationGroups: readonly NavigationGroup[] = [
|
|||||||
{
|
{
|
||||||
id: "marketing-campaigns",
|
id: "marketing-campaigns",
|
||||||
to: "/marketing/campaigns",
|
to: "/marketing/campaigns",
|
||||||
label: "营销活动",
|
label: navigationMessages["marketing-campaigns"],
|
||||||
icon: Discount01Icon,
|
icon: Discount01Icon,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "marketing-coupons",
|
id: "marketing-coupons",
|
||||||
to: "/marketing/coupons",
|
to: "/marketing/coupons",
|
||||||
label: "优惠券管理",
|
label: navigationMessages["marketing-coupons"],
|
||||||
icon: Ticket01Icon,
|
icon: Ticket01Icon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "marketing-flash-sales",
|
id: "marketing-flash-sales",
|
||||||
to: "/marketing/flash-sales",
|
to: "/marketing/flash-sales",
|
||||||
label: "限时折扣",
|
label: navigationMessages["marketing-flash-sales"],
|
||||||
icon: Clock01Icon,
|
icon: Clock01Icon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "marketing-gift-promotions",
|
id: "marketing-gift-promotions",
|
||||||
to: "/marketing/gift-promotions",
|
to: "/marketing/gift-promotions",
|
||||||
label: "满赠活动",
|
label: navigationMessages["marketing-gift-promotions"],
|
||||||
icon: GiftIcon,
|
icon: GiftIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "marketing-referrals",
|
id: "marketing-referrals",
|
||||||
to: "/marketing/referrals",
|
to: "/marketing/referrals",
|
||||||
label: "邀请有礼",
|
label: navigationMessages["marketing-referrals"],
|
||||||
icon: Share01Icon,
|
icon: Share01Icon,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -386,25 +388,25 @@ export const primaryNavigationGroups: readonly NavigationGroup[] = [
|
|||||||
{
|
{
|
||||||
id: "loyalty-center",
|
id: "loyalty-center",
|
||||||
to: "/loyalty",
|
to: "/loyalty",
|
||||||
label: "会员权益",
|
label: navigationMessages["loyalty-center"],
|
||||||
icon: Medal01Icon,
|
icon: Medal01Icon,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "loyalty-points",
|
id: "loyalty-points",
|
||||||
to: "/loyalty/points",
|
to: "/loyalty/points",
|
||||||
label: "积分规则",
|
label: navigationMessages["loyalty-points"],
|
||||||
icon: StarCircleIcon,
|
icon: StarCircleIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "loyalty-stored-value",
|
id: "loyalty-stored-value",
|
||||||
to: "/loyalty/stored-value",
|
to: "/loyalty/stored-value",
|
||||||
label: "储值账户",
|
label: navigationMessages["loyalty-stored-value"],
|
||||||
icon: Wallet01Icon,
|
icon: Wallet01Icon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "loyalty-benefits",
|
id: "loyalty-benefits",
|
||||||
to: "/loyalty/benefits",
|
to: "/loyalty/benefits",
|
||||||
label: "权益配置",
|
label: navigationMessages["loyalty-benefits"],
|
||||||
icon: CheckmarkBadge01Icon,
|
icon: CheckmarkBadge01Icon,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -413,36 +415,36 @@ export const primaryNavigationGroups: readonly NavigationGroup[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "data-and-finance",
|
id: "data-and-finance",
|
||||||
label: "数据与财务",
|
label: navigationMessages["data-and-finance"],
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "report-center",
|
id: "report-center",
|
||||||
to: "/reports",
|
to: "/reports",
|
||||||
label: "数据报表",
|
label: navigationMessages["report-center"],
|
||||||
icon: Chart01Icon,
|
icon: Chart01Icon,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "report-sales",
|
id: "report-sales",
|
||||||
to: "/reports/sales",
|
to: "/reports/sales",
|
||||||
label: "销售分析",
|
label: navigationMessages["report-sales"],
|
||||||
icon: AnalyticsUpIcon,
|
icon: AnalyticsUpIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "report-products",
|
id: "report-products",
|
||||||
to: "/reports/products",
|
to: "/reports/products",
|
||||||
label: "商品分析",
|
label: navigationMessages["report-products"],
|
||||||
icon: ShoppingBag01Icon,
|
icon: ShoppingBag01Icon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "report-customers",
|
id: "report-customers",
|
||||||
to: "/reports/customers",
|
to: "/reports/customers",
|
||||||
label: "客群分析",
|
label: navigationMessages["report-customers"],
|
||||||
icon: UserGroupIcon,
|
icon: UserGroupIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "report-channels",
|
id: "report-channels",
|
||||||
to: "/reports/channels",
|
to: "/reports/channels",
|
||||||
label: "渠道分析",
|
label: navigationMessages["report-channels"],
|
||||||
icon: ChartIncreaseIcon,
|
icon: ChartIncreaseIcon,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -450,31 +452,31 @@ export const primaryNavigationGroups: readonly NavigationGroup[] = [
|
|||||||
{
|
{
|
||||||
id: "finance-center",
|
id: "finance-center",
|
||||||
to: "/finance",
|
to: "/finance",
|
||||||
label: "财务中心",
|
label: navigationMessages["finance-center"],
|
||||||
icon: WalletCardsIcon,
|
icon: WalletCardsIcon,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "finance-settlements",
|
id: "finance-settlements",
|
||||||
to: "/finance/settlements",
|
to: "/finance/settlements",
|
||||||
label: "结算账单",
|
label: navigationMessages["finance-settlements"],
|
||||||
icon: Invoice03Icon,
|
icon: Invoice03Icon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "finance-reconciliation",
|
id: "finance-reconciliation",
|
||||||
to: "/finance/reconciliation",
|
to: "/finance/reconciliation",
|
||||||
label: "对账管理",
|
label: navigationMessages["finance-reconciliation"],
|
||||||
icon: Calculator01Icon,
|
icon: Calculator01Icon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "finance-invoices",
|
id: "finance-invoices",
|
||||||
to: "/finance/invoices",
|
to: "/finance/invoices",
|
||||||
label: "发票管理",
|
label: navigationMessages["finance-invoices"],
|
||||||
icon: File01Icon,
|
icon: File01Icon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "finance-payouts",
|
id: "finance-payouts",
|
||||||
to: "/finance/payouts",
|
to: "/finance/payouts",
|
||||||
label: "打款记录",
|
label: navigationMessages["finance-payouts"],
|
||||||
icon: MoneySendSquareIcon,
|
icon: MoneySendSquareIcon,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -483,30 +485,30 @@ export const primaryNavigationGroups: readonly NavigationGroup[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "system-management",
|
id: "system-management",
|
||||||
label: "系统管理",
|
label: navigationMessages["system-management"],
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "system-members",
|
id: "system-members",
|
||||||
to: "/system/members",
|
to: "/system/members",
|
||||||
label: "组织成员",
|
label: navigationMessages["system-members"],
|
||||||
icon: UserShield01Icon,
|
icon: UserShield01Icon,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "system-members-admins",
|
id: "system-members-admins",
|
||||||
to: "/system/members/admins",
|
to: "/system/members/admins",
|
||||||
label: "管理员账号",
|
label: navigationMessages["system-members-admins"],
|
||||||
icon: UserShield01Icon,
|
icon: UserShield01Icon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "system-members-roles",
|
id: "system-members-roles",
|
||||||
to: "/system/members/roles",
|
to: "/system/members/roles",
|
||||||
label: "角色管理",
|
label: navigationMessages["system-members-roles"],
|
||||||
icon: UserGroupIcon,
|
icon: UserGroupIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "system-members-permissions",
|
id: "system-members-permissions",
|
||||||
to: "/system/members/permissions",
|
to: "/system/members/permissions",
|
||||||
label: "权限策略",
|
label: navigationMessages["system-members-permissions"],
|
||||||
icon: LockKeyIcon,
|
icon: LockKeyIcon,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -514,19 +516,19 @@ export const primaryNavigationGroups: readonly NavigationGroup[] = [
|
|||||||
{
|
{
|
||||||
id: "system-notifications",
|
id: "system-notifications",
|
||||||
to: "/system/notifications",
|
to: "/system/notifications",
|
||||||
label: "消息中心",
|
label: navigationMessages["system-notifications"],
|
||||||
icon: BubbleChatIcon,
|
icon: BubbleChatIcon,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "system-notification-templates",
|
id: "system-notification-templates",
|
||||||
to: "/system/notifications/templates",
|
to: "/system/notifications/templates",
|
||||||
label: "通知模板",
|
label: navigationMessages["system-notification-templates"],
|
||||||
icon: File01Icon,
|
icon: File01Icon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "system-notification-logs",
|
id: "system-notification-logs",
|
||||||
to: "/system/notifications/logs",
|
to: "/system/notifications/logs",
|
||||||
label: "发送记录",
|
label: navigationMessages["system-notification-logs"],
|
||||||
icon: HistoryIcon,
|
icon: HistoryIcon,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -534,25 +536,25 @@ export const primaryNavigationGroups: readonly NavigationGroup[] = [
|
|||||||
{
|
{
|
||||||
id: "system-audit",
|
id: "system-audit",
|
||||||
to: "/system/audit",
|
to: "/system/audit",
|
||||||
label: "操作审计",
|
label: navigationMessages["system-audit"],
|
||||||
icon: Note01Icon,
|
icon: Note01Icon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "system-integrations",
|
id: "system-integrations",
|
||||||
to: "/system/integrations",
|
to: "/system/integrations",
|
||||||
label: "开放集成",
|
label: navigationMessages["system-integrations"],
|
||||||
icon: CodeSquareIcon,
|
icon: CodeSquareIcon,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "system-integration-api-keys",
|
id: "system-integration-api-keys",
|
||||||
to: "/system/integrations/api-keys",
|
to: "/system/integrations/api-keys",
|
||||||
label: "API 密钥",
|
label: navigationMessages["system-integration-api-keys"],
|
||||||
icon: Key01Icon,
|
icon: Key01Icon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "system-integration-webhooks",
|
id: "system-integration-webhooks",
|
||||||
to: "/system/integrations/webhooks",
|
to: "/system/integrations/webhooks",
|
||||||
label: "Webhook 配置",
|
label: navigationMessages["system-integration-webhooks"],
|
||||||
icon: LinkCircleIcon,
|
icon: LinkCircleIcon,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -560,25 +562,25 @@ export const primaryNavigationGroups: readonly NavigationGroup[] = [
|
|||||||
{
|
{
|
||||||
id: "system-settings",
|
id: "system-settings",
|
||||||
to: "/system-configs",
|
to: "/system-configs",
|
||||||
label: "系统配置",
|
label: navigationMessages["system-settings"],
|
||||||
icon: Settings01Icon,
|
icon: Settings01Icon,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "system-settings-store",
|
id: "system-settings-store",
|
||||||
to: "/system-configs/store",
|
to: "/system-configs/store",
|
||||||
label: "门店资料",
|
label: navigationMessages["system-settings-store"],
|
||||||
icon: Store01Icon,
|
icon: Store01Icon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "system-settings-payments",
|
id: "system-settings-payments",
|
||||||
to: "/system-configs/payments",
|
to: "/system-configs/payments",
|
||||||
label: "支付渠道",
|
label: navigationMessages["system-settings-payments"],
|
||||||
icon: CreditCardIcon,
|
icon: CreditCardIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "system-settings-shipping",
|
id: "system-settings-shipping",
|
||||||
to: "/system-configs/shipping",
|
to: "/system-configs/shipping",
|
||||||
label: "配送设置",
|
label: navigationMessages["system-settings-shipping"],
|
||||||
icon: DeliveryTruck01Icon,
|
icon: DeliveryTruck01Icon,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -586,7 +588,7 @@ export const primaryNavigationGroups: readonly NavigationGroup[] = [
|
|||||||
{
|
{
|
||||||
id: "system-recycle-bin",
|
id: "system-recycle-bin",
|
||||||
to: "/system/recycle-bin",
|
to: "/system/recycle-bin",
|
||||||
label: "回收站",
|
label: navigationMessages["system-recycle-bin"],
|
||||||
icon: Delete02Icon,
|
icon: Delete02Icon,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
Mail01Icon,
|
Mail01Icon,
|
||||||
Message01Icon,
|
Message01Icon,
|
||||||
PackageIcon,
|
PackageIcon,
|
||||||
} from "@hugeicons/core-free-icons"
|
} from "@workspace/icons"
|
||||||
import type { QueryFunctionContext } from "@tanstack/react-query"
|
import type { QueryFunctionContext } from "@tanstack/react-query"
|
||||||
|
|
||||||
import type { Notification } from "@workspace/blocks/notifications"
|
import type { Notification } from "@workspace/blocks/notifications"
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { lazy, Suspense } from "react"
|
||||||
|
|
||||||
|
const LazyI18nDevtool = import.meta.env.DEV
|
||||||
|
? lazy(() =>
|
||||||
|
import("@workspace/i18n/devtool").then((module) => ({
|
||||||
|
default: module.I18nDevtool,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
|
||||||
|
export function AppI18nDevtool() {
|
||||||
|
if (!LazyI18nDevtool) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<LazyI18nDevtool dark={"ui\\:dark"} />
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,51 +1,105 @@
|
|||||||
import { lazy, Suspense, type PropsWithChildren } from "react"
|
import {
|
||||||
import { I18nProvider, type LocaleDefinition } from "@workspace/i18n"
|
createContext,
|
||||||
|
startTransition,
|
||||||
|
use,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
type PropsWithChildren,
|
||||||
|
} from "react"
|
||||||
|
import {
|
||||||
|
I18nProvider,
|
||||||
|
useLocaleDefinition,
|
||||||
|
useTranslate,
|
||||||
|
} from "@workspace/i18n"
|
||||||
|
import { loadMessageCatalog } from "@workspace/i18n/catalogs"
|
||||||
|
import {
|
||||||
|
I18nProvider as UiI18nProvider,
|
||||||
|
type CalendarLocale,
|
||||||
|
} from "@workspace/ui/components/i18n"
|
||||||
|
import { calendarLocale as englishCalendarLocale } from "@workspace/ui/locales/en"
|
||||||
|
import { calendarLocale as simplifiedChineseCalendarLocale } from "@workspace/ui/locales/zh-Hans"
|
||||||
|
|
||||||
import { messages as englishMessages } from "@/locales/en/messages"
|
import { appLocales, DEFAULT_APP_LOCALE, type AppLocale } from "./app-locales"
|
||||||
import { messages as simplifiedChineseMessages } from "@/locales/zh-Hans/messages"
|
import { AppI18nDevtool } from "./app-i18n-devtool"
|
||||||
|
import { setAppLocale as persistAppLocale } from "@/server/app-locale.functions"
|
||||||
|
|
||||||
const I18nDevtool = import.meta.env.DEV
|
const appCalendarLocales: Partial<Record<AppLocale, CalendarLocale>> = {
|
||||||
? lazy(() =>
|
en: englishCalendarLocale,
|
||||||
import("@workspace/i18n/devtool").then((module) => ({
|
"zh-Hans": simplifiedChineseCalendarLocale,
|
||||||
default: module.I18nDevtool,
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
: null
|
|
||||||
|
|
||||||
export const appLocales = [
|
|
||||||
{
|
|
||||||
label: "English",
|
|
||||||
locale: "en",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "简体中文",
|
|
||||||
locale: "zh-Hans",
|
|
||||||
},
|
|
||||||
] as const satisfies readonly LocaleDefinition[]
|
|
||||||
|
|
||||||
export type AppLocale = (typeof appLocales)[number]["locale"]
|
|
||||||
|
|
||||||
const appCatalogs = {
|
|
||||||
en: englishMessages,
|
|
||||||
"zh-Hans": simplifiedChineseMessages,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppI18nProviderProps extends PropsWithChildren {
|
export interface AppI18nProviderProps extends PropsWithChildren {
|
||||||
locale?: AppLocale
|
locale?: AppLocale
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AppLocaleContextValue {
|
||||||
|
locale: AppLocale
|
||||||
|
setLocale: (locale: AppLocale) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const AppLocaleContext = createContext<AppLocaleContextValue | null>(null)
|
||||||
|
|
||||||
export function AppI18nProvider({
|
export function AppI18nProvider({
|
||||||
children,
|
children,
|
||||||
locale = "zh-Hans",
|
locale: initialLocale = DEFAULT_APP_LOCALE,
|
||||||
}: AppI18nProviderProps) {
|
}: AppI18nProviderProps) {
|
||||||
|
const [locale, setLocaleState] = useState(initialLocale)
|
||||||
|
const messages = use(loadMessageCatalog(locale))
|
||||||
|
const catalogs = useMemo(() => ({ [locale]: messages }), [locale, messages])
|
||||||
|
const setLocale = useCallback((nextLocale: AppLocale) => {
|
||||||
|
startTransition(() => {
|
||||||
|
setLocaleState(nextLocale)
|
||||||
|
})
|
||||||
|
void persistAppLocale({ data: nextLocale })
|
||||||
|
}, [])
|
||||||
|
const localeContext = useMemo(
|
||||||
|
() => ({ locale, setLocale }),
|
||||||
|
[locale, setLocale]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const definition = appLocales.find(
|
||||||
|
(candidate) => candidate.locale === locale
|
||||||
|
)
|
||||||
|
|
||||||
|
document.documentElement.lang = definition?.languageTag ?? locale
|
||||||
|
document.documentElement.dir = definition?.direction ?? "ltr"
|
||||||
|
}, [locale])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<I18nProvider catalogs={appCatalogs} locale={locale} locales={appLocales}>
|
<AppLocaleContext.Provider value={localeContext}>
|
||||||
{children}
|
<I18nProvider catalogs={catalogs} locale={locale} locales={appLocales}>
|
||||||
{I18nDevtool && (
|
<AppUiI18nProvider>{children}</AppUiI18nProvider>
|
||||||
<Suspense fallback={null}>
|
<AppI18nDevtool />
|
||||||
<I18nDevtool dark={"ui\\:dark"} />
|
</I18nProvider>
|
||||||
</Suspense>
|
</AppLocaleContext.Provider>
|
||||||
)}
|
)
|
||||||
</I18nProvider>
|
}
|
||||||
|
|
||||||
|
export function useAppLocale() {
|
||||||
|
const context = useContext(AppLocaleContext)
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useAppLocale must be used within AppI18nProvider")
|
||||||
|
}
|
||||||
|
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
function AppUiI18nProvider({ children }: PropsWithChildren) {
|
||||||
|
const locale = useLocaleDefinition()
|
||||||
|
const translate = useTranslate()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UiI18nProvider
|
||||||
|
calendarLocale={appCalendarLocales[locale.locale as AppLocale]}
|
||||||
|
locale={locale.languageTag ?? locale.locale}
|
||||||
|
t={translate}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</UiI18nProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
// @vitest-environment jsdom
|
||||||
|
|
||||||
|
import { cleanup, fireEvent, render, screen } from "@testing-library/react"
|
||||||
|
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"
|
||||||
|
|
||||||
|
import { AppLanguageSwitcher } from "./app-language-switcher"
|
||||||
|
|
||||||
|
vi.mock("./app-i18n-provider", () => ({
|
||||||
|
useAppLocale: () => ({
|
||||||
|
locale: "en",
|
||||||
|
setLocale: vi.fn(),
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock("country-flag-icons/react/1x1", () => ({
|
||||||
|
CN: () => <svg />,
|
||||||
|
US: () => <svg />,
|
||||||
|
}))
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
vi.stubGlobal(
|
||||||
|
"ResizeObserver",
|
||||||
|
class {
|
||||||
|
disconnect() {}
|
||||||
|
observe() {}
|
||||||
|
unobserve() {}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(cleanup)
|
||||||
|
|
||||||
|
describe("AppLanguageSwitcher", () => {
|
||||||
|
it("opens its grouped locale choices", async () => {
|
||||||
|
render(<AppLanguageSwitcher />)
|
||||||
|
|
||||||
|
fireEvent.click(
|
||||||
|
screen.getByRole("button", {
|
||||||
|
name: "Change language, currently English",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(await screen.findByText("简体中文")).not.toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { CN, US } from "country-flag-icons/react/1x1"
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@workspace/ui/components/dropdown-menu"
|
||||||
|
import { cn } from "@workspace/ui/lib/utils"
|
||||||
|
import { HeaderActionButton } from "@workspace/blocks/layout"
|
||||||
|
|
||||||
|
import { appLocales, isAppLocale, type AppLocaleCountry } from "./app-locales"
|
||||||
|
import { useAppLocale } from "./app-i18n-provider"
|
||||||
|
|
||||||
|
const localeFlags = {
|
||||||
|
CN,
|
||||||
|
US,
|
||||||
|
} satisfies Record<AppLocaleCountry, typeof CN>
|
||||||
|
|
||||||
|
function LocaleFlag({
|
||||||
|
country,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
className?: string
|
||||||
|
country: AppLocaleCountry
|
||||||
|
}) {
|
||||||
|
const Flag = localeFlags[country]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn(
|
||||||
|
"inline-flex shrink-0 overflow-hidden rounded-full ring-1 ring-foreground/15",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Flag className="size-full" />
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AppLanguageSwitcher() {
|
||||||
|
const { locale, setLocale } = useAppLocale()
|
||||||
|
const activeLocale =
|
||||||
|
appLocales.find((definition) => definition.locale === locale) ??
|
||||||
|
appLocales[0]
|
||||||
|
const menuLabel = locale === "zh-Hans" ? "语言" : "Language"
|
||||||
|
const triggerLabel =
|
||||||
|
locale === "zh-Hans"
|
||||||
|
? `切换语言,当前为${activeLocale.label}`
|
||||||
|
: `Change language, currently ${activeLocale.label}`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger
|
||||||
|
render={
|
||||||
|
<HeaderActionButton
|
||||||
|
aria-label={triggerLabel}
|
||||||
|
className="rounded-full"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<LocaleFlag country={activeLocale.country} className="size-6" />
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
align="end"
|
||||||
|
className="w-44"
|
||||||
|
showArrow={{ cornerRadius: "lg", tailSize: "sm" }}
|
||||||
|
>
|
||||||
|
<DropdownMenuRadioGroup
|
||||||
|
value={locale}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
if (isAppLocale(value)) {
|
||||||
|
setLocale(value)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownMenuLabel>{menuLabel}</DropdownMenuLabel>
|
||||||
|
{appLocales.map((definition) => (
|
||||||
|
<DropdownMenuRadioItem
|
||||||
|
key={definition.locale}
|
||||||
|
className="py-2"
|
||||||
|
value={definition.locale}
|
||||||
|
>
|
||||||
|
<LocaleFlag country={definition.country} className="size-5" />
|
||||||
|
<span lang={definition.languageTag}>{definition.label}</span>
|
||||||
|
</DropdownMenuRadioItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuRadioGroup>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { LocaleDefinition } from "@workspace/i18n"
|
||||||
|
|
||||||
|
export type AppLocale = "en" | "zh-Hans"
|
||||||
|
export type AppLocaleCountry = "CN" | "US"
|
||||||
|
|
||||||
|
export interface AppLocaleDefinition extends LocaleDefinition {
|
||||||
|
country: AppLocaleCountry
|
||||||
|
locale: AppLocale
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_APP_LOCALE: AppLocale = "zh-Hans"
|
||||||
|
|
||||||
|
export const appLocales = [
|
||||||
|
{
|
||||||
|
country: "US",
|
||||||
|
direction: "ltr",
|
||||||
|
languageTag: "en-US",
|
||||||
|
label: "English",
|
||||||
|
locale: "en",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
country: "CN",
|
||||||
|
direction: "ltr",
|
||||||
|
languageTag: "zh-CN",
|
||||||
|
label: "简体中文",
|
||||||
|
locale: "zh-Hans",
|
||||||
|
},
|
||||||
|
] as const satisfies readonly AppLocaleDefinition[]
|
||||||
|
|
||||||
|
export function isAppLocale(value: unknown): value is AppLocale {
|
||||||
|
return appLocales.some((definition) => definition.locale === value)
|
||||||
|
}
|
||||||
@@ -12,3 +12,843 @@ msgstr ""
|
|||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: \n"
|
"Language-Team: \n"
|
||||||
"Plural-Forms: \n"
|
"Plural-Forms: \n"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/layout/messages.ts:4
|
||||||
|
msgid "blocks.userMenu.accountPassword"
|
||||||
|
msgstr "Account password"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:4
|
||||||
|
msgid "blocks.appearance.active"
|
||||||
|
msgstr "Active"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:276
|
||||||
|
msgid "app.navigation.system-members-admins"
|
||||||
|
msgstr "Administrator accounts"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:148
|
||||||
|
msgid "app.navigation.after-sales-center"
|
||||||
|
msgstr "After-sales"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:91
|
||||||
|
msgid "blocks.appearance.color.amber"
|
||||||
|
msgstr "Amber"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:308
|
||||||
|
msgid "app.navigation.system-integration-api-keys"
|
||||||
|
msgstr "API keys"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/layout/messages.ts:8
|
||||||
|
msgid "blocks.userMenu.appearance"
|
||||||
|
msgstr "Appearance"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:8
|
||||||
|
msgid "blocks.appearance.title"
|
||||||
|
msgstr "Appearance"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:92
|
||||||
|
msgid "app.navigation.content-articles"
|
||||||
|
msgstr "Articles"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:300
|
||||||
|
msgid "app.navigation.system-audit"
|
||||||
|
msgstr "Audit log"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:112
|
||||||
|
msgid "app.navigation.trade-orders-awaiting-payment"
|
||||||
|
msgstr "Awaiting payment"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:116
|
||||||
|
msgid "app.navigation.trade-orders-awaiting-shipment"
|
||||||
|
msgstr "Awaiting shipment"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:16
|
||||||
|
msgid "blocks.appearance.baseColor"
|
||||||
|
msgstr "Base color"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:220
|
||||||
|
msgid "app.navigation.loyalty-benefits"
|
||||||
|
msgstr "Benefits configuration"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:95
|
||||||
|
msgid "blocks.appearance.color.blue"
|
||||||
|
msgstr "Blue"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:48
|
||||||
|
msgid "app.navigation.catalog-product-brands"
|
||||||
|
msgstr "Brands"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/ui/src/components/i18n.tsx:25
|
||||||
|
msgid "ui.breadcrumb.label"
|
||||||
|
msgstr "Breadcrumb"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:8
|
||||||
|
msgid "app.navigation.overview-dashboard"
|
||||||
|
msgstr "Business overview"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:128
|
||||||
|
msgid "app.navigation.trade-orders-canceled"
|
||||||
|
msgstr "Canceled orders"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:52
|
||||||
|
msgid "app.navigation.catalog-categories"
|
||||||
|
msgstr "Categories"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:56
|
||||||
|
msgid "app.navigation.catalog-category-tree"
|
||||||
|
msgstr "Category tree"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:244
|
||||||
|
msgid "app.navigation.report-channels"
|
||||||
|
msgstr "Channel analysis"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/chats/messages.ts:4
|
||||||
|
msgid "blocks.chats.title"
|
||||||
|
msgstr "Chats"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/notifications/messages.ts:16
|
||||||
|
msgid "blocks.notifications.error.description"
|
||||||
|
msgstr "Check your network connection and try again."
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/ui/src/components/i18n.tsx:29
|
||||||
|
msgid "ui.common.close"
|
||||||
|
msgstr "Close"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/ui/src/components/i18n.tsx:33
|
||||||
|
msgid "ui.toast.close"
|
||||||
|
msgstr "Close notification"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:24
|
||||||
|
msgid "blocks.appearance.compact.title"
|
||||||
|
msgstr "Compact layout"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:124
|
||||||
|
msgid "app.navigation.trade-orders-completed"
|
||||||
|
msgstr "Completed orders"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:88
|
||||||
|
msgid "app.navigation.content-assets"
|
||||||
|
msgstr "Content"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:192
|
||||||
|
msgid "app.navigation.marketing-coupons"
|
||||||
|
msgstr "Coupons"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:100
|
||||||
|
msgid "app.navigation.content-pages"
|
||||||
|
msgstr "Custom pages"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:240
|
||||||
|
msgid "app.navigation.report-customers"
|
||||||
|
msgstr "Customer analysis"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:172
|
||||||
|
msgid "app.navigation.customer-profiles"
|
||||||
|
msgstr "Customer profiles"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:176
|
||||||
|
msgid "app.navigation.customer-segments"
|
||||||
|
msgstr "Customer segments"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:180
|
||||||
|
msgid "app.navigation.customer-tiers"
|
||||||
|
msgstr "Customer tiers"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:168
|
||||||
|
msgid "app.navigation.customer-center"
|
||||||
|
msgstr "Customers"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:164
|
||||||
|
msgid "app.navigation.customers-and-marketing"
|
||||||
|
msgstr "Customers and marketing"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:99
|
||||||
|
msgid "blocks.appearance.color.cyan"
|
||||||
|
msgstr "Cyan"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:28
|
||||||
|
msgid "blocks.appearance.mode.dark"
|
||||||
|
msgstr "Dark"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:36
|
||||||
|
msgid "blocks.appearance.theme.dark"
|
||||||
|
msgstr "Dark theme"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:224
|
||||||
|
msgid "app.navigation.data-and-finance"
|
||||||
|
msgstr "Data and finance"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:296
|
||||||
|
msgid "app.navigation.system-notification-logs"
|
||||||
|
msgstr "Delivery logs"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:136
|
||||||
|
msgid "app.navigation.fulfillment-deliveries"
|
||||||
|
msgstr "Delivery tasks"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/ui/src/components/i18n.tsx:81
|
||||||
|
msgid "ui.sidebar.description"
|
||||||
|
msgstr "Displays the mobile sidebar."
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:103
|
||||||
|
msgid "blocks.appearance.color.emerald"
|
||||||
|
msgstr "Emerald"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:248
|
||||||
|
msgid "app.navigation.finance-center"
|
||||||
|
msgstr "Finance"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:196
|
||||||
|
msgid "app.navigation.marketing-flash-sales"
|
||||||
|
msgstr "Flash sales"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:107
|
||||||
|
msgid "blocks.appearance.color.fuchsia"
|
||||||
|
msgstr "Fuchsia"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:132
|
||||||
|
msgid "app.navigation.fulfillment-center"
|
||||||
|
msgstr "Fulfillment"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:200
|
||||||
|
msgid "app.navigation.marketing-gift-promotions"
|
||||||
|
msgstr "Gift promotions"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/ui/src/components/i18n.tsx:53
|
||||||
|
msgid "ui.pagination.nextPage"
|
||||||
|
msgstr "Go to next page"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/ui/src/components/i18n.tsx:69
|
||||||
|
msgid "ui.pagination.previousPage"
|
||||||
|
msgstr "Go to previous page"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:111
|
||||||
|
msgid "blocks.appearance.color.green"
|
||||||
|
msgstr "Green"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:120
|
||||||
|
msgid "app.navigation.trade-orders-shipped"
|
||||||
|
msgstr "In transit"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:72
|
||||||
|
msgid "app.navigation.inventory-inbound"
|
||||||
|
msgstr "Inbound orders"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:115
|
||||||
|
msgid "blocks.appearance.color.indigo"
|
||||||
|
msgstr "Indigo"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:304
|
||||||
|
msgid "app.navigation.system-integrations"
|
||||||
|
msgstr "Integrations"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:64
|
||||||
|
msgid "app.navigation.inventory-center"
|
||||||
|
msgstr "Inventory"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:84
|
||||||
|
msgid "app.navigation.inventory-alerts"
|
||||||
|
msgstr "Inventory alerts"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:68
|
||||||
|
msgid "app.navigation.inventory-overview"
|
||||||
|
msgstr "Inventory overview"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:260
|
||||||
|
msgid "app.navigation.finance-invoices"
|
||||||
|
msgstr "Invoices"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/notifications/messages.ts:24
|
||||||
|
msgid "blocks.notifications.justNow"
|
||||||
|
msgstr "just now"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/layout/messages.ts:16
|
||||||
|
msgid "blocks.userMenu.keybindings"
|
||||||
|
msgstr "Keybindings"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:52
|
||||||
|
msgid "blocks.appearance.mode.light"
|
||||||
|
msgstr "Light"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:60
|
||||||
|
msgid "blocks.appearance.theme.light"
|
||||||
|
msgstr "Light theme"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:119
|
||||||
|
msgid "blocks.appearance.color.lime"
|
||||||
|
msgstr "Lime"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/ui/src/components/i18n.tsx:37
|
||||||
|
msgid "ui.common.loading"
|
||||||
|
msgstr "Loading"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/notifications/messages.ts:28
|
||||||
|
msgid "blocks.notifications.loading"
|
||||||
|
msgstr "Loading notifications…"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/layout/messages.ts:20
|
||||||
|
msgid "blocks.userMenu.logout"
|
||||||
|
msgstr "Log out"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/notifications/messages.ts:32
|
||||||
|
msgid "blocks.notifications.markAllAsRead"
|
||||||
|
msgstr "Mark all as read"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:188
|
||||||
|
msgid "app.navigation.marketing-campaigns"
|
||||||
|
msgstr "Marketing campaigns"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:123
|
||||||
|
msgid "blocks.appearance.color.mauve"
|
||||||
|
msgstr "Mauve"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:96
|
||||||
|
msgid "app.navigation.content-media"
|
||||||
|
msgstr "Media"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:208
|
||||||
|
msgid "app.navigation.loyalty-center"
|
||||||
|
msgstr "Membership benefits"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:127
|
||||||
|
msgid "blocks.appearance.color.mist"
|
||||||
|
msgstr "Mist"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/ui/src/components/i18n.tsx:41
|
||||||
|
msgid "ui.common.more"
|
||||||
|
msgstr "More"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/ui/src/components/i18n.tsx:45
|
||||||
|
msgid "ui.pagination.more"
|
||||||
|
msgstr "More pages"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/navigation/messages.ts:4
|
||||||
|
msgid "blocks.navigation.description"
|
||||||
|
msgstr "Navigate through the app."
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/navigation/messages.ts:12
|
||||||
|
msgid "blocks.navigation.title"
|
||||||
|
msgstr "Navigation"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:60
|
||||||
|
msgid "app.navigation.catalog-category-navigation"
|
||||||
|
msgstr "Navigation categories"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:131
|
||||||
|
msgid "blocks.appearance.color.neutral"
|
||||||
|
msgstr "Neutral"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/notifications/messages.ts:8
|
||||||
|
msgid "blocks.notifications.empty.description"
|
||||||
|
msgstr "New activity will appear here."
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/ui/src/components/i18n.tsx:49
|
||||||
|
msgid "ui.pagination.next"
|
||||||
|
msgstr "Next"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/ui/src/components/i18n.tsx:57
|
||||||
|
msgid "ui.carousel.nextSlide"
|
||||||
|
msgstr "Next slide"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/notifications/messages.ts:12
|
||||||
|
msgid "blocks.notifications.empty.title"
|
||||||
|
msgstr "No notifications"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:292
|
||||||
|
msgid "app.navigation.system-notification-templates"
|
||||||
|
msgstr "Notification templates"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/notifications/messages.ts:40
|
||||||
|
msgid "blocks.notifications.title"
|
||||||
|
msgstr "Notifications"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:288
|
||||||
|
msgid "app.navigation.system-notifications"
|
||||||
|
msgstr "Notifications"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:135
|
||||||
|
msgid "blocks.appearance.color.olive"
|
||||||
|
msgstr "Olive"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/layout/messages.ts:12
|
||||||
|
msgid "blocks.breadcrumb.menuLabel"
|
||||||
|
msgstr "Open breadcrumb menu"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/navigation/messages.ts:8
|
||||||
|
msgid "blocks.navigation.open"
|
||||||
|
msgstr "Open navigation"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:12
|
||||||
|
msgid "blocks.appearance.command.description"
|
||||||
|
msgstr "Open theme and layout preferences"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/layout/messages.ts:24
|
||||||
|
msgid "blocks.userMenu.open"
|
||||||
|
msgstr "Open user menu"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:139
|
||||||
|
msgid "blocks.appearance.color.orange"
|
||||||
|
msgstr "Orange"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:108
|
||||||
|
msgid "app.navigation.trade-orders"
|
||||||
|
msgstr "Orders"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:104
|
||||||
|
msgid "app.navigation.trade-and-fulfillment"
|
||||||
|
msgstr "Orders and fulfillment"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:272
|
||||||
|
msgid "app.navigation.system-members"
|
||||||
|
msgstr "Organization members"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:76
|
||||||
|
msgid "app.navigation.inventory-outbound"
|
||||||
|
msgstr "Outbound orders"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:4
|
||||||
|
msgid "app.navigation.overview"
|
||||||
|
msgstr "Overview"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/ui/src/components/i18n.tsx:61
|
||||||
|
msgid "ui.pagination.label"
|
||||||
|
msgstr "Pagination"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:324
|
||||||
|
msgid "app.navigation.system-settings-payments"
|
||||||
|
msgstr "Payment channels"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:264
|
||||||
|
msgid "app.navigation.finance-payouts"
|
||||||
|
msgstr "Payout history"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:284
|
||||||
|
msgid "app.navigation.system-members-permissions"
|
||||||
|
msgstr "Permission policies"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:140
|
||||||
|
msgid "app.navigation.fulfillment-pickups"
|
||||||
|
msgstr "Pickup verification"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:143
|
||||||
|
msgid "blocks.appearance.color.pink"
|
||||||
|
msgstr "Pink"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:212
|
||||||
|
msgid "app.navigation.loyalty-points"
|
||||||
|
msgstr "Points rules"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/ui/src/components/i18n.tsx:65
|
||||||
|
msgid "ui.pagination.previous"
|
||||||
|
msgstr "Previous"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/ui/src/components/i18n.tsx:73
|
||||||
|
msgid "ui.carousel.previousSlide"
|
||||||
|
msgstr "Previous slide"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:68
|
||||||
|
msgid "blocks.appearance.primaryColor"
|
||||||
|
msgstr "Primary color"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:236
|
||||||
|
msgid "app.navigation.report-products"
|
||||||
|
msgstr "Product analysis"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:40
|
||||||
|
msgid "app.navigation.catalog-products-attributes"
|
||||||
|
msgstr "Product attributes"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:36
|
||||||
|
msgid "app.navigation.catalog-products-bundles"
|
||||||
|
msgstr "Product bundles"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:32
|
||||||
|
msgid "app.navigation.catalog-products-drafts"
|
||||||
|
msgstr "Product drafts"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:28
|
||||||
|
msgid "app.navigation.catalog-products-list"
|
||||||
|
msgstr "Product list"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:24
|
||||||
|
msgid "app.navigation.catalog-products"
|
||||||
|
msgstr "Products"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:20
|
||||||
|
msgid "app.navigation.catalog-and-inventory"
|
||||||
|
msgstr "Products and inventory"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:147
|
||||||
|
msgid "blocks.appearance.color.purple"
|
||||||
|
msgstr "Purple"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:12
|
||||||
|
msgid "app.navigation.overview-realtime"
|
||||||
|
msgstr "Real-time monitoring"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:256
|
||||||
|
msgid "app.navigation.finance-reconciliation"
|
||||||
|
msgstr "Reconciliation"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:332
|
||||||
|
msgid "app.navigation.system-recycle-bin"
|
||||||
|
msgstr "Recycle bin"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:151
|
||||||
|
msgid "blocks.appearance.color.red"
|
||||||
|
msgstr "Red"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:204
|
||||||
|
msgid "app.navigation.marketing-referrals"
|
||||||
|
msgstr "Referral rewards"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/layout/messages.ts:32
|
||||||
|
msgid "blocks.refresh.label"
|
||||||
|
msgstr "Refresh current page data"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/layout/messages.ts:28
|
||||||
|
msgid "blocks.refresh.description"
|
||||||
|
msgstr "Refresh the current page data"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:156
|
||||||
|
msgid "app.navigation.after-sales-refunds"
|
||||||
|
msgstr "Refund review"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:228
|
||||||
|
msgid "app.navigation.report-center"
|
||||||
|
msgstr "Reports"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:152
|
||||||
|
msgid "app.navigation.after-sales-returns"
|
||||||
|
msgstr "Return requests"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:184
|
||||||
|
msgid "app.navigation.customer-risk-list"
|
||||||
|
msgstr "Risk list"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:280
|
||||||
|
msgid "app.navigation.system-members-roles"
|
||||||
|
msgstr "Roles"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:155
|
||||||
|
msgid "blocks.appearance.color.rose"
|
||||||
|
msgstr "Rose"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:232
|
||||||
|
msgid "app.navigation.report-sales"
|
||||||
|
msgstr "Sales analysis"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:252
|
||||||
|
msgid "app.navigation.finance-settlements"
|
||||||
|
msgstr "Settlement statements"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:144
|
||||||
|
msgid "app.navigation.fulfillment-freight-rules"
|
||||||
|
msgstr "Shipping rules"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:328
|
||||||
|
msgid "app.navigation.system-settings-shipping"
|
||||||
|
msgstr "Shipping settings"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/ui/src/components/i18n.tsx:77
|
||||||
|
msgid "ui.sidebar.title"
|
||||||
|
msgstr "Sidebar"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:159
|
||||||
|
msgid "blocks.appearance.color.sky"
|
||||||
|
msgstr "Sky"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:44
|
||||||
|
msgid "app.navigation.catalog-product-specifications"
|
||||||
|
msgstr "Specification templates"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:80
|
||||||
|
msgid "app.navigation.inventory-transfers"
|
||||||
|
msgstr "Stock transfers"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:163
|
||||||
|
msgid "blocks.appearance.color.stone"
|
||||||
|
msgstr "Stone"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:320
|
||||||
|
msgid "app.navigation.system-settings-store"
|
||||||
|
msgstr "Store information"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:216
|
||||||
|
msgid "app.navigation.loyalty-stored-value"
|
||||||
|
msgstr "Stored-value accounts"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:84
|
||||||
|
msgid "blocks.appearance.toggleTheme.description"
|
||||||
|
msgstr "Switch between light and dark themes"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:44
|
||||||
|
msgid "blocks.appearance.mode.system"
|
||||||
|
msgstr "System"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:268
|
||||||
|
msgid "app.navigation.system-management"
|
||||||
|
msgstr "System management"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:316
|
||||||
|
msgid "app.navigation.system-settings"
|
||||||
|
msgstr "System settings"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:16
|
||||||
|
msgid "app.navigation.overview-tasks"
|
||||||
|
msgstr "Tasks"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:167
|
||||||
|
msgid "blocks.appearance.color.taupe"
|
||||||
|
msgstr "Taupe"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:171
|
||||||
|
msgid "blocks.appearance.color.teal"
|
||||||
|
msgstr "Teal"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:48
|
||||||
|
msgid "blocks.appearance.mode.system.description"
|
||||||
|
msgstr "The interface theme will follow the system appearance"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:32
|
||||||
|
msgid "blocks.appearance.mode.dark.description"
|
||||||
|
msgstr "The interface will always use the dark theme"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:56
|
||||||
|
msgid "blocks.appearance.mode.light.description"
|
||||||
|
msgstr "The interface will always use the light theme"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:72
|
||||||
|
msgid "blocks.appearance.description"
|
||||||
|
msgstr "Theme and layout preferences"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:76
|
||||||
|
msgid "blocks.appearance.mode.title"
|
||||||
|
msgstr "Theme mode"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:40
|
||||||
|
msgid "blocks.appearance.theme.dark.description"
|
||||||
|
msgstr "This theme is used when the system is in dark mode"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:64
|
||||||
|
msgid "blocks.appearance.theme.light.description"
|
||||||
|
msgstr "This theme is used when the system is in light mode"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/ui/src/components/i18n.tsx:85
|
||||||
|
msgid "ui.sidebar.toggle"
|
||||||
|
msgstr "Toggle sidebar"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:80
|
||||||
|
msgid "blocks.appearance.toggleTheme"
|
||||||
|
msgstr "Toggle theme"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:160
|
||||||
|
msgid "app.navigation.after-sales-disputes"
|
||||||
|
msgstr "Transaction disputes"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/notifications/messages.ts:36
|
||||||
|
msgid "blocks.notifications.retry"
|
||||||
|
msgstr "Try again"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/notifications/messages.ts:20
|
||||||
|
msgid "blocks.notifications.error.title"
|
||||||
|
msgstr "Unable to load notifications"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:20
|
||||||
|
msgid "blocks.appearance.compact.description"
|
||||||
|
msgstr "Use a compact width for the main page content"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/layout/messages.ts:36
|
||||||
|
msgid "blocks.userMenu.title"
|
||||||
|
msgstr "User menu"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/notifications/messages.ts:44
|
||||||
|
msgid "blocks.notifications.viewAll"
|
||||||
|
msgstr "View all"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/notifications/messages.ts:4
|
||||||
|
msgid "blocks.notifications.description"
|
||||||
|
msgstr "View and manage notifications"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:175
|
||||||
|
msgid "blocks.appearance.color.violet"
|
||||||
|
msgstr "Violet"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:312
|
||||||
|
msgid "app.navigation.system-integration-webhooks"
|
||||||
|
msgstr "Webhook configuration"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:179
|
||||||
|
msgid "blocks.appearance.color.yellow"
|
||||||
|
msgstr "Yellow"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:183
|
||||||
|
msgid "blocks.appearance.color.zinc"
|
||||||
|
msgstr "Zinc"
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -12,3 +12,843 @@ msgstr ""
|
|||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: \n"
|
"Language-Team: \n"
|
||||||
"Plural-Forms: \n"
|
"Plural-Forms: \n"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/layout/messages.ts:4
|
||||||
|
msgid "blocks.userMenu.accountPassword"
|
||||||
|
msgstr "账号密码"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:4
|
||||||
|
msgid "blocks.appearance.active"
|
||||||
|
msgstr "使用中"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:276
|
||||||
|
msgid "app.navigation.system-members-admins"
|
||||||
|
msgstr "管理员账号"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:148
|
||||||
|
msgid "app.navigation.after-sales-center"
|
||||||
|
msgstr "售后中心"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:91
|
||||||
|
msgid "blocks.appearance.color.amber"
|
||||||
|
msgstr "琥珀色"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:308
|
||||||
|
msgid "app.navigation.system-integration-api-keys"
|
||||||
|
msgstr "API 密钥"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/layout/messages.ts:8
|
||||||
|
msgid "blocks.userMenu.appearance"
|
||||||
|
msgstr "界面外观"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:8
|
||||||
|
msgid "blocks.appearance.title"
|
||||||
|
msgstr "界面外观"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:92
|
||||||
|
msgid "app.navigation.content-articles"
|
||||||
|
msgstr "内容文章"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:300
|
||||||
|
msgid "app.navigation.system-audit"
|
||||||
|
msgstr "操作审计"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:112
|
||||||
|
msgid "app.navigation.trade-orders-awaiting-payment"
|
||||||
|
msgstr "待付款订单"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:116
|
||||||
|
msgid "app.navigation.trade-orders-awaiting-shipment"
|
||||||
|
msgstr "待发货订单"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:16
|
||||||
|
msgid "blocks.appearance.baseColor"
|
||||||
|
msgstr "基础颜色"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:220
|
||||||
|
msgid "app.navigation.loyalty-benefits"
|
||||||
|
msgstr "权益配置"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:95
|
||||||
|
msgid "blocks.appearance.color.blue"
|
||||||
|
msgstr "蓝色"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:48
|
||||||
|
msgid "app.navigation.catalog-product-brands"
|
||||||
|
msgstr "品牌资料"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/ui/src/components/i18n.tsx:25
|
||||||
|
msgid "ui.breadcrumb.label"
|
||||||
|
msgstr "面包屑导航"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:8
|
||||||
|
msgid "app.navigation.overview-dashboard"
|
||||||
|
msgstr "经营总览"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:128
|
||||||
|
msgid "app.navigation.trade-orders-canceled"
|
||||||
|
msgstr "已取消订单"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:52
|
||||||
|
msgid "app.navigation.catalog-categories"
|
||||||
|
msgstr "分类目录"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:56
|
||||||
|
msgid "app.navigation.catalog-category-tree"
|
||||||
|
msgstr "分类树"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:244
|
||||||
|
msgid "app.navigation.report-channels"
|
||||||
|
msgstr "渠道分析"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/chats/messages.ts:4
|
||||||
|
msgid "blocks.chats.title"
|
||||||
|
msgstr "聊天"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/notifications/messages.ts:16
|
||||||
|
msgid "blocks.notifications.error.description"
|
||||||
|
msgstr "请检查网络连接后重试。"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/ui/src/components/i18n.tsx:29
|
||||||
|
msgid "ui.common.close"
|
||||||
|
msgstr "关闭"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/ui/src/components/i18n.tsx:33
|
||||||
|
msgid "ui.toast.close"
|
||||||
|
msgstr "关闭通知"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:24
|
||||||
|
msgid "blocks.appearance.compact.title"
|
||||||
|
msgstr "紧凑布局"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:124
|
||||||
|
msgid "app.navigation.trade-orders-completed"
|
||||||
|
msgstr "已完成订单"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:88
|
||||||
|
msgid "app.navigation.content-assets"
|
||||||
|
msgstr "内容素材"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:192
|
||||||
|
msgid "app.navigation.marketing-coupons"
|
||||||
|
msgstr "优惠券管理"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:100
|
||||||
|
msgid "app.navigation.content-pages"
|
||||||
|
msgstr "自定义页面"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:240
|
||||||
|
msgid "app.navigation.report-customers"
|
||||||
|
msgstr "客群分析"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:172
|
||||||
|
msgid "app.navigation.customer-profiles"
|
||||||
|
msgstr "客户档案"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:176
|
||||||
|
msgid "app.navigation.customer-segments"
|
||||||
|
msgstr "客户分群"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:180
|
||||||
|
msgid "app.navigation.customer-tiers"
|
||||||
|
msgstr "客户等级"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:168
|
||||||
|
msgid "app.navigation.customer-center"
|
||||||
|
msgstr "客户中心"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:164
|
||||||
|
msgid "app.navigation.customers-and-marketing"
|
||||||
|
msgstr "客户与营销"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:99
|
||||||
|
msgid "blocks.appearance.color.cyan"
|
||||||
|
msgstr "青色"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:28
|
||||||
|
msgid "blocks.appearance.mode.dark"
|
||||||
|
msgstr "深色"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:36
|
||||||
|
msgid "blocks.appearance.theme.dark"
|
||||||
|
msgstr "深色主题"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:224
|
||||||
|
msgid "app.navigation.data-and-finance"
|
||||||
|
msgstr "数据与财务"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:296
|
||||||
|
msgid "app.navigation.system-notification-logs"
|
||||||
|
msgstr "发送记录"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:136
|
||||||
|
msgid "app.navigation.fulfillment-deliveries"
|
||||||
|
msgstr "配送任务"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/ui/src/components/i18n.tsx:81
|
||||||
|
msgid "ui.sidebar.description"
|
||||||
|
msgstr "显示移动端侧栏。"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:103
|
||||||
|
msgid "blocks.appearance.color.emerald"
|
||||||
|
msgstr "翠绿色"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:248
|
||||||
|
msgid "app.navigation.finance-center"
|
||||||
|
msgstr "财务中心"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:196
|
||||||
|
msgid "app.navigation.marketing-flash-sales"
|
||||||
|
msgstr "限时折扣"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:107
|
||||||
|
msgid "blocks.appearance.color.fuchsia"
|
||||||
|
msgstr "紫红色"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:132
|
||||||
|
msgid "app.navigation.fulfillment-center"
|
||||||
|
msgstr "配送履约"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:200
|
||||||
|
msgid "app.navigation.marketing-gift-promotions"
|
||||||
|
msgstr "满赠活动"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/ui/src/components/i18n.tsx:53
|
||||||
|
msgid "ui.pagination.nextPage"
|
||||||
|
msgstr "前往下一页"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/ui/src/components/i18n.tsx:69
|
||||||
|
msgid "ui.pagination.previousPage"
|
||||||
|
msgstr "前往上一页"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:111
|
||||||
|
msgid "blocks.appearance.color.green"
|
||||||
|
msgstr "绿色"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:120
|
||||||
|
msgid "app.navigation.trade-orders-shipped"
|
||||||
|
msgstr "配送中订单"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:72
|
||||||
|
msgid "app.navigation.inventory-inbound"
|
||||||
|
msgstr "入库单"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:115
|
||||||
|
msgid "blocks.appearance.color.indigo"
|
||||||
|
msgstr "靛蓝色"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:304
|
||||||
|
msgid "app.navigation.system-integrations"
|
||||||
|
msgstr "开放集成"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:64
|
||||||
|
msgid "app.navigation.inventory-center"
|
||||||
|
msgstr "库存中心"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:84
|
||||||
|
msgid "app.navigation.inventory-alerts"
|
||||||
|
msgstr "库存预警"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:68
|
||||||
|
msgid "app.navigation.inventory-overview"
|
||||||
|
msgstr "库存总览"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:260
|
||||||
|
msgid "app.navigation.finance-invoices"
|
||||||
|
msgstr "发票管理"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/notifications/messages.ts:24
|
||||||
|
msgid "blocks.notifications.justNow"
|
||||||
|
msgstr "刚刚"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/layout/messages.ts:16
|
||||||
|
msgid "blocks.userMenu.keybindings"
|
||||||
|
msgstr "按键映射"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:52
|
||||||
|
msgid "blocks.appearance.mode.light"
|
||||||
|
msgstr "浅色"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:60
|
||||||
|
msgid "blocks.appearance.theme.light"
|
||||||
|
msgstr "浅色主题"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:119
|
||||||
|
msgid "blocks.appearance.color.lime"
|
||||||
|
msgstr "青柠色"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/ui/src/components/i18n.tsx:37
|
||||||
|
msgid "ui.common.loading"
|
||||||
|
msgstr "正在加载"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/notifications/messages.ts:28
|
||||||
|
msgid "blocks.notifications.loading"
|
||||||
|
msgstr "正在加载通知…"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/layout/messages.ts:20
|
||||||
|
msgid "blocks.userMenu.logout"
|
||||||
|
msgstr "退出登录"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/notifications/messages.ts:32
|
||||||
|
msgid "blocks.notifications.markAllAsRead"
|
||||||
|
msgstr "全部标记为已读"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:188
|
||||||
|
msgid "app.navigation.marketing-campaigns"
|
||||||
|
msgstr "营销活动"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:123
|
||||||
|
msgid "blocks.appearance.color.mauve"
|
||||||
|
msgstr "紫灰"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:96
|
||||||
|
msgid "app.navigation.content-media"
|
||||||
|
msgstr "媒体资源"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:208
|
||||||
|
msgid "app.navigation.loyalty-center"
|
||||||
|
msgstr "会员权益"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:127
|
||||||
|
msgid "blocks.appearance.color.mist"
|
||||||
|
msgstr "雾灰"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/ui/src/components/i18n.tsx:41
|
||||||
|
msgid "ui.common.more"
|
||||||
|
msgstr "更多"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/ui/src/components/i18n.tsx:45
|
||||||
|
msgid "ui.pagination.more"
|
||||||
|
msgstr "更多页面"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/navigation/messages.ts:4
|
||||||
|
msgid "blocks.navigation.description"
|
||||||
|
msgstr "浏览应用中的页面。"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/navigation/messages.ts:12
|
||||||
|
msgid "blocks.navigation.title"
|
||||||
|
msgstr "导航"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:60
|
||||||
|
msgid "app.navigation.catalog-category-navigation"
|
||||||
|
msgstr "导航分类"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:131
|
||||||
|
msgid "blocks.appearance.color.neutral"
|
||||||
|
msgstr "中性灰"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/notifications/messages.ts:8
|
||||||
|
msgid "blocks.notifications.empty.description"
|
||||||
|
msgstr "新的动态会显示在这里。"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/ui/src/components/i18n.tsx:49
|
||||||
|
msgid "ui.pagination.next"
|
||||||
|
msgstr "下一页"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/ui/src/components/i18n.tsx:57
|
||||||
|
msgid "ui.carousel.nextSlide"
|
||||||
|
msgstr "下一张"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/notifications/messages.ts:12
|
||||||
|
msgid "blocks.notifications.empty.title"
|
||||||
|
msgstr "暂无通知"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:292
|
||||||
|
msgid "app.navigation.system-notification-templates"
|
||||||
|
msgstr "通知模板"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/notifications/messages.ts:40
|
||||||
|
msgid "blocks.notifications.title"
|
||||||
|
msgstr "通知"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:288
|
||||||
|
msgid "app.navigation.system-notifications"
|
||||||
|
msgstr "消息中心"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:135
|
||||||
|
msgid "blocks.appearance.color.olive"
|
||||||
|
msgstr "橄榄灰"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/layout/messages.ts:12
|
||||||
|
msgid "blocks.breadcrumb.menuLabel"
|
||||||
|
msgstr "打开面包屑菜单"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/navigation/messages.ts:8
|
||||||
|
msgid "blocks.navigation.open"
|
||||||
|
msgstr "打开导航"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:12
|
||||||
|
msgid "blocks.appearance.command.description"
|
||||||
|
msgstr "打开主题与布局偏好"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/layout/messages.ts:24
|
||||||
|
msgid "blocks.userMenu.open"
|
||||||
|
msgstr "打开用户菜单"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:139
|
||||||
|
msgid "blocks.appearance.color.orange"
|
||||||
|
msgstr "橙色"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:108
|
||||||
|
msgid "app.navigation.trade-orders"
|
||||||
|
msgstr "订单中心"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:104
|
||||||
|
msgid "app.navigation.trade-and-fulfillment"
|
||||||
|
msgstr "交易与履约"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:272
|
||||||
|
msgid "app.navigation.system-members"
|
||||||
|
msgstr "组织成员"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:76
|
||||||
|
msgid "app.navigation.inventory-outbound"
|
||||||
|
msgstr "出库单"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:4
|
||||||
|
msgid "app.navigation.overview"
|
||||||
|
msgstr "概览"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/ui/src/components/i18n.tsx:61
|
||||||
|
msgid "ui.pagination.label"
|
||||||
|
msgstr "分页导航"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:324
|
||||||
|
msgid "app.navigation.system-settings-payments"
|
||||||
|
msgstr "支付渠道"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:264
|
||||||
|
msgid "app.navigation.finance-payouts"
|
||||||
|
msgstr "打款记录"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:284
|
||||||
|
msgid "app.navigation.system-members-permissions"
|
||||||
|
msgstr "权限策略"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:140
|
||||||
|
msgid "app.navigation.fulfillment-pickups"
|
||||||
|
msgstr "自提核销"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:143
|
||||||
|
msgid "blocks.appearance.color.pink"
|
||||||
|
msgstr "粉色"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:212
|
||||||
|
msgid "app.navigation.loyalty-points"
|
||||||
|
msgstr "积分规则"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/ui/src/components/i18n.tsx:65
|
||||||
|
msgid "ui.pagination.previous"
|
||||||
|
msgstr "上一页"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/ui/src/components/i18n.tsx:73
|
||||||
|
msgid "ui.carousel.previousSlide"
|
||||||
|
msgstr "上一张"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:68
|
||||||
|
msgid "blocks.appearance.primaryColor"
|
||||||
|
msgstr "主要颜色"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:236
|
||||||
|
msgid "app.navigation.report-products"
|
||||||
|
msgstr "商品分析"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:40
|
||||||
|
msgid "app.navigation.catalog-products-attributes"
|
||||||
|
msgstr "商品属性"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:36
|
||||||
|
msgid "app.navigation.catalog-products-bundles"
|
||||||
|
msgstr "组合商品"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:32
|
||||||
|
msgid "app.navigation.catalog-products-drafts"
|
||||||
|
msgstr "商品草稿"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:28
|
||||||
|
msgid "app.navigation.catalog-products-list"
|
||||||
|
msgstr "商品列表"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:24
|
||||||
|
msgid "app.navigation.catalog-products"
|
||||||
|
msgstr "商品中心"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:20
|
||||||
|
msgid "app.navigation.catalog-and-inventory"
|
||||||
|
msgstr "商品与库存"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:147
|
||||||
|
msgid "blocks.appearance.color.purple"
|
||||||
|
msgstr "紫色"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:12
|
||||||
|
msgid "app.navigation.overview-realtime"
|
||||||
|
msgstr "实时监控"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:256
|
||||||
|
msgid "app.navigation.finance-reconciliation"
|
||||||
|
msgstr "对账管理"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:332
|
||||||
|
msgid "app.navigation.system-recycle-bin"
|
||||||
|
msgstr "回收站"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:151
|
||||||
|
msgid "blocks.appearance.color.red"
|
||||||
|
msgstr "红色"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:204
|
||||||
|
msgid "app.navigation.marketing-referrals"
|
||||||
|
msgstr "邀请有礼"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/layout/messages.ts:32
|
||||||
|
msgid "blocks.refresh.label"
|
||||||
|
msgstr "刷新当前页面数据"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/layout/messages.ts:28
|
||||||
|
msgid "blocks.refresh.description"
|
||||||
|
msgstr "刷新当前页面的数据"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:156
|
||||||
|
msgid "app.navigation.after-sales-refunds"
|
||||||
|
msgstr "退款审核"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:228
|
||||||
|
msgid "app.navigation.report-center"
|
||||||
|
msgstr "数据报表"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:152
|
||||||
|
msgid "app.navigation.after-sales-returns"
|
||||||
|
msgstr "退货申请"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:184
|
||||||
|
msgid "app.navigation.customer-risk-list"
|
||||||
|
msgstr "风险名单"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:280
|
||||||
|
msgid "app.navigation.system-members-roles"
|
||||||
|
msgstr "角色管理"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:155
|
||||||
|
msgid "blocks.appearance.color.rose"
|
||||||
|
msgstr "玫红色"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:232
|
||||||
|
msgid "app.navigation.report-sales"
|
||||||
|
msgstr "销售分析"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:252
|
||||||
|
msgid "app.navigation.finance-settlements"
|
||||||
|
msgstr "结算账单"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:144
|
||||||
|
msgid "app.navigation.fulfillment-freight-rules"
|
||||||
|
msgstr "运费规则"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:328
|
||||||
|
msgid "app.navigation.system-settings-shipping"
|
||||||
|
msgstr "配送设置"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/ui/src/components/i18n.tsx:77
|
||||||
|
msgid "ui.sidebar.title"
|
||||||
|
msgstr "侧栏"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:159
|
||||||
|
msgid "blocks.appearance.color.sky"
|
||||||
|
msgstr "天蓝色"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:44
|
||||||
|
msgid "app.navigation.catalog-product-specifications"
|
||||||
|
msgstr "规格模板"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:80
|
||||||
|
msgid "app.navigation.inventory-transfers"
|
||||||
|
msgstr "调拨单"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:163
|
||||||
|
msgid "blocks.appearance.color.stone"
|
||||||
|
msgstr "石灰"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:320
|
||||||
|
msgid "app.navigation.system-settings-store"
|
||||||
|
msgstr "门店资料"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:216
|
||||||
|
msgid "app.navigation.loyalty-stored-value"
|
||||||
|
msgstr "储值账户"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:84
|
||||||
|
msgid "blocks.appearance.toggleTheme.description"
|
||||||
|
msgstr "在浅色与深色主题之间切换"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:44
|
||||||
|
msgid "blocks.appearance.mode.system"
|
||||||
|
msgstr "系统"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:268
|
||||||
|
msgid "app.navigation.system-management"
|
||||||
|
msgstr "系统管理"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:316
|
||||||
|
msgid "app.navigation.system-settings"
|
||||||
|
msgstr "系统配置"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:16
|
||||||
|
msgid "app.navigation.overview-tasks"
|
||||||
|
msgstr "待办中心"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:167
|
||||||
|
msgid "blocks.appearance.color.taupe"
|
||||||
|
msgstr "灰褐"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:171
|
||||||
|
msgid "blocks.appearance.color.teal"
|
||||||
|
msgstr "蓝绿色"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:48
|
||||||
|
msgid "blocks.appearance.mode.system.description"
|
||||||
|
msgstr "界面主题将跟随系统外观设置"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:32
|
||||||
|
msgid "blocks.appearance.mode.dark.description"
|
||||||
|
msgstr "界面将始终使用深色主题"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:56
|
||||||
|
msgid "blocks.appearance.mode.light.description"
|
||||||
|
msgstr "界面将始终使用浅色主题"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:72
|
||||||
|
msgid "blocks.appearance.description"
|
||||||
|
msgstr "主题与布局偏好"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:76
|
||||||
|
msgid "blocks.appearance.mode.title"
|
||||||
|
msgstr "主题模式"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:40
|
||||||
|
msgid "blocks.appearance.theme.dark.description"
|
||||||
|
msgstr "当系统设置为深色模式时,将使用此主题"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:64
|
||||||
|
msgid "blocks.appearance.theme.light.description"
|
||||||
|
msgstr "当系统设置为浅色模式时,将使用此主题"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/ui/src/components/i18n.tsx:85
|
||||||
|
msgid "ui.sidebar.toggle"
|
||||||
|
msgstr "切换侧栏"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:80
|
||||||
|
msgid "blocks.appearance.toggleTheme"
|
||||||
|
msgstr "切换主题"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:160
|
||||||
|
msgid "app.navigation.after-sales-disputes"
|
||||||
|
msgstr "交易纠纷"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/notifications/messages.ts:36
|
||||||
|
msgid "blocks.notifications.retry"
|
||||||
|
msgstr "重新加载"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/notifications/messages.ts:20
|
||||||
|
msgid "blocks.notifications.error.title"
|
||||||
|
msgstr "无法加载通知"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:20
|
||||||
|
msgid "blocks.appearance.compact.description"
|
||||||
|
msgstr "启用后,页面主内容将使用紧凑宽度"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/layout/messages.ts:36
|
||||||
|
msgid "blocks.userMenu.title"
|
||||||
|
msgstr "用户菜单"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/notifications/messages.ts:44
|
||||||
|
msgid "blocks.notifications.viewAll"
|
||||||
|
msgstr "查看全部"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/notifications/messages.ts:4
|
||||||
|
msgid "blocks.notifications.description"
|
||||||
|
msgstr "查看并处理通知"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:175
|
||||||
|
msgid "blocks.appearance.color.violet"
|
||||||
|
msgstr "紫罗兰色"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: src/data/navigation-messages.ts:312
|
||||||
|
msgid "app.navigation.system-integration-webhooks"
|
||||||
|
msgstr "Webhook 配置"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:179
|
||||||
|
msgid "blocks.appearance.color.yellow"
|
||||||
|
msgstr "黄色"
|
||||||
|
|
||||||
|
#. js-lingui-explicit-id
|
||||||
|
#: ../../packages/blocks/src/blocks/appearance/messages.ts:183
|
||||||
|
msgid "blocks.appearance.color.zinc"
|
||||||
|
msgstr "锌灰"
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,48 +1,76 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { formatForDisplay } from "@tanstack/react-hotkeys"
|
||||||
|
import {
|
||||||
|
LexicalActions,
|
||||||
|
LexicalBubbleToolbar,
|
||||||
|
LexicalContent,
|
||||||
|
LexicalDraggableBlockPlugin,
|
||||||
|
LexicalFixedToolbar,
|
||||||
|
LexicalFooter,
|
||||||
|
LexicalRoot,
|
||||||
|
} from "@workspace/lexical"
|
||||||
import { Button } from "@workspace/ui/components/button"
|
import { Button } from "@workspace/ui/components/button"
|
||||||
import { Slider } from "@workspace/ui/components/slider"
|
|
||||||
import { Switch } from "@workspace/ui/components/switch"
|
const initialEditorHtml = [
|
||||||
|
"<h2>Welcome to the Lexical editor</h2>",
|
||||||
|
"<p>This example uses declarative actions to compose reusable editor features.</p>",
|
||||||
|
"<ul><li>Format text with the toolbar</li><li>Insert links, images, videos, lists, dates, and more</li><li>Read the generated HTML beside the editor</li></ul>",
|
||||||
|
].join("")
|
||||||
|
|
||||||
export function DashboardPage() {
|
export function DashboardPage() {
|
||||||
return (
|
const [html, setHtml] = React.useState(initialEditorHtml)
|
||||||
<>
|
|
||||||
<img
|
|
||||||
className="h-auto w-full"
|
|
||||||
src="https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/background/background-3-blur.webp"
|
|
||||||
/>
|
|
||||||
<div className="flex max-w-md min-w-0 flex-col gap-4 text-sm leading-loose">
|
|
||||||
<div>
|
|
||||||
<h1 className="font-medium">Project ready!</h1>
|
|
||||||
<p>You may now add components and start building.</p>
|
|
||||||
<p>We've already added the button component for you.</p>
|
|
||||||
<Button variant="ghost" className="mt-2">
|
|
||||||
Button
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Switch />
|
|
||||||
<Slider
|
|
||||||
defaultValue={[75]}
|
|
||||||
max={100}
|
|
||||||
step={1}
|
|
||||||
className="mx-auto w-full max-w-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="font-mono text-xs text-muted-foreground">
|
|
||||||
(Press <kbd>Mod+D</kbd> to toggle dark mode)
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
return (
|
||||||
<span className="mr-2">breakpoint:</span>
|
<div className="flex min-w-0 flex-col gap-6 bg-background">
|
||||||
<span className="hidden mobile:inline">mobile</span>
|
<div>
|
||||||
<span className="hidden tablet:inline">tablet</span>
|
<h1 className="text-xl font-medium">Lexical editor</h1>
|
||||||
<span className="hidden desktop:inline">desktop</span>
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
</div>
|
Actions declare their controls and editor dependencies in one place.
|
||||||
<div>
|
</p>
|
||||||
<span className="mr-2">collapsed:</span>
|
|
||||||
<span className="hidden collapsed:inline">true</span>
|
|
||||||
<span className="inline collapsed:hidden">false</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
|
<div className="grid min-w-0 gap-6 xl:grid-cols-[minmax(0,1fr)_minmax(20rem,0.7fr)]">
|
||||||
|
<section className="min-w-0">
|
||||||
|
<div className="mb-2 flex items-center justify-between gap-3">
|
||||||
|
<h2 className="font-medium">Editor</h2>
|
||||||
|
<code className="text-xs text-muted-foreground">
|
||||||
|
useDefaults="full"
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
<LexicalRoot value={html} onChange={setHtml} className="shadow-xs">
|
||||||
|
<LexicalActions useDefaults="full" />
|
||||||
|
<LexicalFixedToolbar />
|
||||||
|
<LexicalContent
|
||||||
|
className="min-h-72"
|
||||||
|
placeholder="Write something…"
|
||||||
|
/>
|
||||||
|
<LexicalDraggableBlockPlugin />
|
||||||
|
<LexicalBubbleToolbar aria-label="Selected text formatting" />
|
||||||
|
<LexicalFooter className="flex items-center justify-between gap-3 text-xs text-muted-foreground">
|
||||||
|
<span>HTML output updates as you type.</span>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => setHtml("")}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
</LexicalFooter>
|
||||||
|
</LexicalRoot>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="flex min-w-0 flex-col">
|
||||||
|
<h2 className="mb-2 font-medium">Generated HTML</h2>
|
||||||
|
<pre className="min-h-72 flex-1 overflow-auto rounded-lg border bg-muted/30 p-4 text-xs leading-relaxed break-words whitespace-pre-wrap">
|
||||||
|
{html || "The editor is empty."}
|
||||||
|
</pre>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="font-mono text-xs text-muted-foreground">
|
||||||
|
Press <kbd>{formatForDisplay("Mod+D")}</kbd> to toggle dark mode.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,10 +24,19 @@ import { HotkeysProvider } from "@tanstack/react-hotkeys"
|
|||||||
import { TooltipProvider } from "@workspace/ui/components/tooltip"
|
import { TooltipProvider } from "@workspace/ui/components/tooltip"
|
||||||
import { QueryClientProvider } from "@tanstack/react-query"
|
import { QueryClientProvider } from "@tanstack/react-query"
|
||||||
import { AppI18nProvider } from "@/i18n/app-i18n-provider"
|
import { AppI18nProvider } from "@/i18n/app-i18n-provider"
|
||||||
|
import { getAppLocale } from "@/server/app-locale.functions"
|
||||||
|
import type { AppLocale } from "@/i18n/app-locales"
|
||||||
import { queryClient } from "@/lib/query-client"
|
import { queryClient } from "@/lib/query-client"
|
||||||
|
|
||||||
export const Route = createRootRoute({
|
export const Route = createRootRoute({
|
||||||
loader: () => getUiState(),
|
loader: async () => {
|
||||||
|
const [initialState, initialLocale] = await Promise.all([
|
||||||
|
getUiState(),
|
||||||
|
getAppLocale(),
|
||||||
|
])
|
||||||
|
|
||||||
|
return { initialLocale, initialState }
|
||||||
|
},
|
||||||
head: () => ({
|
head: () => ({
|
||||||
meta: [
|
meta: [
|
||||||
{ charSet: "utf-8" },
|
{ charSet: "utf-8" },
|
||||||
@@ -48,11 +57,11 @@ export const Route = createRootRoute({
|
|||||||
})
|
})
|
||||||
|
|
||||||
function RootComponent() {
|
function RootComponent() {
|
||||||
const initialState = Route.useLoaderData()
|
const { initialLocale, initialState } = Route.useLoaderData()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RootDocument initialState={initialState}>
|
<RootDocument initialLocale={initialLocale} initialState={initialState}>
|
||||||
<AppI18nProvider>
|
<AppI18nProvider locale={initialLocale}>
|
||||||
<BreakpointReactor />
|
<BreakpointReactor />
|
||||||
<UiStateProvider
|
<UiStateProvider
|
||||||
initialState={initialState}
|
initialState={initialState}
|
||||||
@@ -94,9 +103,11 @@ function getUiInitializationScript(state: UiState) {
|
|||||||
|
|
||||||
function RootDocument({
|
function RootDocument({
|
||||||
children,
|
children,
|
||||||
|
initialLocale,
|
||||||
initialState,
|
initialState,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
|
initialLocale: AppLocale
|
||||||
initialState: UiState
|
initialState: UiState
|
||||||
}>) {
|
}>) {
|
||||||
const themeMode = initialState["theme-mode"]
|
const themeMode = initialState["theme-mode"]
|
||||||
@@ -110,7 +121,7 @@ function RootDocument({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
lang="zh-CN"
|
lang={initialLocale === "zh-Hans" ? "zh-CN" : "en-US"}
|
||||||
suppressHydrationWarning
|
suppressHydrationWarning
|
||||||
className={className || undefined}
|
className={className || undefined}
|
||||||
style={themeMode === "system" ? undefined : { colorScheme: themeMode }}
|
style={themeMode === "system" ? undefined : { colorScheme: themeMode }}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
fetchMockNotifications,
|
fetchMockNotifications,
|
||||||
notificationsQueryKey,
|
notificationsQueryKey,
|
||||||
} from "@/data/notifications"
|
} from "@/data/notifications"
|
||||||
|
import { AppLanguageSwitcher } from "@/i18n/app-language-switcher"
|
||||||
|
|
||||||
export const Route = createFileRoute("/_app")({
|
export const Route = createFileRoute("/_app")({
|
||||||
component: ApplicationLayoutRoute,
|
component: ApplicationLayoutRoute,
|
||||||
@@ -17,6 +18,7 @@ function ApplicationLayoutRoute() {
|
|||||||
return (
|
return (
|
||||||
<AppLayout
|
<AppLayout
|
||||||
chatThreads={chatThreads}
|
chatThreads={chatThreads}
|
||||||
|
headerActions={<AppLanguageSwitcher />}
|
||||||
navigationGroups={primaryNavigationGroups}
|
navigationGroups={primaryNavigationGroups}
|
||||||
notifications={{
|
notifications={{
|
||||||
queryFn: fetchMockNotifications,
|
queryFn: fetchMockNotifications,
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { createServerFn } from "@tanstack/react-start"
|
||||||
|
|
||||||
|
import { isAppLocale } from "@/i18n/app-locales"
|
||||||
|
import { readAppLocale, writeAppLocale } from "./app-locale.server"
|
||||||
|
|
||||||
|
export const getAppLocale = createServerFn({ method: "GET" }).handler(() => {
|
||||||
|
return readAppLocale()
|
||||||
|
})
|
||||||
|
|
||||||
|
export const setAppLocale = createServerFn({ method: "POST" })
|
||||||
|
.validator((value: unknown) => {
|
||||||
|
if (!isAppLocale(value)) {
|
||||||
|
throw new Error("Invalid app locale")
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
})
|
||||||
|
.handler(({ data }) => {
|
||||||
|
writeAppLocale(data)
|
||||||
|
})
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import "@tanstack/react-start/server-only"
|
||||||
|
import { getCookie, setCookie } from "@tanstack/react-start/server"
|
||||||
|
|
||||||
|
import {
|
||||||
|
DEFAULT_APP_LOCALE,
|
||||||
|
isAppLocale,
|
||||||
|
type AppLocale,
|
||||||
|
} from "@/i18n/app-locales"
|
||||||
|
|
||||||
|
const APP_LOCALE_COOKIE = "ui-locale"
|
||||||
|
const COOKIE_MAX_AGE = 60 * 60 * 24 * 365
|
||||||
|
|
||||||
|
export function readAppLocale(): AppLocale {
|
||||||
|
const locale = getCookie(APP_LOCALE_COOKIE)
|
||||||
|
|
||||||
|
return isAppLocale(locale) ? locale : DEFAULT_APP_LOCALE
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writeAppLocale(locale: AppLocale) {
|
||||||
|
setCookie(APP_LOCALE_COOKIE, locale, {
|
||||||
|
maxAge: COOKIE_MAX_AGE,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "lax",
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
@import "@workspace/ui/globals.css";
|
@import "@workspace/ui/globals.css";
|
||||||
@import "@workspace/blocks/globals.css";
|
@import "@workspace/blocks/globals.css";
|
||||||
|
@import "@workspace/lexical/globals.css";
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--ui-content-top-gap: --spacing(2);
|
--ui-content-top-gap: --spacing(2);
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"resolveJsonModule": true,
|
|
||||||
"verbatimModuleSyntax": true,
|
"verbatimModuleSyntax": true,
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
@@ -21,5 +20,5 @@
|
|||||||
"erasableSyntaxOnly": true,
|
"erasableSyntaxOnly": true,
|
||||||
"noFallthroughCasesInSwitch": true
|
"noFallthroughCasesInSwitch": true
|
||||||
},
|
},
|
||||||
"include": ["vite.config.ts", "lingui.config.ts"]
|
"include": ["vite.config.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import path from "node:path"
|
||||||
|
import react from "@vitejs/plugin-react"
|
||||||
|
import { defineConfig } from "vitest/config"
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": path.resolve(__dirname, "./src"),
|
||||||
|
},
|
||||||
|
tsconfigPaths: true,
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
environment: "jsdom",
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -5,9 +5,8 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "simple-react-app-kit",
|
"name": "simple-react-app-kit",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "^3.8.3",
|
"oxfmt": "^0.61.0",
|
||||||
"prettier-plugin-tailwindcss": "^0.8.0",
|
"oxlint": "^1.76.0",
|
||||||
"turbo": "^2.9.18",
|
|
||||||
"typescript": "~6",
|
"typescript": "~6",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -18,8 +17,6 @@
|
|||||||
"@base-ui/react": "^1.6.0",
|
"@base-ui/react": "^1.6.0",
|
||||||
"@base-ui/utils": "^0.3.1",
|
"@base-ui/utils": "^0.3.1",
|
||||||
"@floating-ui/utils": "^0.2.12",
|
"@floating-ui/utils": "^0.2.12",
|
||||||
"@hugeicons/core-free-icons": "^4.2.3",
|
|
||||||
"@hugeicons/react": "^1.1.9",
|
|
||||||
"@iconify/react": "^6.0.2",
|
"@iconify/react": "^6.0.2",
|
||||||
"@lingui/core": "^6",
|
"@lingui/core": "^6",
|
||||||
"@tanstack/react-form": "^1.33.2",
|
"@tanstack/react-form": "^1.33.2",
|
||||||
@@ -29,9 +26,13 @@
|
|||||||
"@tanstack/react-start": "^1.168.32",
|
"@tanstack/react-start": "^1.168.32",
|
||||||
"@workspace/blocks": "workspace:*",
|
"@workspace/blocks": "workspace:*",
|
||||||
"@workspace/i18n": "workspace:*",
|
"@workspace/i18n": "workspace:*",
|
||||||
|
"@workspace/icons": "workspace:*",
|
||||||
|
"@workspace/lexical": "workspace:*",
|
||||||
"@workspace/ui": "workspace:*",
|
"@workspace/ui": "workspace:*",
|
||||||
|
"country-flag-icons": "^1.6.20",
|
||||||
"lucide-react": "^1.27.0",
|
"lucide-react": "^1.27.0",
|
||||||
"react": "^19.2.6",
|
"react": "^19.2.6",
|
||||||
|
"react-day-picker": "^10.0.1",
|
||||||
"react-dom": "^19.2.6",
|
"react-dom": "^19.2.6",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
},
|
},
|
||||||
@@ -42,6 +43,7 @@
|
|||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"@vitejs/plugin-react": "^6",
|
"@vitejs/plugin-react": "^6",
|
||||||
|
"jsdom": "^30.0.1",
|
||||||
"nitro": "^3.0.260610-beta",
|
"nitro": "^3.0.260610-beta",
|
||||||
"typescript": "~6",
|
"typescript": "~6",
|
||||||
"vite": "^8",
|
"vite": "^8",
|
||||||
@@ -55,12 +57,13 @@
|
|||||||
"@base-ui/react": "^1.6.0",
|
"@base-ui/react": "^1.6.0",
|
||||||
"@base-ui/utils": "^0.3.1",
|
"@base-ui/utils": "^0.3.1",
|
||||||
"@floating-ui/utils": "^0.2.12",
|
"@floating-ui/utils": "^0.2.12",
|
||||||
"@hugeicons/core-free-icons": "^4.2.3",
|
|
||||||
"@hugeicons/react": "^1.1.9",
|
|
||||||
"@tanstack/react-hotkeys": "^0.10.0",
|
"@tanstack/react-hotkeys": "^0.10.0",
|
||||||
"@tanstack/react-query": "^5.101.4",
|
"@tanstack/react-query": "^5.101.4",
|
||||||
"@tanstack/react-router": "^1.170.18",
|
"@tanstack/react-router": "^1.170.18",
|
||||||
|
"@workspace/i18n": "workspace:*",
|
||||||
|
"@workspace/icons": "workspace:*",
|
||||||
"@workspace/ui": "workspace:*",
|
"@workspace/ui": "workspace:*",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
"lucide-react": "^1.27.0",
|
"lucide-react": "^1.27.0",
|
||||||
"react": "^19.2.6",
|
"react": "^19.2.6",
|
||||||
"react-dom": "^19.2.6",
|
"react-dom": "^19.2.6",
|
||||||
@@ -86,7 +89,9 @@
|
|||||||
"@lingui/core": "^6",
|
"@lingui/core": "^6",
|
||||||
"@lingui/react": "^6",
|
"@lingui/react": "^6",
|
||||||
"@vitejs/plugin-react": "^6",
|
"@vitejs/plugin-react": "^6",
|
||||||
|
"ansi-to-react": "^6.2.6",
|
||||||
"cac": "^7.0.0",
|
"cac": "^7.0.0",
|
||||||
|
"lucide-react": "^1.27.0",
|
||||||
"open": "^11.0.0",
|
"open": "^11.0.0",
|
||||||
"react": "^19.2.6",
|
"react": "^19.2.6",
|
||||||
"react-dom": "^19.2.6",
|
"react-dom": "^19.2.6",
|
||||||
@@ -102,6 +107,47 @@
|
|||||||
"vitest": "^4",
|
"vitest": "^4",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"packages/icons": {
|
||||||
|
"name": "@workspace/icons",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@hugeicons/core-free-icons": "^4.2.3",
|
||||||
|
"@hugeicons/react": "^1.1.9",
|
||||||
|
"react": "^19.2.6",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^19",
|
||||||
|
"typescript": "~6",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages/lexical": {
|
||||||
|
"name": "@workspace/lexical",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@lexical/extension": "^0.48.0",
|
||||||
|
"@lexical/html": "^0.48.0",
|
||||||
|
"@lexical/link": "^0.48.0",
|
||||||
|
"@lexical/list": "^0.48.0",
|
||||||
|
"@lexical/react": "^0.48.0",
|
||||||
|
"@lexical/rich-text": "^0.48.0",
|
||||||
|
"@lexical/selection": "^0.48.0",
|
||||||
|
"@lexical/utils": "^0.48.0",
|
||||||
|
"@workspace/i18n": "workspace:*",
|
||||||
|
"@workspace/ui": "workspace:*",
|
||||||
|
"lexical": "^0.48.0",
|
||||||
|
"lucide-react": "^1.27.0",
|
||||||
|
"react": "^19.2.6",
|
||||||
|
"react-dom": "^19.2.6",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@testing-library/react": "^16.3.2",
|
||||||
|
"@types/react": "^19",
|
||||||
|
"@types/react-dom": "^19",
|
||||||
|
"jsdom": "^30.0.1",
|
||||||
|
"typescript": "~6",
|
||||||
|
"vitest": "^4.1.10",
|
||||||
|
},
|
||||||
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@workspace/ui",
|
"name": "@workspace/ui",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
@@ -129,7 +175,6 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4",
|
"@tailwindcss/vite": "^4",
|
||||||
"@turbo/gen": "^2.9.15",
|
|
||||||
"@types/node": "^24",
|
"@types/node": "^24",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
@@ -301,6 +346,8 @@
|
|||||||
|
|
||||||
"@floating-ui/dom": ["@floating-ui/dom@1.8.0", "", { "dependencies": { "@floating-ui/core": "^1.8.0", "@floating-ui/utils": "^0.2.12" } }, "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg=="],
|
"@floating-ui/dom": ["@floating-ui/dom@1.8.0", "", { "dependencies": { "@floating-ui/core": "^1.8.0", "@floating-ui/utils": "^0.2.12" } }, "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg=="],
|
||||||
|
|
||||||
|
"@floating-ui/react": ["@floating-ui/react@0.27.20", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.9", "@floating-ui/utils": "^0.2.12", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, "sha512-CMqMy7OaXl9W0eq1Uy7L7i2Y/anPvHmFmESd2CEw0t5YvZhcVCeo4MBevAmswRllX7Y2dEidA4ozGPunLSTQpw=="],
|
||||||
|
|
||||||
"@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.9", "", { "dependencies": { "@floating-ui/dom": "^1.8.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg=="],
|
"@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.9", "", { "dependencies": { "@floating-ui/dom": "^1.8.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg=="],
|
||||||
|
|
||||||
"@floating-ui/utils": ["@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="],
|
"@floating-ui/utils": ["@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="],
|
||||||
@@ -317,38 +364,6 @@
|
|||||||
|
|
||||||
"@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
|
"@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
|
||||||
|
|
||||||
"@inquirer/ansi": ["@inquirer/ansi@1.0.2", "", {}, "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ=="],
|
|
||||||
|
|
||||||
"@inquirer/checkbox": ["@inquirer/checkbox@4.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/core": "^10.3.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA=="],
|
|
||||||
|
|
||||||
"@inquirer/confirm": ["@inquirer/confirm@5.1.21", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ=="],
|
|
||||||
|
|
||||||
"@inquirer/core": ["@inquirer/core@10.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A=="],
|
|
||||||
|
|
||||||
"@inquirer/editor": ["@inquirer/editor@4.2.23", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/external-editor": "^1.0.3", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ=="],
|
|
||||||
|
|
||||||
"@inquirer/expand": ["@inquirer/expand@4.0.23", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew=="],
|
|
||||||
|
|
||||||
"@inquirer/external-editor": ["@inquirer/external-editor@1.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA=="],
|
|
||||||
|
|
||||||
"@inquirer/figures": ["@inquirer/figures@1.0.15", "", {}, "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g=="],
|
|
||||||
|
|
||||||
"@inquirer/input": ["@inquirer/input@4.3.1", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g=="],
|
|
||||||
|
|
||||||
"@inquirer/number": ["@inquirer/number@3.0.23", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg=="],
|
|
||||||
|
|
||||||
"@inquirer/password": ["@inquirer/password@4.0.23", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA=="],
|
|
||||||
|
|
||||||
"@inquirer/prompts": ["@inquirer/prompts@7.10.1", "", { "dependencies": { "@inquirer/checkbox": "^4.3.2", "@inquirer/confirm": "^5.1.21", "@inquirer/editor": "^4.2.23", "@inquirer/expand": "^4.0.23", "@inquirer/input": "^4.3.1", "@inquirer/number": "^3.0.23", "@inquirer/password": "^4.0.23", "@inquirer/rawlist": "^4.1.11", "@inquirer/search": "^3.2.2", "@inquirer/select": "^4.4.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg=="],
|
|
||||||
|
|
||||||
"@inquirer/rawlist": ["@inquirer/rawlist@4.1.11", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw=="],
|
|
||||||
|
|
||||||
"@inquirer/search": ["@inquirer/search@3.2.2", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA=="],
|
|
||||||
|
|
||||||
"@inquirer/select": ["@inquirer/select@4.4.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/core": "^10.3.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w=="],
|
|
||||||
|
|
||||||
"@inquirer/type": ["@inquirer/type@3.0.10", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="],
|
|
||||||
|
|
||||||
"@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="],
|
"@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="],
|
||||||
|
|
||||||
"@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="],
|
"@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="],
|
||||||
@@ -365,6 +380,52 @@
|
|||||||
|
|
||||||
"@json-render/core": ["@json-render/core@0.19.0", "", { "dependencies": { "zod": "^4.3.6" } }, "sha512-vvcyZ+10EDZKbEyB1J2kXOGfDaiZR2LurZGSqi2r5STHyKr+Te85DWaBxTwRGgM7U1LtIvNx85BzzjElRKoAIg=="],
|
"@json-render/core": ["@json-render/core@0.19.0", "", { "dependencies": { "zod": "^4.3.6" } }, "sha512-vvcyZ+10EDZKbEyB1J2kXOGfDaiZR2LurZGSqi2r5STHyKr+Te85DWaBxTwRGgM7U1LtIvNx85BzzjElRKoAIg=="],
|
||||||
|
|
||||||
|
"@lexical/a11y": ["@lexical/a11y@0.48.0", "", { "dependencies": { "@lexical/extension": "0.48.0", "@lexical/utils": "0.48.0", "lexical": "0.48.0" }, "peerDependencies": { "typescript": ">=5.2" }, "optionalPeers": ["typescript"] }, "sha512-18W4ehyipkUim4YVoDZitoH63Om3j6iCN4c84zdqE9RgkWf/PE4rvI/8BHTm6Ni7NkVE14nimXgkpaP5ok15zA=="],
|
||||||
|
|
||||||
|
"@lexical/clipboard": ["@lexical/clipboard@0.48.0", "", { "dependencies": { "@lexical/extension": "0.48.0", "@lexical/html": "0.48.0", "@lexical/internal": "0.48.0", "@lexical/list": "0.48.0", "@lexical/selection": "0.48.0", "@lexical/utils": "0.48.0", "@types/trusted-types": "^2.0.7", "lexical": "0.48.0" }, "peerDependencies": { "typescript": ">=5.2" }, "optionalPeers": ["typescript"] }, "sha512-xO2trk6+yBl8XXa/VNe20kXczmPxFoWtUHjidbBLEtlGBj+mo63pJj6H5o/WlZsoMKmIOJxwxsn2ejrS8G0/7A=="],
|
||||||
|
|
||||||
|
"@lexical/code-core": ["@lexical/code-core@0.48.0", "", { "dependencies": { "@lexical/extension": "0.48.0", "@lexical/html": "0.48.0", "@lexical/internal": "0.48.0", "@lexical/utils": "0.48.0", "lexical": "0.48.0" }, "peerDependencies": { "typescript": ">=5.2" }, "optionalPeers": ["typescript"] }, "sha512-+O1Ge06AuSo6+r8R2Xk6SkWG07H5/e4K/Scw9aqCM/BRjITxgvFTHTNvgQbaobCsP0uBJiwW1+ZGeWAWcBCY4w=="],
|
||||||
|
|
||||||
|
"@lexical/devtools-core": ["@lexical/devtools-core@0.48.0", "", { "dependencies": { "@lexical/html": "0.48.0", "@lexical/link": "0.48.0", "@lexical/mark": "0.48.0", "@lexical/table": "0.48.0", "@lexical/utils": "0.48.0", "lexical": "0.48.0" }, "peerDependencies": { "react": ">=18.x", "react-dom": ">=18.x", "typescript": ">=5.2" }, "optionalPeers": ["typescript"] }, "sha512-4kvKWW6ebgQnJNLXPLmw7dqgSChvzYIBNYtfuR6c48Sw+V/QXQTWqfIUbCIe5X4uG8EEXd5O/udXaJx7GBuP+w=="],
|
||||||
|
|
||||||
|
"@lexical/dragon": ["@lexical/dragon@0.48.0", "", { "dependencies": { "@lexical/extension": "0.48.0", "lexical": "0.48.0" }, "peerDependencies": { "typescript": ">=5.2" }, "optionalPeers": ["typescript"] }, "sha512-uPuu7fVca9vmL/Oz30CRZ7FIPIodwMrTgNsRmV8jE6Qd6a7RNiTW7r3+EhbhIdkLb/sjcWsYyOMyUR1TJAB0wQ=="],
|
||||||
|
|
||||||
|
"@lexical/extension": ["@lexical/extension@0.48.0", "", { "dependencies": { "@lexical/internal": "0.48.0", "@lexical/utils": "0.48.0", "@preact/signals-core": "^1.14.1", "lexical": "0.48.0" }, "peerDependencies": { "typescript": ">=5.2" }, "optionalPeers": ["typescript"] }, "sha512-4uBObgz84mVbQWiumndmIhkuJL0ojHiMwFSvSUM/FCo1YMVIZvpI56blI0y+2Vix/oLui9EgVQSJjjWV4NAszw=="],
|
||||||
|
|
||||||
|
"@lexical/hashtag": ["@lexical/hashtag@0.48.0", "", { "dependencies": { "@lexical/text": "0.48.0", "@lexical/utils": "0.48.0", "lexical": "0.48.0" }, "peerDependencies": { "typescript": ">=5.2" }, "optionalPeers": ["typescript"] }, "sha512-hPQtdnbVoNAFsmfnCGfgY7mDbvk6mIznlCRmIR7tLeQKXqz/0Tb6eH3W24EESk9hAF8wFUYNKWE1/Kb3Hl2vEQ=="],
|
||||||
|
|
||||||
|
"@lexical/history": ["@lexical/history@0.48.0", "", { "dependencies": { "@lexical/extension": "0.48.0", "@lexical/utils": "0.48.0", "lexical": "0.48.0" }, "peerDependencies": { "typescript": ">=5.2" }, "optionalPeers": ["typescript"] }, "sha512-NllvUfO+u3mfi5uC8k2CodwdzeeopFFVtMZ/NMifzFbZFysdWi9m9mqfO46NrEA1rSFOydyefv6oMzC8ULXInA=="],
|
||||||
|
|
||||||
|
"@lexical/html": ["@lexical/html@0.48.0", "", { "dependencies": { "@lexical/extension": "0.48.0", "@lexical/internal": "0.48.0", "@lexical/selection": "0.48.0", "@lexical/utils": "0.48.0", "lexical": "0.48.0" }, "peerDependencies": { "typescript": ">=5.2" }, "optionalPeers": ["typescript"] }, "sha512-uBxlgKl4YgSNEgHJSshdBqtGDzruWdx1ewop+u6faT67qHUdP3P0cUXIrG6NToDWvsL6fzCstAbN76PMER1Pnw=="],
|
||||||
|
|
||||||
|
"@lexical/internal": ["@lexical/internal@0.48.0", "", { "peerDependencies": { "typescript": ">=5.2" }, "optionalPeers": ["typescript"] }, "sha512-sRwg53K7N0ZQ7KNAvcCY38LSwGizbXP1zlR1lIojZp0GoqHWNvR+vL49t1wYXu1nXx3Osf4ilHHm+aGcwq5hTw=="],
|
||||||
|
|
||||||
|
"@lexical/link": ["@lexical/link@0.48.0", "", { "dependencies": { "@lexical/extension": "0.48.0", "@lexical/html": "0.48.0", "@lexical/internal": "0.48.0", "@lexical/utils": "0.48.0", "lexical": "0.48.0" }, "peerDependencies": { "typescript": ">=5.2" }, "optionalPeers": ["typescript"] }, "sha512-E0UDmNLUXs/yMCnnE7hbFO0CvhWghmqa+qqPksFfzLkpMHdPpdS1yg59YEbYoNHLi/DXIu4cFRvpHIEuooxwNg=="],
|
||||||
|
|
||||||
|
"@lexical/list": ["@lexical/list@0.48.0", "", { "dependencies": { "@lexical/extension": "0.48.0", "@lexical/html": "0.48.0", "@lexical/internal": "0.48.0", "@lexical/utils": "0.48.0", "lexical": "0.48.0" }, "peerDependencies": { "typescript": ">=5.2" }, "optionalPeers": ["typescript"] }, "sha512-9Qe/Vur44v9F9enj55SUzf79FVsijcGOQug7SpiIU8ekLr7JNzcilKYBYcZ6etGEo7bqQHsYMHXeJcSBbCI2zA=="],
|
||||||
|
|
||||||
|
"@lexical/mark": ["@lexical/mark@0.48.0", "", { "dependencies": { "@lexical/utils": "0.48.0", "lexical": "0.48.0" }, "peerDependencies": { "typescript": ">=5.2" }, "optionalPeers": ["typescript"] }, "sha512-DTtypWvnYSXyNUxEmsUnh4y0xXkmdk8Y72EZl8WDHcCwjqaLJlUakH7p/TuSJczs3uVSaoJzu0yh7oGkP1Vsvw=="],
|
||||||
|
|
||||||
|
"@lexical/markdown": ["@lexical/markdown@0.48.0", "", { "dependencies": { "@lexical/code-core": "0.48.0", "@lexical/internal": "0.48.0", "@lexical/link": "0.48.0", "@lexical/list": "0.48.0", "@lexical/rich-text": "0.48.0", "@lexical/selection": "0.48.0", "@lexical/text": "0.48.0", "@lexical/utils": "0.48.0", "lexical": "0.48.0" }, "peerDependencies": { "typescript": ">=5.2" }, "optionalPeers": ["typescript"] }, "sha512-1WasBenW4bEsa5xtnycVo8G2hcxIqyYLn3/r98yD2Y+54ZyeFuIQfOeJYhIgCh4YkKpfqyrFwkMQwAOsQDqZjA=="],
|
||||||
|
|
||||||
|
"@lexical/overflow": ["@lexical/overflow@0.48.0", "", { "dependencies": { "lexical": "0.48.0" }, "peerDependencies": { "typescript": ">=5.2" }, "optionalPeers": ["typescript"] }, "sha512-1YEvMz2tW3EbwrON9mjrkjMVl/vdTcPYSn9P1j6mf5gj0LOoLDNI4TbvSD4SViy+TDghxNdG8YdCISyU2b4YKA=="],
|
||||||
|
|
||||||
|
"@lexical/plain-text": ["@lexical/plain-text@0.48.0", "", { "dependencies": { "@lexical/clipboard": "0.48.0", "@lexical/dragon": "0.48.0", "@lexical/extension": "0.48.0", "@lexical/selection": "0.48.0", "@lexical/utils": "0.48.0", "lexical": "0.48.0" }, "peerDependencies": { "typescript": ">=5.2" }, "optionalPeers": ["typescript"] }, "sha512-q4f/4VZKVgCrIW2FhDFR2RII1BU0ljedPgEmJ8XQn1zc+JOFPom8Lp0lV5nyEvZaAJYwM/TfoJ9g2V7ESFKznA=="],
|
||||||
|
|
||||||
|
"@lexical/react": ["@lexical/react@0.48.0", "", { "dependencies": { "@floating-ui/react": "^0.27.19", "@lexical/a11y": "0.48.0", "@lexical/devtools-core": "0.48.0", "@lexical/dragon": "0.48.0", "@lexical/extension": "0.48.0", "@lexical/hashtag": "0.48.0", "@lexical/history": "0.48.0", "@lexical/internal": "0.48.0", "@lexical/link": "0.48.0", "@lexical/list": "0.48.0", "@lexical/mark": "0.48.0", "@lexical/markdown": "0.48.0", "@lexical/overflow": "0.48.0", "@lexical/plain-text": "0.48.0", "@lexical/rich-text": "0.48.0", "@lexical/table": "0.48.0", "@lexical/text": "0.48.0", "@lexical/utils": "0.48.0", "@lexical/yjs": "0.48.0", "lexical": "0.48.0" }, "peerDependencies": { "react": ">=18.x", "react-dom": ">=18.x", "typescript": ">=5.2", "yjs": ">=13.5.22" }, "optionalPeers": ["typescript", "yjs"] }, "sha512-uVh9/QSrbtjLjVbxfJ+sfiMyhUq/rv7H6uBEVDDIw1rkZJSDY1fvf/CX+dyKgwcDFjKQZ8/9i5f9UCVPeQ01hA=="],
|
||||||
|
|
||||||
|
"@lexical/rich-text": ["@lexical/rich-text@0.48.0", "", { "dependencies": { "@lexical/clipboard": "0.48.0", "@lexical/dragon": "0.48.0", "@lexical/extension": "0.48.0", "@lexical/html": "0.48.0", "@lexical/selection": "0.48.0", "@lexical/utils": "0.48.0", "lexical": "0.48.0" }, "peerDependencies": { "typescript": ">=5.2" }, "optionalPeers": ["typescript"] }, "sha512-QMXFnwCKAQ4yzxvx5FwmANcx3K+NaBkGTxAVD8s8pOKDD/U5rzDS1iIvhH6TLWaFp7VzvLmLB+Sl1Ie/RnkaDQ=="],
|
||||||
|
|
||||||
|
"@lexical/selection": ["@lexical/selection@0.48.0", "", { "dependencies": { "@lexical/internal": "0.48.0", "lexical": "0.48.0" }, "peerDependencies": { "typescript": ">=5.2" }, "optionalPeers": ["typescript"] }, "sha512-Uc0wTrEtHcYK6z/aHHjkgH3vX/R4Bf8mO+qH3VbxfSAKYzNYktM0j+ZGdq5kIEL4frnw/9SulNCXlH7xpjuDgA=="],
|
||||||
|
|
||||||
|
"@lexical/table": ["@lexical/table@0.48.0", "", { "dependencies": { "@lexical/clipboard": "0.48.0", "@lexical/extension": "0.48.0", "@lexical/html": "0.48.0", "@lexical/internal": "0.48.0", "@lexical/utils": "0.48.0", "lexical": "0.48.0" }, "peerDependencies": { "typescript": ">=5.2" }, "optionalPeers": ["typescript"] }, "sha512-t9Mz7q6ODLUz0lG5Xn9EY/5YiVpTHCqlPQP4EtFXlnBQT3DuKeDS3cC0Cn8sGSZc11YY5OLDfWpB64Frs9BL3g=="],
|
||||||
|
|
||||||
|
"@lexical/text": ["@lexical/text@0.48.0", "", { "dependencies": { "@lexical/internal": "0.48.0", "lexical": "0.48.0" }, "peerDependencies": { "typescript": ">=5.2" }, "optionalPeers": ["typescript"] }, "sha512-ktTMRbsX4wKxdG2OpZCkrqtt8k9Vg/ZpWdukOQ0r1xPRtCuL1T+q91l7cy2ywIuCfMGYS0aoZGB4LdpUMe/H1g=="],
|
||||||
|
|
||||||
|
"@lexical/utils": ["@lexical/utils@0.48.0", "", { "dependencies": { "@lexical/internal": "0.48.0", "@lexical/selection": "0.48.0", "lexical": "0.48.0" }, "peerDependencies": { "typescript": ">=5.2" }, "optionalPeers": ["typescript"] }, "sha512-W4k4P+y6jmRfna8+ad4X+iMd5h8es5PC3bUw5tbi7MRApxaaFG/0w+uJiZVSwbT2Q6JnA2xhBaqzPgt/Gn6djg=="],
|
||||||
|
|
||||||
|
"@lexical/yjs": ["@lexical/yjs@0.48.0", "", { "dependencies": { "@lexical/internal": "0.48.0", "@lexical/selection": "0.48.0", "lexical": "0.48.0" }, "peerDependencies": { "typescript": ">=5.2", "yjs": ">=13.5.22" }, "optionalPeers": ["typescript"] }, "sha512-fFsE8EnPM/2KK9rMJ0z6T+Da5UW5V4P+XiAA03LoHTY5YQ/Oy8Q0i7Wcmocv/B/SpsY2o8g07euZEfudl9MVKA=="],
|
||||||
|
|
||||||
"@lingui/babel-plugin-extract-messages": ["@lingui/babel-plugin-extract-messages@6.6.0", "", { "dependencies": { "@lingui/conf": "6.6.0" } }, "sha512-QfCS4SjZYvVZFaiF44e/mLsdzCqQvRSGoVJWkb8nc51FkNpOZkwFLiwLtRnWgvm0ce7GeG51czMsG7RFPPCXqw=="],
|
"@lingui/babel-plugin-extract-messages": ["@lingui/babel-plugin-extract-messages@6.6.0", "", { "dependencies": { "@lingui/conf": "6.6.0" } }, "sha512-QfCS4SjZYvVZFaiF44e/mLsdzCqQvRSGoVJWkb8nc51FkNpOZkwFLiwLtRnWgvm0ce7GeG51czMsG7RFPPCXqw=="],
|
||||||
|
|
||||||
"@lingui/babel-plugin-lingui-macro": ["@lingui/babel-plugin-lingui-macro@6.6.0", "", { "dependencies": { "@lingui/conf": "6.6.0", "@lingui/message-utils": "6.6.0" }, "peerDependencies": { "@babel/core": "^7.20.12 || ^8.0.0", "@babel/types": "^7.20.7 || ^8.0.0" }, "optionalPeers": ["@babel/core", "@babel/types"] }, "sha512-FEBrnIx66lAeD9LGueRIen8IEj+aT7bDaMVmPNIITQNBCm2iC4mNcUtEV1/rAaVpfKKnh5uTdQowWmddB2D7iw=="],
|
"@lingui/babel-plugin-lingui-macro": ["@lingui/babel-plugin-lingui-macro@6.6.0", "", { "dependencies": { "@lingui/conf": "6.6.0", "@lingui/message-utils": "6.6.0" }, "peerDependencies": { "@babel/core": "^7.20.12 || ^8.0.0", "@babel/types": "^7.20.7 || ^8.0.0" }, "optionalPeers": ["@babel/core", "@babel/types"] }, "sha512-FEBrnIx66lAeD9LGueRIen8IEj+aT7bDaMVmPNIITQNBCm2iC4mNcUtEV1/rAaVpfKKnh5uTdQowWmddB2D7iw=="],
|
||||||
@@ -405,6 +466,84 @@
|
|||||||
|
|
||||||
"@oxc-project/types": ["@oxc-project/types@0.139.0", "", {}, "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw=="],
|
"@oxc-project/types": ["@oxc-project/types@0.139.0", "", {}, "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.61.0", "", { "os": "android", "cpu": "arm" }, "sha512-BaS+1OVvg9sr+Xav0+KdWedQRcAzrdoEcwMZeqoc2F6ieC1s/t5eM35YQoRPQ7vAqkZ+p3tbQb1r9I9mrV5oGA=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.61.0", "", { "os": "android", "cpu": "arm64" }, "sha512-of8atAV0M1egGcVOMbgZCvc10sFOP3ayQBNQV5h5G3fNq8gACdEswfFk9bzGrdbM23rtg0Coxi7np7oPLcueNw=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.61.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7l8+5ov4BGwtAcmpzvEik/TG3bciwyw/S3e6j5GKH7pcQqcgCVxD3AuJeP6upto+SOTBKQ4wrrdbMt0gq8fHSQ=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.61.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Fnz4dDDXBb7udk+DmwelNjxbD6yptyxwCqwCH2ebo4RVLxVsRfFsn/AHJC49KIltPrVokamGv4SSOsiV50DTxQ=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.61.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mddOebKNCP+AucmzfNsk3jgbr681qAUvgMqi865GW5gWLJ/AnzXbvjQRrny0e++NAN8aphav/aRSrfFxNsNjpA=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.61.0", "", { "os": "linux", "cpu": "arm" }, "sha512-svx59iYL+DbaZGZUIoice4W0CjRXGExnbz7Re+awIb60rVxBS2KrU7Hnlx+nZYanLGLpjneUEgo/VFEKkSZAyQ=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.61.0", "", { "os": "linux", "cpu": "arm" }, "sha512-BYK9MPJPCf6d+fLKMTruThmEyCtHzQ1zLcsrTlUVkmnoXIaHAbfpeLYQwX1tkjs7W11dyzoi6HFvKcdnvX1zNg=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.61.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-QUaCNLq2/EC6G5ljOuFanl9Lgw6ZWp4co7rs4+KOMUzbGfA4Lq58FHRjjF9sVIG+93XSbo343MxFATrOU1qctA=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.61.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-S6uvJ6MXnRXl+zTs0CARNDvkE+cymj0EVWEKKsyKnlLlqTyQJBjw5s4D2pSIOZc+S46cy4STefzcr/sm0VzVPA=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.61.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-6VDlRcytvZG6UlSIdAFKDLbppo9tvPxrWzle6vHldYFMeuDPQEfMKrkwezp7FaBq1wik9ra554ZZeRPsyIkFpg=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.61.0", "", { "os": "linux", "cpu": "none" }, "sha512-KkBTYbzExpbmn15XjKPLu2fRV2PVlq+KWt+brad5rwIa03vdYoaDRWiS7raHII/dCTR6Ro4UpYUCH4t6lif4WQ=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.61.0", "", { "os": "linux", "cpu": "none" }, "sha512-69tzIq7sJLVB9dxYYtvMzcSSsnZHSO+U2U19O2RqDqgj6+Q4O7HjSXdaszbcgqzhsUwzSH7z5kWvk8nmf6BHTg=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.61.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-Oqi/N0OvtOVXsPKAOOhKgGH3msRYF8BLJaNBbWiupRiKoKVyc8JRKPCfarkQJC+RgP9U8raUKLe+bNwd0HUMiA=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.61.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3TKwv/ed4uwJSemAA8P9XcoqETpjQI4waquF9UilhA9Mn/dhr1PdUEXWlL74mtc6ZNfmKPA9+NEJm01nRF8CVA=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.61.0", "", { "os": "linux", "cpu": "x64" }, "sha512-uFso4u4nLkVSlMCpgjyvWV60Gt7GvDQHnk1mmRxHIkZTMB0ljpUKwCD9FYGgN9H97x2wYl0UwEjgRZaPIuhEhw=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.61.0", "", { "os": "none", "cpu": "arm64" }, "sha512-keGLkzeOvkMpNmPp4hffXWpfoSsY6e1K8++KXD4mSSfxdvM8q9QUDsYY689TB1k6Co832DZn1MnaaVx6cIBMWQ=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.61.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-VzsAISkFxmNhJ5LBDEL9VuH6tJsVJMtqYit2LyIUf/HLnsCe4Pg9SMOjjVQzGWt0bnpyfJ94CrqTqcpNZzK+ug=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.61.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-xv4t7yzwJoYaLB6Zv28B3W+j7brEjsyv50rLTAQgmxJzddce9fAMCxed8dSAkbWES0zz2J29nYK5FaTuD2YBHg=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.61.0", "", { "os": "win32", "cpu": "x64" }, "sha512-6EZXFkqOwxdDYjIn3TSNnPk3ST5E5GiYd4FiM6UF/mCL/LZSfr6D6UygTfW3R1PCQP2quCKpCEGRlij8E3VYbg=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.76.0", "", { "os": "android", "cpu": "arm" }, "sha512-ZHIE5Zt9AsPDcY4nOlofXt0YfneEeo+QrKMPcPzLf2Z6Q8VtV2W73d7SFJ920WUwyik783u/doKCs3KXdwG+7w=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.76.0", "", { "os": "android", "cpu": "arm64" }, "sha512-shm/ngQilHK6bs+ElJWa4oHfNj5vL1Gl/iVEJldTQjpr0/67oSgr0KUpbmcnLig5Fo0v/l6j2567A7TOL89ONA=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.76.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rvJmrAPKSQ9aWJ6wIS6CK2tJjwzfW0ApQH9qokq6sfDvmHwoyIHxHFMq7z7i7GiV6fdE6s8qvBqWKPTu8RmT6Q=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.76.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U/zYdb7VYKGY6pA9Vd2rYl9O/HlCylcOlb5PGPvVLtg+oLGsk6H3XGKEMHKyqD3nmmtmlmwb/8SwU2vfSAtvMw=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.76.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-WvKG9CAriuo0XNiFzpXjDngUZcRGFNpaK2kLyMUsnJlShxkT96u+BpJQ3KqdQwGOrvI14L6V8bAwXwAYNNY6Jg=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.76.0", "", { "os": "linux", "cpu": "arm" }, "sha512-qJ5+RH99TqFRq3UCDxkW0zJJu9c+OAHFY72vGlxZLEpuO+MpKo3POgqb8sYipL9KYm8XY6ofb0HsOuvY6hQNqQ=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.76.0", "", { "os": "linux", "cpu": "arm" }, "sha512-PvPCVptkgVARsucgIqFQQcSmJ6xc6GtnVB5bRBekRahTc9eObMtjHfMjy5M+C2tHt5UCMttWM9RuSk/H9NqYeg=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.76.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-3KeFDx8Bu4HPAXbuHZOr/oHvN+QT+JQhMw/NYPz7Z071xLSsG27Jfh9PIQVEY7hk1I+jr43ExqRIeJ6VKk2yLw=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.76.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-oPFkkKTgl0K/EIg9fQ8oA3IGcI05/Mq1en04iFa41mmNPT+6KEiByVazTOZZJiHMBBrbsns1YJ2e1Scqwzesjw=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.76.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gN7yZ0eqflA5Fhf1wvHxGUltIV3FsvmB1zhNMDEK9vSHhc7E6qg9CuPeBgPZab66Tjzq6w6kHAtNEvnTHf4cyw=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.76.0", "", { "os": "linux", "cpu": "none" }, "sha512-S/HqMbn22mQrjtErUxEoS/a55u8kIeXvreIxiJu5G7Le3UecEd6SQZxrDIpuhtgaFnsY/nVra3ytP+pRljDilA=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.76.0", "", { "os": "linux", "cpu": "none" }, "sha512-ZIga3097VJZolGZk6SrIAUokIGfRkxRlhiHDUznZptGBfwrhD7pNfD1rzEzsCwvk/1DX0A1bLz+liuNh5QKIVQ=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.76.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-ZGiiA7pFzMJSyMWYZTVlPgbTsx+Vl8ihLGMIujPwaslUF7kIPPWAbVmAlTc+9lWDV+DCiB8Ikixu+lSHeOIIWQ=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.76.0", "", { "os": "linux", "cpu": "x64" }, "sha512-JLiy5WuvEBFTT6ErIFV35SLzi0R7Iri6MKU6dZbTxfIx8pndbbPs3Mj780nMipBFcPkti+okAPOJ9POKkHFEgg=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.76.0", "", { "os": "linux", "cpu": "x64" }, "sha512-z7lgKQtbo/I1NIe8G5NHLesxJDv0tRSUWTpXKb9Pm3E9nKFKfO4IOSDtFroKgXtOYb0jQbcdH+0wzTyMXVes+A=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.76.0", "", { "os": "none", "cpu": "arm64" }, "sha512-JOjKymIpb9QcYfEhZsN6h4V9Ivd474W38cNIBRv6bg2TbIvogbMTH0Mg6YWW9TiRDqfcX+/Hyfsbo5vcSE5guQ=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.76.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-pqDWZiwcmByWUEm1NFUBNiT6aentCcaoMWJv0HbXEmuYermJ4sg8ppVrshubYP2MZ6SHccJJcpr6x469PuDFIw=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.76.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-Ba0O659kgMv6pwO3z9PdO+K3aMxQRaw9HnG+e6AtOfgwcKFvYilciQYBoUBmxfQvOCKZe1SwjMkuB542NkuDMQ=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.76.0", "", { "os": "win32", "cpu": "x64" }, "sha512-5qcirPHO8nKfkoowEVWtpAoVTcYDy6g0UT0NGic450Qv8J2NrOqg4uQ8QppRP4MDTC7Xx47lbZnmadTH03CGGA=="],
|
||||||
|
|
||||||
|
"@preact/signals-core": ["@preact/signals-core@1.14.4", "", {}, "sha512-HNB6HYeYKhQbJ1aKl+YRjrS4+QWHLKX6qKoUsfS/m0vqzsVaEBiZiaKbG/e+NKk2ch5ALQr/ihWaMHxiCuuWHA=="],
|
||||||
|
|
||||||
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.7", "", {}, "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q=="],
|
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.7", "", {}, "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q=="],
|
||||||
|
|
||||||
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.5", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA=="],
|
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.5", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA=="],
|
||||||
@@ -571,20 +710,6 @@
|
|||||||
|
|
||||||
"@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="],
|
"@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="],
|
||||||
|
|
||||||
"@turbo/darwin-64": ["@turbo/darwin-64@2.10.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-/c9cSBRermWDv85oufLhoH6XRLOVbvzJLRd+WLyfJCP+i0HFLQj4PVNDrHcY17/ve5l8X0Oua4bJBqJUgJPnZA=="],
|
|
||||||
|
|
||||||
"@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.10.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-8lpCCGWZBl9PIF8w8f2iEWrLMbHBWIfJeV6l2UEGqysD6HRIM4ySj/8R7HGEzbECJ4r/gnJcHmxEoG8yjFe64A=="],
|
|
||||||
|
|
||||||
"@turbo/gen": ["@turbo/gen@2.10.7", "", { "dependencies": { "@inquirer/prompts": "^7.10.1", "esbuild": "^0.28.1" }, "bin": { "gen": "dist/cli.js" } }, "sha512-kxLa//A+Q8oe10Pz2MdLKFwS5QUi1uVIp1oqbFraF1OSgSHyNKRWjpn5ITn2c91CzaVEBY4pGFM9ZlKvtnkcGw=="],
|
|
||||||
|
|
||||||
"@turbo/linux-64": ["@turbo/linux-64@2.10.7", "", { "os": "linux", "cpu": "x64" }, "sha512-Midw9Ed00yw9rqkWN82fY3LmLNFQ6yiL1GXB56DJKUEjWaEd27zh7ohCxzzrjfjQVrEeVWd3UPytTAV16XDQlA=="],
|
|
||||||
|
|
||||||
"@turbo/linux-arm64": ["@turbo/linux-arm64@2.10.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-UVEy+MW/xn4BcsiV3v3uv0/oObyaQgVtRT+Jj4WE53rNH05VEYEc1Z13q3zV6276wCZR4Yxc4hiTTcjw7PjSpg=="],
|
|
||||||
|
|
||||||
"@turbo/windows-64": ["@turbo/windows-64@2.10.7", "", { "os": "win32", "cpu": "x64" }, "sha512-l2nH9KGLV46SWjcXvyc2+xo5gdf5J0NVADknQk9OCJhzJBpiNl/byd26yIfwZBjLupdqT6UOdPhPxcpUPxRykQ=="],
|
|
||||||
|
|
||||||
"@turbo/windows-arm64": ["@turbo/windows-arm64@2.10.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-qjE1apG6RThuX49vUJd5ks2dV2ndXC2qktDChQonFMvUzrMrEnZMQzO+IgxBiYq1j+NbXQcRwj84nGnk1TBw1g=="],
|
|
||||||
|
|
||||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
|
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
|
||||||
|
|
||||||
"@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
|
"@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
|
||||||
@@ -625,6 +750,8 @@
|
|||||||
|
|
||||||
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||||
|
|
||||||
|
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||||
|
|
||||||
"@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="],
|
"@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="],
|
||||||
|
|
||||||
"@types/validate-npm-package-name": ["@types/validate-npm-package-name@4.0.2", "", {}, "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw=="],
|
"@types/validate-npm-package-name": ["@types/validate-npm-package-name@4.0.2", "", {}, "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw=="],
|
||||||
@@ -677,6 +804,10 @@
|
|||||||
|
|
||||||
"@workspace/i18n": ["@workspace/i18n@workspace:packages/i18n"],
|
"@workspace/i18n": ["@workspace/i18n@workspace:packages/i18n"],
|
||||||
|
|
||||||
|
"@workspace/icons": ["@workspace/icons@workspace:packages/icons"],
|
||||||
|
|
||||||
|
"@workspace/lexical": ["@workspace/lexical@workspace:packages/lexical"],
|
||||||
|
|
||||||
"@workspace/ui": ["@workspace/ui@workspace:packages/ui"],
|
"@workspace/ui": ["@workspace/ui@workspace:packages/ui"],
|
||||||
|
|
||||||
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||||
@@ -687,12 +818,16 @@
|
|||||||
|
|
||||||
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||||
|
|
||||||
|
"anser": ["anser@2.3.5", "", {}, "sha512-vcZjxvvVoxTeR5XBNJB38oTu/7eDCZlwdz32N1eNgpyPF7j/Z7Idf+CUwQOkKKpJ7RJyjxgLHCM7vdIK0iCNMQ=="],
|
||||||
|
|
||||||
"ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="],
|
"ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="],
|
||||||
|
|
||||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
"ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
|
"ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
|
||||||
|
|
||||||
|
"ansi-to-react": ["ansi-to-react@6.2.6", "", { "dependencies": { "anser": "^2.3.2", "escape-carriage": "^1.3.1", "linkify-it": "^3.0.3" }, "peerDependencies": { "react": "^16.3.2 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.3.2 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Eqi0iaMK5OZ3jsVFxWvU2B74UZBnGuHlkflKMX6wTOeH+luy9KE2O0gUkc2PxhIP1R4IO0xohv62UMFInQOSeg=="],
|
||||||
|
|
||||||
"ansis": ["ansis@4.3.1", "", {}, "sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA=="],
|
"ansis": ["ansis@4.3.1", "", {}, "sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA=="],
|
||||||
|
|
||||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||||
@@ -745,8 +880,6 @@
|
|||||||
|
|
||||||
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||||
|
|
||||||
"chardet": ["chardet@2.2.0", "", {}, "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA=="],
|
|
||||||
|
|
||||||
"chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
|
"chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
|
||||||
|
|
||||||
"citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="],
|
"citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="],
|
||||||
@@ -759,8 +892,6 @@
|
|||||||
|
|
||||||
"cli-table3": ["cli-table3@0.6.5", "", { "dependencies": { "string-width": "^4.2.0" }, "optionalDependencies": { "@colors/colors": "1.5.0" } }, "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ=="],
|
"cli-table3": ["cli-table3@0.6.5", "", { "dependencies": { "string-width": "^4.2.0" }, "optionalDependencies": { "@colors/colors": "1.5.0" } }, "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ=="],
|
||||||
|
|
||||||
"cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="],
|
|
||||||
|
|
||||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||||
|
|
||||||
"cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="],
|
"cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="],
|
||||||
@@ -795,6 +926,8 @@
|
|||||||
|
|
||||||
"cosmiconfig": ["cosmiconfig@9.0.2", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg=="],
|
"cosmiconfig": ["cosmiconfig@9.0.2", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg=="],
|
||||||
|
|
||||||
|
"country-flag-icons": ["country-flag-icons@1.6.20", "", {}, "sha512-py8JiEKzjhYw6HPJ0L7SxLgCYim36UPRTZX43/kqGueUCZLSvnrqAiwW8HtQibur7mdkFQUkjOgdK+o/9FBtaw=="],
|
||||||
|
|
||||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||||
|
|
||||||
"crossws": ["crossws@0.4.10", "", { "peerDependencies": { "srvx": ">=0.11.5" }, "optionalPeers": ["srvx"] }, "sha512-pz3oubH/dt12KjqsUB0IuXW4nwRDQ583iDsP4555Cpdqx0NoU7pGlWBcayyFI8f/l/idRpgjMEfwuOxSWJYlIA=="],
|
"crossws": ["crossws@0.4.10", "", { "peerDependencies": { "srvx": ">=0.11.5" }, "optionalPeers": ["srvx"] }, "sha512-pz3oubH/dt12KjqsUB0IuXW4nwRDQ583iDsP4555Cpdqx0NoU7pGlWBcayyFI8f/l/idRpgjMEfwuOxSWJYlIA=="],
|
||||||
@@ -913,6 +1046,8 @@
|
|||||||
|
|
||||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||||
|
|
||||||
|
"escape-carriage": ["escape-carriage@1.3.1", "", {}, "sha512-GwBr6yViW3ttx1kb7/Oh+gKQ1/TrhYwxKqVmg5gS+BK+Qe2KrOa/Vh7w3HPBvgGf0LfcDGoY9I6NHKoA5Hozhw=="],
|
||||||
|
|
||||||
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||||
|
|
||||||
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
|
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
|
||||||
@@ -1067,6 +1202,8 @@
|
|||||||
|
|
||||||
"isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
|
"isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
|
||||||
|
|
||||||
|
"isomorphic.js": ["isomorphic.js@0.2.5", "", {}, "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw=="],
|
||||||
|
|
||||||
"jest-get-type": ["jest-get-type@29.6.3", "", {}, "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw=="],
|
"jest-get-type": ["jest-get-type@29.6.3", "", {}, "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw=="],
|
||||||
|
|
||||||
"jest-validate": ["jest-validate@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", "chalk": "^4.0.0", "jest-get-type": "^29.6.3", "leven": "^3.1.0", "pretty-format": "^29.7.0" } }, "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw=="],
|
"jest-validate": ["jest-validate@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", "chalk": "^4.0.0", "jest-get-type": "^29.6.3", "leven": "^3.1.0", "pretty-format": "^29.7.0" } }, "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw=="],
|
||||||
@@ -1099,6 +1236,10 @@
|
|||||||
|
|
||||||
"leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="],
|
"leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="],
|
||||||
|
|
||||||
|
"lexical": ["lexical@0.48.0", "", { "dependencies": { "@lexical/internal": "0.48.0" }, "peerDependencies": { "typescript": ">=5.2" }, "optionalPeers": ["typescript"] }, "sha512-KK4Tyr/cPsleoZ7XvhGRiRmcrZidSmoFUdIXK9nPubIifoC+80Dc5THyc4xtGKtsW24S1TsHzk5gmfBU+TxmEg=="],
|
||||||
|
|
||||||
|
"lib0": ["lib0@0.2.117", "", { "dependencies": { "isomorphic.js": "^0.2.4" }, "bin": { "0serve": "bin/0serve.js", "0gentesthtml": "bin/gentesthtml.js", "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js" } }, "sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw=="],
|
||||||
|
|
||||||
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||||
|
|
||||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
||||||
@@ -1127,6 +1268,8 @@
|
|||||||
|
|
||||||
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
|
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
|
||||||
|
|
||||||
|
"linkify-it": ["linkify-it@3.0.3", "", { "dependencies": { "uc.micro": "^1.0.1" } }, "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ=="],
|
||||||
|
|
||||||
"local-pkg": ["local-pkg@1.2.1", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.3.0", "quansync": "^0.2.11" } }, "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q=="],
|
"local-pkg": ["local-pkg@1.2.1", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.3.0", "quansync": "^0.2.11" } }, "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q=="],
|
||||||
|
|
||||||
"locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="],
|
"locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="],
|
||||||
@@ -1175,8 +1318,6 @@
|
|||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
"mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="],
|
|
||||||
|
|
||||||
"nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="],
|
"nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="],
|
||||||
|
|
||||||
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||||
@@ -1219,6 +1360,10 @@
|
|||||||
|
|
||||||
"ora": ["ora@9.4.1", "", { "dependencies": { "chalk": "^5.6.2", "cli-cursor": "^5.0.0", "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", "stdin-discarder": "^0.3.2", "string-width": "^8.1.0" } }, "sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw=="],
|
"ora": ["ora@9.4.1", "", { "dependencies": { "chalk": "^5.6.2", "cli-cursor": "^5.0.0", "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", "stdin-discarder": "^0.3.2", "string-width": "^8.1.0" } }, "sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw=="],
|
||||||
|
|
||||||
|
"oxfmt": ["oxfmt@0.61.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.61.0", "@oxfmt/binding-android-arm64": "0.61.0", "@oxfmt/binding-darwin-arm64": "0.61.0", "@oxfmt/binding-darwin-x64": "0.61.0", "@oxfmt/binding-freebsd-x64": "0.61.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.61.0", "@oxfmt/binding-linux-arm-musleabihf": "0.61.0", "@oxfmt/binding-linux-arm64-gnu": "0.61.0", "@oxfmt/binding-linux-arm64-musl": "0.61.0", "@oxfmt/binding-linux-ppc64-gnu": "0.61.0", "@oxfmt/binding-linux-riscv64-gnu": "0.61.0", "@oxfmt/binding-linux-riscv64-musl": "0.61.0", "@oxfmt/binding-linux-s390x-gnu": "0.61.0", "@oxfmt/binding-linux-x64-gnu": "0.61.0", "@oxfmt/binding-linux-x64-musl": "0.61.0", "@oxfmt/binding-openharmony-arm64": "0.61.0", "@oxfmt/binding-win32-arm64-msvc": "0.61.0", "@oxfmt/binding-win32-ia32-msvc": "0.61.0", "@oxfmt/binding-win32-x64-msvc": "0.61.0" }, "peerDependencies": { "svelte": "^5.0.0", "vite-plus": "*" }, "optionalPeers": ["svelte", "vite-plus"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-DxdHBEMYpcEnHoUHjjOigUqV2TYKsvxLwUPXnVYBjgFdqrcQ/91OtwubtZ2PUodCs3sStI8R5Qw3fKNGK4e8wQ=="],
|
||||||
|
|
||||||
|
"oxlint": ["oxlint@1.76.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.76.0", "@oxlint/binding-android-arm64": "1.76.0", "@oxlint/binding-darwin-arm64": "1.76.0", "@oxlint/binding-darwin-x64": "1.76.0", "@oxlint/binding-freebsd-x64": "1.76.0", "@oxlint/binding-linux-arm-gnueabihf": "1.76.0", "@oxlint/binding-linux-arm-musleabihf": "1.76.0", "@oxlint/binding-linux-arm64-gnu": "1.76.0", "@oxlint/binding-linux-arm64-musl": "1.76.0", "@oxlint/binding-linux-ppc64-gnu": "1.76.0", "@oxlint/binding-linux-riscv64-gnu": "1.76.0", "@oxlint/binding-linux-riscv64-musl": "1.76.0", "@oxlint/binding-linux-s390x-gnu": "1.76.0", "@oxlint/binding-linux-x64-gnu": "1.76.0", "@oxlint/binding-linux-x64-musl": "1.76.0", "@oxlint/binding-openharmony-arm64": "1.76.0", "@oxlint/binding-win32-arm64-msvc": "1.76.0", "@oxlint/binding-win32-ia32-msvc": "1.76.0", "@oxlint/binding-win32-x64-msvc": "1.76.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-6QoFioEU4fNdiUx/2Eo6TRd6NG7H7njnRCz8rhB66cZmMHDTqcm1Rjvl8Wry+ZTQMBAmyb4Mlf62Mk5X+eHSOw=="],
|
||||||
|
|
||||||
"p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
|
"p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
|
||||||
|
|
||||||
"p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="],
|
"p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="],
|
||||||
@@ -1267,8 +1412,6 @@
|
|||||||
|
|
||||||
"prettier": ["prettier@3.9.6", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g=="],
|
"prettier": ["prettier@3.9.6", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g=="],
|
||||||
|
|
||||||
"prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.8.1", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-svelte"] }, "sha512-iaFMYqDsE4ffdDkn5qup0j5f2aCEBFZrdrZnvu9QKTlWx/iGPeQ4HHu7b7fCPMxeo9nwQBiOAh2nSypdFYWJkw=="],
|
|
||||||
|
|
||||||
"pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
|
"pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
|
||||||
|
|
||||||
"pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="],
|
"pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="],
|
||||||
@@ -1409,6 +1552,8 @@
|
|||||||
|
|
||||||
"systeminformation": ["systeminformation@5.33.1", "", { "os": "!aix", "bin": { "systeminformation": "lib/cli.js" } }, "sha512-DEN6ICHk3Tk0Uf/hrAHh7xlt7iL5CJFBtPZinA0H62DrGG/KPKqq/Nzj6lCXPS4Ay/sf/14zNnk9LpqKzBIc+w=="],
|
"systeminformation": ["systeminformation@5.33.1", "", { "os": "!aix", "bin": { "systeminformation": "lib/cli.js" } }, "sha512-DEN6ICHk3Tk0Uf/hrAHh7xlt7iL5CJFBtPZinA0H62DrGG/KPKqq/Nzj6lCXPS4Ay/sf/14zNnk9LpqKzBIc+w=="],
|
||||||
|
|
||||||
|
"tabbable": ["tabbable@6.5.0", "", {}, "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA=="],
|
||||||
|
|
||||||
"tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="],
|
"tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="],
|
||||||
|
|
||||||
"tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="],
|
"tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="],
|
||||||
@@ -1445,14 +1590,14 @@
|
|||||||
|
|
||||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
"turbo": ["turbo@2.10.7", "", { "optionalDependencies": { "@turbo/darwin-64": "2.10.7", "@turbo/darwin-arm64": "2.10.7", "@turbo/linux-64": "2.10.7", "@turbo/linux-arm64": "2.10.7", "@turbo/windows-64": "2.10.7", "@turbo/windows-arm64": "2.10.7" }, "bin": { "turbo": "bin/turbo" } }, "sha512-GHx6WExIFSKNJ5qMlzDpXBXlu9ApxaMjqxAVrCNcW94xf/+uqgIz41SAuRUMbXva2ExNAaY/h8V0q90SWSzmRw=="],
|
|
||||||
|
|
||||||
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
|
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
|
||||||
|
|
||||||
"type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="],
|
"type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="],
|
||||||
|
|
||||||
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
|
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
|
||||||
|
|
||||||
|
"uc.micro": ["uc.micro@1.0.6", "", {}, "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="],
|
||||||
|
|
||||||
"ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="],
|
"ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="],
|
||||||
|
|
||||||
"undici": ["undici@8.9.0", "", {}, "sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA=="],
|
"undici": ["undici@8.9.0", "", {}, "sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA=="],
|
||||||
@@ -1513,8 +1658,6 @@
|
|||||||
|
|
||||||
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
|
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
|
||||||
|
|
||||||
"wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
|
|
||||||
|
|
||||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||||
|
|
||||||
"wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="],
|
"wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="],
|
||||||
@@ -1527,12 +1670,12 @@
|
|||||||
|
|
||||||
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||||
|
|
||||||
|
"yjs": ["yjs@13.6.31", "", { "dependencies": { "lib0": "^0.2.99" } }, "sha512-Eq+5BRfbeGyqGVrTJL3bEcr8gKkxPuyuoHmAwpk52fDb8kOVMrfVSTRPd6yiGgX5Fskb96qCRjzjbRjrL4YEnw=="],
|
||||||
|
|
||||||
"yocto-spinner": ["yocto-spinner@1.2.2", "", { "dependencies": { "yoctocolors": "^2.1.1" } }, "sha512-DODGl1wJjA/s5pnJFKau9lIYHT81lnhob1i3e1TjxZRxEhWRKl74nTbWE6H5KlkViQQTo/Z29YFdxzTZAMY3ng=="],
|
"yocto-spinner": ["yocto-spinner@1.2.2", "", { "dependencies": { "yoctocolors": "^2.1.1" } }, "sha512-DODGl1wJjA/s5pnJFKau9lIYHT81lnhob1i3e1TjxZRxEhWRKl74nTbWE6H5KlkViQQTo/Z29YFdxzTZAMY3ng=="],
|
||||||
|
|
||||||
"yoctocolors": ["yoctocolors@2.2.0", "", {}, "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg=="],
|
"yoctocolors": ["yoctocolors@2.2.0", "", {}, "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg=="],
|
||||||
|
|
||||||
"yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="],
|
|
||||||
|
|
||||||
"zigpty": ["zigpty@0.2.1", "", {}, "sha512-MR9JqJx2wf5f4wz8zpx050AlqrmWeIW+1h0SO5iEyhG3HFRjY5luC3szS2ux2EGuPjE5OU9ZAuiCBeMWBTrqZw=="],
|
"zigpty": ["zigpty@0.2.1", "", {}, "sha512-MR9JqJx2wf5f4wz8zpx050AlqrmWeIW+1h0SO5iEyhG3HFRjY5luC3szS2ux2EGuPjE5OU9ZAuiCBeMWBTrqZw=="],
|
||||||
|
|
||||||
"zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
|
"zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
|
||||||
@@ -1629,10 +1772,6 @@
|
|||||||
|
|
||||||
"type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
|
"type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
|
||||||
|
|
||||||
"wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
|
||||||
|
|
||||||
"wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
|
||||||
|
|
||||||
"@dotenvx/dotenvx/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
|
"@dotenvx/dotenvx/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
|
||||||
|
|
||||||
"@dotenvx/dotenvx/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
|
"@dotenvx/dotenvx/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
|
||||||
|
|||||||
+10
-7
@@ -3,15 +3,18 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo build",
|
"build": "bun run --filter web build",
|
||||||
"dev": "turbo dev",
|
"check": "bun run format:check && bun run lint && bun run typecheck",
|
||||||
"format": "turbo format",
|
"dev": "bun run --filter web dev",
|
||||||
"typecheck": "turbo typecheck"
|
"format": "oxfmt apps packages package.json tsconfig.json .oxlintrc.json .oxfmtrc.json",
|
||||||
|
"format:check": "oxfmt --check apps packages package.json tsconfig.json .oxlintrc.json .oxfmtrc.json",
|
||||||
|
"lint": "oxlint .",
|
||||||
|
"lint:fix": "oxlint --fix .",
|
||||||
|
"typecheck": "bun run --workspaces typecheck"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "^3.8.3",
|
"oxfmt": "^0.61.0",
|
||||||
"prettier-plugin-tailwindcss": "^0.8.0",
|
"oxlint": "^1.76.0",
|
||||||
"turbo": "^2.9.18",
|
|
||||||
"typescript": "~6"
|
"typescript": "~6"
|
||||||
},
|
},
|
||||||
"packageManager": "bun@1.3.5",
|
"packageManager": "bun@1.3.5",
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"format": "prettier --write \"**/*.{ts,tsx}\"",
|
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
@@ -12,12 +11,13 @@
|
|||||||
"@base-ui/react": "^1.6.0",
|
"@base-ui/react": "^1.6.0",
|
||||||
"@base-ui/utils": "^0.3.1",
|
"@base-ui/utils": "^0.3.1",
|
||||||
"@floating-ui/utils": "^0.2.12",
|
"@floating-ui/utils": "^0.2.12",
|
||||||
"@hugeicons/core-free-icons": "^4.2.3",
|
|
||||||
"@hugeicons/react": "^1.1.9",
|
|
||||||
"@tanstack/react-hotkeys": "^0.10.0",
|
"@tanstack/react-hotkeys": "^0.10.0",
|
||||||
"@tanstack/react-query": "^5.101.4",
|
"@tanstack/react-query": "^5.101.4",
|
||||||
"@tanstack/react-router": "^1.170.18",
|
"@tanstack/react-router": "^1.170.18",
|
||||||
|
"@workspace/icons": "workspace:*",
|
||||||
|
"@workspace/i18n": "workspace:*",
|
||||||
"@workspace/ui": "workspace:*",
|
"@workspace/ui": "workspace:*",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
"lucide-react": "^1.27.0",
|
"lucide-react": "^1.27.0",
|
||||||
"react": "^19.2.6",
|
"react": "^19.2.6",
|
||||||
"react-dom": "^19.2.6"
|
"react-dom": "^19.2.6"
|
||||||
@@ -33,12 +33,28 @@
|
|||||||
"exports": {
|
"exports": {
|
||||||
"./globals.css": "./src/styles/globals.css",
|
"./globals.css": "./src/styles/globals.css",
|
||||||
"./appearance": "./src/blocks/appearance/index.ts",
|
"./appearance": "./src/blocks/appearance/index.ts",
|
||||||
|
"./appearance/locales": "./src/blocks/appearance/locales/catalogs.ts",
|
||||||
|
"./appearance/locales/*": "./src/blocks/appearance/locales/*.ts",
|
||||||
"./chats": "./src/blocks/chats/index.ts",
|
"./chats": "./src/blocks/chats/index.ts",
|
||||||
|
"./chats/locales": "./src/blocks/chats/locales/catalogs.ts",
|
||||||
|
"./chats/locales/*": "./src/blocks/chats/locales/*.ts",
|
||||||
"./components/*": "./src/components/*.tsx",
|
"./components/*": "./src/components/*.tsx",
|
||||||
"./hooks/*": "./src/hooks/*.ts",
|
"./hooks/*": "./src/hooks/*.ts",
|
||||||
"./lib/*": "./src/lib/*.tsx",
|
"./lib/*": "./src/lib/*.tsx",
|
||||||
"./layout": "./src/blocks/layout/index.ts",
|
"./layout": "./src/blocks/layout/index.ts",
|
||||||
|
"./layout/locales": "./src/blocks/layout/locales/catalogs.ts",
|
||||||
|
"./layout/locales/*": "./src/blocks/layout/locales/*.ts",
|
||||||
|
"./media": "./src/blocks/media/index.ts",
|
||||||
|
"./media/locales": "./src/blocks/media/locales/catalogs.ts",
|
||||||
|
"./media/locales/*": "./src/blocks/media/locales/*.ts",
|
||||||
"./navigation": "./src/blocks/navigation/index.ts",
|
"./navigation": "./src/blocks/navigation/index.ts",
|
||||||
"./notifications": "./src/blocks/notifications/index.ts"
|
"./navigation/locales": "./src/blocks/navigation/locales/catalogs.ts",
|
||||||
|
"./navigation/locales/*": "./src/blocks/navigation/locales/*.ts",
|
||||||
|
"./notifications": "./src/blocks/notifications/index.ts",
|
||||||
|
"./notifications/locales": "./src/blocks/notifications/locales/catalogs.ts",
|
||||||
|
"./notifications/locales/*": "./src/blocks/notifications/locales/*.ts",
|
||||||
|
"./search": "./src/blocks/search/index.ts",
|
||||||
|
"./search/locales": "./src/blocks/search/locales/catalogs.ts",
|
||||||
|
"./search/locales/*": "./src/blocks/search/locales/*.ts"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useCallback, useState } from "react"
|
import { useCallback, useState } from "react"
|
||||||
import { useHotkey } from "@tanstack/react-hotkeys"
|
import { useHotkey } from "@tanstack/react-hotkeys"
|
||||||
|
import { LocalizedText, useMessage } from "@workspace/i18n"
|
||||||
import { useResolvedTheme, useUiState } from "./ui-state"
|
import { useResolvedTheme, useUiState } from "./ui-state"
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -15,6 +16,61 @@ import {
|
|||||||
import { appearanceDialogHandle } from "./dialog"
|
import { appearanceDialogHandle } from "./dialog"
|
||||||
import { CompactPreference, ModePreference } from "./preferences"
|
import { CompactPreference, ModePreference } from "./preferences"
|
||||||
import { ThemeConfigPanel } from "./theme-config-panel"
|
import { ThemeConfigPanel } from "./theme-config-panel"
|
||||||
|
import { appearanceMessages } from "./messages"
|
||||||
|
|
||||||
|
interface AppearanceHotkeysProps {
|
||||||
|
onToggleDialog: VoidFunction
|
||||||
|
onToggleTheme: VoidFunction
|
||||||
|
}
|
||||||
|
|
||||||
|
function AppearanceHotkeys({
|
||||||
|
onToggleDialog,
|
||||||
|
onToggleTheme,
|
||||||
|
}: AppearanceHotkeysProps) {
|
||||||
|
const appearanceTitle = useMessage(appearanceMessages.appearance)
|
||||||
|
const appearanceDescription = useMessage(
|
||||||
|
appearanceMessages.appearanceCommandDescription
|
||||||
|
)
|
||||||
|
const toggleThemeTitle = useMessage(appearanceMessages.toggleTheme)
|
||||||
|
const toggleThemeDescription = useMessage(
|
||||||
|
appearanceMessages.toggleThemeDescription
|
||||||
|
)
|
||||||
|
|
||||||
|
useHotkey("Mod+,", onToggleDialog, {
|
||||||
|
ignoreInputs: true,
|
||||||
|
preventDefault: true,
|
||||||
|
stopPropagation: false,
|
||||||
|
meta: {
|
||||||
|
name: appearanceTitle,
|
||||||
|
description: appearanceDescription,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
useHotkey("Mod+D", onToggleTheme, {
|
||||||
|
ignoreInputs: true,
|
||||||
|
preventDefault: true,
|
||||||
|
stopPropagation: false,
|
||||||
|
meta: {
|
||||||
|
name: toggleThemeTitle,
|
||||||
|
description: toggleThemeDescription,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function AppearanceDialogHeader() {
|
||||||
|
return (
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
<LocalizedText message={appearanceMessages.appearance} />
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
<LocalizedText message={appearanceMessages.themeAndLayoutPreferences} />
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function AppearanceController() {
|
export function AppearanceController() {
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
@@ -33,37 +89,18 @@ export function AppearanceController() {
|
|||||||
setThemeMode(resolvedTheme === "dark" ? "light" : "dark")
|
setThemeMode(resolvedTheme === "dark" ? "light" : "dark")
|
||||||
}, [resolvedTheme, setThemeMode])
|
}, [resolvedTheme, setThemeMode])
|
||||||
|
|
||||||
useHotkey("Mod+,", toggleDialog, {
|
|
||||||
ignoreInputs: true,
|
|
||||||
preventDefault: true,
|
|
||||||
stopPropagation: false,
|
|
||||||
meta: {
|
|
||||||
name: "界面外观",
|
|
||||||
description: "打开主题与布局偏好",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
useHotkey("Mod+D", toggleTheme, {
|
|
||||||
ignoreInputs: true,
|
|
||||||
preventDefault: true,
|
|
||||||
stopPropagation: false,
|
|
||||||
meta: {
|
|
||||||
name: "切换主题",
|
|
||||||
description: "在浅色与深色主题之间切换",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog handle={appearanceDialogHandle} onOpenChange={setIsOpen}>
|
<Dialog handle={appearanceDialogHandle} onOpenChange={setIsOpen}>
|
||||||
|
<AppearanceHotkeys
|
||||||
|
onToggleDialog={toggleDialog}
|
||||||
|
onToggleTheme={toggleTheme}
|
||||||
|
/>
|
||||||
<NavigationActionTarget
|
<NavigationActionTarget
|
||||||
action={navigationActions.appearance}
|
action={navigationActions.appearance}
|
||||||
onInvoke={() => appearanceDialogHandle.open(null)}
|
onInvoke={() => appearanceDialogHandle.open(null)}
|
||||||
/>
|
/>
|
||||||
<DialogContent className="sm:max-w-max">
|
<DialogContent className="sm:max-w-max">
|
||||||
<DialogHeader>
|
<AppearanceDialogHeader />
|
||||||
<DialogTitle>界面外观</DialogTitle>
|
|
||||||
<DialogDescription>主题与布局偏好</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="-mx-6 max-h-[75vh] space-y-10 overflow-y-auto px-6">
|
<div className="-mx-6 max-h-[75vh] space-y-10 overflow-y-auto px-6">
|
||||||
<div className="flex flex-col gap-6 lg:flex-row">
|
<div className="flex flex-col gap-6 lg:flex-row">
|
||||||
<ModePreference />
|
<ModePreference />
|
||||||
@@ -73,13 +110,8 @@ export function AppearanceController() {
|
|||||||
<ThemeConfigPanel
|
<ThemeConfigPanel
|
||||||
scheme="light"
|
scheme="light"
|
||||||
active={resolvedTheme === "light"}
|
active={resolvedTheme === "light"}
|
||||||
description="当系统设置为浅色模式时,将使用此主题"
|
|
||||||
/>
|
|
||||||
<ThemeConfigPanel
|
|
||||||
scheme="dark"
|
|
||||||
active={resolvedTheme === "dark"}
|
|
||||||
description="当系统设置为深色模式时,将使用此主题"
|
|
||||||
/>
|
/>
|
||||||
|
<ThemeConfigPanel scheme="dark" active={resolvedTheme === "dark"} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { describe, it } from "vitest"
|
||||||
|
|
||||||
|
import { expectCompleteCatalogs } from "../../../i18n/test-catalogs"
|
||||||
|
import { appearanceMessages, themeColorMessages } from "../messages"
|
||||||
|
import { appearanceCatalogLocales } from "./catalogs"
|
||||||
|
import { messages as de } from "./de"
|
||||||
|
import { messages as en } from "./en"
|
||||||
|
import { messages as es } from "./es"
|
||||||
|
import { messages as fr } from "./fr"
|
||||||
|
import { messages as ja } from "./ja"
|
||||||
|
import { messages as ko } from "./ko"
|
||||||
|
import { messages as zhHans } from "./zh-Hans"
|
||||||
|
import { messages as zhHant } from "./zh-Hant"
|
||||||
|
|
||||||
|
describe("appearance locale catalogs", () => {
|
||||||
|
it("ship every appearance message in every built-in locale", () => {
|
||||||
|
expectCompleteCatalogs({
|
||||||
|
catalogs: {
|
||||||
|
de,
|
||||||
|
en,
|
||||||
|
es,
|
||||||
|
fr,
|
||||||
|
ja,
|
||||||
|
ko,
|
||||||
|
"zh-Hans": zhHans,
|
||||||
|
"zh-Hant": zhHant,
|
||||||
|
},
|
||||||
|
locales: appearanceCatalogLocales,
|
||||||
|
messageIds: [
|
||||||
|
...Object.values(appearanceMessages),
|
||||||
|
...Object.values(themeColorMessages),
|
||||||
|
].map((descriptor) => descriptor.id),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import type { appearanceMessages, themeColorMessages } from "../messages"
|
||||||
|
import {
|
||||||
|
blockCatalogLocales,
|
||||||
|
type BlockCatalogLocale,
|
||||||
|
type BlockMessageCatalog,
|
||||||
|
} from "../../../i18n/catalogs"
|
||||||
|
|
||||||
|
export { blockCatalogLocales as appearanceCatalogLocales }
|
||||||
|
export type AppearanceCatalogLocale = BlockCatalogLocale
|
||||||
|
export type AppearanceMessageCatalog = BlockMessageCatalog<
|
||||||
|
typeof appearanceMessages & typeof themeColorMessages
|
||||||
|
>
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import type { AppearanceMessageCatalog } from "./catalogs"
|
||||||
|
|
||||||
|
export const locale = "de"
|
||||||
|
export const languageTag = "de-DE"
|
||||||
|
|
||||||
|
export const messages = {
|
||||||
|
"blocks.appearance.active": "Aktiv",
|
||||||
|
"blocks.appearance.baseColor": "Basisfarbe",
|
||||||
|
"blocks.appearance.color.amber": "Bernstein",
|
||||||
|
"blocks.appearance.color.blue": "Blau",
|
||||||
|
"blocks.appearance.color.cyan": "Cyan",
|
||||||
|
"blocks.appearance.color.emerald": "Smaragd",
|
||||||
|
"blocks.appearance.color.fuchsia": "Fuchsia",
|
||||||
|
"blocks.appearance.color.green": "Grün",
|
||||||
|
"blocks.appearance.color.indigo": "Indigo",
|
||||||
|
"blocks.appearance.color.lime": "Limette",
|
||||||
|
"blocks.appearance.color.mauve": "Malve",
|
||||||
|
"blocks.appearance.color.mist": "Nebel",
|
||||||
|
"blocks.appearance.color.neutral": "Neutral",
|
||||||
|
"blocks.appearance.color.olive": "Oliv",
|
||||||
|
"blocks.appearance.color.orange": "Orange",
|
||||||
|
"blocks.appearance.color.pink": "Rosa",
|
||||||
|
"blocks.appearance.color.purple": "Lila",
|
||||||
|
"blocks.appearance.color.red": "Rot",
|
||||||
|
"blocks.appearance.color.rose": "Rose",
|
||||||
|
"blocks.appearance.color.sky": "Himmelblau",
|
||||||
|
"blocks.appearance.color.stone": "Stein",
|
||||||
|
"blocks.appearance.color.taupe": "Taupe",
|
||||||
|
"blocks.appearance.color.teal": "Blaugrün",
|
||||||
|
"blocks.appearance.color.violet": "Violett",
|
||||||
|
"blocks.appearance.color.yellow": "Gelb",
|
||||||
|
"blocks.appearance.color.zinc": "Zink",
|
||||||
|
"blocks.appearance.command.description":
|
||||||
|
"Design- und Layout-Einstellungen öffnen",
|
||||||
|
"blocks.appearance.compact.description":
|
||||||
|
"Eine kompakte Breite für den Hauptinhalt verwenden",
|
||||||
|
"blocks.appearance.compact.title": "Kompaktes Layout",
|
||||||
|
"blocks.appearance.description": "Design- und Layout-Einstellungen",
|
||||||
|
"blocks.appearance.mode.dark": "Dunkel",
|
||||||
|
"blocks.appearance.mode.dark.description":
|
||||||
|
"Die Oberfläche verwendet immer das dunkle Design",
|
||||||
|
"blocks.appearance.mode.light": "Hell",
|
||||||
|
"blocks.appearance.mode.light.description":
|
||||||
|
"Die Oberfläche verwendet immer das helle Design",
|
||||||
|
"blocks.appearance.mode.system": "System",
|
||||||
|
"blocks.appearance.mode.system.description":
|
||||||
|
"Das Design der Oberfläche folgt der Systemeinstellung",
|
||||||
|
"blocks.appearance.mode.title": "Darstellungsmodus",
|
||||||
|
"blocks.appearance.primaryColor": "Primärfarbe",
|
||||||
|
"blocks.appearance.theme.dark": "Dunkles Design",
|
||||||
|
"blocks.appearance.theme.dark.description":
|
||||||
|
"Dieses Design wird im dunklen Systemmodus verwendet",
|
||||||
|
"blocks.appearance.theme.light": "Helles Design",
|
||||||
|
"blocks.appearance.theme.light.description":
|
||||||
|
"Dieses Design wird im hellen Systemmodus verwendet",
|
||||||
|
"blocks.appearance.title": "Darstellung",
|
||||||
|
"blocks.appearance.toggleTheme": "Design wechseln",
|
||||||
|
"blocks.appearance.toggleTheme.description":
|
||||||
|
"Zwischen hellem und dunklem Design wechseln",
|
||||||
|
} as const satisfies AppearanceMessageCatalog
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import type { MessageDescriptor } from "@workspace/i18n"
|
||||||
|
|
||||||
|
import { appearanceMessages, themeColorMessages } from "../messages.ts"
|
||||||
|
import type { AppearanceMessageCatalog } from "./catalogs"
|
||||||
|
|
||||||
|
export const locale = "en"
|
||||||
|
export const languageTag = "en-US"
|
||||||
|
|
||||||
|
function createSourceCatalog(
|
||||||
|
...groups: readonly Record<string, MessageDescriptor>[]
|
||||||
|
): AppearanceMessageCatalog {
|
||||||
|
return Object.fromEntries(
|
||||||
|
groups.flatMap((group) =>
|
||||||
|
Object.values(group).map((descriptor) => [
|
||||||
|
descriptor.id,
|
||||||
|
descriptor.message,
|
||||||
|
])
|
||||||
|
)
|
||||||
|
) as AppearanceMessageCatalog
|
||||||
|
}
|
||||||
|
|
||||||
|
export const messages = createSourceCatalog(
|
||||||
|
appearanceMessages,
|
||||||
|
themeColorMessages
|
||||||
|
) satisfies AppearanceMessageCatalog
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import type { AppearanceMessageCatalog } from "./catalogs"
|
||||||
|
|
||||||
|
export const locale = "es"
|
||||||
|
export const languageTag = "es-ES"
|
||||||
|
|
||||||
|
export const messages = {
|
||||||
|
"blocks.appearance.active": "Activo",
|
||||||
|
"blocks.appearance.baseColor": "Color base",
|
||||||
|
"blocks.appearance.color.amber": "Ámbar",
|
||||||
|
"blocks.appearance.color.blue": "Azul",
|
||||||
|
"blocks.appearance.color.cyan": "Cian",
|
||||||
|
"blocks.appearance.color.emerald": "Esmeralda",
|
||||||
|
"blocks.appearance.color.fuchsia": "Fucsia",
|
||||||
|
"blocks.appearance.color.green": "Verde",
|
||||||
|
"blocks.appearance.color.indigo": "Índigo",
|
||||||
|
"blocks.appearance.color.lime": "Lima",
|
||||||
|
"blocks.appearance.color.mauve": "Malva",
|
||||||
|
"blocks.appearance.color.mist": "Niebla",
|
||||||
|
"blocks.appearance.color.neutral": "Neutro",
|
||||||
|
"blocks.appearance.color.olive": "Oliva",
|
||||||
|
"blocks.appearance.color.orange": "Naranja",
|
||||||
|
"blocks.appearance.color.pink": "Rosa",
|
||||||
|
"blocks.appearance.color.purple": "Morado",
|
||||||
|
"blocks.appearance.color.red": "Rojo",
|
||||||
|
"blocks.appearance.color.rose": "Rosa intenso",
|
||||||
|
"blocks.appearance.color.sky": "Celeste",
|
||||||
|
"blocks.appearance.color.stone": "Piedra",
|
||||||
|
"blocks.appearance.color.taupe": "Topo",
|
||||||
|
"blocks.appearance.color.teal": "Verde azulado",
|
||||||
|
"blocks.appearance.color.violet": "Violeta",
|
||||||
|
"blocks.appearance.color.yellow": "Amarillo",
|
||||||
|
"blocks.appearance.color.zinc": "Zinc",
|
||||||
|
"blocks.appearance.command.description":
|
||||||
|
"Abrir las preferencias de tema y diseño",
|
||||||
|
"blocks.appearance.compact.description":
|
||||||
|
"Usar un ancho compacto para el contenido principal",
|
||||||
|
"blocks.appearance.compact.title": "Diseño compacto",
|
||||||
|
"blocks.appearance.description": "Preferencias de tema y diseño",
|
||||||
|
"blocks.appearance.mode.dark": "Oscuro",
|
||||||
|
"blocks.appearance.mode.dark.description":
|
||||||
|
"La interfaz siempre usará el tema oscuro",
|
||||||
|
"blocks.appearance.mode.light": "Claro",
|
||||||
|
"blocks.appearance.mode.light.description":
|
||||||
|
"La interfaz siempre usará el tema claro",
|
||||||
|
"blocks.appearance.mode.system": "Sistema",
|
||||||
|
"blocks.appearance.mode.system.description":
|
||||||
|
"El tema de la interfaz seguirá la apariencia del sistema",
|
||||||
|
"blocks.appearance.mode.title": "Modo de tema",
|
||||||
|
"blocks.appearance.primaryColor": "Color principal",
|
||||||
|
"blocks.appearance.theme.dark": "Tema oscuro",
|
||||||
|
"blocks.appearance.theme.dark.description":
|
||||||
|
"Este tema se usa cuando el sistema está en modo oscuro",
|
||||||
|
"blocks.appearance.theme.light": "Tema claro",
|
||||||
|
"blocks.appearance.theme.light.description":
|
||||||
|
"Este tema se usa cuando el sistema está en modo claro",
|
||||||
|
"blocks.appearance.title": "Apariencia",
|
||||||
|
"blocks.appearance.toggleTheme": "Cambiar tema",
|
||||||
|
"blocks.appearance.toggleTheme.description":
|
||||||
|
"Cambiar entre los temas claro y oscuro",
|
||||||
|
} as const satisfies AppearanceMessageCatalog
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import type { AppearanceMessageCatalog } from "./catalogs"
|
||||||
|
|
||||||
|
export const locale = "fr"
|
||||||
|
export const languageTag = "fr-FR"
|
||||||
|
|
||||||
|
export const messages = {
|
||||||
|
"blocks.appearance.active": "Actif",
|
||||||
|
"blocks.appearance.baseColor": "Couleur de base",
|
||||||
|
"blocks.appearance.color.amber": "Ambre",
|
||||||
|
"blocks.appearance.color.blue": "Bleu",
|
||||||
|
"blocks.appearance.color.cyan": "Cyan",
|
||||||
|
"blocks.appearance.color.emerald": "Émeraude",
|
||||||
|
"blocks.appearance.color.fuchsia": "Fuchsia",
|
||||||
|
"blocks.appearance.color.green": "Vert",
|
||||||
|
"blocks.appearance.color.indigo": "Indigo",
|
||||||
|
"blocks.appearance.color.lime": "Citron vert",
|
||||||
|
"blocks.appearance.color.mauve": "Mauve",
|
||||||
|
"blocks.appearance.color.mist": "Brume",
|
||||||
|
"blocks.appearance.color.neutral": "Neutre",
|
||||||
|
"blocks.appearance.color.olive": "Olive",
|
||||||
|
"blocks.appearance.color.orange": "Orange",
|
||||||
|
"blocks.appearance.color.pink": "Rose",
|
||||||
|
"blocks.appearance.color.purple": "Pourpre",
|
||||||
|
"blocks.appearance.color.red": "Rouge",
|
||||||
|
"blocks.appearance.color.rose": "Rose soutenu",
|
||||||
|
"blocks.appearance.color.sky": "Bleu ciel",
|
||||||
|
"blocks.appearance.color.stone": "Pierre",
|
||||||
|
"blocks.appearance.color.taupe": "Taupe",
|
||||||
|
"blocks.appearance.color.teal": "Sarcelle",
|
||||||
|
"blocks.appearance.color.violet": "Violet",
|
||||||
|
"blocks.appearance.color.yellow": "Jaune",
|
||||||
|
"blocks.appearance.color.zinc": "Zinc",
|
||||||
|
"blocks.appearance.command.description":
|
||||||
|
"Ouvrir les préférences de thème et de mise en page",
|
||||||
|
"blocks.appearance.compact.description":
|
||||||
|
"Utiliser une largeur compacte pour le contenu principal",
|
||||||
|
"blocks.appearance.compact.title": "Mise en page compacte",
|
||||||
|
"blocks.appearance.description": "Préférences de thème et de mise en page",
|
||||||
|
"blocks.appearance.mode.dark": "Sombre",
|
||||||
|
"blocks.appearance.mode.dark.description":
|
||||||
|
"L’interface utilisera toujours le thème sombre",
|
||||||
|
"blocks.appearance.mode.light": "Clair",
|
||||||
|
"blocks.appearance.mode.light.description":
|
||||||
|
"L’interface utilisera toujours le thème clair",
|
||||||
|
"blocks.appearance.mode.system": "Système",
|
||||||
|
"blocks.appearance.mode.system.description":
|
||||||
|
"Le thème de l’interface suivra l’apparence du système",
|
||||||
|
"blocks.appearance.mode.title": "Mode du thème",
|
||||||
|
"blocks.appearance.primaryColor": "Couleur principale",
|
||||||
|
"blocks.appearance.theme.dark": "Thème sombre",
|
||||||
|
"blocks.appearance.theme.dark.description":
|
||||||
|
"Ce thème est utilisé lorsque le système est en mode sombre",
|
||||||
|
"blocks.appearance.theme.light": "Thème clair",
|
||||||
|
"blocks.appearance.theme.light.description":
|
||||||
|
"Ce thème est utilisé lorsque le système est en mode clair",
|
||||||
|
"blocks.appearance.title": "Apparence",
|
||||||
|
"blocks.appearance.toggleTheme": "Changer de thème",
|
||||||
|
"blocks.appearance.toggleTheme.description":
|
||||||
|
"Basculer entre les thèmes clair et sombre",
|
||||||
|
} as const satisfies AppearanceMessageCatalog
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import type { AppearanceMessageCatalog } from "./catalogs"
|
||||||
|
|
||||||
|
export const locale = "ja"
|
||||||
|
export const languageTag = "ja-JP"
|
||||||
|
|
||||||
|
export const messages = {
|
||||||
|
"blocks.appearance.active": "使用中",
|
||||||
|
"blocks.appearance.baseColor": "ベースカラー",
|
||||||
|
"blocks.appearance.color.amber": "アンバー",
|
||||||
|
"blocks.appearance.color.blue": "ブルー",
|
||||||
|
"blocks.appearance.color.cyan": "シアン",
|
||||||
|
"blocks.appearance.color.emerald": "エメラルド",
|
||||||
|
"blocks.appearance.color.fuchsia": "フクシア",
|
||||||
|
"blocks.appearance.color.green": "グリーン",
|
||||||
|
"blocks.appearance.color.indigo": "インディゴ",
|
||||||
|
"blocks.appearance.color.lime": "ライム",
|
||||||
|
"blocks.appearance.color.mauve": "モーブ",
|
||||||
|
"blocks.appearance.color.mist": "ミスト",
|
||||||
|
"blocks.appearance.color.neutral": "ニュートラル",
|
||||||
|
"blocks.appearance.color.olive": "オリーブ",
|
||||||
|
"blocks.appearance.color.orange": "オレンジ",
|
||||||
|
"blocks.appearance.color.pink": "ピンク",
|
||||||
|
"blocks.appearance.color.purple": "パープル",
|
||||||
|
"blocks.appearance.color.red": "レッド",
|
||||||
|
"blocks.appearance.color.rose": "ローズ",
|
||||||
|
"blocks.appearance.color.sky": "スカイ",
|
||||||
|
"blocks.appearance.color.stone": "ストーン",
|
||||||
|
"blocks.appearance.color.taupe": "トープ",
|
||||||
|
"blocks.appearance.color.teal": "ティール",
|
||||||
|
"blocks.appearance.color.violet": "バイオレット",
|
||||||
|
"blocks.appearance.color.yellow": "イエロー",
|
||||||
|
"blocks.appearance.color.zinc": "ジンク",
|
||||||
|
"blocks.appearance.command.description": "テーマとレイアウト設定を開く",
|
||||||
|
"blocks.appearance.compact.description":
|
||||||
|
"メインページのコンテンツをコンパクトな幅で表示します",
|
||||||
|
"blocks.appearance.compact.title": "コンパクトレイアウト",
|
||||||
|
"blocks.appearance.description": "テーマとレイアウトの設定",
|
||||||
|
"blocks.appearance.mode.dark": "ダーク",
|
||||||
|
"blocks.appearance.mode.dark.description": "常にダークテーマを使用します",
|
||||||
|
"blocks.appearance.mode.light": "ライト",
|
||||||
|
"blocks.appearance.mode.light.description": "常にライトテーマを使用します",
|
||||||
|
"blocks.appearance.mode.system": "システム",
|
||||||
|
"blocks.appearance.mode.system.description":
|
||||||
|
"インターフェースのテーマをシステムの外観に合わせます",
|
||||||
|
"blocks.appearance.mode.title": "テーマモード",
|
||||||
|
"blocks.appearance.primaryColor": "プライマリカラー",
|
||||||
|
"blocks.appearance.theme.dark": "ダークテーマ",
|
||||||
|
"blocks.appearance.theme.dark.description":
|
||||||
|
"システムがダークモードのときに使用するテーマです",
|
||||||
|
"blocks.appearance.theme.light": "ライトテーマ",
|
||||||
|
"blocks.appearance.theme.light.description":
|
||||||
|
"システムがライトモードのときに使用するテーマです",
|
||||||
|
"blocks.appearance.title": "外観",
|
||||||
|
"blocks.appearance.toggleTheme": "テーマを切り替える",
|
||||||
|
"blocks.appearance.toggleTheme.description":
|
||||||
|
"ライトテーマとダークテーマを切り替えます",
|
||||||
|
} as const satisfies AppearanceMessageCatalog
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import type { AppearanceMessageCatalog } from "./catalogs"
|
||||||
|
|
||||||
|
export const locale = "ko"
|
||||||
|
export const languageTag = "ko-KR"
|
||||||
|
|
||||||
|
export const messages = {
|
||||||
|
"blocks.appearance.active": "사용 중",
|
||||||
|
"blocks.appearance.baseColor": "기본 색상",
|
||||||
|
"blocks.appearance.color.amber": "호박색",
|
||||||
|
"blocks.appearance.color.blue": "파란색",
|
||||||
|
"blocks.appearance.color.cyan": "청록색",
|
||||||
|
"blocks.appearance.color.emerald": "에메랄드",
|
||||||
|
"blocks.appearance.color.fuchsia": "자홍색",
|
||||||
|
"blocks.appearance.color.green": "초록색",
|
||||||
|
"blocks.appearance.color.indigo": "남색",
|
||||||
|
"blocks.appearance.color.lime": "라임",
|
||||||
|
"blocks.appearance.color.mauve": "연보라",
|
||||||
|
"blocks.appearance.color.mist": "안개색",
|
||||||
|
"blocks.appearance.color.neutral": "중성색",
|
||||||
|
"blocks.appearance.color.olive": "올리브",
|
||||||
|
"blocks.appearance.color.orange": "주황색",
|
||||||
|
"blocks.appearance.color.pink": "분홍색",
|
||||||
|
"blocks.appearance.color.purple": "보라색",
|
||||||
|
"blocks.appearance.color.red": "빨간색",
|
||||||
|
"blocks.appearance.color.rose": "장미색",
|
||||||
|
"blocks.appearance.color.sky": "하늘색",
|
||||||
|
"blocks.appearance.color.stone": "돌색",
|
||||||
|
"blocks.appearance.color.taupe": "회갈색",
|
||||||
|
"blocks.appearance.color.teal": "틸",
|
||||||
|
"blocks.appearance.color.violet": "제비꽃색",
|
||||||
|
"blocks.appearance.color.yellow": "노란색",
|
||||||
|
"blocks.appearance.color.zinc": "아연색",
|
||||||
|
"blocks.appearance.command.description": "테마 및 레이아웃 환경설정 열기",
|
||||||
|
"blocks.appearance.compact.description":
|
||||||
|
"기본 페이지 콘텐츠에 좁은 너비를 사용합니다",
|
||||||
|
"blocks.appearance.compact.title": "컴팩트 레이아웃",
|
||||||
|
"blocks.appearance.description": "테마 및 레이아웃 환경설정",
|
||||||
|
"blocks.appearance.mode.dark": "어둡게",
|
||||||
|
"blocks.appearance.mode.dark.description":
|
||||||
|
"인터페이스에서 항상 어두운 테마를 사용합니다",
|
||||||
|
"blocks.appearance.mode.light": "밝게",
|
||||||
|
"blocks.appearance.mode.light.description":
|
||||||
|
"인터페이스에서 항상 밝은 테마를 사용합니다",
|
||||||
|
"blocks.appearance.mode.system": "시스템",
|
||||||
|
"blocks.appearance.mode.system.description":
|
||||||
|
"인터페이스 테마가 시스템 화면 모드를 따릅니다",
|
||||||
|
"blocks.appearance.mode.title": "테마 모드",
|
||||||
|
"blocks.appearance.primaryColor": "주요 색상",
|
||||||
|
"blocks.appearance.theme.dark": "어두운 테마",
|
||||||
|
"blocks.appearance.theme.dark.description":
|
||||||
|
"시스템이 어두운 모드일 때 사용하는 테마입니다",
|
||||||
|
"blocks.appearance.theme.light": "밝은 테마",
|
||||||
|
"blocks.appearance.theme.light.description":
|
||||||
|
"시스템이 밝은 모드일 때 사용하는 테마입니다",
|
||||||
|
"blocks.appearance.title": "화면 모양",
|
||||||
|
"blocks.appearance.toggleTheme": "테마 전환",
|
||||||
|
"blocks.appearance.toggleTheme.description":
|
||||||
|
"밝은 테마와 어두운 테마 사이를 전환합니다",
|
||||||
|
} as const satisfies AppearanceMessageCatalog
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import type { AppearanceMessageCatalog } from "./catalogs"
|
||||||
|
|
||||||
|
export const locale = "zh-Hans"
|
||||||
|
export const languageTag = "zh-CN"
|
||||||
|
|
||||||
|
export const messages = {
|
||||||
|
"blocks.appearance.active": "使用中",
|
||||||
|
"blocks.appearance.baseColor": "基础颜色",
|
||||||
|
"blocks.appearance.color.amber": "琥珀色",
|
||||||
|
"blocks.appearance.color.blue": "蓝色",
|
||||||
|
"blocks.appearance.color.cyan": "青色",
|
||||||
|
"blocks.appearance.color.emerald": "翠绿色",
|
||||||
|
"blocks.appearance.color.fuchsia": "紫红色",
|
||||||
|
"blocks.appearance.color.green": "绿色",
|
||||||
|
"blocks.appearance.color.indigo": "靛蓝色",
|
||||||
|
"blocks.appearance.color.lime": "青柠色",
|
||||||
|
"blocks.appearance.color.mauve": "紫灰",
|
||||||
|
"blocks.appearance.color.mist": "雾灰",
|
||||||
|
"blocks.appearance.color.neutral": "中性灰",
|
||||||
|
"blocks.appearance.color.olive": "橄榄灰",
|
||||||
|
"blocks.appearance.color.orange": "橙色",
|
||||||
|
"blocks.appearance.color.pink": "粉色",
|
||||||
|
"blocks.appearance.color.purple": "紫色",
|
||||||
|
"blocks.appearance.color.red": "红色",
|
||||||
|
"blocks.appearance.color.rose": "玫红色",
|
||||||
|
"blocks.appearance.color.sky": "天蓝色",
|
||||||
|
"blocks.appearance.color.stone": "石灰",
|
||||||
|
"blocks.appearance.color.taupe": "灰褐",
|
||||||
|
"blocks.appearance.color.teal": "蓝绿色",
|
||||||
|
"blocks.appearance.color.violet": "紫罗兰色",
|
||||||
|
"blocks.appearance.color.yellow": "黄色",
|
||||||
|
"blocks.appearance.color.zinc": "锌灰",
|
||||||
|
"blocks.appearance.command.description": "打开主题与布局偏好",
|
||||||
|
"blocks.appearance.compact.description": "启用后,页面主内容将使用紧凑宽度",
|
||||||
|
"blocks.appearance.compact.title": "紧凑布局",
|
||||||
|
"blocks.appearance.description": "主题与布局偏好",
|
||||||
|
"blocks.appearance.mode.dark": "深色",
|
||||||
|
"blocks.appearance.mode.dark.description": "界面将始终使用深色主题",
|
||||||
|
"blocks.appearance.mode.light": "浅色",
|
||||||
|
"blocks.appearance.mode.light.description": "界面将始终使用浅色主题",
|
||||||
|
"blocks.appearance.mode.system": "系统",
|
||||||
|
"blocks.appearance.mode.system.description": "界面主题将跟随系统外观设置",
|
||||||
|
"blocks.appearance.mode.title": "主题模式",
|
||||||
|
"blocks.appearance.primaryColor": "主要颜色",
|
||||||
|
"blocks.appearance.theme.dark": "深色主题",
|
||||||
|
"blocks.appearance.theme.dark.description":
|
||||||
|
"当系统设置为深色模式时,将使用此主题",
|
||||||
|
"blocks.appearance.theme.light": "浅色主题",
|
||||||
|
"blocks.appearance.theme.light.description":
|
||||||
|
"当系统设置为浅色模式时,将使用此主题",
|
||||||
|
"blocks.appearance.title": "界面外观",
|
||||||
|
"blocks.appearance.toggleTheme": "切换主题",
|
||||||
|
"blocks.appearance.toggleTheme.description": "在浅色与深色主题之间切换",
|
||||||
|
} as const satisfies AppearanceMessageCatalog
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import type { AppearanceMessageCatalog } from "./catalogs"
|
||||||
|
|
||||||
|
export const locale = "zh-Hant"
|
||||||
|
export const languageTag = "zh-TW"
|
||||||
|
|
||||||
|
export const messages = {
|
||||||
|
"blocks.appearance.active": "使用中",
|
||||||
|
"blocks.appearance.baseColor": "基礎顏色",
|
||||||
|
"blocks.appearance.color.amber": "琥珀色",
|
||||||
|
"blocks.appearance.color.blue": "藍色",
|
||||||
|
"blocks.appearance.color.cyan": "青色",
|
||||||
|
"blocks.appearance.color.emerald": "翠綠色",
|
||||||
|
"blocks.appearance.color.fuchsia": "紫紅色",
|
||||||
|
"blocks.appearance.color.green": "綠色",
|
||||||
|
"blocks.appearance.color.indigo": "靛藍色",
|
||||||
|
"blocks.appearance.color.lime": "萊姆色",
|
||||||
|
"blocks.appearance.color.mauve": "淡紫色",
|
||||||
|
"blocks.appearance.color.mist": "霧灰色",
|
||||||
|
"blocks.appearance.color.neutral": "中性灰",
|
||||||
|
"blocks.appearance.color.olive": "橄欖色",
|
||||||
|
"blocks.appearance.color.orange": "橙色",
|
||||||
|
"blocks.appearance.color.pink": "粉紅色",
|
||||||
|
"blocks.appearance.color.purple": "紫色",
|
||||||
|
"blocks.appearance.color.red": "紅色",
|
||||||
|
"blocks.appearance.color.rose": "玫瑰色",
|
||||||
|
"blocks.appearance.color.sky": "天藍色",
|
||||||
|
"blocks.appearance.color.stone": "石灰色",
|
||||||
|
"blocks.appearance.color.taupe": "灰褐色",
|
||||||
|
"blocks.appearance.color.teal": "藍綠色",
|
||||||
|
"blocks.appearance.color.violet": "紫羅蘭色",
|
||||||
|
"blocks.appearance.color.yellow": "黃色",
|
||||||
|
"blocks.appearance.color.zinc": "鋅灰色",
|
||||||
|
"blocks.appearance.command.description": "開啟主題與版面配置偏好",
|
||||||
|
"blocks.appearance.compact.description": "使用較緊湊的主頁內容寬度",
|
||||||
|
"blocks.appearance.compact.title": "緊湊版面配置",
|
||||||
|
"blocks.appearance.description": "主題與版面配置偏好",
|
||||||
|
"blocks.appearance.mode.dark": "深色",
|
||||||
|
"blocks.appearance.mode.dark.description": "介面將一律使用深色主題",
|
||||||
|
"blocks.appearance.mode.light": "淺色",
|
||||||
|
"blocks.appearance.mode.light.description": "介面將一律使用淺色主題",
|
||||||
|
"blocks.appearance.mode.system": "系統",
|
||||||
|
"blocks.appearance.mode.system.description": "介面主題將跟隨系統外觀設定",
|
||||||
|
"blocks.appearance.mode.title": "主題模式",
|
||||||
|
"blocks.appearance.primaryColor": "主要顏色",
|
||||||
|
"blocks.appearance.theme.dark": "深色主題",
|
||||||
|
"blocks.appearance.theme.dark.description": "系統使用深色模式時套用此主題",
|
||||||
|
"blocks.appearance.theme.light": "淺色主題",
|
||||||
|
"blocks.appearance.theme.light.description": "系統使用淺色模式時套用此主題",
|
||||||
|
"blocks.appearance.title": "介面外觀",
|
||||||
|
"blocks.appearance.toggleTheme": "切換主題",
|
||||||
|
"blocks.appearance.toggleTheme.description": "在淺色與深色主題之間切換",
|
||||||
|
} as const satisfies AppearanceMessageCatalog
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
import type { MessageDescriptor } from "@workspace/i18n"
|
||||||
|
|
||||||
|
export const appearanceMessages = {
|
||||||
|
active: /* i18n */ {
|
||||||
|
id: "blocks.appearance.active",
|
||||||
|
message: "Active",
|
||||||
|
},
|
||||||
|
appearance: /* i18n */ {
|
||||||
|
id: "blocks.appearance.title",
|
||||||
|
message: "Appearance",
|
||||||
|
},
|
||||||
|
appearanceCommandDescription: /* i18n */ {
|
||||||
|
id: "blocks.appearance.command.description",
|
||||||
|
message: "Open theme and layout preferences",
|
||||||
|
},
|
||||||
|
baseColor: /* i18n */ {
|
||||||
|
id: "blocks.appearance.baseColor",
|
||||||
|
message: "Base color",
|
||||||
|
},
|
||||||
|
compactDescription: /* i18n */ {
|
||||||
|
id: "blocks.appearance.compact.description",
|
||||||
|
message: "Use a compact width for the main page content",
|
||||||
|
},
|
||||||
|
compactLayout: /* i18n */ {
|
||||||
|
id: "blocks.appearance.compact.title",
|
||||||
|
message: "Compact layout",
|
||||||
|
},
|
||||||
|
darkMode: /* i18n */ {
|
||||||
|
id: "blocks.appearance.mode.dark",
|
||||||
|
message: "Dark",
|
||||||
|
},
|
||||||
|
darkModeDescription: /* i18n */ {
|
||||||
|
id: "blocks.appearance.mode.dark.description",
|
||||||
|
message: "The interface will always use the dark theme",
|
||||||
|
},
|
||||||
|
darkTheme: /* i18n */ {
|
||||||
|
id: "blocks.appearance.theme.dark",
|
||||||
|
message: "Dark theme",
|
||||||
|
},
|
||||||
|
darkThemeDescription: /* i18n */ {
|
||||||
|
id: "blocks.appearance.theme.dark.description",
|
||||||
|
message: "This theme is used when the system is in dark mode",
|
||||||
|
},
|
||||||
|
followSystem: /* i18n */ {
|
||||||
|
id: "blocks.appearance.mode.system",
|
||||||
|
message: "System",
|
||||||
|
},
|
||||||
|
followSystemDescription: /* i18n */ {
|
||||||
|
id: "blocks.appearance.mode.system.description",
|
||||||
|
message: "The interface theme will follow the system appearance",
|
||||||
|
},
|
||||||
|
lightMode: /* i18n */ {
|
||||||
|
id: "blocks.appearance.mode.light",
|
||||||
|
message: "Light",
|
||||||
|
},
|
||||||
|
lightModeDescription: /* i18n */ {
|
||||||
|
id: "blocks.appearance.mode.light.description",
|
||||||
|
message: "The interface will always use the light theme",
|
||||||
|
},
|
||||||
|
lightTheme: /* i18n */ {
|
||||||
|
id: "blocks.appearance.theme.light",
|
||||||
|
message: "Light theme",
|
||||||
|
},
|
||||||
|
lightThemeDescription: /* i18n */ {
|
||||||
|
id: "blocks.appearance.theme.light.description",
|
||||||
|
message: "This theme is used when the system is in light mode",
|
||||||
|
},
|
||||||
|
primaryColor: /* i18n */ {
|
||||||
|
id: "blocks.appearance.primaryColor",
|
||||||
|
message: "Primary color",
|
||||||
|
},
|
||||||
|
themeAndLayoutPreferences: /* i18n */ {
|
||||||
|
id: "blocks.appearance.description",
|
||||||
|
message: "Theme and layout preferences",
|
||||||
|
},
|
||||||
|
themeMode: /* i18n */ {
|
||||||
|
id: "blocks.appearance.mode.title",
|
||||||
|
message: "Theme mode",
|
||||||
|
},
|
||||||
|
toggleTheme: /* i18n */ {
|
||||||
|
id: "blocks.appearance.toggleTheme",
|
||||||
|
message: "Toggle theme",
|
||||||
|
},
|
||||||
|
toggleThemeDescription: /* i18n */ {
|
||||||
|
id: "blocks.appearance.toggleTheme.description",
|
||||||
|
message: "Switch between light and dark themes",
|
||||||
|
},
|
||||||
|
} as const satisfies Record<string, MessageDescriptor>
|
||||||
|
|
||||||
|
export const themeColorMessages = {
|
||||||
|
amber: /* i18n */ {
|
||||||
|
id: "blocks.appearance.color.amber",
|
||||||
|
message: "Amber",
|
||||||
|
},
|
||||||
|
blue: /* i18n */ {
|
||||||
|
id: "blocks.appearance.color.blue",
|
||||||
|
message: "Blue",
|
||||||
|
},
|
||||||
|
cyan: /* i18n */ {
|
||||||
|
id: "blocks.appearance.color.cyan",
|
||||||
|
message: "Cyan",
|
||||||
|
},
|
||||||
|
emerald: /* i18n */ {
|
||||||
|
id: "blocks.appearance.color.emerald",
|
||||||
|
message: "Emerald",
|
||||||
|
},
|
||||||
|
fuchsia: /* i18n */ {
|
||||||
|
id: "blocks.appearance.color.fuchsia",
|
||||||
|
message: "Fuchsia",
|
||||||
|
},
|
||||||
|
green: /* i18n */ {
|
||||||
|
id: "blocks.appearance.color.green",
|
||||||
|
message: "Green",
|
||||||
|
},
|
||||||
|
indigo: /* i18n */ {
|
||||||
|
id: "blocks.appearance.color.indigo",
|
||||||
|
message: "Indigo",
|
||||||
|
},
|
||||||
|
lime: /* i18n */ {
|
||||||
|
id: "blocks.appearance.color.lime",
|
||||||
|
message: "Lime",
|
||||||
|
},
|
||||||
|
mauve: /* i18n */ {
|
||||||
|
id: "blocks.appearance.color.mauve",
|
||||||
|
message: "Mauve",
|
||||||
|
},
|
||||||
|
mist: /* i18n */ {
|
||||||
|
id: "blocks.appearance.color.mist",
|
||||||
|
message: "Mist",
|
||||||
|
},
|
||||||
|
neutral: /* i18n */ {
|
||||||
|
id: "blocks.appearance.color.neutral",
|
||||||
|
message: "Neutral",
|
||||||
|
},
|
||||||
|
olive: /* i18n */ {
|
||||||
|
id: "blocks.appearance.color.olive",
|
||||||
|
message: "Olive",
|
||||||
|
},
|
||||||
|
orange: /* i18n */ {
|
||||||
|
id: "blocks.appearance.color.orange",
|
||||||
|
message: "Orange",
|
||||||
|
},
|
||||||
|
pink: /* i18n */ {
|
||||||
|
id: "blocks.appearance.color.pink",
|
||||||
|
message: "Pink",
|
||||||
|
},
|
||||||
|
purple: /* i18n */ {
|
||||||
|
id: "blocks.appearance.color.purple",
|
||||||
|
message: "Purple",
|
||||||
|
},
|
||||||
|
red: /* i18n */ {
|
||||||
|
id: "blocks.appearance.color.red",
|
||||||
|
message: "Red",
|
||||||
|
},
|
||||||
|
rose: /* i18n */ {
|
||||||
|
id: "blocks.appearance.color.rose",
|
||||||
|
message: "Rose",
|
||||||
|
},
|
||||||
|
sky: /* i18n */ {
|
||||||
|
id: "blocks.appearance.color.sky",
|
||||||
|
message: "Sky",
|
||||||
|
},
|
||||||
|
stone: /* i18n */ {
|
||||||
|
id: "blocks.appearance.color.stone",
|
||||||
|
message: "Stone",
|
||||||
|
},
|
||||||
|
taupe: /* i18n */ {
|
||||||
|
id: "blocks.appearance.color.taupe",
|
||||||
|
message: "Taupe",
|
||||||
|
},
|
||||||
|
teal: /* i18n */ {
|
||||||
|
id: "blocks.appearance.color.teal",
|
||||||
|
message: "Teal",
|
||||||
|
},
|
||||||
|
violet: /* i18n */ {
|
||||||
|
id: "blocks.appearance.color.violet",
|
||||||
|
message: "Violet",
|
||||||
|
},
|
||||||
|
yellow: /* i18n */ {
|
||||||
|
id: "blocks.appearance.color.yellow",
|
||||||
|
message: "Yellow",
|
||||||
|
},
|
||||||
|
zinc: /* i18n */ {
|
||||||
|
id: "blocks.appearance.color.zinc",
|
||||||
|
message: "Zinc",
|
||||||
|
},
|
||||||
|
} as const satisfies Record<string, MessageDescriptor>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { MonitorIcon, MoonIcon, SunIcon } from "lucide-react"
|
import { MonitorIcon, MoonIcon, SunIcon } from "lucide-react"
|
||||||
|
import { LocalizedText, useMessage } from "@workspace/i18n"
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -10,12 +11,7 @@ import { Switch } from "@workspace/ui/components/switch"
|
|||||||
|
|
||||||
import type { ThemeMode } from "./state"
|
import type { ThemeMode } from "./state"
|
||||||
import { useUiState } from "./ui-state"
|
import { useUiState } from "./ui-state"
|
||||||
|
import { appearanceMessages } from "./messages"
|
||||||
const modeItems: ReadonlyArray<{ label: string; value: ThemeMode }> = [
|
|
||||||
{ label: "使用浅色", value: "light" },
|
|
||||||
{ label: "使用深色", value: "dark" },
|
|
||||||
{ label: "跟随系统", value: "system" },
|
|
||||||
]
|
|
||||||
|
|
||||||
function ModeIcon({ mode }: { mode: ThemeMode | null }) {
|
function ModeIcon({ mode }: { mode: ThemeMode | null }) {
|
||||||
if (mode === "light") return <SunIcon />
|
if (mode === "light") return <SunIcon />
|
||||||
@@ -25,15 +21,29 @@ function ModeIcon({ mode }: { mode: ThemeMode | null }) {
|
|||||||
|
|
||||||
export function ModePreference() {
|
export function ModePreference() {
|
||||||
const [themeMode, setThemeMode] = useUiState("theme-mode")
|
const [themeMode, setThemeMode] = useUiState("theme-mode")
|
||||||
|
const lightLabel = useMessage(appearanceMessages.lightMode)
|
||||||
|
const darkLabel = useMessage(appearanceMessages.darkMode)
|
||||||
|
const systemLabel = useMessage(appearanceMessages.followSystem)
|
||||||
|
const lightDescription = useMessage(appearanceMessages.lightModeDescription)
|
||||||
|
const darkDescription = useMessage(appearanceMessages.darkModeDescription)
|
||||||
|
const systemDescription = useMessage(
|
||||||
|
appearanceMessages.followSystemDescription
|
||||||
|
)
|
||||||
|
const title = useMessage(appearanceMessages.themeMode)
|
||||||
|
const modeItems: ReadonlyArray<{ label: string; value: ThemeMode }> = [
|
||||||
|
{ label: lightLabel, value: "light" },
|
||||||
|
{ label: darkLabel, value: "dark" },
|
||||||
|
{ label: systemLabel, value: "system" },
|
||||||
|
]
|
||||||
const description = {
|
const description = {
|
||||||
light: "界面将始终使用浅色主题",
|
light: lightDescription,
|
||||||
dark: "界面将始终使用深色主题",
|
dark: darkDescription,
|
||||||
system: "界面主题将跟随系统外观设置",
|
system: systemDescription,
|
||||||
}[themeMode]
|
}[themeMode]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="lg:flex-1">
|
<div className="lg:flex-1">
|
||||||
<h2 className="font-semibold">主题模式</h2>
|
<h2 className="font-semibold">{title}</h2>
|
||||||
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
|
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
|
||||||
<Select
|
<Select
|
||||||
items={modeItems}
|
items={modeItems}
|
||||||
@@ -47,9 +57,7 @@ export function ModePreference() {
|
|||||||
{(value: ThemeMode | null) => (
|
{(value: ThemeMode | null) => (
|
||||||
<>
|
<>
|
||||||
<ModeIcon mode={value} />
|
<ModeIcon mode={value} />
|
||||||
{modeItems
|
{modeItems.find((item) => item.value === value)?.label}
|
||||||
.find((item) => item.value === value)
|
|
||||||
?.label.slice(-2)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</SelectValue>
|
</SelectValue>
|
||||||
@@ -72,9 +80,11 @@ export function CompactPreference() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="lg:flex-1">
|
<div className="lg:flex-1">
|
||||||
<h2 className="font-semibold">紧凑布局</h2>
|
<h2 className="font-semibold">
|
||||||
|
<LocalizedText message={appearanceMessages.compactLayout} />
|
||||||
|
</h2>
|
||||||
<p className="mt-1 text-sm text-muted-foreground">
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
启用后,页面主内容将使用紧凑宽度
|
<LocalizedText message={appearanceMessages.compactDescription} />
|
||||||
</p>
|
</p>
|
||||||
<Switch
|
<Switch
|
||||||
className="mt-2"
|
className="mt-2"
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { CSSProperties } from "react"
|
import type { CSSProperties, ReactNode } from "react"
|
||||||
|
import { useMessage } from "@workspace/i18n"
|
||||||
import { cn } from "@workspace/ui/lib/utils"
|
import { cn } from "@workspace/ui/lib/utils"
|
||||||
|
import { themeColorMessages } from "./messages"
|
||||||
import {
|
import {
|
||||||
accentColors,
|
accentColors,
|
||||||
baseColors,
|
baseColors,
|
||||||
@@ -12,7 +14,6 @@ type ThemeColor = BaseColor | AccentColor
|
|||||||
|
|
||||||
type ThemeColorSwatch<T extends ThemeColor> = {
|
type ThemeColorSwatch<T extends ThemeColor> = {
|
||||||
name: T
|
name: T
|
||||||
title: string
|
|
||||||
light: {
|
light: {
|
||||||
background: string
|
background: string
|
||||||
foreground: string
|
foreground: string
|
||||||
@@ -31,7 +32,6 @@ function createSwatches<T extends ThemeColor>(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
title: theme.title,
|
|
||||||
light: {
|
light: {
|
||||||
background: theme.cssVars.light.primary,
|
background: theme.cssVars.light.primary,
|
||||||
foreground: theme.cssVars.light["primary-foreground"],
|
foreground: theme.cssVars.light["primary-foreground"],
|
||||||
@@ -54,7 +54,7 @@ export function ThemeColorPicker<T extends ThemeColor>({
|
|||||||
onChange,
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
swatches: readonly ThemeColorSwatch<T>[]
|
swatches: readonly ThemeColorSwatch<T>[]
|
||||||
title: string
|
title: ReactNode
|
||||||
value: T
|
value: T
|
||||||
onChange: (color: T) => void
|
onChange: (color: T) => void
|
||||||
}) {
|
}) {
|
||||||
@@ -86,6 +86,7 @@ function ThemeColorButton({
|
|||||||
active: boolean
|
active: boolean
|
||||||
onClick: () => void
|
onClick: () => void
|
||||||
}) {
|
}) {
|
||||||
|
const swatchTitle = useMessage(themeColorMessages[swatch.name])
|
||||||
const style = {
|
const style = {
|
||||||
"--light-bg": swatch.light.background,
|
"--light-bg": swatch.light.background,
|
||||||
"--light-fg": swatch.light.foreground,
|
"--light-fg": swatch.light.foreground,
|
||||||
@@ -96,7 +97,7 @@ function ThemeColorButton({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={swatch.title}
|
aria-label={swatchTitle}
|
||||||
aria-pressed={active}
|
aria-pressed={active}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-8 min-w-8 appearance-none truncate overflow-hidden rounded-4xl border-2 px-2 text-xs whitespace-nowrap ring-popover ring-inset",
|
"h-8 min-w-8 appearance-none truncate overflow-hidden rounded-4xl border-2 px-2 text-xs whitespace-nowrap ring-popover ring-inset",
|
||||||
@@ -108,7 +109,7 @@ function ThemeColorButton({
|
|||||||
style={style}
|
style={style}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
{swatch.title}
|
{swatchTitle}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { MoonIcon, SunIcon } from "lucide-react"
|
import { MoonIcon, SunIcon } from "lucide-react"
|
||||||
|
import { LocalizedText } from "@workspace/i18n"
|
||||||
import { useUiState } from "./ui-state"
|
import { useUiState } from "./ui-state"
|
||||||
import { Badge } from "@workspace/ui/components/badge"
|
import { Badge } from "@workspace/ui/components/badge"
|
||||||
import {
|
import {
|
||||||
@@ -8,18 +9,25 @@ import {
|
|||||||
} from "./theme-color-picker"
|
} from "./theme-color-picker"
|
||||||
import { ThemePreview } from "./theme-preview"
|
import { ThemePreview } from "./theme-preview"
|
||||||
import type { ThemeScheme } from "./theme"
|
import type { ThemeScheme } from "./theme"
|
||||||
|
import { appearanceMessages } from "./messages"
|
||||||
|
|
||||||
export function ThemeConfigPanel({
|
export function ThemeConfigPanel({
|
||||||
scheme,
|
scheme,
|
||||||
active,
|
active,
|
||||||
description,
|
|
||||||
}: {
|
}: {
|
||||||
scheme: ThemeScheme
|
scheme: ThemeScheme
|
||||||
active: boolean
|
active: boolean
|
||||||
description: string
|
|
||||||
}) {
|
}) {
|
||||||
const stateKey = scheme === "light" ? "theme-light" : "theme-dark"
|
const stateKey = scheme === "light" ? "theme-light" : "theme-dark"
|
||||||
const [config, setConfig] = useUiState(stateKey)
|
const [config, setConfig] = useUiState(stateKey)
|
||||||
|
const titleMessage =
|
||||||
|
scheme === "light"
|
||||||
|
? appearanceMessages.lightTheme
|
||||||
|
: appearanceMessages.darkTheme
|
||||||
|
const descriptionMessage =
|
||||||
|
scheme === "light"
|
||||||
|
? appearanceMessages.lightThemeDescription
|
||||||
|
: appearanceMessages.darkThemeDescription
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-md rounded-xl border">
|
<div className="w-md rounded-xl border">
|
||||||
@@ -29,15 +37,23 @@ export function ThemeConfigPanel({
|
|||||||
) : (
|
) : (
|
||||||
<MoonIcon className="size-5" />
|
<MoonIcon className="size-5" />
|
||||||
)}
|
)}
|
||||||
<span>{scheme === "light" ? "浅色主题" : "深色主题"}</span>
|
<span>
|
||||||
{active && <Badge className="ml-auto">使用中</Badge>}
|
<LocalizedText message={titleMessage} />
|
||||||
|
</span>
|
||||||
|
{active && (
|
||||||
|
<Badge className="ml-auto">
|
||||||
|
<LocalizedText message={appearanceMessages.active} />
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4 p-4">
|
<div className="space-y-4 p-4">
|
||||||
<p>{description}</p>
|
<p>
|
||||||
|
<LocalizedText message={descriptionMessage} />
|
||||||
|
</p>
|
||||||
<ThemePreview scheme={scheme} config={config} />
|
<ThemePreview scheme={scheme} config={config} />
|
||||||
<ThemeColorPicker
|
<ThemeColorPicker
|
||||||
swatches={baseColorSwatches}
|
swatches={baseColorSwatches}
|
||||||
title="基础颜色"
|
title={<LocalizedText message={appearanceMessages.baseColor} />}
|
||||||
value={config.baseColor}
|
value={config.baseColor}
|
||||||
onChange={(baseColor) =>
|
onChange={(baseColor) =>
|
||||||
setConfig((current) => ({ ...current, baseColor }))
|
setConfig((current) => ({ ...current, baseColor }))
|
||||||
@@ -45,7 +61,7 @@ export function ThemeConfigPanel({
|
|||||||
/>
|
/>
|
||||||
<ThemeColorPicker
|
<ThemeColorPicker
|
||||||
swatches={accentColorSwatches}
|
swatches={accentColorSwatches}
|
||||||
title="主要颜色"
|
title={<LocalizedText message={appearanceMessages.primaryColor} />}
|
||||||
value={config.accentColor}
|
value={config.accentColor}
|
||||||
onChange={(accentColor) =>
|
onChange={(accentColor) =>
|
||||||
setConfig((current) => ({ ...current, accentColor }))
|
setConfig((current) => ({ ...current, accentColor }))
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ describe("appearance themes", () => {
|
|||||||
|
|
||||||
expect(themes.map((theme) => theme.name)).toEqual(selectableColors)
|
expect(themes.map((theme) => theme.name)).toEqual(selectableColors)
|
||||||
expect(new Set(selectableColors).size).toBe(selectableColors.length)
|
expect(new Set(selectableColors).size).toBe(selectableColors.length)
|
||||||
|
expect(new Set(themes.map((theme) => theme.name)).size).toBe(themes.length)
|
||||||
expect(themes.every((theme) => theme.title.length > 0)).toBe(true)
|
expect(themes.every((theme) => theme.title.length > 0)).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
export type BaseColor =
|
export type BaseColor =
|
||||||
"neutral" | "stone" | "zinc" | "mauve" | "olive" | "mist" | "taupe"
|
| "neutral"
|
||||||
|
| "stone"
|
||||||
|
| "zinc"
|
||||||
|
| "mauve"
|
||||||
|
| "olive"
|
||||||
|
| "mist"
|
||||||
|
| "taupe"
|
||||||
|
|
||||||
export type AccentColor =
|
export type AccentColor =
|
||||||
| "amber"
|
| "amber"
|
||||||
@@ -69,7 +75,7 @@ export type Theme = {
|
|||||||
export const themes: Theme[] = [
|
export const themes: Theme[] = [
|
||||||
{
|
{
|
||||||
name: "neutral",
|
name: "neutral",
|
||||||
title: "中性灰",
|
title: "Neutral",
|
||||||
cssVars: {
|
cssVars: {
|
||||||
light: {
|
light: {
|
||||||
background: "oklch(1 0 0)",
|
background: "oklch(1 0 0)",
|
||||||
@@ -142,7 +148,7 @@ export const themes: Theme[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "stone",
|
name: "stone",
|
||||||
title: "石灰",
|
title: "Stone",
|
||||||
cssVars: {
|
cssVars: {
|
||||||
light: {
|
light: {
|
||||||
background: "oklch(1 0 0)",
|
background: "oklch(1 0 0)",
|
||||||
@@ -215,7 +221,7 @@ export const themes: Theme[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "zinc",
|
name: "zinc",
|
||||||
title: "锌灰",
|
title: "Zinc",
|
||||||
cssVars: {
|
cssVars: {
|
||||||
light: {
|
light: {
|
||||||
background: "oklch(1 0 0)",
|
background: "oklch(1 0 0)",
|
||||||
@@ -288,7 +294,7 @@ export const themes: Theme[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "mauve",
|
name: "mauve",
|
||||||
title: "紫灰",
|
title: "Mauve",
|
||||||
cssVars: {
|
cssVars: {
|
||||||
light: {
|
light: {
|
||||||
background: "oklch(1 0 0)",
|
background: "oklch(1 0 0)",
|
||||||
@@ -361,7 +367,7 @@ export const themes: Theme[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "olive",
|
name: "olive",
|
||||||
title: "橄榄灰",
|
title: "Olive",
|
||||||
cssVars: {
|
cssVars: {
|
||||||
light: {
|
light: {
|
||||||
background: "oklch(1 0 0)",
|
background: "oklch(1 0 0)",
|
||||||
@@ -434,7 +440,7 @@ export const themes: Theme[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "mist",
|
name: "mist",
|
||||||
title: "雾灰",
|
title: "Mist",
|
||||||
cssVars: {
|
cssVars: {
|
||||||
light: {
|
light: {
|
||||||
background: "oklch(1 0 0)",
|
background: "oklch(1 0 0)",
|
||||||
@@ -507,7 +513,7 @@ export const themes: Theme[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "taupe",
|
name: "taupe",
|
||||||
title: "灰褐",
|
title: "Taupe",
|
||||||
cssVars: {
|
cssVars: {
|
||||||
light: {
|
light: {
|
||||||
background: "oklch(1 0 0)",
|
background: "oklch(1 0 0)",
|
||||||
@@ -580,7 +586,7 @@ export const themes: Theme[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "amber",
|
name: "amber",
|
||||||
title: "琥珀色",
|
title: "Amber",
|
||||||
cssVars: {
|
cssVars: {
|
||||||
light: {
|
light: {
|
||||||
primary: "oklch(0.555 0.163 48.998)",
|
primary: "oklch(0.555 0.163 48.998)",
|
||||||
@@ -612,7 +618,7 @@ export const themes: Theme[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "blue",
|
name: "blue",
|
||||||
title: "蓝色",
|
title: "Blue",
|
||||||
cssVars: {
|
cssVars: {
|
||||||
light: {
|
light: {
|
||||||
primary: "oklch(0.488 0.243 264.376)",
|
primary: "oklch(0.488 0.243 264.376)",
|
||||||
@@ -644,7 +650,7 @@ export const themes: Theme[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "cyan",
|
name: "cyan",
|
||||||
title: "青色",
|
title: "Cyan",
|
||||||
cssVars: {
|
cssVars: {
|
||||||
light: {
|
light: {
|
||||||
primary: "oklch(0.52 0.105 223.128)",
|
primary: "oklch(0.52 0.105 223.128)",
|
||||||
@@ -676,7 +682,7 @@ export const themes: Theme[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "emerald",
|
name: "emerald",
|
||||||
title: "翠绿色",
|
title: "Emerald",
|
||||||
cssVars: {
|
cssVars: {
|
||||||
light: {
|
light: {
|
||||||
primary: "oklch(0.508 0.118 165.612)",
|
primary: "oklch(0.508 0.118 165.612)",
|
||||||
@@ -708,7 +714,7 @@ export const themes: Theme[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "fuchsia",
|
name: "fuchsia",
|
||||||
title: "紫红色",
|
title: "Fuchsia",
|
||||||
cssVars: {
|
cssVars: {
|
||||||
light: {
|
light: {
|
||||||
primary: "oklch(0.518 0.253 323.949)",
|
primary: "oklch(0.518 0.253 323.949)",
|
||||||
@@ -740,7 +746,7 @@ export const themes: Theme[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "green",
|
name: "green",
|
||||||
title: "绿色",
|
title: "Green",
|
||||||
cssVars: {
|
cssVars: {
|
||||||
light: {
|
light: {
|
||||||
primary: "oklch(0.527 0.154 150.069)",
|
primary: "oklch(0.527 0.154 150.069)",
|
||||||
@@ -772,7 +778,7 @@ export const themes: Theme[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "indigo",
|
name: "indigo",
|
||||||
title: "靛蓝色",
|
title: "Indigo",
|
||||||
cssVars: {
|
cssVars: {
|
||||||
light: {
|
light: {
|
||||||
primary: "oklch(0.457 0.24 277.023)",
|
primary: "oklch(0.457 0.24 277.023)",
|
||||||
@@ -804,7 +810,7 @@ export const themes: Theme[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "lime",
|
name: "lime",
|
||||||
title: "青柠色",
|
title: "Lime",
|
||||||
cssVars: {
|
cssVars: {
|
||||||
light: {
|
light: {
|
||||||
primary: "oklch(0.841 0.238 128.85)",
|
primary: "oklch(0.841 0.238 128.85)",
|
||||||
@@ -836,7 +842,7 @@ export const themes: Theme[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "orange",
|
name: "orange",
|
||||||
title: "橙色",
|
title: "Orange",
|
||||||
cssVars: {
|
cssVars: {
|
||||||
light: {
|
light: {
|
||||||
primary: "oklch(0.553 0.195 38.402)",
|
primary: "oklch(0.553 0.195 38.402)",
|
||||||
@@ -868,7 +874,7 @@ export const themes: Theme[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "pink",
|
name: "pink",
|
||||||
title: "粉色",
|
title: "Pink",
|
||||||
cssVars: {
|
cssVars: {
|
||||||
light: {
|
light: {
|
||||||
primary: "oklch(0.525 0.223 3.958)",
|
primary: "oklch(0.525 0.223 3.958)",
|
||||||
@@ -900,7 +906,7 @@ export const themes: Theme[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "purple",
|
name: "purple",
|
||||||
title: "紫色",
|
title: "Purple",
|
||||||
cssVars: {
|
cssVars: {
|
||||||
light: {
|
light: {
|
||||||
primary: "oklch(0.496 0.265 301.924)",
|
primary: "oklch(0.496 0.265 301.924)",
|
||||||
@@ -932,7 +938,7 @@ export const themes: Theme[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "red",
|
name: "red",
|
||||||
title: "红色",
|
title: "Red",
|
||||||
cssVars: {
|
cssVars: {
|
||||||
light: {
|
light: {
|
||||||
primary: "oklch(0.505 0.213 27.518)",
|
primary: "oklch(0.505 0.213 27.518)",
|
||||||
@@ -964,7 +970,7 @@ export const themes: Theme[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "rose",
|
name: "rose",
|
||||||
title: "玫红色",
|
title: "Rose",
|
||||||
cssVars: {
|
cssVars: {
|
||||||
light: {
|
light: {
|
||||||
primary: "oklch(0.514 0.222 16.935)",
|
primary: "oklch(0.514 0.222 16.935)",
|
||||||
@@ -997,7 +1003,7 @@ export const themes: Theme[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "sky",
|
name: "sky",
|
||||||
title: "天蓝色",
|
title: "Sky",
|
||||||
cssVars: {
|
cssVars: {
|
||||||
light: {
|
light: {
|
||||||
primary: "oklch(0.5 0.134 242.749)",
|
primary: "oklch(0.5 0.134 242.749)",
|
||||||
@@ -1029,7 +1035,7 @@ export const themes: Theme[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "teal",
|
name: "teal",
|
||||||
title: "蓝绿色",
|
title: "Teal",
|
||||||
cssVars: {
|
cssVars: {
|
||||||
light: {
|
light: {
|
||||||
primary: "oklch(0.511 0.096 186.391)",
|
primary: "oklch(0.511 0.096 186.391)",
|
||||||
@@ -1061,7 +1067,7 @@ export const themes: Theme[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "violet",
|
name: "violet",
|
||||||
title: "紫罗兰色",
|
title: "Violet",
|
||||||
cssVars: {
|
cssVars: {
|
||||||
light: {
|
light: {
|
||||||
primary: "oklch(0.491 0.27 292.581)",
|
primary: "oklch(0.491 0.27 292.581)",
|
||||||
@@ -1093,7 +1099,7 @@ export const themes: Theme[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "yellow",
|
name: "yellow",
|
||||||
title: "黄色",
|
title: "Yellow",
|
||||||
cssVars: {
|
cssVars: {
|
||||||
light: {
|
light: {
|
||||||
primary: "oklch(0.852 0.199 91.936)",
|
primary: "oklch(0.852 0.199 91.936)",
|
||||||
|
|||||||
@@ -131,13 +131,14 @@ export function UiStateProvider({
|
|||||||
storeRef.current.getSnapshot,
|
storeRef.current.getSnapshot,
|
||||||
storeRef.current.getServerSnapshot
|
storeRef.current.getServerSnapshot
|
||||||
)
|
)
|
||||||
|
const themeMode = state["theme-mode"]
|
||||||
|
|
||||||
React.useLayoutEffect(() => {
|
React.useLayoutEffect(() => {
|
||||||
applyUiState(state)
|
applyUiState(state)
|
||||||
}, [state])
|
}, [state])
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (state["theme-mode"] !== "system") return
|
if (themeMode !== "system") return
|
||||||
|
|
||||||
const mediaQuery = window.matchMedia(systemThemeQuery)
|
const mediaQuery = window.matchMedia(systemThemeQuery)
|
||||||
const applySystemTheme = () => {
|
const applySystemTheme = () => {
|
||||||
@@ -147,7 +148,7 @@ export function UiStateProvider({
|
|||||||
|
|
||||||
mediaQuery.addEventListener("change", applySystemTheme)
|
mediaQuery.addEventListener("change", applySystemTheme)
|
||||||
return () => mediaQuery.removeEventListener("change", applySystemTheme)
|
return () => mediaQuery.removeEventListener("change", applySystemTheme)
|
||||||
}, [state["theme-mode"]])
|
}, [themeMode])
|
||||||
|
|
||||||
const contextValue = React.useMemo(
|
const contextValue = React.useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useMessage } from "@workspace/i18n"
|
||||||
import { ItemGroup } from "@workspace/ui/components/item"
|
import { ItemGroup } from "@workspace/ui/components/item"
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
@@ -8,6 +9,7 @@ import {
|
|||||||
} from "@workspace/ui/components/popover"
|
} from "@workspace/ui/components/popover"
|
||||||
|
|
||||||
import { useOnScroll } from "../../hooks/use-on-scroll"
|
import { useOnScroll } from "../../hooks/use-on-scroll"
|
||||||
|
import { chatMessages } from "./messages"
|
||||||
|
|
||||||
import { chatPopoverHandle } from "./chat-popover-handle"
|
import { chatPopoverHandle } from "./chat-popover-handle"
|
||||||
import { ChatThreadItem } from "./chat-thread-item"
|
import { ChatThreadItem } from "./chat-thread-item"
|
||||||
@@ -18,7 +20,9 @@ export interface ChatPopoverProps {
|
|||||||
title?: string
|
title?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatPopover({ threads, title = "Chats" }: ChatPopoverProps) {
|
export function ChatPopover({ threads, title }: ChatPopoverProps) {
|
||||||
|
const defaultTitle = useMessage(chatMessages.title)
|
||||||
|
|
||||||
useOnScroll(() => {
|
useOnScroll(() => {
|
||||||
chatPopoverHandle.close()
|
chatPopoverHandle.close()
|
||||||
})
|
})
|
||||||
@@ -28,7 +32,7 @@ export function ChatPopover({ threads, title = "Chats" }: ChatPopoverProps) {
|
|||||||
<PopoverContent className="h-120 p-2" alignOffset={-50} showArrow>
|
<PopoverContent className="h-120 p-2" alignOffset={-50} showArrow>
|
||||||
<PopoverHeader className="px-4 pt-4">
|
<PopoverHeader className="px-4 pt-4">
|
||||||
<PopoverTitle className="text-lg">
|
<PopoverTitle className="text-lg">
|
||||||
{title}({threads.length})
|
{title ?? defaultTitle}({threads.length})
|
||||||
</PopoverTitle>
|
</PopoverTitle>
|
||||||
<PopoverDescription />
|
<PopoverDescription />
|
||||||
</PopoverHeader>
|
</PopoverHeader>
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { describe, it } from "vitest"
|
||||||
|
|
||||||
|
import { expectCompleteCatalogs } from "../../../i18n/test-catalogs"
|
||||||
|
import { chatMessages } from "../messages"
|
||||||
|
import { chatCatalogLocales } from "./catalogs"
|
||||||
|
import { messages as de } from "./de"
|
||||||
|
import { messages as en } from "./en"
|
||||||
|
import { messages as es } from "./es"
|
||||||
|
import { messages as fr } from "./fr"
|
||||||
|
import { messages as ja } from "./ja"
|
||||||
|
import { messages as ko } from "./ko"
|
||||||
|
import { messages as zhHans } from "./zh-Hans"
|
||||||
|
import { messages as zhHant } from "./zh-Hant"
|
||||||
|
|
||||||
|
describe("chat locale catalogs", () => {
|
||||||
|
it("ship every chat message in every built-in locale", () => {
|
||||||
|
expectCompleteCatalogs({
|
||||||
|
catalogs: {
|
||||||
|
de,
|
||||||
|
en,
|
||||||
|
es,
|
||||||
|
fr,
|
||||||
|
ja,
|
||||||
|
ko,
|
||||||
|
"zh-Hans": zhHans,
|
||||||
|
"zh-Hant": zhHant,
|
||||||
|
},
|
||||||
|
locales: chatCatalogLocales,
|
||||||
|
messageIds: Object.values(chatMessages).map(
|
||||||
|
(descriptor) => descriptor.id
|
||||||
|
),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import type { chatMessages } from "../messages"
|
||||||
|
import {
|
||||||
|
blockCatalogLocales,
|
||||||
|
type BlockCatalogLocale,
|
||||||
|
type BlockMessageCatalog,
|
||||||
|
} from "../../../i18n/catalogs"
|
||||||
|
|
||||||
|
export { blockCatalogLocales as chatCatalogLocales }
|
||||||
|
export type ChatCatalogLocale = BlockCatalogLocale
|
||||||
|
export type ChatMessageCatalog = BlockMessageCatalog<typeof chatMessages>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { ChatMessageCatalog } from "./catalogs"
|
||||||
|
|
||||||
|
export const locale = "de"
|
||||||
|
export const languageTag = "de-DE"
|
||||||
|
export const messages = {
|
||||||
|
"blocks.chats.title": "Chats",
|
||||||
|
} as const satisfies ChatMessageCatalog
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { ChatMessageCatalog } from "./catalogs"
|
||||||
|
|
||||||
|
export const locale = "en"
|
||||||
|
export const languageTag = "en-US"
|
||||||
|
export const messages = {
|
||||||
|
"blocks.chats.title": "Chats",
|
||||||
|
} as const satisfies ChatMessageCatalog
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { ChatMessageCatalog } from "./catalogs"
|
||||||
|
|
||||||
|
export const locale = "es"
|
||||||
|
export const languageTag = "es-ES"
|
||||||
|
export const messages = {
|
||||||
|
"blocks.chats.title": "Chats",
|
||||||
|
} as const satisfies ChatMessageCatalog
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { ChatMessageCatalog } from "./catalogs"
|
||||||
|
|
||||||
|
export const locale = "fr"
|
||||||
|
export const languageTag = "fr-FR"
|
||||||
|
export const messages = {
|
||||||
|
"blocks.chats.title": "Discussions",
|
||||||
|
} as const satisfies ChatMessageCatalog
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { ChatMessageCatalog } from "./catalogs"
|
||||||
|
|
||||||
|
export const locale = "ja"
|
||||||
|
export const languageTag = "ja-JP"
|
||||||
|
export const messages = {
|
||||||
|
"blocks.chats.title": "チャット",
|
||||||
|
} as const satisfies ChatMessageCatalog
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { ChatMessageCatalog } from "./catalogs"
|
||||||
|
|
||||||
|
export const locale = "ko"
|
||||||
|
export const languageTag = "ko-KR"
|
||||||
|
export const messages = {
|
||||||
|
"blocks.chats.title": "채팅",
|
||||||
|
} as const satisfies ChatMessageCatalog
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { ChatMessageCatalog } from "./catalogs"
|
||||||
|
|
||||||
|
export const locale = "zh-Hans"
|
||||||
|
export const languageTag = "zh-CN"
|
||||||
|
export const messages = {
|
||||||
|
"blocks.chats.title": "聊天",
|
||||||
|
} as const satisfies ChatMessageCatalog
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { ChatMessageCatalog } from "./catalogs"
|
||||||
|
|
||||||
|
export const locale = "zh-Hant"
|
||||||
|
export const languageTag = "zh-TW"
|
||||||
|
export const messages = {
|
||||||
|
"blocks.chats.title": "聊天",
|
||||||
|
} as const satisfies ChatMessageCatalog
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import type { MessageDescriptor } from "@workspace/i18n"
|
||||||
|
|
||||||
|
export const chatMessages = {
|
||||||
|
title: /* i18n */ {
|
||||||
|
id: "blocks.chats.title",
|
||||||
|
message: "Chats",
|
||||||
|
},
|
||||||
|
} as const satisfies Record<string, MessageDescriptor>
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Link } from "@tanstack/react-router"
|
import { Link } from "@tanstack/react-router"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
import { Icon } from "@workspace/icons"
|
||||||
|
import { useMessage } from "@workspace/i18n"
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbEllipsis,
|
BreadcrumbEllipsis,
|
||||||
@@ -21,8 +23,12 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@workspace/ui/components/dropdown-menu"
|
} from "@workspace/ui/components/dropdown-menu"
|
||||||
|
|
||||||
import { type NavigationInfo, useNavigationStack } from "../navigation"
|
import {
|
||||||
import { Icon } from "../../components/icon"
|
NavigationLabel,
|
||||||
|
type NavigationInfo,
|
||||||
|
useNavigationStack,
|
||||||
|
} from "../navigation"
|
||||||
|
import { layoutMessages } from "./messages"
|
||||||
|
|
||||||
const MAX_VISIBLE_ENTRIES = 3
|
const MAX_VISIBLE_ENTRIES = 3
|
||||||
|
|
||||||
@@ -185,7 +191,7 @@ function BreadcrumbEntryContent({ entry }: { entry: BreadcrumbEntry }) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{entry.icon && <Icon data={entry.icon} className="size-4 shrink-0" />}
|
{entry.icon && <Icon data={entry.icon} className="size-4 shrink-0" />}
|
||||||
{entry.label}
|
<NavigationLabel label={entry.label} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -303,11 +309,13 @@ function BreadcrumbRouteLink({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function BreadcrumbMenu({ entries }: { entries: readonly BreadcrumbEntry[] }) {
|
function BreadcrumbMenu({ entries }: { entries: readonly BreadcrumbEntry[] }) {
|
||||||
|
const menuLabel = useMessage(layoutMessages.breadcrumbMenuLabel)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger
|
<DropdownMenuTrigger
|
||||||
render={
|
render={
|
||||||
<Button aria-label="打开面包屑菜单" size="icon-sm" variant="ghost" />
|
<Button aria-label={menuLabel} size="icon-sm" variant="ghost" />
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<BreadcrumbEllipsis />
|
<BreadcrumbEllipsis />
|
||||||
@@ -320,7 +328,7 @@ function BreadcrumbMenu({ entries }: { entries: readonly BreadcrumbEntry[] }) {
|
|||||||
{entry.icon && (
|
{entry.icon && (
|
||||||
<Icon data={entry.icon} className="size-4 shrink-0" />
|
<Icon data={entry.icon} className="size-4 shrink-0" />
|
||||||
)}
|
)}
|
||||||
{entry.label}
|
<NavigationLabel label={entry.label} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -405,7 +413,9 @@ export function AppBreadcrumb() {
|
|||||||
className="size-4 shrink-0"
|
className="size-4 shrink-0"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span className="truncate">{item.entry.label}</span>
|
<span className="truncate">
|
||||||
|
<NavigationLabel label={item.entry.label} />
|
||||||
|
</span>
|
||||||
</BreadcrumbPage>
|
</BreadcrumbPage>
|
||||||
) : (
|
) : (
|
||||||
<BreadcrumbRouteLink
|
<BreadcrumbRouteLink
|
||||||
@@ -432,7 +442,7 @@ export function AppBreadcrumb() {
|
|||||||
{entry.icon && (
|
{entry.icon && (
|
||||||
<Icon data={entry.icon} className="size-4 shrink-0" />
|
<Icon data={entry.icon} className="size-4 shrink-0" />
|
||||||
)}
|
)}
|
||||||
{entry.label}
|
<NavigationLabel label={entry.label} />
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
<span data-breadcrumb-measure-menu>
|
<span data-breadcrumb-measure-menu>
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
import {
|
||||||
|
BellIcon,
|
||||||
|
Icon,
|
||||||
|
MessageMultiple01Icon,
|
||||||
|
Search01Icon,
|
||||||
|
} from "@workspace/icons"
|
||||||
import { Button } from "@workspace/ui/components/button"
|
import { Button } from "@workspace/ui/components/button"
|
||||||
import { Kbd } from "@workspace/ui/components/kbd"
|
import { Kbd } from "@workspace/ui/components/kbd"
|
||||||
import { useIsTablet } from "@workspace/ui/hooks/use-breakpoint"
|
import { useIsTablet } from "@workspace/ui/hooks/use-breakpoint"
|
||||||
@@ -6,7 +12,6 @@ import { cn } from "@workspace/ui/lib/utils"
|
|||||||
import { ChevronLeftIcon, ChevronRightIcon, SearchIcon } from "lucide-react"
|
import { ChevronLeftIcon, ChevronRightIcon, SearchIcon } from "lucide-react"
|
||||||
|
|
||||||
import { ChatPopover, ChatPopoverTrigger, type ChatThread } from "../chats"
|
import { ChatPopover, ChatPopoverTrigger, type ChatThread } from "../chats"
|
||||||
import { Icon } from "../../components/icon"
|
|
||||||
import {
|
import {
|
||||||
MobileNavigationSheet,
|
MobileNavigationSheet,
|
||||||
MobileNavigationSheetTrigger,
|
MobileNavigationSheetTrigger,
|
||||||
@@ -18,12 +23,6 @@ import {
|
|||||||
NotificationCenterTrigger,
|
NotificationCenterTrigger,
|
||||||
type UseNotificationsOptions,
|
type UseNotificationsOptions,
|
||||||
} from "../notifications"
|
} from "../notifications"
|
||||||
import {
|
|
||||||
BellIcon,
|
|
||||||
MessageMultiple01Icon,
|
|
||||||
Search01Icon,
|
|
||||||
} from "@hugeicons/core-free-icons"
|
|
||||||
|
|
||||||
import { AppSidebar } from "./app-sidebar"
|
import { AppSidebar } from "./app-sidebar"
|
||||||
import {
|
import {
|
||||||
headerActionIconClassName,
|
headerActionIconClassName,
|
||||||
@@ -38,6 +37,7 @@ import { AppQueryIndicator } from "./app-query-indicator"
|
|||||||
export interface AppLayoutProps {
|
export interface AppLayoutProps {
|
||||||
chatThreads: readonly ChatThread[]
|
chatThreads: readonly ChatThread[]
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
|
headerActions?: React.ReactNode
|
||||||
navigationGroups: readonly NavigationGroup[]
|
navigationGroups: readonly NavigationGroup[]
|
||||||
notifications: UseNotificationsOptions
|
notifications: UseNotificationsOptions
|
||||||
}
|
}
|
||||||
@@ -45,6 +45,7 @@ export interface AppLayoutProps {
|
|||||||
export function AppLayout({
|
export function AppLayout({
|
||||||
chatThreads,
|
chatThreads,
|
||||||
children,
|
children,
|
||||||
|
headerActions,
|
||||||
navigationGroups,
|
navigationGroups,
|
||||||
notifications,
|
notifications,
|
||||||
}: AppLayoutProps) {
|
}: AppLayoutProps) {
|
||||||
@@ -55,7 +56,7 @@ export function AppLayout({
|
|||||||
<NotificationCenterSheet {...notifications} />
|
<NotificationCenterSheet {...notifications} />
|
||||||
<ChatPopover threads={chatThreads} />
|
<ChatPopover threads={chatThreads} />
|
||||||
<div className="isolate min-h-svh w-svw">
|
<div className="isolate min-h-svh w-svw">
|
||||||
<AppHeader />
|
<AppHeader actions={headerActions} />
|
||||||
<div className="relative z-1 flex min-h-svh w-svw items-start">
|
<div className="relative z-1 flex min-h-svh w-svw items-start">
|
||||||
<AppHeaderBackground />
|
<AppHeaderBackground />
|
||||||
<AppSidebar groups={navigationGroups} />
|
<AppSidebar groups={navigationGroups} />
|
||||||
@@ -66,7 +67,7 @@ export function AppLayout({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function AppHeader() {
|
function AppHeader({ actions }: { actions?: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-3 h-0">
|
<header className="sticky top-0 z-3 h-0">
|
||||||
<div className="flex h-(--ui-header-height) items-center">
|
<div className="flex h-(--ui-header-height) items-center">
|
||||||
@@ -157,6 +158,7 @@ function AppHeader() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="ml-auto flex shrink-0 items-center gap-2">
|
<div className="ml-auto flex shrink-0 items-center gap-2">
|
||||||
<GlobalSearchTrigger />
|
<GlobalSearchTrigger />
|
||||||
|
{actions}
|
||||||
<NotificationCenterTrigger
|
<NotificationCenterTrigger
|
||||||
render={<HeaderActionButton icon={BellIcon} showIndicator />}
|
render={<HeaderActionButton icon={BellIcon} showIndicator />}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { ReloadIcon } from "@workspace/icons"
|
||||||
|
import { useMessage } from "@workspace/i18n"
|
||||||
import { useRefresh } from "../../hooks/use-refresh"
|
import { useRefresh } from "../../hooks/use-refresh"
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -5,11 +7,13 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@workspace/ui/components/tooltip"
|
} from "@workspace/ui/components/tooltip"
|
||||||
import { HeaderActionButton } from "./header-action-button"
|
import { HeaderActionButton } from "./header-action-button"
|
||||||
import { ReloadIcon } from "@hugeicons/core-free-icons"
|
|
||||||
import { cn } from "@workspace/ui/lib/utils"
|
import { cn } from "@workspace/ui/lib/utils"
|
||||||
|
import { layoutMessages } from "./messages"
|
||||||
|
|
||||||
export function AppQueryIndicator({ className }: { className?: string }) {
|
export function AppQueryIndicator({ className }: { className?: string }) {
|
||||||
const { canRefresh, isRefreshing, refresh } = useRefresh()
|
const { canRefresh, isRefreshing, refresh } = useRefresh()
|
||||||
|
const refreshLabel = useMessage(layoutMessages.refreshLabel)
|
||||||
|
const refreshDescription = useMessage(layoutMessages.refreshDescription)
|
||||||
|
|
||||||
if (isRefreshing) {
|
if (isRefreshing) {
|
||||||
return (
|
return (
|
||||||
@@ -29,7 +33,7 @@ export function AppQueryIndicator({ className }: { className?: string }) {
|
|||||||
<TooltipTrigger
|
<TooltipTrigger
|
||||||
render={
|
render={
|
||||||
<HeaderActionButton
|
<HeaderActionButton
|
||||||
aria-label="刷新当前页面数据"
|
aria-label={refreshLabel}
|
||||||
className={cn("[&_svg]:size-4", className)}
|
className={cn("[&_svg]:size-4", className)}
|
||||||
disabled={!canRefresh}
|
disabled={!canRefresh}
|
||||||
icon={ReloadIcon}
|
icon={ReloadIcon}
|
||||||
@@ -37,11 +41,7 @@ export function AppQueryIndicator({ className }: { className?: string }) {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<TooltipContent showArrow>
|
<TooltipContent showArrow>{refreshDescription}</TooltipContent>
|
||||||
点击可以刷新
|
|
||||||
<br />
|
|
||||||
当前页面数据
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Icon, type IconData } from "../../components/icon"
|
import { Icon, type IconData } from "@workspace/icons"
|
||||||
import { Button, type ButtonProps } from "@workspace/ui/components/button"
|
import { Button, type ButtonProps } from "@workspace/ui/components/button"
|
||||||
import { cn } from "@workspace/ui/lib/utils"
|
import { cn } from "@workspace/ui/lib/utils"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { describe, it } from "vitest"
|
||||||
|
|
||||||
|
import { expectCompleteCatalogs } from "../../../i18n/test-catalogs"
|
||||||
|
import { layoutMessages } from "../messages"
|
||||||
|
import { layoutCatalogLocales } from "./catalogs"
|
||||||
|
import { messages as de } from "./de"
|
||||||
|
import { messages as en } from "./en"
|
||||||
|
import { messages as es } from "./es"
|
||||||
|
import { messages as fr } from "./fr"
|
||||||
|
import { messages as ja } from "./ja"
|
||||||
|
import { messages as ko } from "./ko"
|
||||||
|
import { messages as zhHans } from "./zh-Hans"
|
||||||
|
import { messages as zhHant } from "./zh-Hant"
|
||||||
|
|
||||||
|
describe("layout locale catalogs", () => {
|
||||||
|
it("ship every layout message in every built-in locale", () => {
|
||||||
|
expectCompleteCatalogs({
|
||||||
|
catalogs: {
|
||||||
|
de,
|
||||||
|
en,
|
||||||
|
es,
|
||||||
|
fr,
|
||||||
|
ja,
|
||||||
|
ko,
|
||||||
|
"zh-Hans": zhHans,
|
||||||
|
"zh-Hant": zhHant,
|
||||||
|
},
|
||||||
|
locales: layoutCatalogLocales,
|
||||||
|
messageIds: Object.values(layoutMessages).map(
|
||||||
|
(descriptor) => descriptor.id
|
||||||
|
),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import type { layoutMessages } from "../messages"
|
||||||
|
import {
|
||||||
|
blockCatalogLocales,
|
||||||
|
type BlockCatalogLocale,
|
||||||
|
type BlockMessageCatalog,
|
||||||
|
} from "../../../i18n/catalogs"
|
||||||
|
|
||||||
|
export { blockCatalogLocales as layoutCatalogLocales }
|
||||||
|
export type LayoutCatalogLocale = BlockCatalogLocale
|
||||||
|
export type LayoutMessageCatalog = BlockMessageCatalog<typeof layoutMessages>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { LayoutMessageCatalog } from "./catalogs"
|
||||||
|
|
||||||
|
export const locale = "de"
|
||||||
|
export const languageTag = "de-DE"
|
||||||
|
export const messages = {
|
||||||
|
"blocks.breadcrumb.menuLabel": "Breadcrumb-Menü öffnen",
|
||||||
|
"blocks.refresh.description": "Daten der aktuellen Seite aktualisieren",
|
||||||
|
"blocks.refresh.label": "Aktuelle Seitendaten aktualisieren",
|
||||||
|
"blocks.userMenu.accountPassword": "Kontopasswort",
|
||||||
|
"blocks.userMenu.appearance": "Darstellung",
|
||||||
|
"blocks.userMenu.keybindings": "Tastenkürzel",
|
||||||
|
"blocks.userMenu.logout": "Abmelden",
|
||||||
|
"blocks.userMenu.open": "Benutzermenü öffnen",
|
||||||
|
"blocks.userMenu.title": "Benutzermenü",
|
||||||
|
} as const satisfies LayoutMessageCatalog
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { LayoutMessageCatalog } from "./catalogs"
|
||||||
|
|
||||||
|
export const locale = "en"
|
||||||
|
export const languageTag = "en-US"
|
||||||
|
export const messages = {
|
||||||
|
"blocks.breadcrumb.menuLabel": "Open breadcrumb menu",
|
||||||
|
"blocks.refresh.description": "Refresh the current page data",
|
||||||
|
"blocks.refresh.label": "Refresh current page data",
|
||||||
|
"blocks.userMenu.accountPassword": "Account password",
|
||||||
|
"blocks.userMenu.appearance": "Appearance",
|
||||||
|
"blocks.userMenu.keybindings": "Keybindings",
|
||||||
|
"blocks.userMenu.logout": "Log out",
|
||||||
|
"blocks.userMenu.open": "Open user menu",
|
||||||
|
"blocks.userMenu.title": "User menu",
|
||||||
|
} as const satisfies LayoutMessageCatalog
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { LayoutMessageCatalog } from "./catalogs"
|
||||||
|
|
||||||
|
export const locale = "es"
|
||||||
|
export const languageTag = "es-ES"
|
||||||
|
export const messages = {
|
||||||
|
"blocks.breadcrumb.menuLabel": "Abrir el menú de ruta de navegación",
|
||||||
|
"blocks.refresh.description": "Actualizar los datos de la página actual",
|
||||||
|
"blocks.refresh.label": "Actualizar datos de la página actual",
|
||||||
|
"blocks.userMenu.accountPassword": "Contraseña de la cuenta",
|
||||||
|
"blocks.userMenu.appearance": "Apariencia",
|
||||||
|
"blocks.userMenu.keybindings": "Atajos de teclado",
|
||||||
|
"blocks.userMenu.logout": "Cerrar sesión",
|
||||||
|
"blocks.userMenu.open": "Abrir menú de usuario",
|
||||||
|
"blocks.userMenu.title": "Menú de usuario",
|
||||||
|
} as const satisfies LayoutMessageCatalog
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { LayoutMessageCatalog } from "./catalogs"
|
||||||
|
|
||||||
|
export const locale = "fr"
|
||||||
|
export const languageTag = "fr-FR"
|
||||||
|
export const messages = {
|
||||||
|
"blocks.breadcrumb.menuLabel": "Ouvrir le menu du fil d’Ariane",
|
||||||
|
"blocks.refresh.description": "Actualiser les données de la page actuelle",
|
||||||
|
"blocks.refresh.label": "Actualiser les données de la page",
|
||||||
|
"blocks.userMenu.accountPassword": "Mot de passe du compte",
|
||||||
|
"blocks.userMenu.appearance": "Apparence",
|
||||||
|
"blocks.userMenu.keybindings": "Raccourcis clavier",
|
||||||
|
"blocks.userMenu.logout": "Se déconnecter",
|
||||||
|
"blocks.userMenu.open": "Ouvrir le menu utilisateur",
|
||||||
|
"blocks.userMenu.title": "Menu utilisateur",
|
||||||
|
} as const satisfies LayoutMessageCatalog
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { LayoutMessageCatalog } from "./catalogs"
|
||||||
|
|
||||||
|
export const locale = "ja"
|
||||||
|
export const languageTag = "ja-JP"
|
||||||
|
export const messages = {
|
||||||
|
"blocks.breadcrumb.menuLabel": "パンくずメニューを開く",
|
||||||
|
"blocks.refresh.description": "現在のページのデータを更新します",
|
||||||
|
"blocks.refresh.label": "現在のページを更新",
|
||||||
|
"blocks.userMenu.accountPassword": "アカウントのパスワード",
|
||||||
|
"blocks.userMenu.appearance": "外観",
|
||||||
|
"blocks.userMenu.keybindings": "キーバインド",
|
||||||
|
"blocks.userMenu.logout": "ログアウト",
|
||||||
|
"blocks.userMenu.open": "ユーザーメニューを開く",
|
||||||
|
"blocks.userMenu.title": "ユーザーメニュー",
|
||||||
|
} as const satisfies LayoutMessageCatalog
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { LayoutMessageCatalog } from "./catalogs"
|
||||||
|
|
||||||
|
export const locale = "ko"
|
||||||
|
export const languageTag = "ko-KR"
|
||||||
|
export const messages = {
|
||||||
|
"blocks.breadcrumb.menuLabel": "이동 경로 메뉴 열기",
|
||||||
|
"blocks.refresh.description": "현재 페이지 데이터를 새로 고칩니다",
|
||||||
|
"blocks.refresh.label": "현재 페이지 데이터 새로 고침",
|
||||||
|
"blocks.userMenu.accountPassword": "계정 비밀번호",
|
||||||
|
"blocks.userMenu.appearance": "화면 모양",
|
||||||
|
"blocks.userMenu.keybindings": "키 바인딩",
|
||||||
|
"blocks.userMenu.logout": "로그아웃",
|
||||||
|
"blocks.userMenu.open": "사용자 메뉴 열기",
|
||||||
|
"blocks.userMenu.title": "사용자 메뉴",
|
||||||
|
} as const satisfies LayoutMessageCatalog
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { LayoutMessageCatalog } from "./catalogs"
|
||||||
|
|
||||||
|
export const locale = "zh-Hans"
|
||||||
|
export const languageTag = "zh-CN"
|
||||||
|
export const messages = {
|
||||||
|
"blocks.breadcrumb.menuLabel": "打开面包屑菜单",
|
||||||
|
"blocks.refresh.description": "刷新当前页面的数据",
|
||||||
|
"blocks.refresh.label": "刷新当前页面数据",
|
||||||
|
"blocks.userMenu.accountPassword": "账号密码",
|
||||||
|
"blocks.userMenu.appearance": "界面外观",
|
||||||
|
"blocks.userMenu.keybindings": "按键映射",
|
||||||
|
"blocks.userMenu.logout": "退出登录",
|
||||||
|
"blocks.userMenu.open": "打开用户菜单",
|
||||||
|
"blocks.userMenu.title": "用户菜单",
|
||||||
|
} as const satisfies LayoutMessageCatalog
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { LayoutMessageCatalog } from "./catalogs"
|
||||||
|
|
||||||
|
export const locale = "zh-Hant"
|
||||||
|
export const languageTag = "zh-TW"
|
||||||
|
export const messages = {
|
||||||
|
"blocks.breadcrumb.menuLabel": "開啟麵包屑選單",
|
||||||
|
"blocks.refresh.description": "重新整理目前頁面的資料",
|
||||||
|
"blocks.refresh.label": "重新整理目前頁面資料",
|
||||||
|
"blocks.userMenu.accountPassword": "帳號密碼",
|
||||||
|
"blocks.userMenu.appearance": "介面外觀",
|
||||||
|
"blocks.userMenu.keybindings": "按鍵綁定",
|
||||||
|
"blocks.userMenu.logout": "登出",
|
||||||
|
"blocks.userMenu.open": "開啟使用者選單",
|
||||||
|
"blocks.userMenu.title": "使用者選單",
|
||||||
|
} as const satisfies LayoutMessageCatalog
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import type { MessageDescriptor } from "@workspace/i18n"
|
||||||
|
|
||||||
|
export const layoutMessages = {
|
||||||
|
accountPassword: /* i18n */ {
|
||||||
|
id: "blocks.userMenu.accountPassword",
|
||||||
|
message: "Account password",
|
||||||
|
},
|
||||||
|
appearance: /* i18n */ {
|
||||||
|
id: "blocks.userMenu.appearance",
|
||||||
|
message: "Appearance",
|
||||||
|
},
|
||||||
|
breadcrumbMenuLabel: /* i18n */ {
|
||||||
|
id: "blocks.breadcrumb.menuLabel",
|
||||||
|
message: "Open breadcrumb menu",
|
||||||
|
},
|
||||||
|
keybindings: /* i18n */ {
|
||||||
|
id: "blocks.userMenu.keybindings",
|
||||||
|
message: "Keybindings",
|
||||||
|
},
|
||||||
|
logout: /* i18n */ {
|
||||||
|
id: "blocks.userMenu.logout",
|
||||||
|
message: "Log out",
|
||||||
|
},
|
||||||
|
openUserMenu: /* i18n */ {
|
||||||
|
id: "blocks.userMenu.open",
|
||||||
|
message: "Open user menu",
|
||||||
|
},
|
||||||
|
refreshDescription: /* i18n */ {
|
||||||
|
id: "blocks.refresh.description",
|
||||||
|
message: "Refresh the current page data",
|
||||||
|
},
|
||||||
|
refreshLabel: /* i18n */ {
|
||||||
|
id: "blocks.refresh.label",
|
||||||
|
message: "Refresh current page data",
|
||||||
|
},
|
||||||
|
userMenuTitle: /* i18n */ {
|
||||||
|
id: "blocks.userMenu.title",
|
||||||
|
message: "User menu",
|
||||||
|
},
|
||||||
|
} as const satisfies Record<string, MessageDescriptor>
|
||||||
@@ -25,7 +25,7 @@ export function SidebarStateProvider({
|
|||||||
|
|
||||||
const contextValue = React.useMemo(
|
const contextValue = React.useMemo(
|
||||||
() => ({ isCollapsed, setCollapsed }),
|
() => ({ isCollapsed, setCollapsed }),
|
||||||
[isCollapsed]
|
[isCollapsed, setCollapsed]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
import { formatForDisplay } from "@tanstack/react-hotkeys"
|
import { formatForDisplay } from "@tanstack/react-hotkeys"
|
||||||
|
import {
|
||||||
|
Icon,
|
||||||
|
type IconData,
|
||||||
|
KeyboardIcon,
|
||||||
|
LogoutCircle02Icon,
|
||||||
|
ShieldKeyIcon,
|
||||||
|
SwatchIcon,
|
||||||
|
} from "@workspace/icons"
|
||||||
|
import { useMessage } from "@workspace/i18n"
|
||||||
import { Button, type ButtonProps } from "@workspace/ui/components/button"
|
import { Button, type ButtonProps } from "@workspace/ui/components/button"
|
||||||
import {
|
import {
|
||||||
createDialogHandle,
|
createDialogHandle,
|
||||||
@@ -34,15 +43,10 @@ import {
|
|||||||
HeaderActionButton,
|
HeaderActionButton,
|
||||||
} from "./header-action-button"
|
} from "./header-action-button"
|
||||||
import { useOnScroll } from "../../hooks/use-on-scroll"
|
import { useOnScroll } from "../../hooks/use-on-scroll"
|
||||||
import { Icon, type IconData } from "../../components/icon"
|
import { layoutMessages } from "./messages"
|
||||||
import {
|
|
||||||
KeyboardIcon,
|
|
||||||
LogoutCircle02Icon,
|
|
||||||
ShieldKeyIcon,
|
|
||||||
SwatchIcon,
|
|
||||||
} from "@hugeicons/core-free-icons"
|
|
||||||
|
|
||||||
interface UserMenuItem {
|
interface UserMenuItem {
|
||||||
|
id: string
|
||||||
action?: NavigationAction
|
action?: NavigationAction
|
||||||
shortcut?: string
|
shortcut?: string
|
||||||
icon: IconData
|
icon: IconData
|
||||||
@@ -53,24 +57,6 @@ interface UserMenuTriggerProps {
|
|||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const userMenuItems: readonly UserMenuItem[] = [
|
|
||||||
{
|
|
||||||
action: navigationActions.appearance,
|
|
||||||
icon: SwatchIcon,
|
|
||||||
label: "界面外观",
|
|
||||||
shortcut: "Mod+,",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: KeyboardIcon,
|
|
||||||
label: "按键映射",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: ShieldKeyIcon,
|
|
||||||
label: "账号密码",
|
|
||||||
shortcut: "Mod+P",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
let userMenuHandles:
|
let userMenuHandles:
|
||||||
| {
|
| {
|
||||||
dialog: DialogHandle<void>
|
dialog: DialogHandle<void>
|
||||||
@@ -123,8 +109,11 @@ function UserMenuPopoverTrigger({ className }: UserMenuTriggerProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function UserMenuTriggerButton({ className, ...props }: ButtonProps) {
|
function UserMenuTriggerButton({ className, ...props }: ButtonProps) {
|
||||||
|
const openUserMenuLabel = useMessage(layoutMessages.openUserMenu)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HeaderActionButton
|
<HeaderActionButton
|
||||||
|
aria-label={openUserMenuLabel}
|
||||||
className={cn(
|
className={cn(
|
||||||
"overflow-hidden after:pointer-events-none after:absolute after:inset-0 after:z-999 after:rounded-md after:border after:border-foreground/35",
|
"overflow-hidden after:pointer-events-none after:absolute after:inset-0 after:z-999 after:rounded-md after:border after:border-foreground/35",
|
||||||
className
|
className
|
||||||
@@ -140,6 +129,31 @@ function UserMenuTriggerButton({ className, ...props }: ButtonProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function UserMenuContent({ onItemSelect }: { onItemSelect: VoidFunction }) {
|
function UserMenuContent({ onItemSelect }: { onItemSelect: VoidFunction }) {
|
||||||
|
const appearanceLabel = useMessage(layoutMessages.appearance)
|
||||||
|
const keybindingsLabel = useMessage(layoutMessages.keybindings)
|
||||||
|
const accountPasswordLabel = useMessage(layoutMessages.accountPassword)
|
||||||
|
const logoutLabel = useMessage(layoutMessages.logout)
|
||||||
|
const userMenuItems: readonly UserMenuItem[] = [
|
||||||
|
{
|
||||||
|
id: "appearance",
|
||||||
|
action: navigationActions.appearance,
|
||||||
|
icon: SwatchIcon,
|
||||||
|
label: appearanceLabel,
|
||||||
|
shortcut: "Mod+,",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "keybindings",
|
||||||
|
icon: KeyboardIcon,
|
||||||
|
label: keybindingsLabel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "account-password",
|
||||||
|
icon: ShieldKeyIcon,
|
||||||
|
label: accountPasswordLabel,
|
||||||
|
shortcut: "Mod+P",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-72">
|
<div className="w-72">
|
||||||
<div className="flex flex-col items-center pt-10 pb-6">
|
<div className="flex flex-col items-center pt-10 pb-6">
|
||||||
@@ -153,7 +167,7 @@ function UserMenuContent({ onItemSelect }: { onItemSelect: VoidFunction }) {
|
|||||||
<div className="border-t border-dashed border-border p-2">
|
<div className="border-t border-dashed border-border p-2">
|
||||||
<ul className="space-y">
|
<ul className="space-y">
|
||||||
{userMenuItems.map((item) => (
|
{userMenuItems.map((item) => (
|
||||||
<li key={item.label}>
|
<li key={item.id}>
|
||||||
<Button
|
<Button
|
||||||
className="w-full justify-start font-normal"
|
className="w-full justify-start font-normal"
|
||||||
size="lg"
|
size="lg"
|
||||||
@@ -176,7 +190,7 @@ function UserMenuContent({ onItemSelect }: { onItemSelect: VoidFunction }) {
|
|||||||
<div className="rounded-b-md border-t p-2">
|
<div className="rounded-b-md border-t p-2">
|
||||||
<Button className="w-full font-normal" variant="destructive" size="lg">
|
<Button className="w-full font-normal" variant="destructive" size="lg">
|
||||||
<Icon data={LogoutCircle02Icon} size="sm" />
|
<Icon data={LogoutCircle02Icon} size="sm" />
|
||||||
退出登录
|
{logoutLabel}
|
||||||
<MenuShortcutSequence shortcuts={["Q", "Q", "Q"]} />
|
<MenuShortcutSequence shortcuts={["Q", "Q", "Q"]} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -213,12 +227,14 @@ function UserMenuDialogTrigger({ className }: UserMenuTriggerProps) {
|
|||||||
|
|
||||||
function UserMenuDialog() {
|
function UserMenuDialog() {
|
||||||
const { dialog } = getOrCreateUserMenuHandles()
|
const { dialog } = getOrCreateUserMenuHandles()
|
||||||
|
const title = useMessage(layoutMessages.userMenuTitle)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog handle={dialog}>
|
<Dialog handle={dialog}>
|
||||||
<DialogContent className="w-fit p-0">
|
<DialogContent className="w-fit p-0">
|
||||||
<DialogHeader className="sr-only">
|
<DialogHeader className="sr-only">
|
||||||
<DialogTitle></DialogTitle>
|
<DialogTitle>{title}</DialogTitle>
|
||||||
<DialogDescription></DialogDescription>
|
<DialogDescription>{title}</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<UserMenuContent onItemSelect={() => dialog.close()} />
|
<UserMenuContent onItemSelect={() => dialog.close()} />
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import type { MediaAdapter, MediaNotice } from "./types"
|
||||||
|
|
||||||
|
interface MediaContextValue {
|
||||||
|
adapter: MediaAdapter
|
||||||
|
notify?: (notice: MediaNotice) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const MediaContext = React.createContext<MediaContextValue | null>(null)
|
||||||
|
|
||||||
|
export interface MediaProviderProps {
|
||||||
|
adapter: MediaAdapter
|
||||||
|
children: React.ReactNode
|
||||||
|
notify?: (notice: MediaNotice) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Supplies all persistence and transport dependencies for the media blocks. */
|
||||||
|
export function MediaProvider({
|
||||||
|
adapter,
|
||||||
|
children,
|
||||||
|
notify,
|
||||||
|
}: MediaProviderProps) {
|
||||||
|
const value = React.useMemo(() => ({ adapter, notify }), [adapter, notify])
|
||||||
|
|
||||||
|
return <MediaContext.Provider value={value}>{children}</MediaContext.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMedia(): MediaContextValue {
|
||||||
|
const value = React.useContext(MediaContext)
|
||||||
|
if (!value) {
|
||||||
|
throw new Error("Media blocks must be rendered inside a MediaProvider.")
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
export interface MediaDimensions {
|
||||||
|
height: number
|
||||||
|
width: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const METADATA_TIMEOUT_MS = 10_000
|
||||||
|
|
||||||
|
function waitForDimensions(
|
||||||
|
file: File,
|
||||||
|
element: HTMLImageElement | HTMLVideoElement,
|
||||||
|
successEvent: "load" | "loadedmetadata"
|
||||||
|
): Promise<MediaDimensions | undefined> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const objectUrl = URL.createObjectURL(file)
|
||||||
|
let settled = false
|
||||||
|
const timeout = window.setTimeout(finish, METADATA_TIMEOUT_MS)
|
||||||
|
|
||||||
|
function finish(dimensions?: MediaDimensions) {
|
||||||
|
if (settled) return
|
||||||
|
settled = true
|
||||||
|
window.clearTimeout(timeout)
|
||||||
|
URL.revokeObjectURL(objectUrl)
|
||||||
|
resolve(dimensions)
|
||||||
|
}
|
||||||
|
|
||||||
|
element.addEventListener(
|
||||||
|
successEvent,
|
||||||
|
() => {
|
||||||
|
const width =
|
||||||
|
element instanceof HTMLVideoElement
|
||||||
|
? element.videoWidth
|
||||||
|
: element.naturalWidth
|
||||||
|
const height =
|
||||||
|
element instanceof HTMLVideoElement
|
||||||
|
? element.videoHeight
|
||||||
|
: element.naturalHeight
|
||||||
|
finish(width > 0 && height > 0 ? { height, width } : undefined)
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
)
|
||||||
|
element.addEventListener("error", () => finish(), { once: true })
|
||||||
|
element.src = objectUrl
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reads intrinsic dimensions before an adapter uploads an image or video. */
|
||||||
|
export function readMediaDimensions(
|
||||||
|
file: File
|
||||||
|
): Promise<MediaDimensions | undefined> {
|
||||||
|
if (file.type.startsWith("image/")) {
|
||||||
|
return waitForDimensions(file, new Image(), "load")
|
||||||
|
}
|
||||||
|
if (file.type.startsWith("video/")) {
|
||||||
|
const video = document.createElement("video")
|
||||||
|
video.preload = "metadata"
|
||||||
|
return waitForDimensions(file, video, "loadedmetadata")
|
||||||
|
}
|
||||||
|
return Promise.resolve(undefined)
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { useTranslate } from "@workspace/i18n"
|
||||||
|
import { Button } from "@workspace/ui/components/button"
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@workspace/ui/components/dialog"
|
||||||
|
import { Input } from "@workspace/ui/components/input"
|
||||||
|
import {
|
||||||
|
InputGroup,
|
||||||
|
InputGroupAddon,
|
||||||
|
InputGroupInput,
|
||||||
|
InputGroupText,
|
||||||
|
} from "@workspace/ui/components/input-group"
|
||||||
|
|
||||||
|
import { mediaMessages } from "./messages"
|
||||||
|
|
||||||
|
export interface MediaFolderDialogProps {
|
||||||
|
description: string
|
||||||
|
initialName?: string
|
||||||
|
lockedSuffix?: string
|
||||||
|
maxLength?: number
|
||||||
|
open: boolean
|
||||||
|
pending?: boolean
|
||||||
|
title: string
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
onSubmit: (name: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MediaFolderDialog({
|
||||||
|
description,
|
||||||
|
initialName = "",
|
||||||
|
lockedSuffix,
|
||||||
|
maxLength = 50,
|
||||||
|
open,
|
||||||
|
pending,
|
||||||
|
title,
|
||||||
|
onOpenChange,
|
||||||
|
onSubmit,
|
||||||
|
}: MediaFolderDialogProps) {
|
||||||
|
const t = useTranslate()
|
||||||
|
const [name, setName] = React.useState(initialName)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (open) setName(initialName)
|
||||||
|
}, [initialName, open])
|
||||||
|
|
||||||
|
const normalizedName = name.trim()
|
||||||
|
const input = (
|
||||||
|
<Input
|
||||||
|
autoFocus
|
||||||
|
value={name}
|
||||||
|
maxLength={maxLength}
|
||||||
|
placeholder={t(mediaMessages.folderNamePlaceholder)}
|
||||||
|
aria-label={t(mediaMessages.folderName)}
|
||||||
|
onChange={(event) => setName(event.target.value)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent>
|
||||||
|
<form
|
||||||
|
className="contents"
|
||||||
|
onSubmit={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
if (normalizedName) onSubmit(normalizedName)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
<DialogDescription>{description}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{lockedSuffix ? (
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroupInput
|
||||||
|
autoFocus
|
||||||
|
value={name}
|
||||||
|
maxLength={maxLength}
|
||||||
|
placeholder={t(mediaMessages.folderNamePlaceholder)}
|
||||||
|
aria-label={t(mediaMessages.folderName)}
|
||||||
|
onChange={(event) => setName(event.target.value)}
|
||||||
|
/>
|
||||||
|
<InputGroupAddon align="inline-end">
|
||||||
|
<InputGroupText>{lockedSuffix}</InputGroupText>
|
||||||
|
</InputGroupAddon>
|
||||||
|
</InputGroup>
|
||||||
|
) : (
|
||||||
|
input
|
||||||
|
)}
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
disabled={pending}
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
>
|
||||||
|
{t(mediaMessages.cancel)}
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={!normalizedName || pending}>
|
||||||
|
{pending ? t(mediaMessages.saving) : t(mediaMessages.save)}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
export { MediaProvider, useMedia } from "./context"
|
||||||
|
export type { MediaProviderProps } from "./context"
|
||||||
|
export { MediaLibrary } from "./media-library"
|
||||||
|
export type { MediaLibraryProps } from "./media-library"
|
||||||
|
export { MediaPickerDialog } from "./media-picker-dialog"
|
||||||
|
export type { MediaPickerDialogProps } from "./media-picker-dialog"
|
||||||
|
export { defaultMediaReference, formatMediaFileSize } from "./utils"
|
||||||
|
export { readMediaDimensions } from "./dimensions"
|
||||||
|
export type { MediaDimensions } from "./dimensions"
|
||||||
|
export type {
|
||||||
|
CreateMediaFolderInput,
|
||||||
|
MediaAdapter,
|
||||||
|
MediaAsset,
|
||||||
|
MediaAssetPage,
|
||||||
|
MediaAssetQuery,
|
||||||
|
MediaFilter,
|
||||||
|
MediaFolder,
|
||||||
|
MediaFolderSelection,
|
||||||
|
MediaId,
|
||||||
|
MediaKind,
|
||||||
|
MediaNotice,
|
||||||
|
MediaSelectionMode,
|
||||||
|
MediaStorageTarget,
|
||||||
|
MediaUploadInput,
|
||||||
|
UpdateMediaFolderInput,
|
||||||
|
} from "./types"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import type { mediaMessages } from "../messages"
|
||||||
|
import type { BlockMessageCatalog } from "../../../i18n/catalogs"
|
||||||
|
|
||||||
|
export type MediaMessageCatalog = BlockMessageCatalog<typeof mediaMessages>
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import type { MediaMessageCatalog } from "./catalogs"
|
||||||
|
|
||||||
|
export const locale = "en"
|
||||||
|
export const languageTag = "en-US"
|
||||||
|
export const messages = {
|
||||||
|
"blocks.media.actions.cancel": "Cancel",
|
||||||
|
"blocks.media.actions.copyInformation": "Copy information",
|
||||||
|
"blocks.media.actions.copyLink": "Copy link",
|
||||||
|
"blocks.media.actions.copyName": "Copy name",
|
||||||
|
"blocks.media.actions.delete": "Delete",
|
||||||
|
"blocks.media.actions.details": "View details",
|
||||||
|
"blocks.media.actions.favorite": "Favorite",
|
||||||
|
"blocks.media.actions.loadMore": "Load more",
|
||||||
|
"blocks.media.actions.moveToFolder": "Move to folder",
|
||||||
|
"blocks.media.actions.rename": "Rename",
|
||||||
|
"blocks.media.actions.retry": "Try again",
|
||||||
|
"blocks.media.actions.save": "Save",
|
||||||
|
"blocks.media.actions.saving": "Saving…",
|
||||||
|
"blocks.media.actions.select": "Select",
|
||||||
|
"blocks.media.actions.selectFolder": "Select folder {name}",
|
||||||
|
"blocks.media.actions.unfavorite": "Unfavorite",
|
||||||
|
"blocks.media.actions.upload": "Upload",
|
||||||
|
"blocks.media.actions.viewOriginal": "View original",
|
||||||
|
"blocks.media.copy.failed": "Copy failed. Check browser clipboard permissions.",
|
||||||
|
"blocks.media.deleteAsset.description": "This media item will no longer appear in the library.",
|
||||||
|
"blocks.media.deleteAsset.title": "Delete media",
|
||||||
|
"blocks.media.deleteFolder.description": "This folder and its child folders will be deleted. Their media items will become unclassified.",
|
||||||
|
"blocks.media.deleteFolder.title": "Delete folder",
|
||||||
|
"blocks.media.details.dimensions": "Dimensions",
|
||||||
|
"blocks.media.details.fileName": "File name",
|
||||||
|
"blocks.media.details.noDimensions": "Unknown",
|
||||||
|
"blocks.media.details.size": "Size",
|
||||||
|
"blocks.media.details.type": "Type",
|
||||||
|
"blocks.media.details.uploadedAt": "Uploaded",
|
||||||
|
"blocks.media.empty.description": "Upload images or videos to manage them here.",
|
||||||
|
"blocks.media.empty.title": "No media",
|
||||||
|
"blocks.media.emptySearch.description": "No media files match your search.",
|
||||||
|
"blocks.media.filters.all": "All media",
|
||||||
|
"blocks.media.filters.favorite": "Favorites",
|
||||||
|
"blocks.media.filters.image": "Images",
|
||||||
|
"blocks.media.filters.unclassified": "Unclassified",
|
||||||
|
"blocks.media.filters.video": "Videos",
|
||||||
|
"blocks.media.folders.classification": "Folders only organize media in this library and do not change file URLs.",
|
||||||
|
"blocks.media.folders.create": "New folder",
|
||||||
|
"blocks.media.folders.empty": "No folders yet. Create one.",
|
||||||
|
"blocks.media.folders.name": "Folder name",
|
||||||
|
"blocks.media.folders.newChild": "New child folder",
|
||||||
|
"blocks.media.folders.placeholder": "Enter a folder name",
|
||||||
|
"blocks.media.folders.rename": "Rename folder",
|
||||||
|
"blocks.media.folders.title": "Folders",
|
||||||
|
"blocks.media.load.error": "Unable to load media",
|
||||||
|
"blocks.media.loading": "Loading media…",
|
||||||
|
"blocks.media.picker.description": "Choose an uploaded image or video from the media library.",
|
||||||
|
"blocks.media.picker.selectedCount": "{count} selected",
|
||||||
|
"blocks.media.picker.title": "Select media",
|
||||||
|
"blocks.media.picker.useSelected": "Use selected media",
|
||||||
|
"blocks.media.renameMedia.description": "This changes the display name without changing the file or its URL.",
|
||||||
|
"blocks.media.renameMedia.title": "Rename media",
|
||||||
|
"blocks.media.search": "Search media files",
|
||||||
|
"blocks.media.title": "Media library",
|
||||||
|
"blocks.media.upload.chooseStorage": "Choose cloud storage",
|
||||||
|
"blocks.media.upload.cloud": "Upload to cloud",
|
||||||
|
"blocks.media.upload.error": "Unable to upload media.",
|
||||||
|
"blocks.media.upload.server": "Upload to server",
|
||||||
|
"blocks.media.upload.success": "{count} media files uploaded.",
|
||||||
|
} as const satisfies MediaMessageCatalog
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import type { MediaMessageCatalog } from "./catalogs"
|
||||||
|
|
||||||
|
export const locale = "zh-Hans"
|
||||||
|
export const languageTag = "zh-CN"
|
||||||
|
export const messages = {
|
||||||
|
"blocks.media.actions.cancel": "取消",
|
||||||
|
"blocks.media.actions.copyInformation": "复制媒体信息",
|
||||||
|
"blocks.media.actions.copyLink": "复制链接",
|
||||||
|
"blocks.media.actions.copyName": "复制名称",
|
||||||
|
"blocks.media.actions.delete": "删除",
|
||||||
|
"blocks.media.actions.details": "查看详情",
|
||||||
|
"blocks.media.actions.favorite": "收藏",
|
||||||
|
"blocks.media.actions.loadMore": "加载更多",
|
||||||
|
"blocks.media.actions.moveToFolder": "移动到目录",
|
||||||
|
"blocks.media.actions.rename": "重命名",
|
||||||
|
"blocks.media.actions.retry": "重新加载",
|
||||||
|
"blocks.media.actions.save": "保存",
|
||||||
|
"blocks.media.actions.saving": "正在保存…",
|
||||||
|
"blocks.media.actions.select": "选择",
|
||||||
|
"blocks.media.actions.selectFolder": "选择目录:{name}",
|
||||||
|
"blocks.media.actions.unfavorite": "取消收藏",
|
||||||
|
"blocks.media.actions.upload": "上传",
|
||||||
|
"blocks.media.actions.viewOriginal": "查看原文件",
|
||||||
|
"blocks.media.copy.failed": "复制失败,请检查浏览器剪贴板权限。",
|
||||||
|
"blocks.media.deleteAsset.description": "该媒体将不再显示在媒体库中。",
|
||||||
|
"blocks.media.deleteAsset.title": "删除媒体",
|
||||||
|
"blocks.media.deleteFolder.description": "该目录及其子目录将被删除,其中的媒体会变为未分类。",
|
||||||
|
"blocks.media.deleteFolder.title": "删除目录",
|
||||||
|
"blocks.media.details.dimensions": "尺寸",
|
||||||
|
"blocks.media.details.fileName": "文件名称",
|
||||||
|
"blocks.media.details.noDimensions": "未知",
|
||||||
|
"blocks.media.details.size": "大小",
|
||||||
|
"blocks.media.details.type": "类型",
|
||||||
|
"blocks.media.details.uploadedAt": "上传时间",
|
||||||
|
"blocks.media.empty.description": "上传图片或视频后,可在这里统一管理。",
|
||||||
|
"blocks.media.empty.title": "暂无媒体",
|
||||||
|
"blocks.media.emptySearch.description": "没有找到匹配的媒体文件。",
|
||||||
|
"blocks.media.filters.all": "全部媒体",
|
||||||
|
"blocks.media.filters.favorite": "收藏",
|
||||||
|
"blocks.media.filters.image": "图片",
|
||||||
|
"blocks.media.filters.unclassified": "未分类",
|
||||||
|
"blocks.media.filters.video": "视频",
|
||||||
|
"blocks.media.folders.classification": "目录仅用于媒体库内分类,不会改变文件链接。",
|
||||||
|
"blocks.media.folders.create": "新建目录",
|
||||||
|
"blocks.media.folders.empty": "暂无目录,点击新建。",
|
||||||
|
"blocks.media.folders.name": "目录名称",
|
||||||
|
"blocks.media.folders.newChild": "新建子目录",
|
||||||
|
"blocks.media.folders.placeholder": "输入目录名称",
|
||||||
|
"blocks.media.folders.rename": "重命名目录",
|
||||||
|
"blocks.media.folders.title": "目录",
|
||||||
|
"blocks.media.load.error": "无法加载媒体",
|
||||||
|
"blocks.media.loading": "正在加载媒体…",
|
||||||
|
"blocks.media.picker.description": "从媒体库中选择已上传的图片或视频。",
|
||||||
|
"blocks.media.picker.selectedCount": "已选择 {count} 项",
|
||||||
|
"blocks.media.picker.title": "选择媒体",
|
||||||
|
"blocks.media.picker.useSelected": "使用所选媒体",
|
||||||
|
"blocks.media.renameMedia.description": "仅修改展示名称,不会改变文件或已有链接。",
|
||||||
|
"blocks.media.renameMedia.title": "修改媒体名称",
|
||||||
|
"blocks.media.search": "搜索媒体文件",
|
||||||
|
"blocks.media.title": "媒体库",
|
||||||
|
"blocks.media.upload.chooseStorage": "选择云端存储",
|
||||||
|
"blocks.media.upload.cloud": "上传到云端",
|
||||||
|
"blocks.media.upload.error": "无法上传媒体文件。",
|
||||||
|
"blocks.media.upload.server": "上传到服务器",
|
||||||
|
"blocks.media.upload.success": "已上传 {count} 个媒体文件。",
|
||||||
|
} as const satisfies MediaMessageCatalog
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import {
|
||||||
|
ClipboardCopyIcon,
|
||||||
|
CloudUploadIcon,
|
||||||
|
CopyIcon,
|
||||||
|
FolderIcon,
|
||||||
|
FolderInputIcon,
|
||||||
|
HeartIcon,
|
||||||
|
InfoIcon,
|
||||||
|
LinkIcon,
|
||||||
|
PencilIcon,
|
||||||
|
Trash2Icon,
|
||||||
|
UploadIcon,
|
||||||
|
} from "lucide-react"
|
||||||
|
import { useTranslate } from "@workspace/i18n"
|
||||||
|
import {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuContent,
|
||||||
|
ContextMenuGroup,
|
||||||
|
ContextMenuItem,
|
||||||
|
ContextMenuLabel,
|
||||||
|
ContextMenuSeparator,
|
||||||
|
ContextMenuSub,
|
||||||
|
ContextMenuSubContent,
|
||||||
|
ContextMenuSubTrigger,
|
||||||
|
ContextMenuTrigger,
|
||||||
|
} from "@workspace/ui/components/context-menu"
|
||||||
|
|
||||||
|
import { useMedia } from "./context"
|
||||||
|
import { mediaMessages } from "./messages"
|
||||||
|
import type {
|
||||||
|
MediaAsset,
|
||||||
|
MediaFolder,
|
||||||
|
MediaId,
|
||||||
|
MediaStorageTarget,
|
||||||
|
} from "./types"
|
||||||
|
import { formatMediaFileSize, getMediaFolderPath } from "./utils"
|
||||||
|
|
||||||
|
export interface MediaContextMenuProps {
|
||||||
|
asset?: MediaAsset
|
||||||
|
children: React.ReactElement
|
||||||
|
folders: readonly MediaFolder[]
|
||||||
|
storageTargets: readonly MediaStorageTarget[]
|
||||||
|
onDelete: (asset: MediaAsset) => void
|
||||||
|
onDetails: (asset: MediaAsset) => void
|
||||||
|
onFavorite: (asset: MediaAsset) => void
|
||||||
|
onMove: (asset: MediaAsset, folderId: MediaId | null) => void
|
||||||
|
onRename: (asset: MediaAsset) => void
|
||||||
|
onUpload: (target: MediaStorageTarget) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MediaContextMenu({
|
||||||
|
asset,
|
||||||
|
children,
|
||||||
|
folders,
|
||||||
|
storageTargets,
|
||||||
|
onDelete,
|
||||||
|
onDetails,
|
||||||
|
onFavorite,
|
||||||
|
onMove,
|
||||||
|
onRename,
|
||||||
|
onUpload,
|
||||||
|
}: MediaContextMenuProps) {
|
||||||
|
const { adapter, notify } = useMedia()
|
||||||
|
const t = useTranslate()
|
||||||
|
const resolveUrl = React.useCallback(
|
||||||
|
(item: MediaAsset) => adapter.resolveUrl?.(item) ?? item.url,
|
||||||
|
[adapter]
|
||||||
|
)
|
||||||
|
const localTargets = storageTargets.filter(
|
||||||
|
(target) => target.kind === "local"
|
||||||
|
)
|
||||||
|
const cloudTargets = storageTargets.filter(
|
||||||
|
(target) => target.kind === "cloud"
|
||||||
|
)
|
||||||
|
const runAfterClose = (action: VoidFunction) => window.setTimeout(action)
|
||||||
|
|
||||||
|
const copy = async (text: string, successMessage: string) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text)
|
||||||
|
notify?.({ message: successMessage, tone: "success" })
|
||||||
|
} catch {
|
||||||
|
notify?.({ message: t(mediaMessages.copyFailed), tone: "error" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContextMenu>
|
||||||
|
<ContextMenuTrigger render={children} />
|
||||||
|
<ContextMenuContent>
|
||||||
|
{asset ? (
|
||||||
|
<>
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={() => runAfterClose(() => onDetails(asset))}
|
||||||
|
>
|
||||||
|
<InfoIcon />
|
||||||
|
{t(mediaMessages.details)}
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem onClick={() => onFavorite(asset)}>
|
||||||
|
<HeartIcon
|
||||||
|
className={asset.favorite ? "fill-current" : undefined}
|
||||||
|
/>
|
||||||
|
{asset.favorite
|
||||||
|
? t(mediaMessages.unfavorite)
|
||||||
|
: t(mediaMessages.favoriteAction)}
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuSub>
|
||||||
|
<ContextMenuSubTrigger>
|
||||||
|
<FolderInputIcon />
|
||||||
|
{t(mediaMessages.moveToFolder)}
|
||||||
|
</ContextMenuSubTrigger>
|
||||||
|
<ContextMenuSubContent>
|
||||||
|
<ContextMenuItem
|
||||||
|
disabled={asset.folderId === null}
|
||||||
|
onClick={() => onMove(asset, null)}
|
||||||
|
>
|
||||||
|
<FolderIcon />
|
||||||
|
{t(mediaMessages.unclassified)}
|
||||||
|
</ContextMenuItem>
|
||||||
|
{folders.map((folder) => (
|
||||||
|
<ContextMenuItem
|
||||||
|
key={folder.id}
|
||||||
|
disabled={asset.folderId === folder.id}
|
||||||
|
onClick={() => onMove(asset, folder.id)}
|
||||||
|
>
|
||||||
|
<FolderIcon />
|
||||||
|
{getMediaFolderPath(folder, folders)}
|
||||||
|
</ContextMenuItem>
|
||||||
|
))}
|
||||||
|
</ContextMenuSubContent>
|
||||||
|
</ContextMenuSub>
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={() => runAfterClose(() => onRename(asset))}
|
||||||
|
>
|
||||||
|
<PencilIcon />
|
||||||
|
{t(mediaMessages.rename)}
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuSeparator />
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={() => void copy(asset.name, t(mediaMessages.copyName))}
|
||||||
|
>
|
||||||
|
<CopyIcon />
|
||||||
|
{t(mediaMessages.copyName)}
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={() =>
|
||||||
|
void copy(resolveUrl(asset), t(mediaMessages.copyLink))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<LinkIcon />
|
||||||
|
{t(mediaMessages.copyLink)}
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={() =>
|
||||||
|
void copy(
|
||||||
|
[
|
||||||
|
`${t(mediaMessages.fileName)}: ${asset.name}`,
|
||||||
|
`${t(mediaMessages.type)}: ${asset.mimeType}`,
|
||||||
|
`${t(mediaMessages.size)}: ${formatMediaFileSize(asset.sizeBytes)}`,
|
||||||
|
`${t(mediaMessages.uploadedAt)}: ${new Date(asset.createdAt).toLocaleString()}`,
|
||||||
|
`${t(mediaMessages.copyLink)}: ${resolveUrl(asset)}`,
|
||||||
|
].join("\n"),
|
||||||
|
t(mediaMessages.copyInformation)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ClipboardCopyIcon />
|
||||||
|
{t(mediaMessages.copyInformation)}
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuSeparator />
|
||||||
|
<ContextMenuItem
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => runAfterClose(() => onDelete(asset))}
|
||||||
|
>
|
||||||
|
<Trash2Icon />
|
||||||
|
{t(mediaMessages.delete)}
|
||||||
|
</ContextMenuItem>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<UploadItems
|
||||||
|
cloudTargets={cloudTargets}
|
||||||
|
localTargets={localTargets}
|
||||||
|
onUpload={onUpload}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ContextMenuContent>
|
||||||
|
</ContextMenu>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function UploadItems({
|
||||||
|
cloudTargets,
|
||||||
|
localTargets,
|
||||||
|
onUpload,
|
||||||
|
}: {
|
||||||
|
cloudTargets: readonly MediaStorageTarget[]
|
||||||
|
localTargets: readonly MediaStorageTarget[]
|
||||||
|
onUpload: (target: MediaStorageTarget) => void
|
||||||
|
}) {
|
||||||
|
const t = useTranslate()
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{localTargets.map((target) => (
|
||||||
|
<UploadTargetItem key={target.id} target={target} onUpload={onUpload} />
|
||||||
|
))}
|
||||||
|
{cloudTargets.length > 1 ? (
|
||||||
|
<>
|
||||||
|
{localTargets.length > 0 && <ContextMenuSeparator />}
|
||||||
|
<ContextMenuGroup>
|
||||||
|
<ContextMenuLabel>
|
||||||
|
{t(mediaMessages.chooseStorage)}
|
||||||
|
</ContextMenuLabel>
|
||||||
|
{cloudTargets.map((target) => (
|
||||||
|
<UploadTargetItem
|
||||||
|
key={target.id}
|
||||||
|
target={target}
|
||||||
|
onUpload={onUpload}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ContextMenuGroup>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
cloudTargets.map((target) => (
|
||||||
|
<UploadTargetItem
|
||||||
|
key={target.id}
|
||||||
|
target={target}
|
||||||
|
onUpload={onUpload}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function UploadTargetItem({
|
||||||
|
target,
|
||||||
|
onUpload,
|
||||||
|
}: {
|
||||||
|
target: MediaStorageTarget
|
||||||
|
onUpload: (target: MediaStorageTarget) => void
|
||||||
|
}) {
|
||||||
|
const t = useTranslate()
|
||||||
|
return (
|
||||||
|
<ContextMenuItem onClick={() => onUpload(target)}>
|
||||||
|
{target.kind === "local" ? <UploadIcon /> : <CloudUploadIcon />}
|
||||||
|
{target.kind === "local"
|
||||||
|
? t(mediaMessages.serverUpload)
|
||||||
|
: `${t(mediaMessages.cloudUpload)}: ${target.label}`}
|
||||||
|
</ContextMenuItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import {
|
||||||
|
ExternalLinkIcon,
|
||||||
|
FilmIcon,
|
||||||
|
HeartIcon,
|
||||||
|
ImageIcon,
|
||||||
|
Trash2Icon,
|
||||||
|
} from "lucide-react"
|
||||||
|
import { useTranslate } from "@workspace/i18n"
|
||||||
|
import { Button } from "@workspace/ui/components/button"
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@workspace/ui/components/dialog"
|
||||||
|
import { Separator } from "@workspace/ui/components/separator"
|
||||||
|
import { cn } from "@workspace/ui/lib/utils"
|
||||||
|
|
||||||
|
import { mediaMessages } from "./messages"
|
||||||
|
import type { MediaAsset } from "./types"
|
||||||
|
import { formatMediaFileSize } from "./utils"
|
||||||
|
|
||||||
|
export interface MediaDetailDialogProps {
|
||||||
|
asset?: MediaAsset
|
||||||
|
deletePending?: boolean
|
||||||
|
favoritePending?: boolean
|
||||||
|
open: boolean
|
||||||
|
resolveUrl: (asset: MediaAsset) => string
|
||||||
|
onDelete: (asset: MediaAsset) => void
|
||||||
|
onFavorite: (asset: MediaAsset) => void
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MediaDetailDialog({
|
||||||
|
asset,
|
||||||
|
deletePending,
|
||||||
|
favoritePending,
|
||||||
|
open,
|
||||||
|
resolveUrl,
|
||||||
|
onDelete,
|
||||||
|
onFavorite,
|
||||||
|
onOpenChange,
|
||||||
|
}: MediaDetailDialogProps) {
|
||||||
|
const t = useTranslate()
|
||||||
|
const [zoom, setZoom] = React.useState(1)
|
||||||
|
const imageSource = asset?.kind === "image" ? resolveUrl(asset) : undefined
|
||||||
|
|
||||||
|
React.useEffect(() => setZoom(1), [imageSource, open])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-h-[calc(100dvh-2rem)] overflow-y-auto sm:max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t(mediaMessages.details)}</DialogTitle>
|
||||||
|
<DialogDescription className="truncate">
|
||||||
|
{asset?.name}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{asset && (
|
||||||
|
<>
|
||||||
|
<div className="grid max-h-[55dvh] min-h-56 place-items-center overflow-hidden rounded-lg bg-muted/50">
|
||||||
|
{asset.kind === "image" ? (
|
||||||
|
<img
|
||||||
|
src={imageSource}
|
||||||
|
alt={asset.name}
|
||||||
|
draggable={false}
|
||||||
|
className="max-h-[55dvh] max-w-full touch-none object-contain select-none"
|
||||||
|
style={{ transform: `scale(${zoom})` }}
|
||||||
|
onWheel={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
setZoom((current) =>
|
||||||
|
Math.min(4, Math.max(1, current - event.deltaY / 400))
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
onDoubleClick={() => setZoom(1)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<video
|
||||||
|
src={resolveUrl(asset)}
|
||||||
|
controls
|
||||||
|
preload="metadata"
|
||||||
|
className="max-h-[55dvh] max-w-full bg-black object-contain"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex min-w-0 items-center gap-2">
|
||||||
|
{asset.kind === "image" ? (
|
||||||
|
<ImageIcon className="size-4 shrink-0" />
|
||||||
|
) : (
|
||||||
|
<FilmIcon className="size-4 shrink-0" />
|
||||||
|
)}
|
||||||
|
<strong className="truncate">{asset.name}</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dl className="grid grid-cols-2 gap-x-6 gap-y-4 sm:grid-cols-[repeat(3,minmax(0,1fr))_auto]">
|
||||||
|
<MediaMetadata
|
||||||
|
label={t(mediaMessages.type)}
|
||||||
|
value={asset.mimeType}
|
||||||
|
/>
|
||||||
|
<MediaMetadata
|
||||||
|
label={t(mediaMessages.size)}
|
||||||
|
value={formatMediaFileSize(asset.sizeBytes)}
|
||||||
|
/>
|
||||||
|
<MediaMetadata
|
||||||
|
label={t(mediaMessages.dimensions)}
|
||||||
|
value={
|
||||||
|
asset.width && asset.height
|
||||||
|
? `${asset.width} × ${asset.height}`
|
||||||
|
: t(mediaMessages.noDimensions)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<MediaMetadata
|
||||||
|
label={t(mediaMessages.fileName)}
|
||||||
|
value={asset.name}
|
||||||
|
/>
|
||||||
|
<MediaMetadata
|
||||||
|
label={t(mediaMessages.uploadedAt)}
|
||||||
|
value={new Date(asset.createdAt).toLocaleString()}
|
||||||
|
/>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<DialogFooter className="flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
disabled={favoritePending}
|
||||||
|
onClick={() => onFavorite(asset)}
|
||||||
|
>
|
||||||
|
<HeartIcon
|
||||||
|
className={asset.favorite ? "fill-current" : undefined}
|
||||||
|
/>
|
||||||
|
{asset.favorite
|
||||||
|
? t(mediaMessages.unfavorite)
|
||||||
|
: t(mediaMessages.favoriteAction)}
|
||||||
|
</Button>
|
||||||
|
<div className="flex flex-col gap-2 sm:flex-row">
|
||||||
|
<Button
|
||||||
|
nativeButton={false}
|
||||||
|
variant="outline"
|
||||||
|
render={
|
||||||
|
<a
|
||||||
|
href={resolveUrl(asset)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ExternalLinkIcon />
|
||||||
|
{t(mediaMessages.viewOriginal)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
disabled={deletePending}
|
||||||
|
onClick={() => onDelete(asset)}
|
||||||
|
>
|
||||||
|
<Trash2Icon />
|
||||||
|
{t(mediaMessages.delete)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogFooter>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function MediaMetadata({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
label: string
|
||||||
|
value: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="min-w-0">
|
||||||
|
<dt className="text-xs text-muted-foreground">{label}</dt>
|
||||||
|
<dd className={cn("mt-1 font-medium wrap-break-word tabular-nums")}>
|
||||||
|
{value}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { CheckIcon, FilmIcon, HeartIcon } from "lucide-react"
|
||||||
|
import { useTranslate } from "@workspace/i18n"
|
||||||
|
import { cn } from "@workspace/ui/lib/utils"
|
||||||
|
|
||||||
|
import { mediaMessages } from "./messages"
|
||||||
|
import type { MediaAsset } from "./types"
|
||||||
|
|
||||||
|
export interface MediaGridProps {
|
||||||
|
activeId?: MediaAsset["id"]
|
||||||
|
assets: readonly MediaAsset[]
|
||||||
|
resolveUrl: (asset: MediaAsset) => string
|
||||||
|
selectedIds: ReadonlySet<MediaAsset["id"]>
|
||||||
|
onActivate: (asset: MediaAsset) => void
|
||||||
|
onDetails: (asset: MediaAsset) => void
|
||||||
|
onToggle?: (asset: MediaAsset) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MediaGrid({
|
||||||
|
activeId,
|
||||||
|
assets,
|
||||||
|
resolveUrl,
|
||||||
|
selectedIds,
|
||||||
|
onActivate,
|
||||||
|
onDetails,
|
||||||
|
onToggle,
|
||||||
|
}: MediaGridProps) {
|
||||||
|
const t = useTranslate()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="columns-2 gap-3 p-3 sm:columns-3 lg:columns-4 2xl:columns-5">
|
||||||
|
{assets.map((asset) => {
|
||||||
|
const selected = selectedIds.has(asset.id)
|
||||||
|
const active = activeId === asset.id
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={asset.id}
|
||||||
|
type="button"
|
||||||
|
data-media-id={asset.id}
|
||||||
|
aria-label={`${t(mediaMessages.select)} ${asset.name}`}
|
||||||
|
aria-pressed={selected}
|
||||||
|
onClick={() => {
|
||||||
|
onActivate(asset)
|
||||||
|
onToggle?.(asset)
|
||||||
|
}}
|
||||||
|
onDoubleClick={() => onDetails(asset)}
|
||||||
|
className={cn(
|
||||||
|
"group relative mb-3 block w-full break-inside-avoid overflow-hidden rounded-lg bg-muted text-left ring-offset-2 ring-offset-background transition-shadow outline-none focus-visible:ring-3 focus-visible:ring-ring",
|
||||||
|
(active || selected) && "ring-2 ring-primary"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{asset.kind === "image" ? (
|
||||||
|
<img
|
||||||
|
src={resolveUrl(asset)}
|
||||||
|
alt={asset.name}
|
||||||
|
loading="lazy"
|
||||||
|
className="block min-h-24 w-full bg-muted object-cover"
|
||||||
|
style={
|
||||||
|
asset.width && asset.height
|
||||||
|
? { aspectRatio: `${asset.width} / ${asset.height}` }
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="relative aspect-video min-h-28 bg-black"
|
||||||
|
style={
|
||||||
|
asset.width && asset.height
|
||||||
|
? { aspectRatio: `${asset.width} / ${asset.height}` }
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<video
|
||||||
|
src={resolveUrl(asset)}
|
||||||
|
muted
|
||||||
|
preload="metadata"
|
||||||
|
className="size-full object-cover"
|
||||||
|
/>
|
||||||
|
<span className="absolute inset-0 grid place-items-center bg-black/10">
|
||||||
|
<FilmIcon className="size-8 text-white drop-shadow-md" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<span className="absolute inset-x-0 bottom-0 flex items-end justify-between gap-2 bg-linear-to-t from-black/75 to-transparent px-2 pt-8 pb-2 text-white opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100">
|
||||||
|
<span className="min-w-0 truncate text-xs">{asset.name}</span>
|
||||||
|
{asset.favorite && (
|
||||||
|
<HeartIcon className="size-3.5 shrink-0 fill-current" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{selected && (
|
||||||
|
<span className="absolute top-2 right-2 grid size-6 place-items-center rounded-full bg-primary text-primary-foreground shadow">
|
||||||
|
<CheckIcon className="size-4" strokeWidth={3} />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,828 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import {
|
||||||
|
ChevronDownIcon,
|
||||||
|
CloudIcon,
|
||||||
|
CloudUploadIcon,
|
||||||
|
FolderPlusIcon,
|
||||||
|
LoaderCircleIcon,
|
||||||
|
SearchIcon,
|
||||||
|
UploadIcon,
|
||||||
|
} from "lucide-react"
|
||||||
|
import { useTranslate } from "@workspace/i18n"
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from "@workspace/ui/components/alert-dialog"
|
||||||
|
import { Button } from "@workspace/ui/components/button"
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@workspace/ui/components/dropdown-menu"
|
||||||
|
import {
|
||||||
|
Empty,
|
||||||
|
EmptyContent,
|
||||||
|
EmptyDescription,
|
||||||
|
EmptyMedia,
|
||||||
|
EmptyTitle,
|
||||||
|
} from "@workspace/ui/components/empty"
|
||||||
|
import { Input } from "@workspace/ui/components/input"
|
||||||
|
import { ScrollArea } from "@workspace/ui/components/scroll-area"
|
||||||
|
import { cn } from "@workspace/ui/lib/utils"
|
||||||
|
|
||||||
|
import { useMedia } from "./context"
|
||||||
|
import { MediaContextMenu } from "./media-context-menu"
|
||||||
|
import { MediaDetailDialog } from "./media-detail"
|
||||||
|
import { MediaFolderDialog } from "./folder-dialog"
|
||||||
|
import { MediaGrid } from "./media-grid"
|
||||||
|
import { mediaMessages } from "./messages"
|
||||||
|
import { MediaFilterTabs, MediaSidebar } from "./sidebar"
|
||||||
|
import type {
|
||||||
|
MediaAsset,
|
||||||
|
MediaFilter,
|
||||||
|
MediaFolder,
|
||||||
|
MediaFolderSelection,
|
||||||
|
MediaId,
|
||||||
|
MediaKind,
|
||||||
|
MediaSelectionMode,
|
||||||
|
MediaStorageTarget,
|
||||||
|
} from "./types"
|
||||||
|
import {
|
||||||
|
defaultMediaReference,
|
||||||
|
getMediaFolderPath,
|
||||||
|
isMediaFile,
|
||||||
|
mediaKindForFile,
|
||||||
|
MEDIA_ACCEPT,
|
||||||
|
splitMediaName,
|
||||||
|
} from "./utils"
|
||||||
|
|
||||||
|
const PAGE_SIZE = 40
|
||||||
|
const EMPTY_ASSETS: readonly MediaAsset[] = []
|
||||||
|
const EMPTY_FOLDERS: readonly MediaFolder[] = []
|
||||||
|
const DEFAULT_STORAGE_TARGET: MediaStorageTarget = {
|
||||||
|
id: "local",
|
||||||
|
kind: "local",
|
||||||
|
label: "Local storage",
|
||||||
|
}
|
||||||
|
|
||||||
|
type FolderDialogState =
|
||||||
|
| { mode: "create"; parentId: MediaId | null }
|
||||||
|
| { folder: MediaFolder; mode: "rename" }
|
||||||
|
|
||||||
|
export interface MediaLibraryProps extends Omit<
|
||||||
|
React.ComponentProps<"section">,
|
||||||
|
"onChange"
|
||||||
|
> {
|
||||||
|
allowedKinds?: readonly MediaKind[]
|
||||||
|
footer?: React.ReactNode
|
||||||
|
selectedAssets?: readonly MediaAsset[]
|
||||||
|
selectionMode?: MediaSelectionMode
|
||||||
|
onSelectionChange?: (assets: readonly MediaAsset[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MediaLibrary({
|
||||||
|
allowedKinds,
|
||||||
|
className,
|
||||||
|
footer,
|
||||||
|
selectedAssets = EMPTY_ASSETS,
|
||||||
|
selectionMode = "none",
|
||||||
|
onSelectionChange,
|
||||||
|
...props
|
||||||
|
}: MediaLibraryProps) {
|
||||||
|
const { adapter, notify } = useMedia()
|
||||||
|
const t = useTranslate()
|
||||||
|
const inputRef = React.useRef<HTMLInputElement>(null)
|
||||||
|
const uploadTargetRef = React.useRef(DEFAULT_STORAGE_TARGET)
|
||||||
|
const assetRequestId = React.useRef(0)
|
||||||
|
const foldersRequestId = React.useRef(0)
|
||||||
|
const targetsRequestId = React.useRef(0)
|
||||||
|
const [filter, setFilter] = React.useState<MediaFilter>(
|
||||||
|
allowedKinds?.length === 1 ? allowedKinds[0] : "all"
|
||||||
|
)
|
||||||
|
const [folder, setFolder] = React.useState<MediaFolderSelection>()
|
||||||
|
const [keyword, setKeyword] = React.useState("")
|
||||||
|
const deferredKeyword = React.useDeferredValue(keyword.trim())
|
||||||
|
const [assets, setAssets] = React.useState<readonly MediaAsset[]>([])
|
||||||
|
const [assetPage, setAssetPage] = React.useState(1)
|
||||||
|
const [hasNextPage, setHasNextPage] = React.useState(false)
|
||||||
|
const [assetError, setAssetError] = React.useState<Error>()
|
||||||
|
const [isLoadingAssets, setIsLoadingAssets] = React.useState(true)
|
||||||
|
const [isLoadingMore, setIsLoadingMore] = React.useState(false)
|
||||||
|
const [folders, setFolders] = React.useState<readonly MediaFolder[]>([])
|
||||||
|
const [storageTargets, setStorageTargets] = React.useState<
|
||||||
|
readonly MediaStorageTarget[]
|
||||||
|
>([DEFAULT_STORAGE_TARGET])
|
||||||
|
const [activeAsset, setActiveAsset] = React.useState<MediaAsset>()
|
||||||
|
const [contextAsset, setContextAsset] = React.useState<MediaAsset>()
|
||||||
|
const [detailOpen, setDetailOpen] = React.useState(false)
|
||||||
|
const [deleteTarget, setDeleteTarget] = React.useState<MediaAsset>()
|
||||||
|
const [folderDeleteTarget, setFolderDeleteTarget] =
|
||||||
|
React.useState<MediaFolder>()
|
||||||
|
const [renameTarget, setRenameTarget] = React.useState<MediaAsset>()
|
||||||
|
const [folderDialog, setFolderDialog] = React.useState<FolderDialogState>()
|
||||||
|
const [pendingOperation, setPendingOperation] = React.useState<string>()
|
||||||
|
|
||||||
|
const allowedKindsKey = allowedKinds?.join(",") ?? ""
|
||||||
|
const selectedIds = React.useMemo(
|
||||||
|
() => new Set(selectedAssets.map((asset) => asset.id)),
|
||||||
|
[selectedAssets]
|
||||||
|
)
|
||||||
|
const resolveUrl = React.useCallback(
|
||||||
|
(asset: MediaAsset) => adapter.resolveUrl?.(asset) ?? asset.url,
|
||||||
|
[adapter]
|
||||||
|
)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (
|
||||||
|
(filter === "image" || filter === "video") &&
|
||||||
|
allowedKinds &&
|
||||||
|
!allowedKinds.includes(filter)
|
||||||
|
) {
|
||||||
|
setFilter(allowedKinds.length === 1 ? allowedKinds[0] : "all")
|
||||||
|
}
|
||||||
|
}, [allowedKinds, allowedKindsKey, filter])
|
||||||
|
|
||||||
|
const reportError = React.useCallback(
|
||||||
|
(error: unknown, fallback: string) => {
|
||||||
|
const message =
|
||||||
|
error instanceof Error && error.message ? error.message : fallback
|
||||||
|
notify?.({ message, tone: "error" })
|
||||||
|
},
|
||||||
|
[notify]
|
||||||
|
)
|
||||||
|
|
||||||
|
const refreshFolders = React.useCallback(async () => {
|
||||||
|
const requestId = foldersRequestId.current + 1
|
||||||
|
foldersRequestId.current = requestId
|
||||||
|
try {
|
||||||
|
const nextFolders = await adapter.listFolders()
|
||||||
|
if (foldersRequestId.current === requestId) setFolders(nextFolders)
|
||||||
|
} catch (error) {
|
||||||
|
if (foldersRequestId.current === requestId) {
|
||||||
|
reportError(error, t(mediaMessages.loadError))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [adapter, reportError, t])
|
||||||
|
|
||||||
|
const refreshStorageTargets = React.useCallback(async () => {
|
||||||
|
if (!adapter.listStorageTargets) return
|
||||||
|
const requestId = targetsRequestId.current + 1
|
||||||
|
targetsRequestId.current = requestId
|
||||||
|
try {
|
||||||
|
const targets = await adapter.listStorageTargets()
|
||||||
|
if (targetsRequestId.current === requestId) {
|
||||||
|
setStorageTargets(targets.length ? targets : [DEFAULT_STORAGE_TARGET])
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (targetsRequestId.current === requestId) {
|
||||||
|
reportError(error, t(mediaMessages.loadError))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [adapter, reportError, t])
|
||||||
|
|
||||||
|
const refreshAssets = React.useCallback(async () => {
|
||||||
|
const requestId = assetRequestId.current + 1
|
||||||
|
assetRequestId.current = requestId
|
||||||
|
setIsLoadingAssets(true)
|
||||||
|
setAssetError(undefined)
|
||||||
|
try {
|
||||||
|
const page = await adapter.listAssets({
|
||||||
|
allowedKinds,
|
||||||
|
filter,
|
||||||
|
folder,
|
||||||
|
keyword: deferredKeyword,
|
||||||
|
page: 1,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
})
|
||||||
|
if (assetRequestId.current !== requestId) return
|
||||||
|
setAssets(page.assets)
|
||||||
|
setAssetPage(1)
|
||||||
|
setHasNextPage(page.hasNextPage)
|
||||||
|
} catch (error) {
|
||||||
|
if (assetRequestId.current !== requestId) return
|
||||||
|
setAssets([])
|
||||||
|
setAssetError(
|
||||||
|
error instanceof Error ? error : new Error(t(mediaMessages.loadError))
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
if (assetRequestId.current === requestId) setIsLoadingAssets(false)
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
adapter,
|
||||||
|
allowedKinds,
|
||||||
|
allowedKindsKey,
|
||||||
|
deferredKeyword,
|
||||||
|
filter,
|
||||||
|
folder,
|
||||||
|
t,
|
||||||
|
])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
void refreshFolders()
|
||||||
|
void refreshStorageTargets()
|
||||||
|
}, [refreshFolders, refreshStorageTargets])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
void refreshAssets()
|
||||||
|
}, [refreshAssets])
|
||||||
|
|
||||||
|
const loadMore = async () => {
|
||||||
|
if (isLoadingMore || !hasNextPage) return
|
||||||
|
const nextPage = assetPage + 1
|
||||||
|
setIsLoadingMore(true)
|
||||||
|
try {
|
||||||
|
const page = await adapter.listAssets({
|
||||||
|
allowedKinds,
|
||||||
|
filter,
|
||||||
|
folder,
|
||||||
|
keyword: deferredKeyword,
|
||||||
|
page: nextPage,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
})
|
||||||
|
setAssets((current) => [...current, ...page.assets])
|
||||||
|
setAssetPage(nextPage)
|
||||||
|
setHasNextPage(page.hasNextPage)
|
||||||
|
} catch (error) {
|
||||||
|
reportError(error, t(mediaMessages.loadError))
|
||||||
|
} finally {
|
||||||
|
setIsLoadingMore(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const runOperation = async <Value,>(
|
||||||
|
name: string,
|
||||||
|
operation: () => Promise<Value>,
|
||||||
|
onSuccess?: (value: Value) => void
|
||||||
|
): Promise<{ ok: false } | { ok: true; value: Value }> => {
|
||||||
|
setPendingOperation(name)
|
||||||
|
try {
|
||||||
|
const value = await operation()
|
||||||
|
onSuccess?.(value)
|
||||||
|
return { ok: true, value }
|
||||||
|
} catch (error) {
|
||||||
|
reportError(error, t(mediaMessages.loadError))
|
||||||
|
return { ok: false }
|
||||||
|
} finally {
|
||||||
|
setPendingOperation(undefined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestUpload = (target: MediaStorageTarget) => {
|
||||||
|
uploadTargetRef.current = target
|
||||||
|
inputRef.current?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadFiles = async (files: readonly File[]) => {
|
||||||
|
const acceptedFiles = files.filter(isMediaFile)
|
||||||
|
if (!acceptedFiles.length) {
|
||||||
|
reportError(
|
||||||
|
new Error(t(mediaMessages.uploadError)),
|
||||||
|
t(mediaMessages.uploadError)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
allowedKinds &&
|
||||||
|
acceptedFiles.some((file) => {
|
||||||
|
const kind = mediaKindForFile(file)
|
||||||
|
return !kind || !allowedKinds.includes(kind)
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
reportError(
|
||||||
|
new Error(t(mediaMessages.uploadError)),
|
||||||
|
t(mediaMessages.uploadError)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploaded = await runOperation(
|
||||||
|
"upload",
|
||||||
|
() =>
|
||||||
|
adapter.upload({
|
||||||
|
files: acceptedFiles,
|
||||||
|
folderId:
|
||||||
|
folder !== "unclassified" &&
|
||||||
|
(typeof folder === "string" || typeof folder === "number")
|
||||||
|
? folder
|
||||||
|
: undefined,
|
||||||
|
target: uploadTargetRef.current,
|
||||||
|
}),
|
||||||
|
(nextAssets) => setActiveAsset(nextAssets[0])
|
||||||
|
)
|
||||||
|
if (!uploaded.ok) return
|
||||||
|
notify?.({
|
||||||
|
message: t(mediaMessages.uploadSuccess, {
|
||||||
|
count: uploaded.value.length,
|
||||||
|
}),
|
||||||
|
tone: "success",
|
||||||
|
})
|
||||||
|
await Promise.all([refreshAssets(), refreshFolders()])
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleSelection = (asset: MediaAsset) => {
|
||||||
|
if (selectionMode === "none") return
|
||||||
|
if (selectionMode === "single") {
|
||||||
|
onSelectionChange?.(selectedIds.has(asset.id) ? [] : [asset])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
onSelectionChange?.(
|
||||||
|
selectedIds.has(asset.id)
|
||||||
|
? selectedAssets.filter((item) => item.id !== asset.id)
|
||||||
|
: [...selectedAssets, asset]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateFavorite = async (asset: MediaAsset) => {
|
||||||
|
const updated = await runOperation("favorite", () =>
|
||||||
|
adapter.updateFavorite(asset.id, !asset.favorite)
|
||||||
|
)
|
||||||
|
if (!updated.ok) return
|
||||||
|
setActiveAsset((current) =>
|
||||||
|
current?.id === updated.value.id ? updated.value : current
|
||||||
|
)
|
||||||
|
setContextAsset(updated.value)
|
||||||
|
onSelectionChange?.(
|
||||||
|
selectedAssets.map((item) =>
|
||||||
|
item.id === updated.value.id ? updated.value : item
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await refreshAssets()
|
||||||
|
}
|
||||||
|
|
||||||
|
const moveAsset = async (asset: MediaAsset, folderId: MediaId | null) => {
|
||||||
|
const updated = await runOperation("move", () =>
|
||||||
|
adapter.moveAsset(asset.id, folderId)
|
||||||
|
)
|
||||||
|
if (!updated.ok) return
|
||||||
|
setActiveAsset((current) =>
|
||||||
|
current?.id === updated.value.id ? updated.value : current
|
||||||
|
)
|
||||||
|
setContextAsset(updated.value)
|
||||||
|
onSelectionChange?.(
|
||||||
|
selectedAssets.map((item) =>
|
||||||
|
item.id === updated.value.id ? updated.value : item
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await Promise.all([refreshAssets(), refreshFolders()])
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveFolder = async (name: string) => {
|
||||||
|
if (!folderDialog) return
|
||||||
|
const saved = await runOperation("folder", () =>
|
||||||
|
folderDialog.mode === "create"
|
||||||
|
? adapter.createFolder({ name, parentId: folderDialog.parentId })
|
||||||
|
: adapter.updateFolder({
|
||||||
|
id: folderDialog.folder.id,
|
||||||
|
name,
|
||||||
|
parentId: folderDialog.folder.parentId,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
if (!saved.ok) return
|
||||||
|
if (folderDialog.mode === "create") {
|
||||||
|
setFilter("all")
|
||||||
|
setFolder(saved.value.id)
|
||||||
|
}
|
||||||
|
setFolderDialog(undefined)
|
||||||
|
await Promise.all([refreshAssets(), refreshFolders()])
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveRename = async (baseName: string) => {
|
||||||
|
if (!renameTarget) return
|
||||||
|
const { extension } = splitMediaName(renameTarget.name)
|
||||||
|
const updated = await runOperation("rename", () =>
|
||||||
|
adapter.renameAsset(renameTarget.id, `${baseName}${extension}`)
|
||||||
|
)
|
||||||
|
if (!updated.ok) return
|
||||||
|
setActiveAsset((current) =>
|
||||||
|
current?.id === updated.value.id ? updated.value : current
|
||||||
|
)
|
||||||
|
setContextAsset(updated.value)
|
||||||
|
setRenameTarget(undefined)
|
||||||
|
await refreshAssets()
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteAsset = async () => {
|
||||||
|
if (!deleteTarget) return
|
||||||
|
const deleted = deleteTarget
|
||||||
|
const result = await runOperation("deleteAsset", () =>
|
||||||
|
adapter.deleteAsset(deleted.id)
|
||||||
|
)
|
||||||
|
if (!result.ok) return
|
||||||
|
if (activeAsset?.id === deleted.id) {
|
||||||
|
setActiveAsset(undefined)
|
||||||
|
setDetailOpen(false)
|
||||||
|
}
|
||||||
|
if (selectedIds.has(deleted.id)) {
|
||||||
|
onSelectionChange?.(
|
||||||
|
selectedAssets.filter((asset) => asset.id !== deleted.id)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
setDeleteTarget(undefined)
|
||||||
|
await Promise.all([refreshAssets(), refreshFolders()])
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteFolder = async () => {
|
||||||
|
if (!folderDeleteTarget) return
|
||||||
|
const deleted = folderDeleteTarget
|
||||||
|
const result = await runOperation("deleteFolder", () =>
|
||||||
|
adapter.deleteFolder(deleted.id)
|
||||||
|
)
|
||||||
|
if (!result.ok) return
|
||||||
|
if (folder === deleted.id) setFolder("unclassified")
|
||||||
|
setFolderDeleteTarget(undefined)
|
||||||
|
await Promise.all([refreshAssets(), refreshFolders()])
|
||||||
|
}
|
||||||
|
|
||||||
|
const showFilter = (nextFilter: MediaFilter) => {
|
||||||
|
setFilter(nextFilter)
|
||||||
|
setFolder(undefined)
|
||||||
|
}
|
||||||
|
const showFolder = (nextFolder: MediaFolderSelection) => {
|
||||||
|
setFolder(nextFolder)
|
||||||
|
setFilter("all")
|
||||||
|
}
|
||||||
|
const showDetails = (asset: MediaAsset) => {
|
||||||
|
setActiveAsset(asset)
|
||||||
|
setDetailOpen(true)
|
||||||
|
}
|
||||||
|
const selectContextAsset = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
|
const target =
|
||||||
|
event.target instanceof Element
|
||||||
|
? event.target.closest<HTMLElement>("[data-media-id]")
|
||||||
|
: null
|
||||||
|
const asset = assets.find(
|
||||||
|
(item) => String(item.id) === target?.dataset.mediaId
|
||||||
|
)
|
||||||
|
setContextAsset(asset)
|
||||||
|
if (asset) setActiveAsset(asset)
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentFolder =
|
||||||
|
typeof folder === "string" || typeof folder === "number"
|
||||||
|
? folders.find((item) => item.id === folder)
|
||||||
|
: undefined
|
||||||
|
const searchPlaceholder = currentFolder
|
||||||
|
? `${t(mediaMessages.search)}: ${getMediaFolderPath(currentFolder, folders)}`
|
||||||
|
: t(mediaMessages.search)
|
||||||
|
const localTarget =
|
||||||
|
storageTargets.find((target) => target.kind === "local") ??
|
||||||
|
DEFAULT_STORAGE_TARGET
|
||||||
|
const cloudTargets = storageTargets.filter(
|
||||||
|
(target) => target.kind === "cloud"
|
||||||
|
)
|
||||||
|
const renameName = splitMediaName(renameTarget?.name ?? "")
|
||||||
|
const isUploading = pendingOperation === "upload"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section
|
||||||
|
className={cn(
|
||||||
|
"grid min-h-0 overflow-hidden rounded-lg border bg-background md:grid-cols-[14rem_minmax(0,1fr)]",
|
||||||
|
footer && "grid-rows-[minmax(0,1fr)_auto]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<MediaSidebar
|
||||||
|
filter={filter}
|
||||||
|
folder={folder}
|
||||||
|
folders={folders}
|
||||||
|
allowedKinds={allowedKinds}
|
||||||
|
onFilterChange={showFilter}
|
||||||
|
onFolderChange={showFolder}
|
||||||
|
onCreateFolder={(parentId) =>
|
||||||
|
setFolderDialog({ mode: "create", parentId })
|
||||||
|
}
|
||||||
|
onRenameFolder={(item) =>
|
||||||
|
setFolderDialog({ folder: item, mode: "rename" })
|
||||||
|
}
|
||||||
|
onDeleteFolder={setFolderDeleteTarget}
|
||||||
|
className="hidden min-h-0 md:flex"
|
||||||
|
/>
|
||||||
|
<main className="flex min-h-0 min-w-0 flex-col">
|
||||||
|
<div className="flex min-h-14 items-center gap-2 border-b p-2">
|
||||||
|
<label className="relative min-w-0 flex-1">
|
||||||
|
<SearchIcon className="pointer-events-none absolute start-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
value={keyword}
|
||||||
|
placeholder={searchPlaceholder}
|
||||||
|
className="ps-9"
|
||||||
|
onChange={(event) => setKeyword(event.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="icon"
|
||||||
|
variant="outline"
|
||||||
|
className="md:hidden"
|
||||||
|
aria-label={t(mediaMessages.createFolder)}
|
||||||
|
onClick={() =>
|
||||||
|
setFolderDialog({ mode: "create", parentId: null })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FolderPlusIcon />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
disabled={isUploading}
|
||||||
|
onClick={() => requestUpload(localTarget)}
|
||||||
|
>
|
||||||
|
{isUploading && uploadTargetRef.current.id === localTarget.id ? (
|
||||||
|
<LoaderCircleIcon className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
<UploadIcon />
|
||||||
|
)}
|
||||||
|
<span className="hidden sm:inline">
|
||||||
|
{t(mediaMessages.serverUpload)}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
{cloudTargets.length === 1 && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
disabled={isUploading}
|
||||||
|
onClick={() => requestUpload(cloudTargets[0])}
|
||||||
|
>
|
||||||
|
<CloudUploadIcon />
|
||||||
|
<span className="hidden sm:inline">
|
||||||
|
{t(mediaMessages.cloudUpload)}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{cloudTargets.length > 1 && (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger
|
||||||
|
render={
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
disabled={isUploading}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isUploading && uploadTargetRef.current.kind === "cloud" ? (
|
||||||
|
<LoaderCircleIcon className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
<CloudUploadIcon />
|
||||||
|
)}
|
||||||
|
<span className="hidden sm:inline">
|
||||||
|
{t(mediaMessages.cloudUpload)}
|
||||||
|
</span>
|
||||||
|
<ChevronDownIcon data-icon="inline-end" />
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuLabel>
|
||||||
|
{t(mediaMessages.chooseStorage)}
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
{cloudTargets.map((target) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={target.id}
|
||||||
|
onClick={() => requestUpload(target)}
|
||||||
|
>
|
||||||
|
<CloudIcon />
|
||||||
|
{target.label}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
accept={
|
||||||
|
allowedKinds?.length === 1 && allowedKinds[0] === "image"
|
||||||
|
? "image/*"
|
||||||
|
: MEDIA_ACCEPT
|
||||||
|
}
|
||||||
|
className="sr-only"
|
||||||
|
onChange={(event) => {
|
||||||
|
const files = Array.from(event.target.files ?? [])
|
||||||
|
if (files.length) void uploadFiles(files)
|
||||||
|
event.target.value = ""
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<MediaFilterTabs
|
||||||
|
filter={filter}
|
||||||
|
folder={folder}
|
||||||
|
folders={folders}
|
||||||
|
allowedKinds={allowedKinds}
|
||||||
|
onFilterChange={showFilter}
|
||||||
|
onFolderChange={showFolder}
|
||||||
|
/>
|
||||||
|
<ScrollArea className="min-h-0 flex-1">
|
||||||
|
<MediaContextMenu
|
||||||
|
asset={contextAsset}
|
||||||
|
folders={folders}
|
||||||
|
storageTargets={storageTargets}
|
||||||
|
onUpload={requestUpload}
|
||||||
|
onDetails={showDetails}
|
||||||
|
onFavorite={(asset) => void updateFavorite(asset)}
|
||||||
|
onRename={setRenameTarget}
|
||||||
|
onMove={(asset, folderId) => void moveAsset(asset, folderId)}
|
||||||
|
onDelete={setDeleteTarget}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="min-h-full"
|
||||||
|
onContextMenuCapture={selectContextAsset}
|
||||||
|
>
|
||||||
|
{isLoadingAssets ? (
|
||||||
|
<MediaEmpty
|
||||||
|
icon={<LoaderCircleIcon className="animate-spin" />}
|
||||||
|
title={t(mediaMessages.loading)}
|
||||||
|
/>
|
||||||
|
) : assetError ? (
|
||||||
|
<MediaEmpty
|
||||||
|
description={assetError.message}
|
||||||
|
title={t(mediaMessages.loadError)}
|
||||||
|
action={
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => void refreshAssets()}
|
||||||
|
>
|
||||||
|
{t(mediaMessages.retry)}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : assets.length ? (
|
||||||
|
<>
|
||||||
|
<MediaGrid
|
||||||
|
assets={assets}
|
||||||
|
activeId={activeAsset?.id}
|
||||||
|
selectedIds={selectedIds}
|
||||||
|
resolveUrl={resolveUrl}
|
||||||
|
onActivate={setActiveAsset}
|
||||||
|
onToggle={
|
||||||
|
selectionMode === "none" ? undefined : toggleSelection
|
||||||
|
}
|
||||||
|
onDetails={showDetails}
|
||||||
|
/>
|
||||||
|
{hasNextPage && (
|
||||||
|
<div className="flex justify-center p-4 pt-1">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
disabled={isLoadingMore}
|
||||||
|
onClick={() => void loadMore()}
|
||||||
|
>
|
||||||
|
{isLoadingMore && (
|
||||||
|
<LoaderCircleIcon className="animate-spin" />
|
||||||
|
)}
|
||||||
|
{t(mediaMessages.loadMore)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<MediaEmpty
|
||||||
|
icon={<UploadIcon />}
|
||||||
|
title={t(mediaMessages.emptyTitle)}
|
||||||
|
description={
|
||||||
|
deferredKeyword
|
||||||
|
? t(mediaMessages.emptySearchDescription)
|
||||||
|
: t(mediaMessages.emptyDescription)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</MediaContextMenu>
|
||||||
|
</ScrollArea>
|
||||||
|
</main>
|
||||||
|
{footer && (
|
||||||
|
<footer className="col-span-full border-t bg-background p-3">
|
||||||
|
{footer}
|
||||||
|
</footer>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<MediaDetailDialog
|
||||||
|
asset={activeAsset}
|
||||||
|
open={detailOpen}
|
||||||
|
resolveUrl={resolveUrl}
|
||||||
|
favoritePending={pendingOperation === "favorite"}
|
||||||
|
deletePending={pendingOperation === "deleteAsset"}
|
||||||
|
onOpenChange={setDetailOpen}
|
||||||
|
onFavorite={(asset) => void updateFavorite(asset)}
|
||||||
|
onDelete={setDeleteTarget}
|
||||||
|
/>
|
||||||
|
<MediaFolderDialog
|
||||||
|
open={Boolean(renameTarget)}
|
||||||
|
title={t(mediaMessages.renameMedia)}
|
||||||
|
description={t(mediaMessages.renameMediaDescription)}
|
||||||
|
initialName={renameName.baseName}
|
||||||
|
maxLength={255 - renameName.extension.length}
|
||||||
|
lockedSuffix={renameName.extension}
|
||||||
|
pending={pendingOperation === "rename"}
|
||||||
|
onOpenChange={(open) => !open && setRenameTarget(undefined)}
|
||||||
|
onSubmit={(name) => void saveRename(name)}
|
||||||
|
/>
|
||||||
|
<MediaFolderDialog
|
||||||
|
open={Boolean(folderDialog)}
|
||||||
|
title={t(
|
||||||
|
folderDialog?.mode === "rename"
|
||||||
|
? mediaMessages.renameFolder
|
||||||
|
: mediaMessages.createFolder
|
||||||
|
)}
|
||||||
|
description={t(mediaMessages.folderOnlyClassifies)}
|
||||||
|
initialName={
|
||||||
|
folderDialog?.mode === "rename" ? folderDialog.folder.name : ""
|
||||||
|
}
|
||||||
|
pending={pendingOperation === "folder"}
|
||||||
|
onOpenChange={(open) => !open && setFolderDialog(undefined)}
|
||||||
|
onSubmit={(name) => void saveFolder(name)}
|
||||||
|
/>
|
||||||
|
<MediaConfirmDialog
|
||||||
|
open={Boolean(deleteTarget)}
|
||||||
|
pending={pendingOperation === "deleteAsset"}
|
||||||
|
title={t(mediaMessages.deleteAssetTitle)}
|
||||||
|
description={t(mediaMessages.deleteAssetDescription)}
|
||||||
|
onOpenChange={(open) => !open && setDeleteTarget(undefined)}
|
||||||
|
onConfirm={() => void deleteAsset()}
|
||||||
|
/>
|
||||||
|
<MediaConfirmDialog
|
||||||
|
open={Boolean(folderDeleteTarget)}
|
||||||
|
pending={pendingOperation === "deleteFolder"}
|
||||||
|
title={t(mediaMessages.deleteFolderTitle)}
|
||||||
|
description={t(mediaMessages.deleteFolderDescription)}
|
||||||
|
onOpenChange={(open) => !open && setFolderDeleteTarget(undefined)}
|
||||||
|
onConfirm={() => void deleteFolder()}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function MediaEmpty({
|
||||||
|
action,
|
||||||
|
description,
|
||||||
|
icon,
|
||||||
|
title,
|
||||||
|
}: {
|
||||||
|
action?: React.ReactNode
|
||||||
|
description?: string
|
||||||
|
icon?: React.ReactNode
|
||||||
|
title: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Empty className="min-h-72">
|
||||||
|
<EmptyContent>
|
||||||
|
{icon && <EmptyMedia variant="icon">{icon}</EmptyMedia>}
|
||||||
|
<EmptyTitle>{title}</EmptyTitle>
|
||||||
|
{description && <EmptyDescription>{description}</EmptyDescription>}
|
||||||
|
{action}
|
||||||
|
</EmptyContent>
|
||||||
|
</Empty>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function MediaConfirmDialog({
|
||||||
|
description,
|
||||||
|
open,
|
||||||
|
pending,
|
||||||
|
title,
|
||||||
|
onConfirm,
|
||||||
|
onOpenChange,
|
||||||
|
}: {
|
||||||
|
description: string
|
||||||
|
open: boolean
|
||||||
|
pending: boolean
|
||||||
|
title: string
|
||||||
|
onConfirm: VoidFunction
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
}) {
|
||||||
|
const t = useTranslate()
|
||||||
|
return (
|
||||||
|
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>{title}</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>{description}</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel disabled={pending}>
|
||||||
|
{t(mediaMessages.cancel)}
|
||||||
|
</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
variant="destructive"
|
||||||
|
disabled={pending}
|
||||||
|
onClick={onConfirm}
|
||||||
|
>
|
||||||
|
{t(mediaMessages.delete)}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { useTranslate } from "@workspace/i18n"
|
||||||
|
import { Button } from "@workspace/ui/components/button"
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@workspace/ui/components/dialog"
|
||||||
|
|
||||||
|
import { useMedia } from "./context"
|
||||||
|
import { MediaLibrary } from "./media-library"
|
||||||
|
import { mediaMessages } from "./messages"
|
||||||
|
import type { MediaAsset, MediaKind } from "./types"
|
||||||
|
import { defaultMediaReference } from "./utils"
|
||||||
|
|
||||||
|
const EMPTY_SELECTION: readonly MediaAsset[] = []
|
||||||
|
|
||||||
|
export interface MediaPickerDialogProps {
|
||||||
|
allowedKinds?: readonly MediaKind[]
|
||||||
|
initialSelection?: readonly MediaAsset[]
|
||||||
|
multiple?: boolean
|
||||||
|
open: boolean
|
||||||
|
onConfirm: (assets: readonly MediaAsset[]) => void
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MediaPickerDialog({
|
||||||
|
allowedKinds,
|
||||||
|
initialSelection = EMPTY_SELECTION,
|
||||||
|
multiple = false,
|
||||||
|
open,
|
||||||
|
onConfirm,
|
||||||
|
onOpenChange,
|
||||||
|
}: MediaPickerDialogProps) {
|
||||||
|
const { adapter } = useMedia()
|
||||||
|
const t = useTranslate()
|
||||||
|
const [selected, setSelected] =
|
||||||
|
React.useState<readonly MediaAsset[]>(initialSelection)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (open) setSelected(initialSelection)
|
||||||
|
}, [initialSelection, open])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent
|
||||||
|
className="flex h-[min(88dvh,54rem)] max-h-[calc(100dvh-2rem)] flex-col gap-3 p-4 sm:max-w-[min(94vw,86rem)]"
|
||||||
|
showCloseButton={false}
|
||||||
|
>
|
||||||
|
<DialogHeader className="sr-only">
|
||||||
|
<DialogTitle>{t(mediaMessages.selectMedia)}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{t(mediaMessages.selectMediaDescription)}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<MediaLibrary
|
||||||
|
allowedKinds={allowedKinds}
|
||||||
|
selectionMode={multiple ? "multiple" : "single"}
|
||||||
|
selectedAssets={selected}
|
||||||
|
onSelectionChange={setSelected}
|
||||||
|
className="min-h-0 flex-1"
|
||||||
|
footer={
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{t(mediaMessages.selectedCount, { count: selected.length })}
|
||||||
|
</span>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
>
|
||||||
|
{t(mediaMessages.cancel)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
disabled={selected.length === 0}
|
||||||
|
onClick={() => {
|
||||||
|
onConfirm(
|
||||||
|
selected.map((asset) => {
|
||||||
|
const url =
|
||||||
|
adapter.resolveReference?.(asset) ??
|
||||||
|
defaultMediaReference(asset)
|
||||||
|
return url === asset.url ? asset : { ...asset, url }
|
||||||
|
})
|
||||||
|
)
|
||||||
|
onOpenChange(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t(mediaMessages.useSelected)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
// @vitest-environment jsdom
|
||||||
|
|
||||||
|
import type { ReactNode } from "react"
|
||||||
|
import {
|
||||||
|
cleanup,
|
||||||
|
fireEvent,
|
||||||
|
render,
|
||||||
|
screen,
|
||||||
|
waitFor,
|
||||||
|
} from "@testing-library/react"
|
||||||
|
import { afterEach, describe, expect, it, vi } from "vitest"
|
||||||
|
import { I18nProvider } from "@workspace/i18n"
|
||||||
|
|
||||||
|
import { MediaProvider } from "./context"
|
||||||
|
import { MediaLibrary } from "./media-library"
|
||||||
|
import { MediaPickerDialog } from "./media-picker-dialog"
|
||||||
|
import type { MediaAdapter, MediaAsset } from "./types"
|
||||||
|
import {
|
||||||
|
defaultMediaReference,
|
||||||
|
getMediaFolderPath,
|
||||||
|
splitMediaName,
|
||||||
|
} from "./utils"
|
||||||
|
|
||||||
|
const imageAsset: MediaAsset = {
|
||||||
|
createdAt: "2026-08-01T00:00:00Z",
|
||||||
|
favorite: false,
|
||||||
|
folderId: null,
|
||||||
|
height: 600,
|
||||||
|
id: "image-1",
|
||||||
|
kind: "image",
|
||||||
|
mimeType: "image/png",
|
||||||
|
name: "product.png",
|
||||||
|
sizeBytes: 1024,
|
||||||
|
url: "/media/product.png",
|
||||||
|
width: 800,
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAdapter(): MediaAdapter {
|
||||||
|
return {
|
||||||
|
createFolder: vi.fn(async ({ name, parentId }) => ({
|
||||||
|
id: name,
|
||||||
|
name,
|
||||||
|
parentId,
|
||||||
|
})),
|
||||||
|
deleteAsset: vi.fn(async () => undefined),
|
||||||
|
deleteFolder: vi.fn(async () => undefined),
|
||||||
|
listAssets: vi.fn(async () => ({
|
||||||
|
assets: [imageAsset],
|
||||||
|
hasNextPage: false,
|
||||||
|
})),
|
||||||
|
listFolders: vi.fn(async () => []),
|
||||||
|
listStorageTargets: vi.fn(async () => [
|
||||||
|
{ id: "local", kind: "local" as const, label: "Local" },
|
||||||
|
]),
|
||||||
|
moveAsset: vi.fn(async () => imageAsset),
|
||||||
|
renameAsset: vi.fn(async () => imageAsset),
|
||||||
|
updateFavorite: vi.fn(async () => imageAsset),
|
||||||
|
updateFolder: vi.fn(async ({ id, name, parentId }) => ({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
parentId,
|
||||||
|
})),
|
||||||
|
upload: vi.fn(async () => [imageAsset]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function MediaTestProvider({
|
||||||
|
adapter,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
adapter: MediaAdapter
|
||||||
|
children: ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<I18nProvider locale="en">
|
||||||
|
<MediaProvider adapter={adapter}>{children}</MediaProvider>
|
||||||
|
</I18nProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("media blocks", () => {
|
||||||
|
afterEach(cleanup)
|
||||||
|
|
||||||
|
it("loads and uploads media only through the provider adapter", async () => {
|
||||||
|
const adapter = createAdapter()
|
||||||
|
const { container } = render(
|
||||||
|
<MediaTestProvider adapter={adapter}>
|
||||||
|
<MediaLibrary />
|
||||||
|
</MediaTestProvider>
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(await screen.findByRole("img", { name: "product.png" })).toBeTruthy()
|
||||||
|
const fileInput =
|
||||||
|
container.querySelector<HTMLInputElement>('input[type="file"]')
|
||||||
|
expect(fileInput).not.toBeNull()
|
||||||
|
|
||||||
|
fireEvent.change(fileInput!, {
|
||||||
|
target: {
|
||||||
|
files: [new File(["image"], "new-image.png", { type: "image/png" })],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await waitFor(() => expect(adapter.upload).toHaveBeenCalledOnce())
|
||||||
|
expect(adapter.upload).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
files: expect.arrayContaining([expect.any(File)]),
|
||||||
|
target: expect.objectContaining({ id: "local" }),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns image picker selections with an intrinsic-size reference URL", async () => {
|
||||||
|
const adapter = createAdapter()
|
||||||
|
const onConfirm = vi.fn()
|
||||||
|
render(
|
||||||
|
<MediaTestProvider adapter={adapter}>
|
||||||
|
<MediaPickerDialog
|
||||||
|
open
|
||||||
|
onOpenChange={() => undefined}
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
/>
|
||||||
|
</MediaTestProvider>
|
||||||
|
)
|
||||||
|
|
||||||
|
const image = await screen.findByRole("img", { name: "product.png" })
|
||||||
|
fireEvent.click(image.closest("button")!)
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(
|
||||||
|
(
|
||||||
|
screen.getByRole("button", {
|
||||||
|
name: "Use selected media",
|
||||||
|
}) as HTMLButtonElement
|
||||||
|
).disabled
|
||||||
|
).toBe(false)
|
||||||
|
)
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Use selected media" }))
|
||||||
|
|
||||||
|
expect(onConfirm).toHaveBeenCalledWith([
|
||||||
|
expect.objectContaining({ url: "/media/product.png?w=800&h=600" }),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("builds stable media references and folder paths", () => {
|
||||||
|
expect(defaultMediaReference(imageAsset)).toBe(
|
||||||
|
"/media/product.png?w=800&h=600"
|
||||||
|
)
|
||||||
|
expect(splitMediaName("archive.tar.gz")).toEqual({
|
||||||
|
baseName: "archive.tar",
|
||||||
|
extension: ".gz",
|
||||||
|
})
|
||||||
|
expect(
|
||||||
|
getMediaFolderPath({ id: "child", name: "Shoes", parentId: "root" }, [
|
||||||
|
{ id: "root", name: "Products", parentId: null },
|
||||||
|
{ id: "child", name: "Shoes", parentId: "root" },
|
||||||
|
])
|
||||||
|
).toBe("Products / Shoes")
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import type { MessageDescriptor } from "@workspace/i18n"
|
||||||
|
|
||||||
|
export const mediaMessages = {
|
||||||
|
allMedia: { id: "blocks.media.filters.all", message: "All media" },
|
||||||
|
cancel: { id: "blocks.media.actions.cancel", message: "Cancel" },
|
||||||
|
chooseStorage: {
|
||||||
|
id: "blocks.media.upload.chooseStorage",
|
||||||
|
message: "Choose cloud storage",
|
||||||
|
},
|
||||||
|
cloudUpload: {
|
||||||
|
id: "blocks.media.upload.cloud",
|
||||||
|
message: "Upload to cloud",
|
||||||
|
},
|
||||||
|
copyFailed: {
|
||||||
|
id: "blocks.media.copy.failed",
|
||||||
|
message: "Copy failed. Check browser clipboard permissions.",
|
||||||
|
},
|
||||||
|
copyInformation: {
|
||||||
|
id: "blocks.media.actions.copyInformation",
|
||||||
|
message: "Copy information",
|
||||||
|
},
|
||||||
|
copyLink: { id: "blocks.media.actions.copyLink", message: "Copy link" },
|
||||||
|
copyName: { id: "blocks.media.actions.copyName", message: "Copy name" },
|
||||||
|
createFolder: {
|
||||||
|
id: "blocks.media.folders.create",
|
||||||
|
message: "New folder",
|
||||||
|
},
|
||||||
|
delete: { id: "blocks.media.actions.delete", message: "Delete" },
|
||||||
|
deleteAssetDescription: {
|
||||||
|
id: "blocks.media.deleteAsset.description",
|
||||||
|
message: "This media item will no longer appear in the library.",
|
||||||
|
},
|
||||||
|
deleteAssetTitle: {
|
||||||
|
id: "blocks.media.deleteAsset.title",
|
||||||
|
message: "Delete media",
|
||||||
|
},
|
||||||
|
deleteFolderDescription: {
|
||||||
|
id: "blocks.media.deleteFolder.description",
|
||||||
|
message:
|
||||||
|
"This folder and its child folders will be deleted. Their media items will become unclassified.",
|
||||||
|
},
|
||||||
|
deleteFolderTitle: {
|
||||||
|
id: "blocks.media.deleteFolder.title",
|
||||||
|
message: "Delete folder",
|
||||||
|
},
|
||||||
|
details: { id: "blocks.media.actions.details", message: "View details" },
|
||||||
|
dimensions: { id: "blocks.media.details.dimensions", message: "Dimensions" },
|
||||||
|
emptyDescription: {
|
||||||
|
id: "blocks.media.empty.description",
|
||||||
|
message: "Upload images or videos to manage them here.",
|
||||||
|
},
|
||||||
|
emptySearchDescription: {
|
||||||
|
id: "blocks.media.emptySearch.description",
|
||||||
|
message: "No media files match your search.",
|
||||||
|
},
|
||||||
|
emptyTitle: { id: "blocks.media.empty.title", message: "No media" },
|
||||||
|
favorite: { id: "blocks.media.filters.favorite", message: "Favorites" },
|
||||||
|
favoriteAction: { id: "blocks.media.actions.favorite", message: "Favorite" },
|
||||||
|
fileName: { id: "blocks.media.details.fileName", message: "File name" },
|
||||||
|
folder: { id: "blocks.media.folders.title", message: "Folders" },
|
||||||
|
folderName: {
|
||||||
|
id: "blocks.media.folders.name",
|
||||||
|
message: "Folder name",
|
||||||
|
},
|
||||||
|
folderNamePlaceholder: {
|
||||||
|
id: "blocks.media.folders.placeholder",
|
||||||
|
message: "Enter a folder name",
|
||||||
|
},
|
||||||
|
folderOnlyClassifies: {
|
||||||
|
id: "blocks.media.folders.classification",
|
||||||
|
message:
|
||||||
|
"Folders only organize media in this library and do not change file URLs.",
|
||||||
|
},
|
||||||
|
image: { id: "blocks.media.filters.image", message: "Images" },
|
||||||
|
library: { id: "blocks.media.title", message: "Media library" },
|
||||||
|
loadError: {
|
||||||
|
id: "blocks.media.load.error",
|
||||||
|
message: "Unable to load media",
|
||||||
|
},
|
||||||
|
loadMore: { id: "blocks.media.actions.loadMore", message: "Load more" },
|
||||||
|
loading: { id: "blocks.media.loading", message: "Loading media…" },
|
||||||
|
moveToFolder: {
|
||||||
|
id: "blocks.media.actions.moveToFolder",
|
||||||
|
message: "Move to folder",
|
||||||
|
},
|
||||||
|
newChildFolder: {
|
||||||
|
id: "blocks.media.folders.newChild",
|
||||||
|
message: "New child folder",
|
||||||
|
},
|
||||||
|
noDimensions: { id: "blocks.media.details.noDimensions", message: "Unknown" },
|
||||||
|
noFolders: {
|
||||||
|
id: "blocks.media.folders.empty",
|
||||||
|
message: "No folders yet. Create one.",
|
||||||
|
},
|
||||||
|
rename: { id: "blocks.media.actions.rename", message: "Rename" },
|
||||||
|
renameFolder: {
|
||||||
|
id: "blocks.media.folders.rename",
|
||||||
|
message: "Rename folder",
|
||||||
|
},
|
||||||
|
renameMedia: {
|
||||||
|
id: "blocks.media.renameMedia.title",
|
||||||
|
message: "Rename media",
|
||||||
|
},
|
||||||
|
renameMediaDescription: {
|
||||||
|
id: "blocks.media.renameMedia.description",
|
||||||
|
message:
|
||||||
|
"This changes the display name without changing the file or its URL.",
|
||||||
|
},
|
||||||
|
retry: { id: "blocks.media.actions.retry", message: "Try again" },
|
||||||
|
save: { id: "blocks.media.actions.save", message: "Save" },
|
||||||
|
saving: { id: "blocks.media.actions.saving", message: "Saving…" },
|
||||||
|
search: { id: "blocks.media.search", message: "Search media files" },
|
||||||
|
select: { id: "blocks.media.actions.select", message: "Select" },
|
||||||
|
selectFolder: {
|
||||||
|
id: "blocks.media.actions.selectFolder",
|
||||||
|
message: "Select folder {name}",
|
||||||
|
},
|
||||||
|
selectMedia: {
|
||||||
|
id: "blocks.media.picker.title",
|
||||||
|
message: "Select media",
|
||||||
|
},
|
||||||
|
selectMediaDescription: {
|
||||||
|
id: "blocks.media.picker.description",
|
||||||
|
message: "Choose an uploaded image or video from the media library.",
|
||||||
|
},
|
||||||
|
selectedCount: {
|
||||||
|
id: "blocks.media.picker.selectedCount",
|
||||||
|
message: "{count} selected",
|
||||||
|
},
|
||||||
|
serverUpload: {
|
||||||
|
id: "blocks.media.upload.server",
|
||||||
|
message: "Upload to server",
|
||||||
|
},
|
||||||
|
size: { id: "blocks.media.details.size", message: "Size" },
|
||||||
|
type: { id: "blocks.media.details.type", message: "Type" },
|
||||||
|
uploadedAt: {
|
||||||
|
id: "blocks.media.details.uploadedAt",
|
||||||
|
message: "Uploaded",
|
||||||
|
},
|
||||||
|
unclassified: {
|
||||||
|
id: "blocks.media.filters.unclassified",
|
||||||
|
message: "Unclassified",
|
||||||
|
},
|
||||||
|
unfavorite: {
|
||||||
|
id: "blocks.media.actions.unfavorite",
|
||||||
|
message: "Unfavorite",
|
||||||
|
},
|
||||||
|
upload: { id: "blocks.media.actions.upload", message: "Upload" },
|
||||||
|
uploadError: {
|
||||||
|
id: "blocks.media.upload.error",
|
||||||
|
message: "Unable to upload media.",
|
||||||
|
},
|
||||||
|
uploadSuccess: {
|
||||||
|
id: "blocks.media.upload.success",
|
||||||
|
message: "{count} media files uploaded.",
|
||||||
|
},
|
||||||
|
useSelected: {
|
||||||
|
id: "blocks.media.picker.useSelected",
|
||||||
|
message: "Use selected media",
|
||||||
|
},
|
||||||
|
video: { id: "blocks.media.filters.video", message: "Videos" },
|
||||||
|
viewOriginal: {
|
||||||
|
id: "blocks.media.actions.viewOriginal",
|
||||||
|
message: "View original",
|
||||||
|
},
|
||||||
|
} as const satisfies Record<string, MessageDescriptor>
|
||||||
@@ -0,0 +1,399 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import {
|
||||||
|
ChevronRightIcon,
|
||||||
|
FilmIcon,
|
||||||
|
FolderIcon,
|
||||||
|
FolderOpenIcon,
|
||||||
|
HeartIcon,
|
||||||
|
ImagesIcon,
|
||||||
|
LibraryIcon,
|
||||||
|
MoreHorizontalIcon,
|
||||||
|
PencilIcon,
|
||||||
|
PlusIcon,
|
||||||
|
Trash2Icon,
|
||||||
|
UnlinkIcon,
|
||||||
|
} from "lucide-react"
|
||||||
|
import { useTranslate } from "@workspace/i18n"
|
||||||
|
import { Button } from "@workspace/ui/components/button"
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from "@workspace/ui/components/collapsible"
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@workspace/ui/components/dropdown-menu"
|
||||||
|
import { cn } from "@workspace/ui/lib/utils"
|
||||||
|
|
||||||
|
import { mediaMessages } from "./messages"
|
||||||
|
import type {
|
||||||
|
MediaFilter,
|
||||||
|
MediaFolder,
|
||||||
|
MediaFolderSelection,
|
||||||
|
MediaId,
|
||||||
|
MediaKind,
|
||||||
|
} from "./types"
|
||||||
|
import {
|
||||||
|
getChildMediaFolders,
|
||||||
|
getMediaFolderDepth,
|
||||||
|
getMediaFolderPath,
|
||||||
|
} from "./utils"
|
||||||
|
|
||||||
|
const filters = [
|
||||||
|
{ icon: LibraryIcon, message: mediaMessages.allMedia, value: "all" },
|
||||||
|
{ icon: ImagesIcon, message: mediaMessages.image, value: "image" },
|
||||||
|
{ icon: FilmIcon, message: mediaMessages.video, value: "video" },
|
||||||
|
{ icon: HeartIcon, message: mediaMessages.favorite, value: "favorite" },
|
||||||
|
] as const satisfies readonly {
|
||||||
|
icon: React.ComponentType<{ className?: string }>
|
||||||
|
message: (typeof mediaMessages)[keyof typeof mediaMessages]
|
||||||
|
value: MediaFilter
|
||||||
|
}[]
|
||||||
|
|
||||||
|
export interface MediaSidebarProps {
|
||||||
|
allowedKinds?: readonly MediaKind[]
|
||||||
|
className?: string
|
||||||
|
filter: MediaFilter
|
||||||
|
folder: MediaFolderSelection
|
||||||
|
folders: readonly MediaFolder[]
|
||||||
|
onCreateFolder: (parentId: MediaId | null) => void
|
||||||
|
onDeleteFolder: (folder: MediaFolder) => void
|
||||||
|
onFilterChange: (filter: MediaFilter) => void
|
||||||
|
onFolderChange: (folder: MediaFolderSelection) => void
|
||||||
|
onRenameFolder: (folder: MediaFolder) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function visibleFilters(allowedKinds?: readonly MediaKind[]) {
|
||||||
|
return filters.filter(
|
||||||
|
(filter) =>
|
||||||
|
filter.value === "all" ||
|
||||||
|
filter.value === "favorite" ||
|
||||||
|
!allowedKinds ||
|
||||||
|
allowedKinds.includes(filter.value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MediaSidebar({
|
||||||
|
allowedKinds,
|
||||||
|
className,
|
||||||
|
filter,
|
||||||
|
folder,
|
||||||
|
folders,
|
||||||
|
onCreateFolder,
|
||||||
|
onDeleteFolder,
|
||||||
|
onFilterChange,
|
||||||
|
onFolderChange,
|
||||||
|
onRenameFolder,
|
||||||
|
}: MediaSidebarProps) {
|
||||||
|
const t = useTranslate()
|
||||||
|
const rootFolders = getChildMediaFolders(folders, null)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside
|
||||||
|
className={cn(
|
||||||
|
"flex min-h-0 flex-col border-e bg-muted/15 py-3",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<h2 className="px-4 py-2 text-sm font-semibold">
|
||||||
|
{t(mediaMessages.library)}
|
||||||
|
</h2>
|
||||||
|
<nav aria-label={t(mediaMessages.library)} className="px-3">
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{visibleFilters(allowedKinds).map(({ icon, message, value }) => (
|
||||||
|
<li key={value}>
|
||||||
|
<SidebarButton
|
||||||
|
active={folder === undefined && filter === value}
|
||||||
|
icon={icon}
|
||||||
|
label={t(message)}
|
||||||
|
onClick={() => onFilterChange(value)}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
<li>
|
||||||
|
<SidebarButton
|
||||||
|
active={folder === "unclassified"}
|
||||||
|
icon={UnlinkIcon}
|
||||||
|
label={t(mediaMessages.unclassified)}
|
||||||
|
onClick={() => onFolderChange("unclassified")}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div className="mt-4 flex min-h-0 flex-1 flex-col border-t pt-3">
|
||||||
|
<div className="flex items-center px-4 py-1">
|
||||||
|
<h3 className="text-xs font-medium text-muted-foreground">
|
||||||
|
{t(mediaMessages.folder)}
|
||||||
|
</h3>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="icon-xs"
|
||||||
|
variant="ghost"
|
||||||
|
className="ms-auto"
|
||||||
|
aria-label={t(mediaMessages.createFolder)}
|
||||||
|
onClick={() => onCreateFolder(null)}
|
||||||
|
>
|
||||||
|
<PlusIcon />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<nav
|
||||||
|
aria-label={t(mediaMessages.folder)}
|
||||||
|
className="min-h-0 overflow-y-auto px-3"
|
||||||
|
>
|
||||||
|
{rootFolders.length ? (
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{rootFolders.map((rootFolder) => (
|
||||||
|
<FolderItem
|
||||||
|
key={rootFolder.id}
|
||||||
|
folder={rootFolder}
|
||||||
|
folders={folders}
|
||||||
|
depth={0}
|
||||||
|
activeFolder={folder}
|
||||||
|
onSelect={onFolderChange}
|
||||||
|
onCreateFolder={onCreateFolder}
|
||||||
|
onRename={onRenameFolder}
|
||||||
|
onDelete={onDeleteFolder}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="w-full px-3 py-4 text-start text-xs text-muted-foreground hover:text-foreground"
|
||||||
|
onClick={() => onCreateFolder(null)}
|
||||||
|
>
|
||||||
|
{t(mediaMessages.noFolders)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MediaFilterTabs({
|
||||||
|
allowedKinds,
|
||||||
|
filter,
|
||||||
|
folder,
|
||||||
|
folders,
|
||||||
|
onFilterChange,
|
||||||
|
onFolderChange,
|
||||||
|
}: Omit<
|
||||||
|
MediaSidebarProps,
|
||||||
|
"className" | "onCreateFolder" | "onDeleteFolder" | "onRenameFolder"
|
||||||
|
>) {
|
||||||
|
const t = useTranslate()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-1 overflow-x-auto border-b p-2 md:hidden">
|
||||||
|
{visibleFilters(allowedKinds).map(({ icon: Icon, message, value }) => (
|
||||||
|
<button
|
||||||
|
key={value}
|
||||||
|
type="button"
|
||||||
|
aria-pressed={folder === undefined && filter === value}
|
||||||
|
onClick={() => onFilterChange(value)}
|
||||||
|
className={cn(
|
||||||
|
"flex h-8 shrink-0 items-center gap-1.5 rounded-lg px-3 text-xs",
|
||||||
|
folder === undefined && filter === value
|
||||||
|
? "bg-primary text-primary-foreground"
|
||||||
|
: "bg-muted"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className="size-3.5" />
|
||||||
|
{t(message)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
<select
|
||||||
|
aria-label={t(mediaMessages.folder)}
|
||||||
|
className="h-8 max-w-40 rounded-lg border bg-background px-2 text-xs outline-none"
|
||||||
|
value={
|
||||||
|
folder === undefined
|
||||||
|
? ""
|
||||||
|
: folder === "unclassified"
|
||||||
|
? "unclassified"
|
||||||
|
: String(folder)
|
||||||
|
}
|
||||||
|
onChange={(event) => {
|
||||||
|
const next = event.target.value
|
||||||
|
onFolderChange(
|
||||||
|
next === ""
|
||||||
|
? undefined
|
||||||
|
: next === "unclassified"
|
||||||
|
? "unclassified"
|
||||||
|
: (folders.find((item) => String(item.id) === next)?.id ?? next)
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">{t(mediaMessages.allMedia)}</option>
|
||||||
|
<option value="unclassified">{t(mediaMessages.unclassified)}</option>
|
||||||
|
{folders.map((item) => (
|
||||||
|
<option key={item.id} value={item.id}>
|
||||||
|
{" ".repeat(getMediaFolderDepth(item, folders))}
|
||||||
|
{getMediaFolderPath(item, folders)}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarButton({
|
||||||
|
active,
|
||||||
|
icon: Icon,
|
||||||
|
label,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
active: boolean
|
||||||
|
icon: React.ComponentType<{ className?: string }>
|
||||||
|
label: string
|
||||||
|
onClick: VoidFunction
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-pressed={active}
|
||||||
|
onClick={onClick}
|
||||||
|
className={cn(
|
||||||
|
"flex h-9 w-full items-center gap-2 rounded-lg px-3 text-start text-sm transition-colors outline-none hover:bg-muted focus-visible:ring-3 focus-visible:ring-ring/30",
|
||||||
|
active && "bg-primary text-primary-foreground hover:bg-primary"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className="size-4" />
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FolderItem({
|
||||||
|
folder,
|
||||||
|
folders,
|
||||||
|
depth,
|
||||||
|
activeFolder,
|
||||||
|
onSelect,
|
||||||
|
onCreateFolder,
|
||||||
|
onRename,
|
||||||
|
onDelete,
|
||||||
|
}: {
|
||||||
|
folder: MediaFolder
|
||||||
|
folders: readonly MediaFolder[]
|
||||||
|
depth: number
|
||||||
|
activeFolder: MediaFolderSelection
|
||||||
|
onSelect: (folder: MediaFolderSelection) => void
|
||||||
|
onCreateFolder: (parentId: MediaId | null) => void
|
||||||
|
onRename: (folder: MediaFolder) => void
|
||||||
|
onDelete: (folder: MediaFolder) => void
|
||||||
|
}) {
|
||||||
|
const t = useTranslate()
|
||||||
|
const children = getChildMediaFolders(folders, folder.id)
|
||||||
|
const [open, setOpen] = React.useState(true)
|
||||||
|
const hasChildren = children.length > 0
|
||||||
|
const active = activeFolder === folder.id
|
||||||
|
const Folder = active || (hasChildren && open) ? FolderOpenIcon : FolderIcon
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
<Collapsible open={open} onOpenChange={setOpen}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"group flex h-9 items-center rounded-lg transition-colors hover:bg-muted",
|
||||||
|
active && "bg-secondary text-secondary-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{hasChildren ? (
|
||||||
|
<CollapsibleTrigger
|
||||||
|
render={
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="group/folder-toggle relative ms-2 grid size-7 shrink-0 place-items-center rounded-md outline-none hover:bg-muted-foreground/10 focus-visible:ring-3 focus-visible:ring-ring/30"
|
||||||
|
aria-label={folder.name}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Folder className="size-4 transition-opacity group-hover/folder-toggle:opacity-0 group-focus-visible/folder-toggle:opacity-0" />
|
||||||
|
<ChevronRightIcon
|
||||||
|
className={cn(
|
||||||
|
"absolute size-4 opacity-0 transition-[rotate,opacity] group-hover/folder-toggle:opacity-100 group-focus-visible/folder-toggle:opacity-100",
|
||||||
|
open && "rotate-90"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
) : (
|
||||||
|
<span className="ms-2 grid size-7 shrink-0 place-items-center">
|
||||||
|
<Folder className="size-4" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={t(mediaMessages.selectFolder, { name: folder.name })}
|
||||||
|
className="flex min-w-0 flex-1 items-center gap-2 self-stretch py-0 ps-1 pe-1.5 text-start text-sm outline-none focus-visible:ring-3 focus-visible:ring-ring/30"
|
||||||
|
onClick={() => onSelect(folder.id)}
|
||||||
|
>
|
||||||
|
<span className="min-w-0 flex-1 truncate">{folder.name}</span>
|
||||||
|
{(folder.assetCount ?? 0) > 0 && (
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{folder.assetCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger
|
||||||
|
render={
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="icon-xs"
|
||||||
|
variant="ghost"
|
||||||
|
className="me-1 opacity-0 group-hover:opacity-100 aria-expanded:opacity-100"
|
||||||
|
aria-label={folder.name}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MoreHorizontalIcon />
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
{depth < 2 && (
|
||||||
|
<DropdownMenuItem onClick={() => onCreateFolder(folder.id)}>
|
||||||
|
<PlusIcon />
|
||||||
|
{t(mediaMessages.newChildFolder)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
<DropdownMenuItem onClick={() => onRename(folder)}>
|
||||||
|
<PencilIcon />
|
||||||
|
{t(mediaMessages.rename)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => onDelete(folder)}
|
||||||
|
>
|
||||||
|
<Trash2Icon />
|
||||||
|
{t(mediaMessages.delete)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
{hasChildren && (
|
||||||
|
<CollapsibleContent>
|
||||||
|
<ul className="mt-1 space-y-1 ps-4">
|
||||||
|
{children.map((child) => (
|
||||||
|
<FolderItem
|
||||||
|
key={child.id}
|
||||||
|
folder={child}
|
||||||
|
folders={folders}
|
||||||
|
depth={depth + 1}
|
||||||
|
activeFolder={activeFolder}
|
||||||
|
onSelect={onSelect}
|
||||||
|
onCreateFolder={onCreateFolder}
|
||||||
|
onRename={onRename}
|
||||||
|
onDelete={onDelete}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</CollapsibleContent>
|
||||||
|
)}
|
||||||
|
</Collapsible>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
export type MediaId = number | string
|
||||||
|
export type MediaKind = "image" | "video"
|
||||||
|
export type MediaFilter = "all" | MediaKind | "favorite"
|
||||||
|
export type MediaFolderSelection = MediaId | "unclassified" | undefined
|
||||||
|
export type MediaSelectionMode = "multiple" | "none" | "single"
|
||||||
|
|
||||||
|
export interface MediaAsset {
|
||||||
|
createdAt: Date | number | string
|
||||||
|
favorite: boolean
|
||||||
|
folderId: MediaId | null
|
||||||
|
height?: number | null
|
||||||
|
id: MediaId
|
||||||
|
kind: MediaKind
|
||||||
|
mimeType: string
|
||||||
|
name: string
|
||||||
|
sizeBytes: number
|
||||||
|
updatedAt?: Date | number | string
|
||||||
|
url: string
|
||||||
|
width?: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MediaFolder {
|
||||||
|
assetCount?: number
|
||||||
|
createdAt?: Date | number | string
|
||||||
|
id: MediaId
|
||||||
|
name: string
|
||||||
|
parentId: MediaId | null
|
||||||
|
sortOrder?: number
|
||||||
|
updatedAt?: Date | number | string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MediaStorageTarget {
|
||||||
|
id: string
|
||||||
|
kind: "cloud" | "local"
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MediaAssetQuery {
|
||||||
|
allowedKinds?: readonly MediaKind[]
|
||||||
|
filter: MediaFilter
|
||||||
|
folder: MediaFolderSelection
|
||||||
|
keyword: string
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MediaAssetPage {
|
||||||
|
assets: readonly MediaAsset[]
|
||||||
|
hasNextPage: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateMediaFolderInput {
|
||||||
|
name: string
|
||||||
|
parentId: MediaId | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateMediaFolderInput extends CreateMediaFolderInput {
|
||||||
|
id: MediaId
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MediaUploadInput {
|
||||||
|
files: readonly File[]
|
||||||
|
folderId?: MediaId
|
||||||
|
target: MediaStorageTarget
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The host application owns all persistence and transport. This adapter can
|
||||||
|
* be backed by a database API, direct-to-object-storage uploads, or mocks.
|
||||||
|
*/
|
||||||
|
export interface MediaAdapter {
|
||||||
|
createFolder(input: CreateMediaFolderInput): Promise<MediaFolder>
|
||||||
|
deleteAsset(id: MediaId): Promise<void>
|
||||||
|
deleteFolder(id: MediaId): Promise<void>
|
||||||
|
listAssets(query: MediaAssetQuery): Promise<MediaAssetPage>
|
||||||
|
listFolders(): Promise<readonly MediaFolder[]>
|
||||||
|
listStorageTargets?(): Promise<readonly MediaStorageTarget[]>
|
||||||
|
moveAsset(id: MediaId, folderId: MediaId | null): Promise<MediaAsset>
|
||||||
|
renameAsset(id: MediaId, name: string): Promise<MediaAsset>
|
||||||
|
updateFavorite(id: MediaId, favorite: boolean): Promise<MediaAsset>
|
||||||
|
updateFolder(input: UpdateMediaFolderInput): Promise<MediaFolder>
|
||||||
|
upload(input: MediaUploadInput): Promise<readonly MediaAsset[]>
|
||||||
|
|
||||||
|
/** Resolves a stored media URL for rendering or opening the source file. */
|
||||||
|
resolveUrl?(asset: MediaAsset): string
|
||||||
|
|
||||||
|
/** Produces the URL returned by MediaPickerDialog. */
|
||||||
|
resolveReference?(asset: MediaAsset): string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MediaNotice {
|
||||||
|
message: string
|
||||||
|
tone: "error" | "success"
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import type { MediaAsset, MediaFolder, MediaId } from "./types"
|
||||||
|
|
||||||
|
export const MEDIA_ACCEPT =
|
||||||
|
"image/png,image/jpeg,image/webp,image/gif,video/mp4,video/webm,video/ogg,video/quicktime"
|
||||||
|
|
||||||
|
export function formatMediaFileSize(bytes: number): string {
|
||||||
|
if (bytes < 1024) return `${bytes} B`
|
||||||
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
||||||
|
return `${(bytes / 1024 / 1024).toFixed(1)} MB`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMediaFile(file: File): boolean {
|
||||||
|
return file.type.startsWith("image/") || file.type.startsWith("video/")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mediaKindForFile(file: File): MediaAsset["kind"] | undefined {
|
||||||
|
if (file.type.startsWith("image/")) return "image"
|
||||||
|
if (file.type.startsWith("video/")) return "video"
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getChildMediaFolders(
|
||||||
|
folders: readonly MediaFolder[],
|
||||||
|
parentId: MediaId | null
|
||||||
|
): MediaFolder[] {
|
||||||
|
return folders
|
||||||
|
.filter((folder) => folder.parentId === parentId)
|
||||||
|
.sort(
|
||||||
|
(left, right) =>
|
||||||
|
(left.sortOrder ?? 0) - (right.sortOrder ?? 0) ||
|
||||||
|
left.name.localeCompare(right.name)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMediaFolderPath(
|
||||||
|
folder: MediaFolder,
|
||||||
|
folders: readonly MediaFolder[]
|
||||||
|
): string {
|
||||||
|
const names = [folder.name]
|
||||||
|
const seen = new Set<MediaId>([folder.id])
|
||||||
|
let current = folder
|
||||||
|
|
||||||
|
while (current.parentId !== null) {
|
||||||
|
const parent = folders.find((item) => item.id === current.parentId)
|
||||||
|
if (!parent || seen.has(parent.id)) break
|
||||||
|
seen.add(parent.id)
|
||||||
|
names.unshift(parent.name)
|
||||||
|
current = parent
|
||||||
|
}
|
||||||
|
|
||||||
|
return names.join(" / ")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMediaFolderDepth(
|
||||||
|
folder: MediaFolder,
|
||||||
|
folders: readonly MediaFolder[]
|
||||||
|
): number {
|
||||||
|
let depth = 0
|
||||||
|
const seen = new Set<MediaId>([folder.id])
|
||||||
|
let current = folder
|
||||||
|
|
||||||
|
while (current.parentId !== null) {
|
||||||
|
const parent = folders.find((item) => item.id === current.parentId)
|
||||||
|
if (!parent || seen.has(parent.id)) break
|
||||||
|
seen.add(parent.id)
|
||||||
|
depth += 1
|
||||||
|
current = parent
|
||||||
|
}
|
||||||
|
|
||||||
|
return depth
|
||||||
|
}
|
||||||
|
|
||||||
|
export function splitMediaName(name: string) {
|
||||||
|
const extensionIndex = name.lastIndexOf(".")
|
||||||
|
if (extensionIndex <= 0 || extensionIndex === name.length - 1) {
|
||||||
|
return { baseName: name, extension: "" }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
baseName: name.slice(0, extensionIndex),
|
||||||
|
extension: name.slice(extensionIndex),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function defaultMediaReference(asset: MediaAsset): string {
|
||||||
|
if (
|
||||||
|
asset.kind !== "image" ||
|
||||||
|
!asset.width ||
|
||||||
|
!asset.height ||
|
||||||
|
asset.width <= 0 ||
|
||||||
|
asset.height <= 0
|
||||||
|
) {
|
||||||
|
return asset.url
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashIndex = asset.url.indexOf("#")
|
||||||
|
const url = hashIndex === -1 ? asset.url : asset.url.slice(0, hashIndex)
|
||||||
|
const hash = hashIndex === -1 ? "" : asset.url.slice(hashIndex)
|
||||||
|
const separator = url.includes("?")
|
||||||
|
? url.endsWith("?") || url.endsWith("&")
|
||||||
|
? ""
|
||||||
|
: "&"
|
||||||
|
: "?"
|
||||||
|
|
||||||
|
return `${url}${separator}w=${asset.width}&h=${asset.height}${hash}`
|
||||||
|
}
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
|
import { Icon } from "@workspace/icons"
|
||||||
import {
|
import {
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuSub,
|
DropdownMenuSub,
|
||||||
DropdownMenuSubContent,
|
DropdownMenuSubContent,
|
||||||
DropdownMenuSubTrigger,
|
DropdownMenuSubTrigger,
|
||||||
} from "@workspace/ui/components/dropdown-menu"
|
} from "@workspace/ui/components/dropdown-menu"
|
||||||
|
|
||||||
import { Icon } from "../../components/icon"
|
|
||||||
|
|
||||||
import { resolveNavigationItemIcon } from "./item-icon"
|
import { resolveNavigationItemIcon } from "./item-icon"
|
||||||
import { NavigationBadge, NavigationLink } from "./navigation-button"
|
import { NavigationBadge, NavigationLink } from "./navigation-button"
|
||||||
|
import { NavigationLabel } from "./navigation-label"
|
||||||
import type { GetNavigationRouteState } from "./route-state"
|
import type { GetNavigationRouteState } from "./route-state"
|
||||||
import type { NavigationItem } from "./types"
|
import type { NavigationItem } from "./types"
|
||||||
|
|
||||||
@@ -37,7 +36,9 @@ export function SidebarFlyoutMenuItems({
|
|||||||
className="h-9 gap-3 px-3 py-1 data-route-active:bg-primary/10 data-route-active:text-primary! data-route-active:**:text-primary!"
|
className="h-9 gap-3 px-3 py-1 data-route-active:bg-primary/10 data-route-active:text-primary! data-route-active:**:text-primary!"
|
||||||
>
|
>
|
||||||
{iconElement}
|
{iconElement}
|
||||||
<span className="min-w-0 flex-1 truncate">{item.label}</span>
|
<span className="min-w-0 flex-1 truncate">
|
||||||
|
<NavigationLabel label={item.label} />
|
||||||
|
</span>
|
||||||
<NavigationBadge className="ms-auto" itemId={item.id} />
|
<NavigationBadge className="ms-auto" itemId={item.id} />
|
||||||
</DropdownMenuSubTrigger>
|
</DropdownMenuSubTrigger>
|
||||||
<DropdownMenuSubContent className="grid w-fit max-w-(--available-width) min-w-44 gap-0.5">
|
<DropdownMenuSubContent className="grid w-fit max-w-(--available-width) min-w-44 gap-0.5">
|
||||||
@@ -59,7 +60,9 @@ export function SidebarFlyoutMenuItems({
|
|||||||
render={item.to ? <NavigationLink to={item.to} /> : undefined}
|
render={item.to ? <NavigationLink to={item.to} /> : undefined}
|
||||||
>
|
>
|
||||||
{iconElement}
|
{iconElement}
|
||||||
<span className="min-w-0 flex-1 truncate">{item.label}</span>
|
<span className="min-w-0 flex-1 truncate">
|
||||||
|
<NavigationLabel label={item.label} />
|
||||||
|
</span>
|
||||||
<NavigationBadge className="ms-auto" itemId={item.id} />
|
<NavigationBadge className="ms-auto" itemId={item.id} />
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export {
|
|||||||
} from "./mobile-navigation"
|
} from "./mobile-navigation"
|
||||||
export { resolveNavigationItemIcon } from "./item-icon"
|
export { resolveNavigationItemIcon } from "./item-icon"
|
||||||
export { PrimaryNavigation } from "./primary-navigation"
|
export { PrimaryNavigation } from "./primary-navigation"
|
||||||
|
export { NavigationLabel, TruncatedNavigationLabel } from "./navigation-label"
|
||||||
export { resolveNavigationRouteState } from "./route-state"
|
export { resolveNavigationRouteState } from "./route-state"
|
||||||
export {
|
export {
|
||||||
NavigationProvider,
|
NavigationProvider,
|
||||||
@@ -16,4 +17,8 @@ export type {
|
|||||||
NavigationRouteState,
|
NavigationRouteState,
|
||||||
} from "./route-state"
|
} from "./route-state"
|
||||||
export type { NavigationInfo, NavigationKey } from "./use-navigation"
|
export type { NavigationInfo, NavigationKey } from "./use-navigation"
|
||||||
export type { NavigationGroup, NavigationItem } from "./types"
|
export type {
|
||||||
|
NavigationGroup,
|
||||||
|
NavigationItem,
|
||||||
|
NavigationLabelValue,
|
||||||
|
} from "./types"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { IconData } from "../../components/icon"
|
import type { IconData } from "@workspace/icons"
|
||||||
|
|
||||||
import type { NavigationItem } from "./types"
|
import type { NavigationItem } from "./types"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { describe, it } from "vitest"
|
||||||
|
|
||||||
|
import { expectCompleteCatalogs } from "../../../i18n/test-catalogs"
|
||||||
|
import { navigationMessages } from "../messages"
|
||||||
|
import { navigationCatalogLocales } from "./catalogs"
|
||||||
|
import { messages as de } from "./de"
|
||||||
|
import { messages as en } from "./en"
|
||||||
|
import { messages as es } from "./es"
|
||||||
|
import { messages as fr } from "./fr"
|
||||||
|
import { messages as ja } from "./ja"
|
||||||
|
import { messages as ko } from "./ko"
|
||||||
|
import { messages as zhHans } from "./zh-Hans"
|
||||||
|
import { messages as zhHant } from "./zh-Hant"
|
||||||
|
|
||||||
|
describe("navigation locale catalogs", () => {
|
||||||
|
it("ship every navigation message in every built-in locale", () => {
|
||||||
|
expectCompleteCatalogs({
|
||||||
|
catalogs: {
|
||||||
|
de,
|
||||||
|
en,
|
||||||
|
es,
|
||||||
|
fr,
|
||||||
|
ja,
|
||||||
|
ko,
|
||||||
|
"zh-Hans": zhHans,
|
||||||
|
"zh-Hant": zhHant,
|
||||||
|
},
|
||||||
|
locales: navigationCatalogLocales,
|
||||||
|
messageIds: Object.values(navigationMessages).map(
|
||||||
|
(descriptor) => descriptor.id
|
||||||
|
),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import type { navigationMessages } from "../messages"
|
||||||
|
import {
|
||||||
|
blockCatalogLocales,
|
||||||
|
type BlockCatalogLocale,
|
||||||
|
type BlockMessageCatalog,
|
||||||
|
} from "../../../i18n/catalogs"
|
||||||
|
|
||||||
|
export { blockCatalogLocales as navigationCatalogLocales }
|
||||||
|
export type NavigationCatalogLocale = BlockCatalogLocale
|
||||||
|
export type NavigationMessageCatalog = BlockMessageCatalog<
|
||||||
|
typeof navigationMessages
|
||||||
|
>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user