mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-14 00:01:53 +00:00
revert: undo t0ng7u UI design-system refactor
Revert the following commits: -308e3e347feat(web): polish themed data views and add task log details -b2a890e75fix: Fontsource asset resolution across workspace layouts -9d1ca545erefactor(web): refine data-table cards and pricing page layout -0918bdb49refactor(web): consolidate design-system primitives and responsive data views -262ab9312style(web): unify design system across default frontend
This commit is contained in:
+1
-1
@@ -80,7 +80,7 @@ var SessionCookieTrustedURLs []string
|
|||||||
var OptionMap map[string]string
|
var OptionMap map[string]string
|
||||||
var OptionMapRWMutex sync.RWMutex
|
var OptionMapRWMutex sync.RWMutex
|
||||||
|
|
||||||
var ItemsPerPage = 20
|
var ItemsPerPage = 10
|
||||||
var MaxRecentItems = 1000
|
var MaxRecentItems = 1000
|
||||||
|
|
||||||
var PasswordLoginEnabled = true
|
var PasswordLoginEnabled = true
|
||||||
|
|||||||
@@ -1488,7 +1488,7 @@ func ManageMultiKeys(c *gin.Context) {
|
|||||||
page = 1
|
page = 1
|
||||||
}
|
}
|
||||||
if pageSize <= 0 {
|
if pageSize <= 0 {
|
||||||
pageSize = common.ItemsPerPage
|
pageSize = 50 // Default page size
|
||||||
}
|
}
|
||||||
|
|
||||||
// Statistics for all keys (unchanged by filtering)
|
// Statistics for all keys (unchanged by filtering)
|
||||||
|
|||||||
Vendored
+18
-10
@@ -83,6 +83,7 @@
|
|||||||
"@tanstack/react-query": "^5.101.2",
|
"@tanstack/react-query": "^5.101.2",
|
||||||
"@tanstack/react-router": "^1.170.17",
|
"@tanstack/react-router": "^1.170.17",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
|
"@tanstack/react-virtual": "^3.14.5",
|
||||||
"@visactor/react-vchart": "^2.1.2",
|
"@visactor/react-vchart": "^2.1.2",
|
||||||
"@visactor/vchart": "^2.1.2",
|
"@visactor/vchart": "^2.1.2",
|
||||||
"ai": "^7.0.14",
|
"ai": "^7.0.14",
|
||||||
@@ -91,7 +92,6 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "catalog:",
|
"clsx": "catalog:",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^4.4.0",
|
|
||||||
"dayjs": "catalog:",
|
"dayjs": "catalog:",
|
||||||
"dompurify": "3.4.11",
|
"dompurify": "3.4.11",
|
||||||
"i18next": "^26.3.4",
|
"i18next": "^26.3.4",
|
||||||
@@ -102,6 +102,7 @@
|
|||||||
"marked": "^18.0.5",
|
"marked": "^18.0.5",
|
||||||
"motion": "^12.42.2",
|
"motion": "^12.42.2",
|
||||||
"nanoid": "^5.1.16",
|
"nanoid": "^5.1.16",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
"qrcode.react": "catalog:",
|
"qrcode.react": "catalog:",
|
||||||
"react": "catalog:",
|
"react": "catalog:",
|
||||||
"react-day-picker": "^10.0.1",
|
"react-day-picker": "^10.0.1",
|
||||||
@@ -111,7 +112,7 @@
|
|||||||
"react-icons": "catalog:",
|
"react-icons": "catalog:",
|
||||||
"react-resizable-panels": "^4.12.0",
|
"react-resizable-panels": "^4.12.0",
|
||||||
"react-top-loading-bar": "^3.0.2",
|
"react-top-loading-bar": "^3.0.2",
|
||||||
"recharts": "3.8.0",
|
"recharts": "3.9.1",
|
||||||
"shiki": "^4.3.0",
|
"shiki": "^4.3.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"sse.js": "catalog:",
|
"sse.js": "catalog:",
|
||||||
@@ -121,6 +122,7 @@
|
|||||||
"tokenlens": "^1.3.1",
|
"tokenlens": "^1.3.1",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"use-stick-to-bottom": "^1.1.6",
|
"use-stick-to-bottom": "^1.1.6",
|
||||||
|
"vaul": "^1.1.2",
|
||||||
"zod": "^4.4.3",
|
"zod": "^4.4.3",
|
||||||
"zustand": "^5.0.14",
|
"zustand": "^5.0.14",
|
||||||
},
|
},
|
||||||
@@ -956,6 +958,8 @@
|
|||||||
|
|
||||||
"@tanstack/react-table": ["@tanstack/react-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww=="],
|
"@tanstack/react-table": ["@tanstack/react-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww=="],
|
||||||
|
|
||||||
|
"@tanstack/react-virtual": ["@tanstack/react-virtual@3.14.5", "", { "dependencies": { "@tanstack/virtual-core": "3.17.3" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-4EKRXh7zBLkbKbFmG3AUVkircuHd+7OdT1pocJSepxtfBd3qnrJgJ5rtPkRYyo9fmyVb2+pI2xPy5oYvMLQy6A=="],
|
||||||
|
|
||||||
"@tanstack/router-core": ["@tanstack/router-core@1.171.8", "", { "dependencies": { "@tanstack/history": "1.162.0", "cookie-es": "^3.0.0", "seroval": "^1.5.4", "seroval-plugins": "^1.5.4" } }, "sha512-PbrTBbofFcacrH3RLgHYILRqTFnAGq+gXrXoA/vo7qUSkJpSO4GWfLtrtCahD4VayzRm19IPwcjPPLEugag6pw=="],
|
"@tanstack/router-core": ["@tanstack/router-core@1.171.8", "", { "dependencies": { "@tanstack/history": "1.162.0", "cookie-es": "^3.0.0", "seroval": "^1.5.4", "seroval-plugins": "^1.5.4" } }, "sha512-PbrTBbofFcacrH3RLgHYILRqTFnAGq+gXrXoA/vo7qUSkJpSO4GWfLtrtCahD4VayzRm19IPwcjPPLEugag6pw=="],
|
||||||
|
|
||||||
"@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.168.0", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16" }, "peerDependencies": { "@tanstack/router-core": "^1.170.0", "csstype": "^3.0.10" }, "optionalPeers": ["csstype"] }, "sha512-wQoQhlBK7nlZgqzaqdYXKWNTpdHdsaREdaPhFZVH0/Ador+F+eM3/NF2i3f2LPeS0GgKraZUQXe1Q/1+KHyEYg=="],
|
"@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.168.0", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16" }, "peerDependencies": { "@tanstack/router-core": "^1.170.0", "csstype": "^3.0.10" }, "optionalPeers": ["csstype"] }, "sha512-wQoQhlBK7nlZgqzaqdYXKWNTpdHdsaREdaPhFZVH0/Ador+F+eM3/NF2i3f2LPeS0GgKraZUQXe1Q/1+KHyEYg=="],
|
||||||
@@ -970,6 +974,8 @@
|
|||||||
|
|
||||||
"@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="],
|
"@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="],
|
||||||
|
|
||||||
|
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.17.3", "", {}, "sha512-8Np/TFELpI0ySuJoVmjvOrQYXH/8sTX0Biv9szhFhY39xOdAAY+smrMxjxOum/ux3eM8MUJQsEJ0/R0UpvC8dw=="],
|
||||||
|
|
||||||
"@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.162.0", "", {}, "sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA=="],
|
"@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.162.0", "", {}, "sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA=="],
|
||||||
|
|
||||||
"@tiptap/core": ["@tiptap/core@3.24.0", "", { "peerDependencies": { "@tiptap/pm": "3.24.0" } }, "sha512-GTAsXAI32p4hEZgPzvUv2RPrObxamy9AFhmhG10fXSvN/cDUs8naEYVIqDV3Sh99jMwQEbTFKW1E1mcspsY6ow=="],
|
"@tiptap/core": ["@tiptap/core@3.24.0", "", { "peerDependencies": { "@tiptap/pm": "3.24.0" } }, "sha512-GTAsXAI32p4hEZgPzvUv2RPrObxamy9AFhmhG10fXSvN/cDUs8naEYVIqDV3Sh99jMwQEbTFKW1E1mcspsY6ow=="],
|
||||||
@@ -1854,7 +1860,7 @@
|
|||||||
|
|
||||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||||
|
|
||||||
"immer": ["immer@10.2.0", "", {}, "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw=="],
|
"immer": ["immer@11.1.8", "", {}, "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA=="],
|
||||||
|
|
||||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||||
|
|
||||||
@@ -2242,6 +2248,8 @@
|
|||||||
|
|
||||||
"newapi-web": ["newapi-web@workspace:default"],
|
"newapi-web": ["newapi-web@workspace:default"],
|
||||||
|
|
||||||
|
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
|
||||||
|
|
||||||
"node-releases": ["node-releases@2.0.46", "", {}, "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ=="],
|
"node-releases": ["node-releases@2.0.46", "", {}, "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ=="],
|
||||||
|
|
||||||
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||||
@@ -2530,7 +2538,7 @@
|
|||||||
|
|
||||||
"recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="],
|
"recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="],
|
||||||
|
|
||||||
"recharts": ["recharts@3.8.0", "", { "dependencies": { "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ=="],
|
"recharts": ["recharts@3.9.1", "", { "dependencies": { "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^11.1.8", "react-redux": "8.x.x || 9.x.x", "reselect": "5.2.0", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-WMcwlXcB7l+BbxiEdyClkG+1sxrMHNZpzT577LEvU4+rXPd8oTAy1wXk72hnk2KOOmxuLvw3z5DtXT7HEAydtg=="],
|
||||||
|
|
||||||
"recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="],
|
"recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="],
|
||||||
|
|
||||||
@@ -2582,7 +2590,7 @@
|
|||||||
|
|
||||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||||
|
|
||||||
"reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="],
|
"reselect": ["reselect@5.2.0", "", {}, "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw=="],
|
||||||
|
|
||||||
"resize-observer-polyfill": ["resize-observer-polyfill@1.5.1", "", {}, "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="],
|
"resize-observer-polyfill": ["resize-observer-polyfill@1.5.1", "", {}, "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="],
|
||||||
|
|
||||||
@@ -2870,6 +2878,8 @@
|
|||||||
|
|
||||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||||
|
|
||||||
|
"vaul": ["vaul@1.1.2", "", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA=="],
|
||||||
|
|
||||||
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
||||||
|
|
||||||
"vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="],
|
"vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="],
|
||||||
@@ -2916,8 +2926,6 @@
|
|||||||
|
|
||||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||||
|
|
||||||
"@base-ui/utils/reselect": ["reselect@5.2.0", "", {}, "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw=="],
|
|
||||||
|
|
||||||
"@codemirror/autocomplete/@codemirror/view": ["@codemirror/view@6.43.3", "", { "dependencies": { "@codemirror/state": "^6.7.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-MwEwCAr/o0agJefhC2+reBv5kfOQpMcDRUNQrRYZgWlhH8IwQcerMZrpqWyUFSyO0ebgN2cnh/w87F7G4BGSng=="],
|
"@codemirror/autocomplete/@codemirror/view": ["@codemirror/view@6.43.3", "", { "dependencies": { "@codemirror/state": "^6.7.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-MwEwCAr/o0agJefhC2+reBv5kfOQpMcDRUNQrRYZgWlhH8IwQcerMZrpqWyUFSyO0ebgN2cnh/w87F7G4BGSng=="],
|
||||||
|
|
||||||
"@codemirror/lang-html/@codemirror/view": ["@codemirror/view@6.43.3", "", { "dependencies": { "@codemirror/state": "^6.7.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-MwEwCAr/o0agJefhC2+reBv5kfOQpMcDRUNQrRYZgWlhH8IwQcerMZrpqWyUFSyO0ebgN2cnh/w87F7G4BGSng=="],
|
"@codemirror/lang-html/@codemirror/view": ["@codemirror/view@6.43.3", "", { "dependencies": { "@codemirror/state": "^6.7.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-MwEwCAr/o0agJefhC2+reBv5kfOQpMcDRUNQrRYZgWlhH8IwQcerMZrpqWyUFSyO0ebgN2cnh/w87F7G4BGSng=="],
|
||||||
@@ -2966,8 +2974,6 @@
|
|||||||
|
|
||||||
"@lobehub/ui/@shikijs/core": ["@shikijs/core@4.1.0", "", { "dependencies": { "@shikijs/primitive": "4.1.0", "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-jLJtSJeuFffqX6/inRE1zqU5aFv2hrszvYgq3OjbAgFRZiWv7abKMDdQzYxuSDfmUPQozZvI/kuy6VMTvnvqTQ=="],
|
"@lobehub/ui/@shikijs/core": ["@shikijs/core@4.1.0", "", { "dependencies": { "@shikijs/primitive": "4.1.0", "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-jLJtSJeuFffqX6/inRE1zqU5aFv2hrszvYgq3OjbAgFRZiWv7abKMDdQzYxuSDfmUPQozZvI/kuy6VMTvnvqTQ=="],
|
||||||
|
|
||||||
"@lobehub/ui/immer": ["immer@11.1.8", "", {}, "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA=="],
|
|
||||||
|
|
||||||
"@lobehub/ui/katex": ["katex@0.16.47", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg=="],
|
"@lobehub/ui/katex": ["katex@0.16.47", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg=="],
|
||||||
|
|
||||||
"@lobehub/ui/lucide-react": ["lucide-react@1.17.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w=="],
|
"@lobehub/ui/lucide-react": ["lucide-react@1.17.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w=="],
|
||||||
@@ -3010,7 +3016,7 @@
|
|||||||
|
|
||||||
"@rc-component/trigger/@rc-component/portal": ["@rc-component/portal@2.2.0", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="],
|
"@rc-component/trigger/@rc-component/portal": ["@rc-component/portal@2.2.0", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="],
|
||||||
|
|
||||||
"@reduxjs/toolkit/immer": ["immer@11.1.8", "", {}, "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA=="],
|
"@reduxjs/toolkit/reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="],
|
||||||
|
|
||||||
"@rspack/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="],
|
"@rspack/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="],
|
||||||
|
|
||||||
@@ -3456,6 +3462,8 @@
|
|||||||
|
|
||||||
"type-is/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
"type-is/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||||
|
|
||||||
|
"@lobehub/ui/@base-ui/react/@base-ui/utils/reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="],
|
||||||
|
|
||||||
"@lobehub/ui/motion/framer-motion/motion-dom": ["motion-dom@12.40.0", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg=="],
|
"@lobehub/ui/motion/framer-motion/motion-dom": ["motion-dom@12.40.0", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg=="],
|
||||||
|
|
||||||
"@ts-morph/common/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
"@ts-morph/common/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ const ChannelSelectorModal = forwardRef(
|
|||||||
) => {
|
) => {
|
||||||
const [searchText, setSearchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(20);
|
const [pageSize, setPageSize] = useState(10);
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
const [filteredData, setFilteredData] = useState([]);
|
const [filteredData, setFilteredData] = useState([]);
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ const MultiKeyManageModal = ({ visible, onCancel, channel, onRefresh }) => {
|
|||||||
|
|
||||||
// Pagination states
|
// Pagination states
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(20);
|
const [pageSize, setPageSize] = useState(10);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const [totalPages, setTotalPages] = useState(0);
|
const [totalPages, setTotalPages] = useState(0);
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ const MultiKeyManageModal = ({ visible, onCancel, channel, onRefresh }) => {
|
|||||||
setKeyStatusList(data.keys || []);
|
setKeyStatusList(data.keys || []);
|
||||||
setTotal(data.total || 0);
|
setTotal(data.total || 0);
|
||||||
setCurrentPage(data.page || 1);
|
setCurrentPage(data.page || 1);
|
||||||
setPageSize(data.page_size || 20);
|
setPageSize(data.page_size || 10);
|
||||||
setTotalPages(data.total_pages || 0);
|
setTotalPages(data.total_pages || 0);
|
||||||
|
|
||||||
// Update statistics (these are always the overall statistics)
|
// Update statistics (these are always the overall statistics)
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ const UserSubscriptionsModal = ({ visible, onCancel, user, t, onSuccess }) => {
|
|||||||
|
|
||||||
const [subs, setSubs] = useState([]);
|
const [subs, setSubs] = useState([]);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const pageSize = 20;
|
const pageSize = 10;
|
||||||
|
|
||||||
const planTitleMap = useMemo(() => {
|
const planTitleMap = useMemo(() => {
|
||||||
const map = new Map();
|
const map = new Map();
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => {
|
|||||||
const [topups, setTopups] = useState([]);
|
const [topups, setTopups] = useState([]);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(20);
|
const [pageSize, setPageSize] = useState(10);
|
||||||
const [keyword, setKeyword] = useState('');
|
const [keyword, setKeyword] = useState('');
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -196,4 +196,4 @@ export const MODEL_FETCHABLE_CHANNEL_TYPES = new Set([
|
|||||||
1, 4, 14, 34, 17, 26, 27, 24, 47, 25, 20, 23, 31, 40, 42, 48, 43,
|
1, 4, 14, 34, 17, 26, 27, 24, 47, 25, 20, 23, 31, 40, 42, 48, 43,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const MODEL_TABLE_PAGE_SIZE = 20;
|
export const MODEL_TABLE_PAGE_SIZE = 10;
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
For commercial licensing, please contact support@quantumnous.com
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const ITEMS_PER_PAGE = 20; // this value must keep same as the one defined in backend!
|
export const ITEMS_PER_PAGE = 10; // this value must keep same as the one defined in backend!
|
||||||
|
|
||||||
export const DEFAULT_ENDPOINT = '/api/pricing';
|
export const DEFAULT_ENDPOINT = '/api/pricing';
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -144,6 +144,6 @@ export const STORAGE_KEYS = {
|
|||||||
export const DEFAULTS = {
|
export const DEFAULTS = {
|
||||||
PAGE_SIZE: 20,
|
PAGE_SIZE: 20,
|
||||||
CHART_HEIGHT: 96,
|
CHART_HEIGHT: 96,
|
||||||
MODEL_TABLE_PAGE_SIZE: 20,
|
MODEL_TABLE_PAGE_SIZE: 10,
|
||||||
MAX_TREND_POINTS: 7,
|
MAX_TREND_POINTS: 7,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export const useSubscriptionsData = () => {
|
|||||||
|
|
||||||
// Pagination (client-side for now)
|
// Pagination (client-side for now)
|
||||||
const [activePage, setActivePage] = useState(1);
|
const [activePage, setActivePage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(20);
|
const [pageSize, setPageSize] = useState(10);
|
||||||
|
|
||||||
// Drawer states
|
// Drawer states
|
||||||
const [showEdit, setShowEdit] = useState(false);
|
const [showEdit, setShowEdit] = useState(false);
|
||||||
|
|||||||
Vendored
-16
@@ -1021,22 +1021,6 @@ html.dark .with-pastel-balls::before {
|
|||||||
padding: 10px !important;
|
padding: 10px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Keep badge text complete and on one line. */
|
|
||||||
.semi-tag {
|
|
||||||
flex-shrink: 0;
|
|
||||||
min-width: max-content;
|
|
||||||
max-width: none;
|
|
||||||
overflow: visible;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.semi-tag-content {
|
|
||||||
max-width: none;
|
|
||||||
overflow: visible;
|
|
||||||
text-overflow: clip;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ==================== 使用日志: channel affinity tag ==================== */
|
/* ==================== 使用日志: channel affinity tag ==================== */
|
||||||
.semi-tag.channel-affinity-tag {
|
.semi-tag.channel-affinity-tag {
|
||||||
border: 1px solid rgba(var(--semi-cyan-5), 0.35);
|
border: 1px solid rgba(var(--semi-cyan-5), 0.35);
|
||||||
|
|||||||
+1
-1
@@ -469,7 +469,7 @@ export default function SettingsChats(props) {
|
|||||||
dataSource={filteredConfigs}
|
dataSource={filteredConfigs}
|
||||||
rowKey='id'
|
rowKey='id'
|
||||||
pagination={{
|
pagination={{
|
||||||
pageSize: 20,
|
pageSize: 10,
|
||||||
showSizeChanger: false,
|
showSizeChanger: false,
|
||||||
showQuickJumper: true,
|
showQuickJumper: true,
|
||||||
showTotal: (total, range) =>
|
showTotal: (total, range) =>
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ const SettingsAPIInfo = ({ options, refresh }) => {
|
|||||||
color: 'blue',
|
color: 'blue',
|
||||||
});
|
});
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(20);
|
const [pageSize, setPageSize] = useState(10);
|
||||||
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
||||||
|
|
||||||
// 面板启用状态 state
|
// 面板启用状态 state
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ const SettingsAnnouncements = ({ options, refresh }) => {
|
|||||||
extra: '',
|
extra: '',
|
||||||
});
|
});
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(20);
|
const [pageSize, setPageSize] = useState(10);
|
||||||
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
||||||
|
|
||||||
// 面板启用状态
|
// 面板启用状态
|
||||||
|
|||||||
+1
-1
@@ -56,7 +56,7 @@ const SettingsFAQ = ({ options, refresh }) => {
|
|||||||
answer: '',
|
answer: '',
|
||||||
});
|
});
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(20);
|
const [pageSize, setPageSize] = useState(10);
|
||||||
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
||||||
|
|
||||||
// 面板启用状态
|
// 面板启用状态
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ const SettingsUptimeKuma = ({ options, refresh }) => {
|
|||||||
slug: '',
|
slug: '',
|
||||||
});
|
});
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(20);
|
const [pageSize, setPageSize] = useState(10);
|
||||||
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
||||||
const [panelEnabled, setPanelEnabled] = useState(true);
|
const [panelEnabled, setPanelEnabled] = useState(true);
|
||||||
|
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ export default function UpstreamRatioSync(props) {
|
|||||||
|
|
||||||
// 分页相关状态
|
// 分页相关状态
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(20);
|
const [pageSize, setPageSize] = useState(10);
|
||||||
|
|
||||||
// 搜索相关状态
|
// 搜索相关状态
|
||||||
const [searchKeyword, setSearchKeyword] = useState('');
|
const [searchKeyword, setSearchKeyword] = useState('');
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
splitBillingExprAndRequestRules,
|
splitBillingExprAndRequestRules,
|
||||||
} from '../components/requestRuleExpr';
|
} from '../components/requestRuleExpr';
|
||||||
|
|
||||||
export const PAGE_SIZE = 20;
|
export const PAGE_SIZE = 10;
|
||||||
export const PRICE_SUFFIX = '$/1M tokens';
|
export const PRICE_SUFFIX = '$/1M tokens';
|
||||||
const EMPTY_CANDIDATE_MODEL_NAMES = [];
|
const EMPTY_CANDIDATE_MODEL_NAMES = [];
|
||||||
|
|
||||||
|
|||||||
Vendored
-40
@@ -44,33 +44,6 @@
|
|||||||
"no-duplicate-imports": "error",
|
"no-duplicate-imports": "error",
|
||||||
"no-nested-ternary": "error",
|
"no-nested-ternary": "error",
|
||||||
"no-new-wrappers": "error",
|
"no-new-wrappers": "error",
|
||||||
"no-restricted-imports": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"patterns": [
|
|
||||||
{
|
|
||||||
"group": [
|
|
||||||
"@/components/ui/alert-dialog",
|
|
||||||
"@/components/ui/button",
|
|
||||||
"@/components/ui/combobox",
|
|
||||||
"@/components/ui/combobox-input",
|
|
||||||
"@/components/ui/command",
|
|
||||||
"@/components/ui/dialog",
|
|
||||||
"@/components/ui/input",
|
|
||||||
"@/components/ui/input-group",
|
|
||||||
"@/components/ui/pagination",
|
|
||||||
"@/components/ui/select",
|
|
||||||
"@/components/ui/sidebar",
|
|
||||||
"@/components/ui/table",
|
|
||||||
"@/components/ui/tabs",
|
|
||||||
"@/components/ui/toggle",
|
|
||||||
"@/components/ui/toggle-group"
|
|
||||||
],
|
|
||||||
"message": "Import policy-bearing controls from @/components/design-system so responsive sizing stays centralized."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-regex-spaces": "error",
|
"no-regex-spaces": "error",
|
||||||
"no-unsafe-finally": "warn",
|
"no-unsafe-finally": "warn",
|
||||||
"no-unsafe-negation": "error",
|
"no-unsafe-negation": "error",
|
||||||
@@ -223,19 +196,6 @@
|
|||||||
"react/only-export-components": "off"
|
"react/only-export-components": "off"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"files": ["src/components/design-system/**/*.{ts,tsx}"],
|
|
||||||
"rules": {
|
|
||||||
"no-restricted-imports": "off",
|
|
||||||
"react/only-export-components": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"allowConstantExport": true,
|
|
||||||
"allowExportNames": ["useComboboxAnchor", "useSidebar"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"files": ["**/scripts/**/*.{js,mjs,cjs}"],
|
"files": ["**/scripts/**/*.{js,mjs,cjs}"],
|
||||||
"rules": {
|
"rules": {
|
||||||
|
|||||||
Vendored
+1
-53
@@ -125,58 +125,10 @@
|
|||||||
- 以 Tailwind 工具类为主,动态类名用 `cn()` 合并;非动态场景避免内联样式。
|
- 以 Tailwind 工具类为主,动态类名用 `cn()` 合并;非动态场景避免内联样式。
|
||||||
- 响应式采用移动优先与 Tailwind 断点(`sm:`、`md:`、`lg:` 等);主题与暗色用 CSS 变量与 `dark:`,自定义样式集中在 `src/styles/`,组件内尽量少写自定义 CSS。
|
- 响应式采用移动优先与 Tailwind 断点(`sm:`、`md:`、`lg:` 等);主题与暗色用 CSS 变量与 `dark:`,自定义样式集中在 `src/styles/`,组件内尽量少写自定义 CSS。
|
||||||
|
|
||||||
#### 3.10.1 设计系统纪律(强约束)
|
|
||||||
|
|
||||||
以下规则为硬约束,代码评审与 AI 改动一律遵循;违反即为缺陷。
|
|
||||||
|
|
||||||
**颜色**
|
|
||||||
|
|
||||||
- 后台界面(除 `features/home/` 营销页外)**禁止硬编码 Tailwind 调色板类**(如 `text-emerald-600`、`bg-amber-50`、`border-rose-200`),一律使用语义令牌:`success` / `warning` / `destructive` / `info` / `neutral`、`primary` / `muted` / `accent` / `border` / `ring`。暗色适配由令牌完成,禁止 `dark:text-emerald-400` 之类逐处覆写。
|
|
||||||
- 状态色仅表达状态(成功/警告/错误/信息),不得用于装饰或分类;分类信息(模型名、分组名、渠道名等)一律中性呈现,身份靠文本与图标传达。`StatusBadge` 的 `autoColor` 已废弃为 neutral,禁止恢复字符串哈希取色。
|
|
||||||
- 需要浅底状态胶囊时使用 `status-badge.tsx` 的 `tintedBadgeClassMap`,不要再手写 `bg-xxx-50 text-xxx-700 dark:...` 组合。
|
|
||||||
- 图表用 `--chart-1..5`;用户头像身份色走 `getAvatarColorClass`。这两处是仅有的多色场景。
|
|
||||||
- 禁止用 `!important`(`!text-*` 等)压制文字颜色/字号;若需要覆盖,说明层级设计有误,先修组件。
|
|
||||||
|
|
||||||
**排版**
|
|
||||||
|
|
||||||
- 字体族只有两轨:正文 `--font-sans`(含 CJK 回退,勿删栈中中文字体)与 `--font-serif`(serif 主题轴)。`font-mono` 仅用于密钥、ID、代码、原始 JSON;**数字对齐一律 `tabular-nums`,不得再加 `font-mono`**。
|
|
||||||
- 字号只用 `text-xs/sm/base/lg/xl/2xl`,**禁止任意值字号**(`text-[10px]`、`text-[11px]` 等;营销页除外)。
|
|
||||||
- 字重只用 `font-medium`(强调/表头)与 `font-semibold`(标题/关键数值);后台正文与标题**禁用 `font-bold`**。
|
|
||||||
- 标题三档契约:页题 `text-lg font-semibold tracking-tight`;卡片/区块题 `text-base font-semibold`;面板/小节题 `text-sm font-medium`。禁止响应式跳字号的标题(如 `text-base sm:text-lg`)。
|
|
||||||
- 表头不使用 `uppercase + tracking-wider`。
|
|
||||||
|
|
||||||
**控件尺寸(移动优先响应式)**
|
|
||||||
|
|
||||||
- `src/components/ui/` 是 shadcn CLI 管理的原始源码层:允许通过 `bunx --bun shadcn@latest add <component> --dry-run/--diff` 审查并合并上游更新,但**禁止**在这里加入产品级响应式策略、业务语义或一次性样式。不得用 `--overwrite` 覆盖本地代码,除非用户明确批准。
|
|
||||||
- `src/components/design-system/` 是产品设计系统适配层,只包装确实承载稳定跨站策略的控件(尺寸、产品级 variant、复合控件内部的策略传递);不要为了“统一入口”代理所有 shadcn 组件。业务代码对受管控件必须从该目录导入,`.oxlintrc.json` 的 `no-restricted-imports` 会阻止绕过边界。
|
|
||||||
- 控件标准高度是响应式的:**手机(<640px)28px,桌面(`sm:` 起)32px**。断点由 `design-system` 适配层统一注入(`Button`/`Toggle` default 与 icon 档、`Input`、`InputGroup`、`SelectTrigger` default 档、`TabsList`、`Combobox`、`CommandInput`、分页、表头 `h-9 sm:h-10`),业务代码**不写**断点类即可获得正确尺寸。
|
|
||||||
- 独立场景(工具栏、表单、对话框、卡片/列表行操作、页脚)一律用默认档,**禁止** `size='sm'`,也**禁止**用 `className` 写 `h-7`/`h-8`/`h-9`/`size-8` 等钉死尺寸——钉死的类会在某一断点上与内置的 `sm:h-8`/`sm:size-8` 打架(twMerge 不会跨断点去重)。
|
|
||||||
- 密集场景(表格单元格内、行内编辑器一行多控件、紧凑抽屉行)统一 `size='sm'`(28px 恒定);微型场景用 `xs`(24px 恒定)。`sm`/`xs`/`icon-sm`/`icon-xs` 是**固定档**,不随断点缩放;需要"固定自定义尺寸"的特例(如覆盖层小按钮)应以固定档为基底再覆写,绝不以 default/icon 为基底。
|
|
||||||
- 图标按钮用 `icon`(28→32px 响应式)/`icon-sm`(28px 恒定)/`icon-xs`(24px 恒定)并带 `aria-label`;禁止 `size-*`、`h-* w-*`、`p-0` 组合模拟。头部全局操作(侧栏开关/主题/语言/通知/头像)统一 `icon`。
|
|
||||||
- CTA 档 `xl`(40→44px 响应式):auth 页主按钮(登录/注册/OTP/重置/OAuth/Passkey)与营销页 hero 按钮统一 `size='xl'`,禁止再写 `h-11`。
|
|
||||||
- 确需自定义高度时必须**成对覆盖两个断点**:内容自适应写 `h-auto sm:h-auto`,特大输入写 `h-9 sm:h-10` 这类成对值;`min-h-*` 不与内置高度冲突可单独使用。flex-wrap 的 `TabsList` 用 `group-data-horizontal/tabs:h-auto sm:group-data-horizontal/tabs:h-auto`。
|
|
||||||
- 手机端的“视觉紧凑”不得牺牲可操作性:所有指针目标至少满足 WCAG 2.2 AA 的 24×24 CSS px;相邻 28px 控件必须保留足够间距,主要提交/认证/高频操作优先使用 40px 的 `xl` 档。不要在手机端新增小于 `xs` 的交互目标。
|
|
||||||
- 仅有例外:日历格(`--cell-size`)、OTP 输入格、营销页装饰元素。新增例外须在评审说明。
|
|
||||||
|
|
||||||
**圆角与阴影**
|
|
||||||
|
|
||||||
- 圆角单源 `--radius`(默认 0.625rem),组件用派生档 `rounded-md/lg/xl`;禁止在业务代码里写死圆角像素或混用 `rounded-2xl`/`rounded-4xl` 表达同类容器。
|
|
||||||
- 卡片边界「描边或投影二选一」:默认 `border`(或 Card 自带 ring),禁止再叠 `shadow-*`;`shadow` 只保留给浮层(popover/dropdown/dialog)。
|
|
||||||
|
|
||||||
**动效**
|
|
||||||
|
|
||||||
- 后台界面禁止入场动画:无 stagger、无 translate/scale/blur 入场、无按钮按压缩放。页面切换只允许纯透明度 fade(见 `lib/motion.ts` 的 `pageEnter`)。`page-transition.tsx` 的 Stagger 系列组件已固化为纯容器,禁止恢复动画。
|
|
||||||
- 允许的动效上限:颜色/透明度过渡 ≤150ms、骨架屏 shimmer、`Collapsible/Accordion` 展开、加载 spinner。装饰性动效只允许出现在 `features/home/`(landing)。
|
|
||||||
|
|
||||||
**徽章与图标**
|
|
||||||
|
|
||||||
- 文本徽章统一 `StatusBadge`(五种语义 voice);模型/分组/渠道等实体徽章统一中性底(`border-border/60 bg-muted/30`)。不要新造第三种徽章样式。
|
|
||||||
- 业务代码图标一律 `lucide-react`;`components/ui/` 由 shadcn 生成器维护(Hugeicons),不要手改基础组件图标库;AI 品牌图标用 `@lobehub/icons`(经 `getLobeIcon`)。
|
|
||||||
|
|
||||||
### 3.11 文件组织
|
### 3.11 文件组织
|
||||||
|
|
||||||
- **功能模块**:置于 `src/features/<feature>/`,内含 `components/`、`lib/`、`hooks/`,以及按需的 `api.ts`、`types.ts`、`constants.ts`、入口组件等。
|
- **功能模块**:置于 `src/features/<feature>/`,内含 `components/`、`lib/`、`hooks/`,以及按需的 `api.ts`、`types.ts`、`constants.ts`、入口组件等。
|
||||||
- **通用**:通用组件放 `src/components/`,其中 `components/ui/` 仅存 shadcn 原始源码、`components/design-system/` 存产品策略适配器;通用工具与类型放 `src/lib/`。组件文件 PascalCase,工具/类型文件 kebab-case 或 `types.ts`,类型使用 PascalCase 命名并 `export type`。
|
- **通用**:通用组件放 `src/components/`,通用工具与类型放 `src/lib/`;组件文件 PascalCase,工具/类型文件 kebab-case 或 `types.ts`,类型使用 PascalCase 命名并 `export type`。
|
||||||
|
|
||||||
### 3.12 可访问性
|
### 3.12 可访问性
|
||||||
|
|
||||||
@@ -226,7 +178,3 @@
|
|||||||
- **2026-01-29**:重组文档结构,合并重复内容,明确主次与交叉引用。
|
- **2026-01-29**:重组文档结构,合并重复内容,明确主次与交叉引用。
|
||||||
- **2026-01-31**:在 3.2 中补充「类型检查」要求:改动 TS/TSX 后须执行 typecheck 并修复至无错。
|
- **2026-01-31**:在 3.2 中补充「类型检查」要求:改动 TS/TSX 后须执行 typecheck 并修复至无错。
|
||||||
- **2026-06-21**:在 3.2 中补充「Lint 检查」要求:完成代码改动前须修复所涉及文件的所有 lint error。
|
- **2026-06-21**:在 3.2 中补充「Lint 检查」要求:完成代码改动前须修复所涉及文件的所有 lint error。
|
||||||
- **2026-07-11**:新增 3.10.1「设计系统纪律」:语义色硬约束(去彩虹徽章/硬编码调色板)、排版契约(字号/字重/标题三档/mono 边界)、圆角单源、后台零入场动效、徽章与图标单轨。
|
|
||||||
- **2026-07-11**:3.10.1 新增「控件尺寸」:控件默认 32px(`h-8`)单一标准;独立场景禁用 `size='sm'` 与 `className` 高度硬改;密集行内统一 `sm`、微型 `xs`;图标按钮走 `icon/icon-sm/icon-xs` 档位。
|
|
||||||
- **2026-07-11**:控件尺寸升级为移动优先响应式:设计系统统一手机 28px / 桌面 32px(default、icon 档随断点缩放,`sm`/`xs`/`icon-sm`/`icon-xs` 恒定);新增 CTA 档 `xl`(40→44px),auth 与 hero 主按钮统一迁入;调用点禁止钉死 `h-*`/`size-*`,自定义高度必须成对覆盖两个断点(如 `h-auto sm:h-auto`)。
|
|
||||||
- **2026-07-11**:将响应式尺寸策略从 shadcn 原始源码迁至 `components/design-system/` 适配层;业务受管控件统一改从适配层导入,并由 lint 禁止绕过;`components/ui/` 恢复为可用 CLI `--diff` 维护的上游源码边界。
|
|
||||||
|
|||||||
Vendored
+1
-1
@@ -18,7 +18,7 @@
|
|||||||
"lib": "@/lib",
|
"lib": "@/lib",
|
||||||
"hooks": "@/hooks"
|
"hooks": "@/hooks"
|
||||||
},
|
},
|
||||||
"menuColor": "default",
|
"menuColor": "inverted",
|
||||||
"menuAccent": "subtle",
|
"menuAccent": "subtle",
|
||||||
"registries": {
|
"registries": {
|
||||||
"@ai-elements": "https://registry.ai-sdk.dev/{name}.json"
|
"@ai-elements": "https://registry.ai-sdk.dev/{name}.json"
|
||||||
|
|||||||
Vendored
+4
-2
@@ -34,6 +34,7 @@
|
|||||||
"@tanstack/react-query": "^5.101.2",
|
"@tanstack/react-query": "^5.101.2",
|
||||||
"@tanstack/react-router": "^1.170.17",
|
"@tanstack/react-router": "^1.170.17",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
|
"@tanstack/react-virtual": "^3.14.5",
|
||||||
"@visactor/react-vchart": "^2.1.2",
|
"@visactor/react-vchart": "^2.1.2",
|
||||||
"@visactor/vchart": "^2.1.2",
|
"@visactor/vchart": "^2.1.2",
|
||||||
"ai": "^7.0.14",
|
"ai": "^7.0.14",
|
||||||
@@ -42,7 +43,6 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "catalog:",
|
"clsx": "catalog:",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^4.4.0",
|
|
||||||
"dayjs": "catalog:",
|
"dayjs": "catalog:",
|
||||||
"dompurify": "3.4.11",
|
"dompurify": "3.4.11",
|
||||||
"i18next": "^26.3.4",
|
"i18next": "^26.3.4",
|
||||||
@@ -53,6 +53,7 @@
|
|||||||
"marked": "^18.0.5",
|
"marked": "^18.0.5",
|
||||||
"motion": "^12.42.2",
|
"motion": "^12.42.2",
|
||||||
"nanoid": "^5.1.16",
|
"nanoid": "^5.1.16",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
"qrcode.react": "catalog:",
|
"qrcode.react": "catalog:",
|
||||||
"react": "catalog:",
|
"react": "catalog:",
|
||||||
"react-day-picker": "^10.0.1",
|
"react-day-picker": "^10.0.1",
|
||||||
@@ -62,7 +63,7 @@
|
|||||||
"react-icons": "catalog:",
|
"react-icons": "catalog:",
|
||||||
"react-resizable-panels": "^4.12.0",
|
"react-resizable-panels": "^4.12.0",
|
||||||
"react-top-loading-bar": "^3.0.2",
|
"react-top-loading-bar": "^3.0.2",
|
||||||
"recharts": "3.8.0",
|
"recharts": "3.9.1",
|
||||||
"shiki": "^4.3.0",
|
"shiki": "^4.3.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"sse.js": "catalog:",
|
"sse.js": "catalog:",
|
||||||
@@ -72,6 +73,7 @@
|
|||||||
"tokenlens": "^1.3.1",
|
"tokenlens": "^1.3.1",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"use-stick-to-bottom": "^1.1.6",
|
"use-stick-to-bottom": "^1.1.6",
|
||||||
|
"vaul": "^1.1.2",
|
||||||
"zod": "^4.4.3",
|
"zod": "^4.4.3",
|
||||||
"zustand": "^5.0.14"
|
"zustand": "^5.0.14"
|
||||||
},
|
},
|
||||||
|
|||||||
+3
-3
@@ -20,7 +20,7 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
|
|
||||||
import type { ComponentProps } from 'react'
|
import type { ComponentProps } from 'react'
|
||||||
|
|
||||||
import { Button } from '@/components/design-system/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@@ -48,13 +48,13 @@ export const Action = ({
|
|||||||
label,
|
label,
|
||||||
className,
|
className,
|
||||||
variant = 'ghost',
|
variant = 'ghost',
|
||||||
size = 'icon',
|
size = 'sm',
|
||||||
...props
|
...props
|
||||||
}: ActionProps) => {
|
}: ActionProps) => {
|
||||||
const button = (
|
const button = (
|
||||||
<Button
|
<Button
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-muted-foreground hover:text-foreground relative',
|
'text-muted-foreground hover:text-foreground relative size-9 p-1.5',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
size={size}
|
size={size}
|
||||||
|
|||||||
+11
-5
@@ -22,7 +22,7 @@ import { type LucideIcon, XIcon } from 'lucide-react'
|
|||||||
import type { ComponentProps, HTMLAttributes } from 'react'
|
import type { ComponentProps, HTMLAttributes } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { Button } from '@/components/design-system/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@@ -63,14 +63,17 @@ export type ArtifactCloseProps = ComponentProps<typeof Button>
|
|||||||
export const ArtifactClose = ({
|
export const ArtifactClose = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
size = 'icon',
|
size = 'sm',
|
||||||
variant = 'ghost',
|
variant = 'ghost',
|
||||||
...props
|
...props
|
||||||
}: ArtifactCloseProps) => {
|
}: ArtifactCloseProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
className={cn('text-muted-foreground hover:text-foreground', className)}
|
className={cn(
|
||||||
|
'text-muted-foreground hover:text-foreground size-8 p-0',
|
||||||
|
className
|
||||||
|
)}
|
||||||
size={size}
|
size={size}
|
||||||
type='button'
|
type='button'
|
||||||
variant={variant}
|
variant={variant}
|
||||||
@@ -121,13 +124,16 @@ export const ArtifactAction = ({
|
|||||||
icon: Icon,
|
icon: Icon,
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
size = 'icon',
|
size = 'sm',
|
||||||
variant = 'ghost',
|
variant = 'ghost',
|
||||||
...props
|
...props
|
||||||
}: ArtifactActionProps) => {
|
}: ArtifactActionProps) => {
|
||||||
const button = (
|
const button = (
|
||||||
<Button
|
<Button
|
||||||
className={cn('text-muted-foreground hover:text-foreground', className)}
|
className={cn(
|
||||||
|
'text-muted-foreground hover:text-foreground size-8 p-0',
|
||||||
|
className
|
||||||
|
)}
|
||||||
size={size}
|
size={size}
|
||||||
type='button'
|
type='button'
|
||||||
variant={variant}
|
variant={variant}
|
||||||
|
|||||||
+5
-5
@@ -32,7 +32,7 @@ import {
|
|||||||
} from 'react'
|
} from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { Button } from '@/components/design-system/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
type BranchContextType = {
|
type BranchContextType = {
|
||||||
@@ -178,14 +178,14 @@ export const BranchPrevious = ({
|
|||||||
<Button
|
<Button
|
||||||
aria-label={t('Previous branch')}
|
aria-label={t('Previous branch')}
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-muted-foreground shrink-0 transition-colors',
|
'text-muted-foreground size-7 shrink-0 transition-colors',
|
||||||
'hover:bg-accent hover:text-foreground',
|
'hover:bg-accent hover:text-foreground',
|
||||||
'disabled:pointer-events-none disabled:opacity-50',
|
'disabled:pointer-events-none disabled:opacity-50',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
disabled={totalBranches <= 1}
|
disabled={totalBranches <= 1}
|
||||||
onClick={goToPrevious}
|
onClick={goToPrevious}
|
||||||
size='icon-sm'
|
size='icon'
|
||||||
type='button'
|
type='button'
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
{...props}
|
{...props}
|
||||||
@@ -209,14 +209,14 @@ export const BranchNext = ({
|
|||||||
<Button
|
<Button
|
||||||
aria-label={t('Next branch')}
|
aria-label={t('Next branch')}
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-muted-foreground shrink-0 transition-colors',
|
'text-muted-foreground size-7 shrink-0 transition-colors',
|
||||||
'hover:bg-accent hover:text-foreground',
|
'hover:bg-accent hover:text-foreground',
|
||||||
'disabled:pointer-events-none disabled:opacity-50',
|
'disabled:pointer-events-none disabled:opacity-50',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
disabled={totalBranches <= 1}
|
disabled={totalBranches <= 1}
|
||||||
onClick={goToNext}
|
onClick={goToNext}
|
||||||
size='icon-sm'
|
size='icon'
|
||||||
type='button'
|
type='button'
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
+8
-6
@@ -46,7 +46,7 @@ import {
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import type { BundledLanguage } from 'shiki'
|
import type { BundledLanguage } from 'shiki'
|
||||||
|
|
||||||
import { Button } from '@/components/design-system/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@@ -413,7 +413,7 @@ export const CodeBlockFrame = ({
|
|||||||
{showToolbar && (
|
{showToolbar && (
|
||||||
<div className='bg-muted/35 border-border/70 flex min-h-10 items-center gap-2 border-b px-2 py-1.5'>
|
<div className='bg-muted/35 border-border/70 flex min-h-10 items-center gap-2 border-b px-2 py-1.5'>
|
||||||
<div className='min-w-0 flex-1'>
|
<div className='min-w-0 flex-1'>
|
||||||
<div className='text-muted-foreground truncate font-mono text-xs font-medium tracking-wide uppercase'>
|
<div className='text-muted-foreground truncate font-mono text-[11px] font-medium tracking-wide uppercase'>
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -507,8 +507,9 @@ export const CodeBlock = ({
|
|||||||
render={
|
render={
|
||||||
<Button
|
<Button
|
||||||
aria-label={isCodeCollapsed ? t('Expand') : t('Collapse')}
|
aria-label={isCodeCollapsed ? t('Expand') : t('Collapse')}
|
||||||
|
className='size-8'
|
||||||
onClick={() => setIsCollapsed((value) => !value)}
|
onClick={() => setIsCollapsed((value) => !value)}
|
||||||
size='icon'
|
size='icon-sm'
|
||||||
type='button'
|
type='button'
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
>
|
>
|
||||||
@@ -531,8 +532,9 @@ export const CodeBlock = ({
|
|||||||
render={
|
render={
|
||||||
<Button
|
<Button
|
||||||
aria-label={t('Download')}
|
aria-label={t('Download')}
|
||||||
|
className='size-8'
|
||||||
onClick={downloadCode}
|
onClick={downloadCode}
|
||||||
size='icon'
|
size='icon-sm'
|
||||||
type='button'
|
type='button'
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
>
|
>
|
||||||
@@ -639,9 +641,9 @@ export const CodeBlockCopyButton = ({
|
|||||||
const button = (
|
const button = (
|
||||||
<Button
|
<Button
|
||||||
aria-label={isCopied ? t('Copied!') : t('Copy code')}
|
aria-label={isCopied ? t('Copied!') : t('Copy code')}
|
||||||
className={cn('shrink-0', className)}
|
className={cn('size-8 shrink-0', className)}
|
||||||
onClick={copyToClipboard}
|
onClick={copyToClipboard}
|
||||||
size='icon'
|
size='icon-sm'
|
||||||
type='button'
|
type='button'
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
+2
-2
@@ -26,8 +26,8 @@ import {
|
|||||||
useContext,
|
useContext,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
|
|
||||||
import { Button } from '@/components/design-system/button'
|
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
// Workaround for missing types in 'ai' package
|
// Workaround for missing types in 'ai' package
|
||||||
@@ -169,5 +169,5 @@ export const ConfirmationActions = ({
|
|||||||
export type ConfirmationActionProps = ComponentProps<typeof Button>
|
export type ConfirmationActionProps = ComponentProps<typeof Button>
|
||||||
|
|
||||||
export const ConfirmationAction = (props: ConfirmationActionProps) => (
|
export const ConfirmationAction = (props: ConfirmationActionProps) => (
|
||||||
<Button type='button' {...props} />
|
<Button className='h-8 px-3 text-sm' type='button' {...props} />
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@ import { type ComponentProps, createContext, useContext } from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { getUsage } from 'tokenlens'
|
import { getUsage } from 'tokenlens'
|
||||||
|
|
||||||
import { Button } from '@/components/design-system/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
HoverCard,
|
HoverCard,
|
||||||
HoverCardContent,
|
HoverCardContent,
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@ import { type ComponentProps, useCallback } from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { StickToBottom, useStickToBottomContext } from 'use-stick-to-bottom'
|
import { StickToBottom, useStickToBottomContext } from 'use-stick-to-bottom'
|
||||||
|
|
||||||
import { Button } from '@/components/design-system/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
export type ConversationProps = ComponentProps<typeof StickToBottom>
|
export type ConversationProps = ComponentProps<typeof StickToBottom>
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ import {
|
|||||||
} from 'react'
|
} from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { Button } from '@/components/design-system/button'
|
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
Carousel,
|
Carousel,
|
||||||
type CarouselApi,
|
type CarouselApi,
|
||||||
|
|||||||
+1
-1
@@ -24,7 +24,7 @@ import {
|
|||||||
import { type ComponentProps, createContext, useContext } from 'react'
|
import { type ComponentProps, createContext, useContext } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { Button } from '@/components/design-system/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
|||||||
+2
-2
@@ -22,7 +22,7 @@ import { ChevronsUpDownIcon } from 'lucide-react'
|
|||||||
import { type ComponentProps, createContext, useContext } from 'react'
|
import { type ComponentProps, createContext, useContext } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { Button } from '@/components/design-system/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardAction,
|
CardAction,
|
||||||
@@ -156,7 +156,7 @@ export const PlanTrigger = ({ className, ...props }: PlanTriggerProps) => {
|
|||||||
<CollapsibleTrigger
|
<CollapsibleTrigger
|
||||||
render={
|
render={
|
||||||
<Button
|
<Button
|
||||||
className={cn(className)}
|
className={cn('size-8', className)}
|
||||||
data-slot='plan-trigger'
|
data-slot='plan-trigger'
|
||||||
size='icon'
|
size='icon'
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
|
|||||||
+15
-16
@@ -55,7 +55,7 @@ import {
|
|||||||
} from 'react'
|
} from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { Button } from '@/components/design-system/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
@@ -64,20 +64,7 @@ import {
|
|||||||
CommandItem,
|
CommandItem,
|
||||||
CommandList,
|
CommandList,
|
||||||
CommandSeparator,
|
CommandSeparator,
|
||||||
} from '@/components/design-system/command'
|
} from '@/components/ui/command'
|
||||||
import {
|
|
||||||
InputGroup,
|
|
||||||
InputGroupAddon,
|
|
||||||
InputGroupButton,
|
|
||||||
InputGroupTextarea,
|
|
||||||
} from '@/components/design-system/input-group'
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/components/design-system/select'
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -89,6 +76,19 @@ import {
|
|||||||
HoverCardContent,
|
HoverCardContent,
|
||||||
HoverCardTrigger,
|
HoverCardTrigger,
|
||||||
} from '@/components/ui/hover-card'
|
} from '@/components/ui/hover-card'
|
||||||
|
import {
|
||||||
|
InputGroup,
|
||||||
|
InputGroupAddon,
|
||||||
|
InputGroupButton,
|
||||||
|
InputGroupTextarea,
|
||||||
|
} from '@/components/ui/input-group'
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -326,7 +326,6 @@ export function PromptInputAttachment({
|
|||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
aria-label={t('Remove attachment')}
|
aria-label={t('Remove attachment')}
|
||||||
size='icon-xs'
|
|
||||||
className='absolute inset-0 size-5 cursor-pointer rounded p-0 opacity-0 transition-opacity group-hover:pointer-events-auto group-hover:opacity-100 [&>svg]:size-2.5'
|
className='absolute inset-0 size-5 cursor-pointer rounded p-0 opacity-0 transition-opacity group-hover:pointer-events-auto group-hover:opacity-100 [&>svg]:size-2.5'
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|||||||
+2
-2
@@ -21,7 +21,7 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
import { ChevronDownIcon, PaperclipIcon } from 'lucide-react'
|
import { ChevronDownIcon, PaperclipIcon } from 'lucide-react'
|
||||||
import type { ComponentProps } from 'react'
|
import type { ComponentProps } from 'react'
|
||||||
|
|
||||||
import { Button } from '@/components/design-system/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
@@ -236,7 +236,7 @@ export const QueueSectionTrigger = ({
|
|||||||
<Button
|
<Button
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
className={cn(
|
className={cn(
|
||||||
'group bg-muted/40 text-muted-foreground hover:bg-muted h-auto sm:h-auto w-full justify-between px-3 py-2 text-left',
|
'group bg-muted/40 text-muted-foreground hover:bg-muted h-auto w-full justify-between px-3 py-2 text-left',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
type='button'
|
type='button'
|
||||||
|
|||||||
@@ -32,28 +32,32 @@ import type {
|
|||||||
const alertConfig = {
|
const alertConfig = {
|
||||||
note: {
|
note: {
|
||||||
label: 'Note',
|
label: 'Note',
|
||||||
className: 'border-info/25 bg-info/8 text-foreground',
|
className:
|
||||||
markerClassName: 'text-status-info',
|
'border-blue-500/40 bg-blue-500/8 text-blue-950 dark:text-blue-100',
|
||||||
|
markerClassName: 'text-blue-600 dark:text-blue-300',
|
||||||
},
|
},
|
||||||
tip: {
|
tip: {
|
||||||
label: 'Tip',
|
label: 'Tip',
|
||||||
className: 'border-success/25 bg-success/8 text-foreground',
|
className:
|
||||||
markerClassName: 'text-status-success',
|
'border-emerald-500/40 bg-emerald-500/8 text-emerald-950 dark:text-emerald-100',
|
||||||
|
markerClassName: 'text-emerald-600 dark:text-emerald-300',
|
||||||
},
|
},
|
||||||
important: {
|
important: {
|
||||||
label: 'Important',
|
label: 'Important',
|
||||||
className: 'border-border bg-muted/40 text-foreground',
|
className:
|
||||||
markerClassName: 'text-foreground',
|
'border-violet-500/40 bg-violet-500/8 text-violet-950 dark:text-violet-100',
|
||||||
|
markerClassName: 'text-violet-600 dark:text-violet-300',
|
||||||
},
|
},
|
||||||
warning: {
|
warning: {
|
||||||
label: 'Warning',
|
label: 'Warning',
|
||||||
className: 'border-warning/30 bg-warning/8 text-foreground',
|
className:
|
||||||
markerClassName: 'text-status-warning',
|
'border-amber-500/40 bg-amber-500/8 text-amber-950 dark:text-amber-100',
|
||||||
|
markerClassName: 'text-amber-600 dark:text-amber-300',
|
||||||
},
|
},
|
||||||
caution: {
|
caution: {
|
||||||
label: 'Caution',
|
label: 'Caution',
|
||||||
className: 'border-destructive/25 bg-destructive/8 text-foreground',
|
className: 'border-red-500/40 bg-red-500/8 text-red-950 dark:text-red-100',
|
||||||
markerClassName: 'text-status-destructive',
|
markerClassName: 'text-red-600 dark:text-red-300',
|
||||||
},
|
},
|
||||||
} satisfies Record<AlertKind, AlertConfig>
|
} satisfies Record<AlertKind, AlertConfig>
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
|
|
||||||
import type { ComponentProps } from 'react'
|
import type { ComponentProps } from 'react'
|
||||||
|
|
||||||
import { Button } from '@/components/design-system/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
|
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -29,13 +29,13 @@ import {
|
|||||||
} from 'react'
|
} from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { Button } from '@/components/design-system/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/design-system/input'
|
|
||||||
import {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from '@/components/ui/collapsible'
|
} from '@/components/ui/collapsible'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@@ -135,10 +135,10 @@ export const WebPreviewNavigationButton = ({
|
|||||||
<TooltipTrigger
|
<TooltipTrigger
|
||||||
render={
|
render={
|
||||||
<Button
|
<Button
|
||||||
className='hover:text-foreground'
|
className='hover:text-foreground h-8 w-8 p-0'
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
size='icon'
|
size='sm'
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@@ -185,7 +185,7 @@ export const WebPreviewUrl = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
className='flex-1 text-sm'
|
className='h-8 flex-1 text-sm'
|
||||||
onChange={onChange ?? handleChange}
|
onChange={onChange ?? handleChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder={t('Enter URL...')}
|
placeholder={t('Enter URL...')}
|
||||||
|
|||||||
+1
-1
@@ -30,7 +30,7 @@ import {
|
|||||||
CommandItem,
|
CommandItem,
|
||||||
CommandList,
|
CommandList,
|
||||||
CommandSeparator,
|
CommandSeparator,
|
||||||
} from '@/components/design-system/command'
|
} from '@/components/ui/command'
|
||||||
import { useSearch } from '@/context/search-provider'
|
import { useSearch } from '@/context/search-provider'
|
||||||
import { useTheme } from '@/context/theme-provider'
|
import { useTheme } from '@/context/theme-provider'
|
||||||
import { useSidebarData } from '@/hooks/use-sidebar-data'
|
import { useSidebarData } from '@/hooks/use-sidebar-data'
|
||||||
|
|||||||
+5
-3
@@ -32,14 +32,13 @@ import { IconSidebarSidebar } from '@/assets/custom/icon-sidebar-sidebar'
|
|||||||
import { IconThemeDark } from '@/assets/custom/icon-theme-dark'
|
import { IconThemeDark } from '@/assets/custom/icon-theme-dark'
|
||||||
import { IconThemeLight } from '@/assets/custom/icon-theme-light'
|
import { IconThemeLight } from '@/assets/custom/icon-theme-light'
|
||||||
import { IconThemeSystem } from '@/assets/custom/icon-theme-system'
|
import { IconThemeSystem } from '@/assets/custom/icon-theme-system'
|
||||||
import { Button } from '@/components/design-system/button'
|
|
||||||
import { useSidebar } from '@/components/design-system/sidebar'
|
|
||||||
import {
|
import {
|
||||||
sideDrawerContentClassName,
|
sideDrawerContentClassName,
|
||||||
sideDrawerFooterClassName,
|
sideDrawerFooterClassName,
|
||||||
sideDrawerFormClassName,
|
sideDrawerFormClassName,
|
||||||
sideDrawerHeaderClassName,
|
sideDrawerHeaderClassName,
|
||||||
} from '@/components/drawer-layout'
|
} from '@/components/drawer-layout'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
Sheet,
|
Sheet,
|
||||||
SheetContent,
|
SheetContent,
|
||||||
@@ -63,6 +62,8 @@ import {
|
|||||||
} from '@/lib/theme-customization'
|
} from '@/lib/theme-customization'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
import { useSidebar } from './ui/sidebar'
|
||||||
|
|
||||||
const Item = RadioPrimitive.Root
|
const Item = RadioPrimitive.Root
|
||||||
|
|
||||||
export function ConfigDrawer() {
|
export function ConfigDrawer() {
|
||||||
@@ -144,8 +145,9 @@ function SectionTitle(props: {
|
|||||||
{props.title}
|
{props.title}
|
||||||
{props.showReset && props.onReset && (
|
{props.showReset && props.onReset && (
|
||||||
<Button
|
<Button
|
||||||
size='icon-xs'
|
size='icon'
|
||||||
variant='secondary'
|
variant='secondary'
|
||||||
|
className='size-4'
|
||||||
onClick={props.onReset}
|
onClick={props.onReset}
|
||||||
aria-label='Reset'
|
aria-label='Reset'
|
||||||
>
|
>
|
||||||
|
|||||||
+2
-2
@@ -26,8 +26,8 @@ import {
|
|||||||
AlertDialogFooter,
|
AlertDialogFooter,
|
||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from '@/components/design-system/alert-dialog'
|
} from '@/components/ui/alert-dialog'
|
||||||
import { Button } from '@/components/design-system/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
type ConfirmDialogProps = {
|
type ConfirmDialogProps = {
|
||||||
|
|||||||
+2
-2
@@ -20,7 +20,7 @@ import { Check, Copy } from 'lucide-react'
|
|||||||
import { type ReactNode } from 'react'
|
import { type ReactNode } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { Button } from '@/components/design-system/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@@ -35,7 +35,7 @@ interface CopyButtonProps {
|
|||||||
className?: string
|
className?: string
|
||||||
iconClassName?: string
|
iconClassName?: string
|
||||||
variant?: 'ghost' | 'outline' | 'default' | 'secondary' | 'destructive'
|
variant?: 'ghost' | 'outline' | 'default' | 'secondary' | 'destructive'
|
||||||
size?: 'default' | 'sm' | 'lg' | 'icon' | 'icon-sm' | 'icon-xs'
|
size?: 'default' | 'sm' | 'lg' | 'icon'
|
||||||
tooltip?: string
|
tooltip?: string
|
||||||
successTooltip?: string
|
successTooltip?: string
|
||||||
'aria-label'?: string
|
'aria-label'?: string
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function BadgeCell({ className, ...props }: BadgeCellProps) {
|
|||||||
<div
|
<div
|
||||||
data-slot='badge-cell'
|
data-slot='badge-cell'
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex max-w-full min-w-0 items-center gap-1 overflow-visible',
|
'flex max-w-full min-w-0 items-center gap-1 overflow-hidden [&_[data-slot=status-badge]]:max-w-full [&_[data-slot=status-badge]]:min-w-0',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright (C) 2023-2026 QuantumNous
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as
|
|
||||||
published by the Free Software Foundation, either version 3 of the
|
|
||||||
License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
|
||||||
*/
|
|
||||||
import { createContext } from 'react'
|
|
||||||
|
|
||||||
export type BadgeListCellDisplay = 'compact' | 'full'
|
|
||||||
|
|
||||||
export const BadgeListCellDisplayContext =
|
|
||||||
createContext<BadgeListCellDisplay>('compact')
|
|
||||||
@@ -26,8 +26,6 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '@/components/ui/tooltip'
|
} from '@/components/ui/tooltip'
|
||||||
|
|
||||||
import { BadgeListCellDisplayContext } from './badge-list-cell-context'
|
|
||||||
|
|
||||||
interface BadgeListCellProps {
|
interface BadgeListCellProps {
|
||||||
items: React.ReactNode[]
|
items: React.ReactNode[]
|
||||||
max?: number
|
max?: number
|
||||||
@@ -35,31 +33,18 @@ interface BadgeListCellProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Badge collection that stays compact in table cells and can expose every
|
* Table cell renderer for a list of badges with overflow tooltip.
|
||||||
* item when rendered inside a detail-oriented card.
|
* Displays up to `max` badges inline; remaining items appear in a tooltip.
|
||||||
*/
|
*/
|
||||||
export function BadgeListCell({
|
export function BadgeListCell({
|
||||||
items,
|
items,
|
||||||
max = 2,
|
max = 2,
|
||||||
tooltipClassName,
|
tooltipClassName,
|
||||||
}: BadgeListCellProps) {
|
}: BadgeListCellProps) {
|
||||||
const display = React.useContext(BadgeListCellDisplayContext)
|
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
return <span className='text-muted-foreground text-xs'>-</span>
|
return <span className='text-muted-foreground text-xs'>-</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
if (display === 'full') {
|
|
||||||
return (
|
|
||||||
<StatusBadgeList
|
|
||||||
items={items}
|
|
||||||
max={items.length}
|
|
||||||
renderItem={(item) => item}
|
|
||||||
className='flex-wrap overflow-visible'
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const showTooltip = items.length > max
|
const showTooltip = items.length > max
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { Button } from '@/components/design-system/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -58,7 +58,8 @@ export function DataTableColumnHeader<TData, TValue>({
|
|||||||
render={
|
render={
|
||||||
<Button
|
<Button
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
className='data-popup-open:bg-accent -ms-3'
|
size='sm'
|
||||||
|
className='data-popup-open:bg-accent -ms-3 h-8'
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -60,8 +60,8 @@ function getPinnedColumnClassName(
|
|||||||
) {
|
) {
|
||||||
const edgeClassName =
|
const edgeClassName =
|
||||||
pinnedColumn.side === 'left'
|
pinnedColumn.side === 'left'
|
||||||
? 'shadow-[8px_0_10px_-10px_var(--foreground)]'
|
? 'shadow-[8px_0_10px_-10px_hsl(var(--foreground))]'
|
||||||
: 'shadow-[-8px_0_10px_-10px_var(--foreground)]'
|
: 'shadow-[-8px_0_10px_-10px_hsl(var(--foreground))]'
|
||||||
|
|
||||||
return cn(
|
return cn(
|
||||||
'sticky whitespace-nowrap',
|
'sticky whitespace-nowrap',
|
||||||
|
|||||||
@@ -16,9 +16,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
export function isContentSizedColumn(
|
export function isContentSizedColumn(columnId: string): boolean {
|
||||||
columnId: string,
|
return columnId === 'actions'
|
||||||
contentSized = false
|
|
||||||
): boolean {
|
|
||||||
return contentSized || columnId === 'actions'
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,8 +27,7 @@ export function DataTableColgroup<TData>({
|
|||||||
}) {
|
}) {
|
||||||
const columns = table.getVisibleLeafColumns()
|
const columns = table.getVisibleLeafColumns()
|
||||||
const sizedColumns = columns.filter(
|
const sizedColumns = columns.filter(
|
||||||
(column) =>
|
(column) => !isContentSizedColumn(column.id)
|
||||||
!isContentSizedColumn(column.id, column.columnDef.meta?.contentSized)
|
|
||||||
)
|
)
|
||||||
const totalSize = sizedColumns.reduce((sum, col) => sum + col.getSize(), 0)
|
const totalSize = sizedColumns.reduce((sum, col) => sum + col.getSize(), 0)
|
||||||
|
|
||||||
@@ -39,8 +38,7 @@ export function DataTableColgroup<TData>({
|
|||||||
table,
|
table,
|
||||||
column.id,
|
column.id,
|
||||||
column.getSize(),
|
column.getSize(),
|
||||||
totalSize,
|
totalSize
|
||||||
column.columnDef.meta?.contentSized
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return <col key={column.id} style={{ width }} />
|
return <col key={column.id} style={{ width }} />
|
||||||
@@ -53,10 +51,9 @@ function getColumnWidth<TData>(
|
|||||||
table: TanstackTable<TData>,
|
table: TanstackTable<TData>,
|
||||||
columnId: string,
|
columnId: string,
|
||||||
columnSize: number,
|
columnSize: number,
|
||||||
totalSize: number,
|
totalSize: number
|
||||||
contentSized?: boolean
|
|
||||||
) {
|
) {
|
||||||
if (isContentSizedColumn(columnId, contentSized)) {
|
if (isContentSizedColumn(columnId)) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,11 +24,7 @@ import {
|
|||||||
import type { KeyboardEvent, MouseEvent } from 'react'
|
import type { KeyboardEvent, MouseEvent } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import {
|
import { TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from '@/components/design-system/table'
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
import { DataTableColumnHeader } from './column-header'
|
import { DataTableColumnHeader } from './column-header'
|
||||||
@@ -249,10 +245,7 @@ function shouldRenderColumnResizer<TData>(
|
|||||||
table.options.enableColumnResizing === true &&
|
table.options.enableColumnResizing === true &&
|
||||||
!header.isPlaceholder &&
|
!header.isPlaceholder &&
|
||||||
header.column.getCanResize() &&
|
header.column.getCanResize() &&
|
||||||
!isContentSizedColumn(
|
!isContentSizedColumn(header.column.id)
|
||||||
header.column.id,
|
|
||||||
header.column.columnDef.meta?.contentSized
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,13 +253,7 @@ function getHeaderSizeStyle<TData>(
|
|||||||
header: Header<TData, unknown>,
|
header: Header<TData, unknown>,
|
||||||
applyHeaderSize: boolean | undefined
|
applyHeaderSize: boolean | undefined
|
||||||
) {
|
) {
|
||||||
if (
|
if (!applyHeaderSize || isContentSizedColumn(header.column.id)) {
|
||||||
!applyHeaderSize ||
|
|
||||||
isContentSizedColumn(
|
|
||||||
header.column.id,
|
|
||||||
header.column.columnDef.meta?.contentSized
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+62
-12
@@ -16,18 +16,25 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
import { flexRender, type Row } from '@tanstack/react-table'
|
import {
|
||||||
|
flexRender,
|
||||||
|
type Cell,
|
||||||
|
type Row,
|
||||||
|
type Table as TanstackTable,
|
||||||
|
} from '@tanstack/react-table'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import { TableCell, TableRow } from '@/components/design-system/table'
|
import { TableCell, TableRow } from '@/components/ui/table'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
import { TruncatedCell } from './truncated-cell'
|
||||||
import type { DataTableColumnClassName } from './types'
|
import type { DataTableColumnClassName } from './types'
|
||||||
|
|
||||||
type DataTableRowProps<TData> = {
|
type DataTableRowProps<TData> = {
|
||||||
row: Row<TData>
|
row: Row<TData>
|
||||||
className?: string
|
className?: string
|
||||||
getColumnClassName?: DataTableColumnClassName
|
getColumnClassName?: DataTableColumnClassName
|
||||||
|
cellRenderColumns?: TanstackTable<TData>['options']['columns']
|
||||||
} & Omit<React.ComponentProps<typeof TableRow>, 'children'>
|
} & Omit<React.ComponentProps<typeof TableRow>, 'children'>
|
||||||
|
|
||||||
type DataTableRowInnerProps<TData> = DataTableRowProps<TData> & {
|
type DataTableRowInnerProps<TData> = DataTableRowProps<TData> & {
|
||||||
@@ -39,8 +46,13 @@ function DataTableRowInner<TData>({
|
|||||||
isSelected,
|
isSelected,
|
||||||
className,
|
className,
|
||||||
getColumnClassName,
|
getColumnClassName,
|
||||||
|
cellRenderColumns,
|
||||||
...rowProps
|
...rowProps
|
||||||
}: DataTableRowInnerProps<TData>) {
|
}: DataTableRowInnerProps<TData>) {
|
||||||
|
// Destructured only to keep it out of `rowProps` (it is not a valid DOM attr)
|
||||||
|
// and to feed the memo comparator below; it is intentionally unused here.
|
||||||
|
void cellRenderColumns
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<TableRow
|
||||||
data-state={isSelected ? 'selected' : undefined}
|
data-state={isSelected ? 'selected' : undefined}
|
||||||
@@ -48,25 +60,19 @@ function DataTableRowInner<TData>({
|
|||||||
{...rowProps}
|
{...rowProps}
|
||||||
>
|
>
|
||||||
{row.getVisibleCells().map((cell) => {
|
{row.getVisibleCells().map((cell) => {
|
||||||
const contentMode = cell.column.columnDef.meta?.contentMode ?? 'wrap'
|
const renderedCell = renderCellContent(cell)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableCell
|
<TableCell
|
||||||
key={cell.id}
|
key={cell.id}
|
||||||
data-column-id={cell.column.id}
|
data-column-id={cell.column.id}
|
||||||
data-content-mode={contentMode}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'max-w-full min-w-0',
|
'max-w-full min-w-0',
|
||||||
contentMode === 'full' &&
|
renderedCell.isPrimitive && 'overflow-hidden',
|
||||||
'max-w-none overflow-visible [&_.truncate]:overflow-visible [&_.truncate]:text-clip',
|
|
||||||
contentMode === 'wrap' &&
|
|
||||||
'whitespace-normal break-words [overflow-wrap:anywhere] [&_.truncate]:overflow-visible [&_.truncate]:text-clip [&_.truncate]:whitespace-normal',
|
|
||||||
contentMode === 'summary' &&
|
|
||||||
'whitespace-normal break-words [overflow-wrap:anywhere]',
|
|
||||||
getColumnClassName?.(cell.column.id, 'cell')
|
getColumnClassName?.(cell.column.id, 'cell')
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
{renderedCell.content}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@@ -74,6 +80,50 @@ function DataTableRowInner<TData>({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MemoizedDataTableRow = React.memo(DataTableRowInner, (prev, next) => {
|
||||||
|
// Do not read row.getIsSelected() inside the comparator: TanStack row objects
|
||||||
|
// keep a stable reference while their selection state mutates, so reading it
|
||||||
|
// here compares identical live values and misses selection changes. Selection
|
||||||
|
// is lifted to the `isSelected` prop, captured per render in DataTableRow.
|
||||||
|
//
|
||||||
|
// Column cell renderers (and getColumnClassName) can close over external
|
||||||
|
// state while the row stays stable, so column definitions and the class
|
||||||
|
// resolver are part of the render identity and must be compared too.
|
||||||
|
return (
|
||||||
|
prev.row === next.row &&
|
||||||
|
prev.className === next.className &&
|
||||||
|
prev.isSelected === next.isSelected &&
|
||||||
|
prev.getColumnClassName === next.getColumnClassName &&
|
||||||
|
prev.cellRenderColumns === next.cellRenderColumns
|
||||||
|
)
|
||||||
|
}) as typeof DataTableRowInner
|
||||||
|
|
||||||
export function DataTableRow<TData>(props: DataTableRowProps<TData>) {
|
export function DataTableRow<TData>(props: DataTableRowProps<TData>) {
|
||||||
return <DataTableRowInner {...props} isSelected={props.row.getIsSelected()} />
|
return (
|
||||||
|
<MemoizedDataTableRow {...props} isSelected={props.row.getIsSelected()} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCellContent<TData>(cell: Cell<TData, unknown>) {
|
||||||
|
const content = flexRender(cell.column.columnDef.cell, cell.getContext())
|
||||||
|
const textContent = getPrimitiveTextContent(content)
|
||||||
|
|
||||||
|
if (!textContent) {
|
||||||
|
return { content, isPrimitive: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: (
|
||||||
|
<TruncatedCell tooltipContent={textContent}>{content}</TruncatedCell>
|
||||||
|
),
|
||||||
|
isPrimitive: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPrimitiveTextContent(content: React.ReactNode): string | null {
|
||||||
|
if (typeof content === 'string' || typeof content === 'number') {
|
||||||
|
return String(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-22
@@ -19,12 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
import type { Row, Table as TanstackTable } from '@tanstack/react-table'
|
import type { Row, Table as TanstackTable } from '@tanstack/react-table'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import {
|
import { Table, TableBody, TableCell, TableRow } from '@/components/ui/table'
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableRow,
|
|
||||||
} from '@/components/design-system/table'
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -54,7 +49,10 @@ export { DataTableRowActionMenu } from './row-action-menu'
|
|||||||
|
|
||||||
export function DataTableView<TData>(props: DataTableViewProps<TData>) {
|
export function DataTableView<TData>(props: DataTableViewProps<TData>) {
|
||||||
const rows = props.rows ?? props.table.getRowModel().rows
|
const rows = props.rows ?? props.table.getRowModel().rows
|
||||||
const colSpan = props.table.getVisibleLeafColumns().length
|
const colSpan = React.useMemo(
|
||||||
|
() => props.table.getVisibleLeafColumns().length,
|
||||||
|
[props.table]
|
||||||
|
)
|
||||||
const columnClassName = useResolvedColumnClassName(
|
const columnClassName = useResolvedColumnClassName(
|
||||||
props.table,
|
props.table,
|
||||||
props.getColumnClassName,
|
props.getColumnClassName,
|
||||||
@@ -64,7 +62,7 @@ export function DataTableView<TData>(props: DataTableViewProps<TData>) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full min-w-0 overflow-hidden rounded-lg border',
|
'overflow-hidden rounded-lg border',
|
||||||
props.containerClassName
|
props.containerClassName
|
||||||
)}
|
)}
|
||||||
{...props.containerProps}
|
{...props.containerProps}
|
||||||
@@ -103,15 +101,7 @@ function UnifiedTableView<TData>({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={props.tableContainerClassName}>
|
<div className={props.tableContainerClassName}>
|
||||||
<Table
|
<Table className={props.tableClassName} style={tableSizing.style}>
|
||||||
className={props.tableClassName}
|
|
||||||
style={tableSizing.style}
|
|
||||||
containerProps={{
|
|
||||||
role: props.scrollLabel ? 'region' : undefined,
|
|
||||||
tabIndex: 0,
|
|
||||||
'aria-label': props.scrollLabel,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{tableSizing.colgroup}
|
{tableSizing.colgroup}
|
||||||
<DataTableHeader
|
<DataTableHeader
|
||||||
table={props.table}
|
table={props.table}
|
||||||
@@ -148,15 +138,12 @@ function SplitHeaderTableView<TData>({
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'min-h-0 min-w-0 flex-1 overscroll-contain overflow-auto',
|
'min-h-0 flex-1 overflow-auto',
|
||||||
'**:data-[slot=table-header]:[--table-header-bg:var(--table-header)]',
|
'**:data-[slot=table-header]:[--table-header-bg:var(--table-header)]',
|
||||||
'**:data-[slot=table-header]:bg-(--table-header-bg)',
|
'**:data-[slot=table-header]:bg-(--table-header-bg)',
|
||||||
props.splitHeaderScrollClassName,
|
props.splitHeaderScrollClassName,
|
||||||
props.bodyContainerClassName
|
props.bodyContainerClassName
|
||||||
)}
|
)}
|
||||||
role={props.scrollLabel ? 'region' : undefined}
|
|
||||||
tabIndex={0}
|
|
||||||
aria-label={props.scrollLabel}
|
|
||||||
>
|
>
|
||||||
<table
|
<table
|
||||||
data-slot='table'
|
data-slot='table'
|
||||||
@@ -170,7 +157,7 @@ function SplitHeaderTableView<TData>({
|
|||||||
<DataTableHeader
|
<DataTableHeader
|
||||||
table={props.table}
|
table={props.table}
|
||||||
applyHeaderSize={props.applyHeaderSize}
|
applyHeaderSize={props.applyHeaderSize}
|
||||||
className={cn('sticky top-0 z-20', props.tableHeaderClassName)}
|
className={cn('sticky top-0 z-10', props.tableHeaderClassName)}
|
||||||
rowClassName={props.tableHeaderRowClassName}
|
rowClassName={props.tableHeaderRowClassName}
|
||||||
getColumnClassName={getColumnClassName}
|
getColumnClassName={getColumnClassName}
|
||||||
/>
|
/>
|
||||||
@@ -338,6 +325,7 @@ function renderDefaultRow<TData>(
|
|||||||
row={row}
|
row={row}
|
||||||
className={cn(props.tableBodyRowClassName, props.getRowClassName?.(row))}
|
className={cn(props.tableBodyRowClassName, props.getRowClassName?.(row))}
|
||||||
getColumnClassName={getColumnClassName}
|
getColumnClassName={getColumnClassName}
|
||||||
|
cellRenderColumns={props.table.options.columns}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-12
@@ -25,7 +25,7 @@ import {
|
|||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { Button } from '@/components/design-system/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -33,7 +33,7 @@ import {
|
|||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/design-system/select'
|
} from '@/components/ui/select'
|
||||||
import { cn, getPageNumbers } from '@/lib/utils'
|
import { cn, getPageNumbers } from '@/lib/utils'
|
||||||
|
|
||||||
type DataTablePaginationProps<TData> = {
|
type DataTablePaginationProps<TData> = {
|
||||||
@@ -83,7 +83,7 @@ export function DataTablePagination<TData>({
|
|||||||
table.setPageSize(Number(value))
|
table.setPageSize(Number(value))
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className='text-foreground w-[64px] font-medium tabular-nums sm:w-[70px]'>
|
<SelectTrigger className='text-foreground h-8 w-[64px] font-medium tabular-nums sm:w-[70px]'>
|
||||||
<SelectValue placeholder={pageSize} />
|
<SelectValue placeholder={pageSize} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent side='top' alignItemWithTrigger={false}>
|
<SelectContent side='top' alignItemWithTrigger={false}>
|
||||||
@@ -100,9 +100,8 @@ export function DataTablePagination<TData>({
|
|||||||
|
|
||||||
<div className='flex min-w-0 shrink-0 items-center gap-1 @lg/pagination:gap-1.5 @xl/pagination:gap-2'>
|
<div className='flex min-w-0 shrink-0 items-center gap-1 @lg/pagination:gap-1.5 @xl/pagination:gap-2'>
|
||||||
<Button
|
<Button
|
||||||
size='icon'
|
|
||||||
variant='outline'
|
variant='outline'
|
||||||
className='text-muted-foreground hover:text-foreground disabled:text-muted-foreground/50 @max-lg/pagination:hidden'
|
className='text-muted-foreground hover:text-foreground disabled:text-muted-foreground/50 size-8 p-0 @max-lg/pagination:hidden'
|
||||||
onClick={() => table.setPageIndex(0)}
|
onClick={() => table.setPageIndex(0)}
|
||||||
disabled={!table.getCanPreviousPage()}
|
disabled={!table.getCanPreviousPage()}
|
||||||
>
|
>
|
||||||
@@ -110,9 +109,8 @@ export function DataTablePagination<TData>({
|
|||||||
<DoubleArrowLeftIcon className='h-4 w-4' />
|
<DoubleArrowLeftIcon className='h-4 w-4' />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size='icon'
|
|
||||||
variant='outline'
|
variant='outline'
|
||||||
className='text-muted-foreground hover:text-foreground disabled:text-muted-foreground/50'
|
className='text-muted-foreground hover:text-foreground disabled:text-muted-foreground/50 size-8 p-0'
|
||||||
onClick={() => table.previousPage()}
|
onClick={() => table.previousPage()}
|
||||||
disabled={!table.getCanPreviousPage()}
|
disabled={!table.getCanPreviousPage()}
|
||||||
>
|
>
|
||||||
@@ -130,7 +128,7 @@ export function DataTablePagination<TData>({
|
|||||||
<Button
|
<Button
|
||||||
variant={currentPage === pageNumber ? 'default' : 'outline'}
|
variant={currentPage === pageNumber ? 'default' : 'outline'}
|
||||||
className={cn(
|
className={cn(
|
||||||
'min-w-8 px-2 tabular-nums',
|
'h-8 min-w-8 px-2 tabular-nums',
|
||||||
currentPage === pageNumber
|
currentPage === pageNumber
|
||||||
? 'font-semibold'
|
? 'font-semibold'
|
||||||
: 'text-muted-foreground hover:text-foreground'
|
: 'text-muted-foreground hover:text-foreground'
|
||||||
@@ -147,9 +145,8 @@ export function DataTablePagination<TData>({
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
size='icon'
|
|
||||||
variant='outline'
|
variant='outline'
|
||||||
className='text-muted-foreground hover:text-foreground disabled:text-muted-foreground/50'
|
className='text-muted-foreground hover:text-foreground disabled:text-muted-foreground/50 size-8 p-0'
|
||||||
onClick={() => table.nextPage()}
|
onClick={() => table.nextPage()}
|
||||||
disabled={!table.getCanNextPage()}
|
disabled={!table.getCanNextPage()}
|
||||||
>
|
>
|
||||||
@@ -157,9 +154,8 @@ export function DataTablePagination<TData>({
|
|||||||
<ChevronRightIcon className='h-4 w-4' />
|
<ChevronRightIcon className='h-4 w-4' />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size='icon'
|
|
||||||
variant='outline'
|
variant='outline'
|
||||||
className='text-muted-foreground hover:text-foreground disabled:text-muted-foreground/50 @max-lg/pagination:hidden'
|
className='text-muted-foreground hover:text-foreground disabled:text-muted-foreground/50 size-8 p-0 @max-lg/pagination:hidden'
|
||||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||||
disabled={!table.getCanNextPage()}
|
disabled={!table.getCanNextPage()}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
import { MoreHorizontal } from 'lucide-react'
|
import { MoreHorizontal } from 'lucide-react'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import { Button } from '@/components/design-system/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -42,7 +42,7 @@ export function DataTableRowActionMenu(props: DataTableRowActionMenuProps) {
|
|||||||
render={
|
render={
|
||||||
<Button
|
<Button
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
size='icon-sm'
|
size='icon'
|
||||||
className='data-popup-open:bg-muted'
|
className='data-popup-open:bg-muted'
|
||||||
aria-label={props.ariaLabel}
|
aria-label={props.ariaLabel}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
import { Database } from 'lucide-react'
|
import { Database } from 'lucide-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { TableRow, TableCell } from '@/components/design-system/table'
|
|
||||||
import {
|
import {
|
||||||
Empty,
|
Empty,
|
||||||
EmptyDescription,
|
EmptyDescription,
|
||||||
@@ -27,6 +26,7 @@ import {
|
|||||||
EmptyMedia,
|
EmptyMedia,
|
||||||
EmptyTitle,
|
EmptyTitle,
|
||||||
} from '@/components/ui/empty'
|
} from '@/components/ui/empty'
|
||||||
|
import { TableRow, TableCell } from '@/components/ui/table'
|
||||||
|
|
||||||
interface TableEmptyProps {
|
interface TableEmptyProps {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -26,10 +26,7 @@ export function getTableSizeStyle<TData>(
|
|||||||
): React.CSSProperties {
|
): React.CSSProperties {
|
||||||
const width = table
|
const width = table
|
||||||
.getVisibleLeafColumns()
|
.getVisibleLeafColumns()
|
||||||
.filter(
|
.filter((column) => !isContentSizedColumn(column.id))
|
||||||
(column) =>
|
|
||||||
!isContentSizedColumn(column.id, column.columnDef.meta?.contentSized)
|
|
||||||
)
|
|
||||||
.reduce((total, column) => total + column.getSize(), 0)
|
.reduce((total, column) => total + column.getSize(), 0)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
*/
|
*/
|
||||||
import type { Table } from '@tanstack/react-table'
|
import type { Table } from '@tanstack/react-table'
|
||||||
|
|
||||||
import { TableRow, TableCell } from '@/components/design-system/table'
|
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
|
import { TableRow, TableCell } from '@/components/ui/table'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
const SKELETON_WIDTHS = [
|
const SKELETON_WIDTHS = [
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export function TruncatedCell({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'block max-w-full min-w-0 whitespace-normal break-words [overflow-wrap:anywhere]',
|
'block max-w-full min-w-0 truncate',
|
||||||
cellClassName,
|
cellClassName,
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
@@ -64,8 +64,7 @@ export function TruncatedCell({
|
|||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger
|
<TooltipTrigger
|
||||||
render={
|
render={
|
||||||
<span
|
<div
|
||||||
tabIndex={0}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'block max-w-full min-w-0 truncate',
|
'block max-w-full min-w-0 truncate',
|
||||||
cellClassName,
|
cellClassName,
|
||||||
@@ -74,9 +73,7 @@ export function TruncatedCell({
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span className={cn('block truncate', contentClassName)}>
|
<div className={cn('truncate', contentClassName)}>{children}</div>
|
||||||
{children}
|
|
||||||
</span>
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent
|
<TooltipContent
|
||||||
side={side}
|
side={side}
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ export type DataTableViewProps<TData> = {
|
|||||||
rows?: Row<TData>[]
|
rows?: Row<TData>[]
|
||||||
emptyTitle?: string
|
emptyTitle?: string
|
||||||
emptyDescription?: string
|
emptyDescription?: string
|
||||||
scrollLabel?: string
|
|
||||||
emptyIcon?: React.ReactNode
|
emptyIcon?: React.ReactNode
|
||||||
emptyAction?: React.ReactNode
|
emptyAction?: React.ReactNode
|
||||||
emptyContent?: React.ReactNode
|
emptyContent?: React.ReactNode
|
||||||
|
|||||||
+1
-19
@@ -20,19 +20,9 @@ export { DataTablePagination } from './core/pagination'
|
|||||||
export { DataTableColumnHeader } from './core/column-header'
|
export { DataTableColumnHeader } from './core/column-header'
|
||||||
export { BadgeCell } from './core/badge-cell'
|
export { BadgeCell } from './core/badge-cell'
|
||||||
export { BadgeListCell } from './core/badge-list-cell'
|
export { BadgeListCell } from './core/badge-list-cell'
|
||||||
export {
|
|
||||||
BadgeListCellDisplayContext,
|
|
||||||
type BadgeListCellDisplay,
|
|
||||||
} from './core/badge-list-cell-context'
|
|
||||||
export { TruncatedCell } from './core/truncated-cell'
|
export { TruncatedCell } from './core/truncated-cell'
|
||||||
export { DataTableViewOptions } from './toolbar/view-options'
|
export { DataTableViewOptions } from './toolbar/view-options'
|
||||||
export { DataTableToolbar } from './toolbar/toolbar'
|
export { DataTableToolbar } from './toolbar/toolbar'
|
||||||
export {
|
|
||||||
DataTableFilterField,
|
|
||||||
DataTableFilterInput,
|
|
||||||
DataTableFilterPanel,
|
|
||||||
type DataTableFilterPanelProps,
|
|
||||||
} from './toolbar/filter-panel'
|
|
||||||
export { DataTableBulkActions } from './toolbar/bulk-actions'
|
export { DataTableBulkActions } from './toolbar/bulk-actions'
|
||||||
export {
|
export {
|
||||||
StaticDataTable,
|
StaticDataTable,
|
||||||
@@ -48,21 +38,13 @@ export {
|
|||||||
type DataTablePinnedColumn,
|
type DataTablePinnedColumn,
|
||||||
type DataTableRenderRowHelpers,
|
type DataTableRenderRowHelpers,
|
||||||
} from './core/data-table-view'
|
} from './core/data-table-view'
|
||||||
export {
|
export { MobileCardList } from './layout/mobile-card-list'
|
||||||
MobileCardList,
|
|
||||||
type MobileCardListProps,
|
|
||||||
} from './layout/mobile-card-list'
|
|
||||||
export {
|
export {
|
||||||
DataTableCardGrid,
|
DataTableCardGrid,
|
||||||
type DataTableCardGridProps,
|
type DataTableCardGridProps,
|
||||||
type DataTableCardHelpers,
|
type DataTableCardHelpers,
|
||||||
} from './layout/card-grid'
|
} from './layout/card-grid'
|
||||||
export { CardRowContent } from './layout/card-row-content'
|
export { CardRowContent } from './layout/card-row-content'
|
||||||
export {
|
|
||||||
DataTableCardField,
|
|
||||||
DataTableCardRow,
|
|
||||||
type DataTableContentMode,
|
|
||||||
} from './layout/card-field'
|
|
||||||
export { tableHasCompactMeta } from './layout/card-cell-utils'
|
export { tableHasCompactMeta } from './layout/card-cell-utils'
|
||||||
export {
|
export {
|
||||||
DataTablePage,
|
DataTablePage,
|
||||||
|
|||||||
@@ -46,10 +46,14 @@ export function renderCellContent<TData>(
|
|||||||
return cell.getValue() as ReactNode
|
return cell.getValue() as ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Whether visible columns declare a title/status card hierarchy. */
|
/**
|
||||||
|
* Whether any visible column declares `mobileTitle`/`mobileBadge` meta. When
|
||||||
|
* true the compact two-tier layout is used; otherwise the condensed
|
||||||
|
* label:value fallback layout is used.
|
||||||
|
*/
|
||||||
export function tableHasCompactMeta<TData>(table: Table<TData>): boolean {
|
export function tableHasCompactMeta<TData>(table: Table<TData>): boolean {
|
||||||
return table.getVisibleLeafColumns().some((col) => {
|
return table.getVisibleLeafColumns().some((col) => {
|
||||||
const meta = col.columnDef.meta
|
const meta = col.columnDef.meta
|
||||||
return meta?.cardRole === 'title' || meta?.cardRole === 'badge'
|
return Boolean(meta?.mobileTitle || meta?.mobileBadge)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,119 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright (C) 2023-2026 QuantumNous
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as
|
|
||||||
published by the Free Software Foundation, either version 3 of the
|
|
||||||
License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
|
||||||
*/
|
|
||||||
import type { ReactNode } from 'react'
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
export type DataTableContentMode = 'full' | 'wrap' | 'summary'
|
|
||||||
|
|
||||||
const VALUE_WRAP_CLASS =
|
|
||||||
'whitespace-normal break-words [overflow-wrap:anywhere] [&_.truncate]:overflow-visible [&_.truncate]:text-clip [&_.truncate]:whitespace-normal'
|
|
||||||
|
|
||||||
interface DataTableCardFieldProps {
|
|
||||||
children: ReactNode
|
|
||||||
className?: string
|
|
||||||
contentMode?: DataTableContentMode
|
|
||||||
label?: ReactNode
|
|
||||||
span?: 1 | 2
|
|
||||||
valueClassName?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stacked label-above-value field. Prefer {@link DataTableCardRow} for dense
|
|
||||||
* scannable cards; keep this for multi-line badge collections.
|
|
||||||
*/
|
|
||||||
export function DataTableCardField({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
contentMode = 'wrap',
|
|
||||||
label,
|
|
||||||
span = 1,
|
|
||||||
valueClassName,
|
|
||||||
}: DataTableCardFieldProps) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot='data-table-card-field'
|
|
||||||
className={cn('min-w-0', span === 2 && 'col-span-2', className)}
|
|
||||||
>
|
|
||||||
{label && (
|
|
||||||
<div className='text-muted-foreground mb-1.5 text-xs leading-none select-none'>
|
|
||||||
{label}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
data-slot='data-table-card-value'
|
|
||||||
className={cn(
|
|
||||||
'min-w-0 text-sm leading-snug',
|
|
||||||
(contentMode === 'full' || contentMode === 'wrap') &&
|
|
||||||
VALUE_WRAP_CLASS,
|
|
||||||
contentMode === 'full' && 'break-all',
|
|
||||||
valueClassName
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children ?? '-'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DataTableCardRowProps {
|
|
||||||
children: ReactNode
|
|
||||||
className?: string
|
|
||||||
contentMode?: DataTableContentMode
|
|
||||||
label: ReactNode
|
|
||||||
valueClassName?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dense definition-list row: muted label left, value right.
|
|
||||||
* Always visible — no progressive disclosure / "More" click required.
|
|
||||||
*/
|
|
||||||
export function DataTableCardRow({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
contentMode = 'wrap',
|
|
||||||
label,
|
|
||||||
valueClassName,
|
|
||||||
}: DataTableCardRowProps) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot='data-table-card-row'
|
|
||||||
className={cn(
|
|
||||||
'flex min-h-6 items-start justify-between gap-4 py-0.5',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span className='text-muted-foreground shrink-0 pt-0.5 text-xs select-none'>
|
|
||||||
{label}
|
|
||||||
</span>
|
|
||||||
<div
|
|
||||||
data-slot='data-table-card-value'
|
|
||||||
className={cn(
|
|
||||||
'flex min-w-0 flex-wrap items-center justify-end gap-1 text-right text-sm leading-snug',
|
|
||||||
(contentMode === 'full' || contentMode === 'wrap') &&
|
|
||||||
VALUE_WRAP_CLASS,
|
|
||||||
contentMode === 'full' && 'break-all',
|
|
||||||
valueClassName
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children ?? <span className='text-muted-foreground'>-</span>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -37,7 +37,7 @@ import { CardRowContent } from './card-row-content'
|
|||||||
/** Helpers passed to a custom {@link DataTableCardGridProps.renderCard}. */
|
/** Helpers passed to a custom {@link DataTableCardGridProps.renderCard}. */
|
||||||
export type DataTableCardHelpers = {
|
export type DataTableCardHelpers = {
|
||||||
/**
|
/**
|
||||||
* Whether the table declares title/status `cardRole` metadata.
|
* Whether the table declares compact card meta (`mobileTitle`/`mobileBadge`).
|
||||||
* Provided so custom renderers can match the default layout decision.
|
* Provided so custom renderers can match the default layout decision.
|
||||||
*/
|
*/
|
||||||
compact: boolean
|
compact: boolean
|
||||||
@@ -53,7 +53,6 @@ export interface DataTableCardGridProps<TData> {
|
|||||||
emptyTitle?: string
|
emptyTitle?: string
|
||||||
emptyDescription?: string
|
emptyDescription?: string
|
||||||
emptyIcon?: React.ReactNode
|
emptyIcon?: React.ReactNode
|
||||||
emptyAction?: React.ReactNode
|
|
||||||
getRowKey?: (row: Row<TData>) => string | number
|
getRowKey?: (row: Row<TData>) => string | number
|
||||||
getRowClassName?: (row: Row<TData>) => string | undefined
|
getRowClassName?: (row: Row<TData>) => string | undefined
|
||||||
/**
|
/**
|
||||||
@@ -152,7 +151,6 @@ export function DataTableCardGrid<TData>(props: DataTableCardGridProps<TData>) {
|
|||||||
<EmptyTitle>{resolvedEmptyTitle}</EmptyTitle>
|
<EmptyTitle>{resolvedEmptyTitle}</EmptyTitle>
|
||||||
<EmptyDescription>{resolvedEmptyDescription}</EmptyDescription>
|
<EmptyDescription>{resolvedEmptyDescription}</EmptyDescription>
|
||||||
</EmptyHeader>
|
</EmptyHeader>
|
||||||
{props.emptyAction}
|
|
||||||
</Empty>
|
</Empty>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -169,7 +167,7 @@ export function DataTableCardGrid<TData>(props: DataTableCardGridProps<TData>) {
|
|||||||
data-slot='data-table-card'
|
data-slot='data-table-card'
|
||||||
data-state={isSelected ? 'selected' : undefined}
|
data-state={isSelected ? 'selected' : undefined}
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-lg border bg-(--data-table-card-bg,var(--table-row)) px-3.5 py-3 transition-[background-color,border-color] duration-150 data-[state=selected]:[--data-table-card-bg:color-mix(in_oklch,var(--primary)_7%,var(--table-row))] data-[state=selected]:border-primary/40',
|
'rounded-lg border bg-(--data-table-card-bg,var(--table-row)) px-3 py-2.5 transition-[background-color,border-color] duration-150 data-[state=selected]:[--data-table-card-bg:color-mix(in_oklch,var(--primary)_7%,var(--table-row))] data-[state=selected]:border-primary/40',
|
||||||
props.getRowClassName?.(row)
|
props.getRowClassName?.(row)
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
+157
-88
@@ -17,24 +17,18 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
For commercial licensing, please contact support@quantumnous.com
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
import type { Cell, Row } from '@tanstack/react-table'
|
import type { Cell, Row } from '@tanstack/react-table'
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
import { StatusBadgeTypeContext } from '@/components/status-badge'
|
||||||
|
|
||||||
import { getCellLabel, renderCellContent } from './card-cell-utils'
|
import { getCellLabel, renderCellContent } from './card-cell-utils'
|
||||||
import { DataTableCardField, DataTableCardRow } from './card-field'
|
|
||||||
|
|
||||||
type CardRole = 'title' | 'badge' | 'primary' | 'secondary' | 'hidden'
|
|
||||||
|
|
||||||
function getCardRole<TData>(cell: Cell<TData, unknown>): CardRole {
|
|
||||||
const meta = cell.column.columnDef.meta
|
|
||||||
if (meta?.cardRole) return meta.cardRole
|
|
||||||
return 'primary'
|
|
||||||
}
|
|
||||||
|
|
||||||
function orderCardCells<TData>(
|
function orderCardCells<TData>(
|
||||||
cells: Cell<TData, unknown>[]
|
cells: Cell<TData, unknown>[]
|
||||||
): Cell<TData, unknown>[] {
|
): Cell<TData, unknown>[] {
|
||||||
return [...cells].sort((a, b) => {
|
return [...cells].sort((a, b) => {
|
||||||
const aOrder = a.column.columnDef.meta?.cardOrder
|
const aOrder = a.column.columnDef.meta?.mobileOrder
|
||||||
const bOrder = b.column.columnDef.meta?.cardOrder
|
const bOrder = b.column.columnDef.meta?.mobileOrder
|
||||||
|
|
||||||
if (aOrder == null && bOrder == null) return 0
|
if (aOrder == null && bOrder == null) return 0
|
||||||
if (aOrder == null) return 1
|
if (aOrder == null) return 1
|
||||||
@@ -43,103 +37,178 @@ function orderCardCells<TData>(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function isWideField<TData>(cell: Cell<TData, unknown>): boolean {
|
/**
|
||||||
const meta = cell.column.columnDef.meta
|
* Shared, column-meta-driven card content rendering for TanStack rows.
|
||||||
return meta?.cardSpan === 2 || meta?.contentMode === 'summary'
|
*
|
||||||
}
|
* Both {@link MobileCardList} (mobile) and {@link DataTableCardGrid} (desktop
|
||||||
|
* card view) render the same inner content; only the surrounding container
|
||||||
|
* differs (single bordered list vs. responsive grid of cards). Keeping the
|
||||||
|
* per-row content here guarantees the two stay visually consistent.
|
||||||
|
*
|
||||||
|
* Column meta extensions (see `card-cell-utils.ts`):
|
||||||
|
* - `mobileTitle` — card header (left, larger text)
|
||||||
|
* - `mobileBadge` — inline with title (right, e.g. status badge)
|
||||||
|
* - `mobileHidden` — hidden in card content
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shared row content for both the mobile list and optional desktop card grid.
|
* Compact content — structured layout with title header + side-by-side fields.
|
||||||
* All visible fields render immediately — no "More" click to reveal content.
|
* Used when columns define mobileTitle or mobileBadge meta.
|
||||||
|
*
|
||||||
|
* Visual structure:
|
||||||
|
* [Title content] [Badge]
|
||||||
|
* [Field1 label] [Field2 label]
|
||||||
|
* [Field1 value] [Field2 value]
|
||||||
|
* [Actions ⋯]
|
||||||
*/
|
*/
|
||||||
export function CardRowContent<TData>(props: {
|
function CompactContent<TData>({ row }: { row: Row<TData> }) {
|
||||||
row: Row<TData>
|
const allCells = row
|
||||||
compact: boolean
|
|
||||||
}) {
|
|
||||||
const cells = props.row
|
|
||||||
.getVisibleCells()
|
.getVisibleCells()
|
||||||
.filter((cell) => cell.column.id !== 'select')
|
.filter((cell) => cell.column.id !== 'select')
|
||||||
const titleCell = cells.find((cell) => getCardRole(cell) === 'title')
|
|
||||||
const badgeCell = cells.find((cell) => getCardRole(cell) === 'badge')
|
// Read each cell's meta once, then reuse for all categorisation checks.
|
||||||
const actionsCell = cells.find((cell) => cell.column.id === 'actions')
|
const cellMetas = React.useMemo(
|
||||||
const bodyCells = orderCardCells(
|
() => allCells.map((c) => c.column.columnDef.meta),
|
||||||
cells.filter(
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
(cell) =>
|
[allCells.map((c) => c.id).join(',')]
|
||||||
cell !== titleCell &&
|
)
|
||||||
cell !== badgeCell &&
|
|
||||||
cell !== actionsCell &&
|
const titleCell = allCells.find((_, i) => cellMetas[i]?.mobileTitle)
|
||||||
getCardRole(cell) !== 'hidden'
|
const badgeCell = allCells.find((_, i) => cellMetas[i]?.mobileBadge)
|
||||||
|
const actionsCell = allCells.find((c) => c.column.id === 'actions')
|
||||||
|
const fieldCells = orderCardCells(
|
||||||
|
allCells.filter(
|
||||||
|
(c, i) =>
|
||||||
|
c !== titleCell &&
|
||||||
|
c !== badgeCell &&
|
||||||
|
c !== actionsCell &&
|
||||||
|
!cellMetas[i]?.mobileHidden
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
const rowCells = bodyCells.filter((cell) => !isWideField(cell))
|
|
||||||
const wideCells = bodyCells.filter((cell) => isWideField(cell))
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex min-w-0 flex-col'>
|
<>
|
||||||
{props.compact && (titleCell || badgeCell) && (
|
{/* Row 1: Title + Badge */}
|
||||||
<div className='flex min-w-0 flex-wrap items-start justify-between gap-3'>
|
<div className='flex items-center justify-between gap-2'>
|
||||||
<div className='min-w-0 flex-1 text-[15px] leading-tight font-semibold [overflow-wrap:anywhere] break-words whitespace-normal [&_.truncate]:overflow-visible [&_.truncate]:text-clip [&_.truncate]:whitespace-normal'>
|
{titleCell && (
|
||||||
{titleCell ? renderCellContent(titleCell) : null}
|
<div className='min-w-0 flex-1 text-sm font-medium [&_[data-slot=status-badge]]:max-w-full [&_[data-slot=status-badge]]:whitespace-normal'>
|
||||||
|
{renderCellContent(titleCell)}
|
||||||
</div>
|
</div>
|
||||||
{badgeCell && (
|
)}
|
||||||
<div className='shrink-0 text-right'>
|
{badgeCell && (
|
||||||
{renderCellContent(badgeCell)}
|
<div className='flex-none [&_[data-slot=status-badge]]:max-w-none'>
|
||||||
</div>
|
{renderCellContent(badgeCell)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
{!props.compact && (
|
{/* Row 2: Key fields wrap into compact columns instead of squeezing */}
|
||||||
<div className='grid grid-cols-2 gap-x-3 gap-y-2'>
|
{fieldCells.length > 0 && (
|
||||||
{bodyCells.map((cell) => {
|
<div className='mt-1.5 grid grid-cols-2 gap-x-3 gap-y-1.5'>
|
||||||
const meta = cell.column.columnDef.meta
|
{fieldCells.map((cell) => {
|
||||||
|
const label = getCellLabel(cell)
|
||||||
return (
|
return (
|
||||||
<DataTableCardField
|
<div key={cell.id} className='min-w-0 flex-1 overflow-hidden'>
|
||||||
key={cell.id}
|
{label && (
|
||||||
label={getCellLabel(cell)}
|
<div className='text-muted-foreground mb-0.5 text-[10px] leading-none select-none'>
|
||||||
contentMode={meta?.contentMode}
|
{label}
|
||||||
span={meta?.cardSpan}
|
</div>
|
||||||
>
|
)}
|
||||||
{renderCellContent(cell)}
|
<div className='min-w-0 overflow-hidden text-xs'>
|
||||||
</DataTableCardField>
|
<StatusBadgeTypeContext.Provider value='text'>
|
||||||
|
{renderCellContent(cell) ?? '-'}
|
||||||
|
</StatusBadgeTypeContext.Provider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{props.compact && rowCells.length > 0 && (
|
{/* Actions */}
|
||||||
<div className='mt-3 space-y-0.5 border-t pt-3'>
|
|
||||||
{rowCells.map((cell) => (
|
|
||||||
<DataTableCardRow
|
|
||||||
key={cell.id}
|
|
||||||
label={getCellLabel(cell)}
|
|
||||||
contentMode={cell.column.columnDef.meta?.contentMode}
|
|
||||||
>
|
|
||||||
{renderCellContent(cell)}
|
|
||||||
</DataTableCardRow>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{props.compact && wideCells.length > 0 && (
|
|
||||||
<div className='mt-3 space-y-3 border-t pt-3'>
|
|
||||||
{wideCells.map((cell) => (
|
|
||||||
<DataTableCardField
|
|
||||||
key={cell.id}
|
|
||||||
label={getCellLabel(cell)}
|
|
||||||
contentMode={cell.column.columnDef.meta?.contentMode ?? 'full'}
|
|
||||||
>
|
|
||||||
{renderCellContent(cell)}
|
|
||||||
</DataTableCardField>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{actionsCell && (
|
{actionsCell && (
|
||||||
<div className='mt-3 flex justify-end border-t pt-2'>
|
<div className='mt-1 -mb-0.5 flex justify-end'>
|
||||||
{renderCellContent(actionsCell)}
|
{renderCellContent(actionsCell)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fallback content — condensed label:value pairs for tables without
|
||||||
|
* mobileTitle/mobileBadge. Still respects mobileHidden.
|
||||||
|
*/
|
||||||
|
function FallbackContent<TData>({ row }: { row: Row<TData> }) {
|
||||||
|
const allCells = row
|
||||||
|
.getVisibleCells()
|
||||||
|
.filter((cell) => cell.column.id !== 'select')
|
||||||
|
|
||||||
|
const cellMetas = React.useMemo(
|
||||||
|
() => allCells.map((c) => c.column.columnDef.meta),
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[allCells.map((c) => c.id).join(',')]
|
||||||
|
)
|
||||||
|
|
||||||
|
const actionsCell = allCells.find((c) => c.column.id === 'actions')
|
||||||
|
const contentCells = orderCardCells(
|
||||||
|
allCells.filter(
|
||||||
|
(c, i) => c.column.id !== 'actions' && !cellMetas[i]?.mobileHidden
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{contentCells.map((cell) => {
|
||||||
|
const label = getCellLabel(cell)
|
||||||
|
|
||||||
|
if (!label) {
|
||||||
|
return (
|
||||||
|
<div key={cell.id} className='flex justify-end overflow-hidden'>
|
||||||
|
<StatusBadgeTypeContext.Provider value='text'>
|
||||||
|
{renderCellContent(cell)}
|
||||||
|
</StatusBadgeTypeContext.Provider>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={cell.id}
|
||||||
|
className='flex items-start justify-between gap-2 overflow-hidden'
|
||||||
|
>
|
||||||
|
<span className='text-muted-foreground shrink-0 text-[10px] font-medium select-none'>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
<div className='flex min-w-0 flex-1 items-center justify-end overflow-hidden text-xs'>
|
||||||
|
<StatusBadgeTypeContext.Provider value='text'>
|
||||||
|
{renderCellContent(cell) ?? '-'}
|
||||||
|
</StatusBadgeTypeContext.Provider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
{actionsCell && (
|
||||||
|
<div className='-mb-0.5 flex justify-end pt-0.5'>
|
||||||
|
{renderCellContent(actionsCell)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a single row's card content, auto-selecting the compact or fallback
|
||||||
|
* layout. Callers compute `compact` once per table (via `tableHasCompactMeta`)
|
||||||
|
* and pass it down to avoid recomputation per row.
|
||||||
|
*/
|
||||||
|
export function CardRowContent<TData>(props: {
|
||||||
|
row: Row<TData>
|
||||||
|
compact: boolean
|
||||||
|
}) {
|
||||||
|
return props.compact ? (
|
||||||
|
<CompactContent row={props.row} />
|
||||||
|
) : (
|
||||||
|
<FallbackContent row={props.row} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,11 +64,6 @@ export type DataTablePageProps<TData> = {
|
|||||||
*/
|
*/
|
||||||
columns: ColumnDef<TData, unknown>[]
|
columns: ColumnDef<TData, unknown>[]
|
||||||
|
|
||||||
/**
|
|
||||||
* Localized accessible name for the keyboard-scrollable table region.
|
|
||||||
*/
|
|
||||||
tableLabel?: string
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initial loading state — renders {@link TableSkeleton} or mobile skeleton.
|
* Initial loading state — renders {@link TableSkeleton} or mobile skeleton.
|
||||||
*/
|
*/
|
||||||
@@ -324,7 +319,6 @@ export function DataTablePage<TData>(props: DataTablePageProps<TData>) {
|
|||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full min-w-0',
|
|
||||||
props.fixedHeight !== false
|
props.fixedHeight !== false
|
||||||
? 'flex h-full min-h-0 flex-col gap-2.5 sm:gap-3'
|
? 'flex h-full min-h-0 flex-col gap-2.5 sm:gap-3'
|
||||||
: 'space-y-2.5 sm:space-y-3',
|
: 'space-y-2.5 sm:space-y-3',
|
||||||
@@ -412,7 +406,6 @@ function renderMobile<TData>(
|
|||||||
isLoading={props.isLoading}
|
isLoading={props.isLoading}
|
||||||
emptyTitle={props.emptyTitle}
|
emptyTitle={props.emptyTitle}
|
||||||
emptyDescription={props.emptyDescription}
|
emptyDescription={props.emptyDescription}
|
||||||
scrollLabel={props.tableLabel}
|
|
||||||
emptyIcon={props.emptyIcon}
|
emptyIcon={props.emptyIcon}
|
||||||
emptyAction={props.emptyAction}
|
emptyAction={props.emptyAction}
|
||||||
skeletonKeyPrefix={props.skeletonKeyPrefix}
|
skeletonKeyPrefix={props.skeletonKeyPrefix}
|
||||||
@@ -424,7 +417,11 @@ function renderMobile<TData>(
|
|||||||
)}
|
)}
|
||||||
getColumnClassName={props.getColumnClassName}
|
getColumnClassName={props.getColumnClassName}
|
||||||
pinnedColumns={props.pinnedColumns}
|
pinnedColumns={props.pinnedColumns}
|
||||||
containerClassName={props.tableClassName}
|
containerClassName={cn(
|
||||||
|
'transition-opacity duration-150',
|
||||||
|
isFetchingOnly && 'pointer-events-none opacity-60',
|
||||||
|
props.tableClassName
|
||||||
|
)}
|
||||||
getRowClassName={(row) =>
|
getRowClassName={(row) =>
|
||||||
props.getRowClassName?.(row, { isMobile: false })
|
props.getRowClassName?.(row, { isMobile: false })
|
||||||
}
|
}
|
||||||
@@ -438,7 +435,6 @@ function renderMobile<TData>(
|
|||||||
emptyTitle={props.emptyTitle}
|
emptyTitle={props.emptyTitle}
|
||||||
emptyDescription={props.emptyDescription}
|
emptyDescription={props.emptyDescription}
|
||||||
emptyIcon={props.emptyIcon}
|
emptyIcon={props.emptyIcon}
|
||||||
emptyAction={props.emptyAction}
|
|
||||||
renderCard={props.renderCard}
|
renderCard={props.renderCard}
|
||||||
gridClassName={props.cardGridClassName ?? 'grid grid-cols-1 gap-3'}
|
gridClassName={props.cardGridClassName ?? 'grid grid-cols-1 gap-3'}
|
||||||
skeletonKeyPrefix={props.skeletonKeyPrefix}
|
skeletonKeyPrefix={props.skeletonKeyPrefix}
|
||||||
@@ -453,8 +449,6 @@ function renderMobile<TData>(
|
|||||||
isLoading={props.isLoading}
|
isLoading={props.isLoading}
|
||||||
emptyTitle={props.emptyTitle}
|
emptyTitle={props.emptyTitle}
|
||||||
emptyDescription={props.emptyDescription}
|
emptyDescription={props.emptyDescription}
|
||||||
emptyIcon={props.emptyIcon}
|
|
||||||
emptyAction={props.emptyAction}
|
|
||||||
getRowKey={props.mobileProps?.getRowKey}
|
getRowKey={props.mobileProps?.getRowKey}
|
||||||
getRowClassName={mobileGetRowClassName}
|
getRowClassName={mobileGetRowClassName}
|
||||||
/>
|
/>
|
||||||
@@ -462,17 +456,7 @@ function renderMobile<TData>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <div className='min-h-0 flex-1 overflow-y-auto'>{mobileContent}</div>
|
||||||
<div
|
|
||||||
aria-busy={props.isLoading || props.isFetching || undefined}
|
|
||||||
className={cn(
|
|
||||||
'min-h-0 min-w-0 flex-1 overflow-x-hidden overflow-y-auto transition-opacity duration-150',
|
|
||||||
isFetchingOnly && 'opacity-60'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{mobileContent}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDesktop<TData>(
|
function renderDesktop<TData>(
|
||||||
@@ -494,9 +478,8 @@ function renderDesktop<TData>(
|
|||||||
className={cn(
|
className={cn(
|
||||||
fixedHeight && 'min-h-0 flex-1 overflow-y-auto',
|
fixedHeight && 'min-h-0 flex-1 overflow-y-auto',
|
||||||
'transition-opacity duration-150',
|
'transition-opacity duration-150',
|
||||||
isFetchingOnly && 'opacity-60'
|
isFetchingOnly && 'pointer-events-none opacity-60'
|
||||||
)}
|
)}
|
||||||
aria-busy={props.isLoading || props.isFetching || undefined}
|
|
||||||
>
|
>
|
||||||
<DataTableCardGrid
|
<DataTableCardGrid
|
||||||
table={props.table}
|
table={props.table}
|
||||||
@@ -504,7 +487,6 @@ function renderDesktop<TData>(
|
|||||||
emptyTitle={props.emptyTitle}
|
emptyTitle={props.emptyTitle}
|
||||||
emptyDescription={props.emptyDescription}
|
emptyDescription={props.emptyDescription}
|
||||||
emptyIcon={props.emptyIcon}
|
emptyIcon={props.emptyIcon}
|
||||||
emptyAction={props.emptyAction}
|
|
||||||
renderCard={props.renderCard}
|
renderCard={props.renderCard}
|
||||||
gridClassName={props.cardGridClassName}
|
gridClassName={props.cardGridClassName}
|
||||||
skeletonKeyPrefix={props.skeletonKeyPrefix}
|
skeletonKeyPrefix={props.skeletonKeyPrefix}
|
||||||
@@ -522,7 +504,6 @@ function renderDesktop<TData>(
|
|||||||
isLoading={props.isLoading}
|
isLoading={props.isLoading}
|
||||||
emptyTitle={props.emptyTitle}
|
emptyTitle={props.emptyTitle}
|
||||||
emptyDescription={props.emptyDescription}
|
emptyDescription={props.emptyDescription}
|
||||||
scrollLabel={props.tableLabel}
|
|
||||||
emptyIcon={props.emptyIcon}
|
emptyIcon={props.emptyIcon}
|
||||||
emptyAction={props.emptyAction}
|
emptyAction={props.emptyAction}
|
||||||
skeletonKeyPrefix={props.skeletonKeyPrefix}
|
skeletonKeyPrefix={props.skeletonKeyPrefix}
|
||||||
@@ -539,12 +520,9 @@ function renderDesktop<TData>(
|
|||||||
containerClassName={cn(
|
containerClassName={cn(
|
||||||
fixedHeight && 'min-h-0 flex-1',
|
fixedHeight && 'min-h-0 flex-1',
|
||||||
'transition-opacity duration-150',
|
'transition-opacity duration-150',
|
||||||
isFetchingOnly && 'opacity-60',
|
isFetchingOnly && 'pointer-events-none opacity-60',
|
||||||
props.tableClassName
|
props.tableClassName
|
||||||
)}
|
)}
|
||||||
containerProps={{
|
|
||||||
'aria-busy': props.isLoading || props.isFetching || undefined,
|
|
||||||
}}
|
|
||||||
getRowClassName={(row) =>
|
getRowClassName={(row) =>
|
||||||
props.getRowClassName?.(row, { isMobile: false })
|
props.getRowClassName?.(row, { isMobile: false })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,16 +34,13 @@ import { cn } from '@/lib/utils'
|
|||||||
import { tableHasCompactMeta } from './card-cell-utils'
|
import { tableHasCompactMeta } from './card-cell-utils'
|
||||||
import { CardRowContent } from './card-row-content'
|
import { CardRowContent } from './card-row-content'
|
||||||
|
|
||||||
export interface MobileCardListProps<TData> {
|
interface MobileCardListProps<TData> {
|
||||||
table: Table<TData>
|
table: Table<TData>
|
||||||
isLoading?: boolean
|
isLoading?: boolean
|
||||||
emptyTitle?: string
|
emptyTitle?: string
|
||||||
emptyDescription?: string
|
emptyDescription?: string
|
||||||
emptyIcon?: React.ReactNode
|
|
||||||
emptyAction?: React.ReactNode
|
|
||||||
getRowKey?: (row: Row<TData>) => string | number
|
getRowKey?: (row: Row<TData>) => string | number
|
||||||
getRowClassName?: (row: Row<TData>) => string | undefined
|
getRowClassName?: (row: Row<TData>) => string | undefined
|
||||||
renderCard?: (row: Row<TData>) => React.ReactNode
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ListSkeleton() {
|
function ListSkeleton() {
|
||||||
@@ -96,7 +93,7 @@ function FallbackListSkeleton() {
|
|||||||
*
|
*
|
||||||
* Per-row content is shared with the desktop card view via
|
* Per-row content is shared with the desktop card view via
|
||||||
* {@link CardRowContent}; see `card-row-content.tsx` for the column-meta
|
* {@link CardRowContent}; see `card-row-content.tsx` for the column-meta
|
||||||
* extensions (`cardRole`, `cardOrder`, `cardSpan`, `contentMode`).
|
* extensions (`mobileTitle`, `mobileBadge`, `mobileHidden`).
|
||||||
*/
|
*/
|
||||||
export function MobileCardList<TData>(props: MobileCardListProps<TData>) {
|
export function MobileCardList<TData>(props: MobileCardListProps<TData>) {
|
||||||
const {
|
const {
|
||||||
@@ -131,34 +128,29 @@ export function MobileCardList<TData>(props: MobileCardListProps<TData>) {
|
|||||||
<Empty className='border-none p-0'>
|
<Empty className='border-none p-0'>
|
||||||
<EmptyHeader>
|
<EmptyHeader>
|
||||||
<EmptyMedia variant='icon'>
|
<EmptyMedia variant='icon'>
|
||||||
{props.emptyIcon ?? <Database className='size-6' />}
|
<Database className='size-6' />
|
||||||
</EmptyMedia>
|
</EmptyMedia>
|
||||||
<EmptyTitle>{resolvedEmptyTitle}</EmptyTitle>
|
<EmptyTitle>{resolvedEmptyTitle}</EmptyTitle>
|
||||||
<EmptyDescription>{resolvedEmptyDescription}</EmptyDescription>
|
<EmptyDescription>{resolvedEmptyDescription}</EmptyDescription>
|
||||||
</EmptyHeader>
|
</EmptyHeader>
|
||||||
{props.emptyAction}
|
|
||||||
</Empty>
|
</Empty>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='min-w-0 divide-y overflow-hidden rounded-lg border'>
|
<div className='divide-y overflow-hidden rounded-lg border'>
|
||||||
{rows.map((row) => {
|
{rows.map((row) => {
|
||||||
const key = getRowKey ? getRowKey(row) : row.id
|
const key = getRowKey ? getRowKey(row) : row.id
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={key}
|
key={key}
|
||||||
className={cn(
|
className={cn(
|
||||||
'[background-color:var(--data-table-card-bg,var(--table-row))] px-3.5 py-3',
|
'[background-color:var(--data-table-card-bg,var(--table-row))] px-3 py-2.5',
|
||||||
getRowClassName?.(row)
|
getRowClassName?.(row)
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{props.renderCard ? (
|
<CardRowContent row={row} compact={hasCompactMeta} />
|
||||||
props.renderCard(row)
|
|
||||||
) : (
|
|
||||||
<CardRowContent row={row} compact={hasCompactMeta} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
+3
-2
@@ -24,9 +24,10 @@ export const staticDataTableClassNames = {
|
|||||||
compactHeaderRow: 'hover:bg-transparent',
|
compactHeaderRow: 'hover:bg-transparent',
|
||||||
mutedHeaderRow:
|
mutedHeaderRow:
|
||||||
'[background-color:var(--table-header)] hover:[background-color:var(--table-header-hover)]',
|
'[background-color:var(--table-header)] hover:[background-color:var(--table-header-hover)]',
|
||||||
compactHeaderCell: 'text-muted-foreground py-2 text-xs font-medium',
|
compactHeaderCell:
|
||||||
|
'text-muted-foreground py-2 text-[10px] font-medium tracking-wider uppercase',
|
||||||
compactHeaderCellRight:
|
compactHeaderCellRight:
|
||||||
'text-muted-foreground py-2 text-right text-xs font-medium',
|
'text-muted-foreground py-2 text-right text-[10px] font-medium tracking-wider uppercase',
|
||||||
compactCell: 'py-2.5',
|
compactCell: 'py-2.5',
|
||||||
compactTopCell: 'py-2.5 align-top',
|
compactTopCell: 'py-2.5 align-top',
|
||||||
compactTopNumericCell: 'py-2.5 text-right align-top font-mono',
|
compactTopNumericCell: 'py-2.5 text-right align-top font-mono',
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/design-system/table'
|
} from '@/components/ui/table'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
import { TruncatedCell } from '../core/truncated-cell'
|
import { TruncatedCell } from '../core/truncated-cell'
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
*/
|
*/
|
||||||
import { Pencil, Trash2 } from 'lucide-react'
|
import { Pencil, Trash2 } from 'lucide-react'
|
||||||
|
|
||||||
import { Button } from '@/components/design-system/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuShortcut,
|
DropdownMenuShortcut,
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ import { X } from 'lucide-react'
|
|||||||
import { useState, useEffect, useLayoutEffect, useRef } from 'react'
|
import { useState, useEffect, useLayoutEffect, useRef } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { Button } from '@/components/design-system/button'
|
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -184,8 +184,9 @@ export function DataTableBulkActions<TData>({
|
|||||||
render={
|
render={
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
size='icon-xs'
|
size='icon'
|
||||||
onClick={handleClearSelection}
|
onClick={handleClearSelection}
|
||||||
|
className='size-6'
|
||||||
aria-label={t('Clear selection')}
|
aria-label={t('Clear selection')}
|
||||||
title={t('Clear selection (Escape)')}
|
title={t('Clear selection (Escape)')}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -16,12 +16,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
import type { Column } from '@tanstack/react-table'
|
import { type Column } from '@tanstack/react-table'
|
||||||
import { Check as CheckIcon, ChevronDown } from 'lucide-react'
|
import { Check as CheckIcon, PlusCircle as PlusCircledIcon } from 'lucide-react'
|
||||||
import type { ComponentType, ReactNode } from 'react'
|
import * as React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { Button } from '@/components/design-system/button'
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
@@ -30,12 +31,13 @@ import {
|
|||||||
CommandItem,
|
CommandItem,
|
||||||
CommandList,
|
CommandList,
|
||||||
CommandSeparator,
|
CommandSeparator,
|
||||||
} from '@/components/design-system/command'
|
} from '@/components/ui/command'
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from '@/components/ui/popover'
|
} from '@/components/ui/popover'
|
||||||
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
type DataTableFacetedFilterProps<TData, TValue> = {
|
type DataTableFacetedFilterProps<TData, TValue> = {
|
||||||
@@ -44,15 +46,15 @@ type DataTableFacetedFilterProps<TData, TValue> = {
|
|||||||
options: {
|
options: {
|
||||||
label: string
|
label: string
|
||||||
value: string
|
value: string
|
||||||
icon?: ComponentType<{ className?: string }>
|
icon?: React.ComponentType<{ className?: string }>
|
||||||
iconNode?: ReactNode
|
iconNode?: React.ReactNode
|
||||||
count?: number
|
count?: number
|
||||||
}[]
|
}[]
|
||||||
/** Enable single select mode (only one option can be selected at a time) */
|
/** Enable single select mode (only one option can be selected at a time) */
|
||||||
singleSelect?: boolean
|
singleSelect?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DataTableFacetedFilter<TData, TValue>({
|
function DataTableFacetedFilterInner<TData, TValue>({
|
||||||
column,
|
column,
|
||||||
title,
|
title,
|
||||||
options,
|
options,
|
||||||
@@ -62,21 +64,6 @@ export function DataTableFacetedFilter<TData, TValue>({
|
|||||||
const facets = column?.getFacetedUniqueValues()
|
const facets = column?.getFacetedUniqueValues()
|
||||||
const filterValue = column?.getFilterValue() as string[] | undefined
|
const filterValue = column?.getFilterValue() as string[] | undefined
|
||||||
const selectedValues = new Set(filterValue)
|
const selectedValues = new Set(filterValue)
|
||||||
const resolvedTitle = title ?? t('Filter')
|
|
||||||
const selectedOptions = options.filter((option) =>
|
|
||||||
selectedValues.has(option.value)
|
|
||||||
)
|
|
||||||
|
|
||||||
let selectedSummary: string | null = null
|
|
||||||
if (selectedValues.size === 1 && selectedOptions.length === 1) {
|
|
||||||
selectedSummary = t(selectedOptions[0].label)
|
|
||||||
} else if (selectedValues.size === 2 && selectedOptions.length === 2) {
|
|
||||||
selectedSummary = selectedOptions
|
|
||||||
.map((option) => t(option.label))
|
|
||||||
.join(', ')
|
|
||||||
} else if (selectedValues.size > 0) {
|
|
||||||
selectedSummary = `${selectedValues.size} ${t('selected')}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleOptionSelect = (optionValue: string) => {
|
const handleOptionSelect = (optionValue: string) => {
|
||||||
const nextSelectedValues = getNextSelectedValues(
|
const nextSelectedValues = getNextSelectedValues(
|
||||||
@@ -94,69 +81,57 @@ export function DataTableFacetedFilter<TData, TValue>({
|
|||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger
|
<PopoverTrigger
|
||||||
render={
|
render={
|
||||||
<Button
|
<Button variant='outline' size='sm' className='h-8 border-dashed' />
|
||||||
variant='outline'
|
|
||||||
className='w-full min-w-0 justify-start font-normal'
|
|
||||||
aria-label={
|
|
||||||
selectedSummary
|
|
||||||
? `${resolvedTitle}: ${selectedSummary}`
|
|
||||||
: resolvedTitle
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span className='text-muted-foreground min-w-0 truncate text-left'>
|
<PlusCircledIcon className='size-4' />
|
||||||
{resolvedTitle}
|
{title}
|
||||||
</span>
|
{selectedValues?.size > 0 && (
|
||||||
{selectedSummary && (
|
|
||||||
<>
|
<>
|
||||||
<span className='text-muted-foreground' aria-hidden='true'>
|
<Separator orientation='vertical' className='mx-2 h-4' />
|
||||||
:
|
<Badge
|
||||||
</span>
|
variant='secondary'
|
||||||
<span className='text-foreground min-w-0 flex-1 truncate text-left'>
|
className='rounded-sm px-1 font-normal lg:hidden'
|
||||||
{selectedSummary}
|
>
|
||||||
</span>
|
{selectedValues.size}
|
||||||
|
</Badge>
|
||||||
|
<div className='hidden space-x-1 lg:flex'>
|
||||||
|
{selectedValues.size > 2 ? (
|
||||||
|
<Badge
|
||||||
|
variant='secondary'
|
||||||
|
className='rounded-sm px-1 font-normal'
|
||||||
|
>
|
||||||
|
{selectedValues.size} {t('selected')}
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
options
|
||||||
|
.filter((option) => selectedValues.has(option.value))
|
||||||
|
.map((option) => (
|
||||||
|
<Badge
|
||||||
|
variant='secondary'
|
||||||
|
key={option.value}
|
||||||
|
className='rounded-sm px-1 font-normal'
|
||||||
|
>
|
||||||
|
{t(option.label)}
|
||||||
|
</Badge>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<ChevronDown className='text-muted-foreground ms-auto size-3.5 shrink-0 transition-transform duration-150 group-data-[popup-open]/button:rotate-180' />
|
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent
|
<PopoverContent className='max-w-[360px] min-w-[200px] p-0' align='start'>
|
||||||
className='w-(--anchor-width) max-w-[360px] min-w-52 p-0'
|
|
||||||
align='start'
|
|
||||||
>
|
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput
|
<CommandInput placeholder={title} />
|
||||||
placeholder={resolvedTitle}
|
|
||||||
aria-label={resolvedTitle}
|
|
||||||
/>
|
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>{t('No results found.')}</CommandEmpty>
|
<CommandEmpty>{t('No results found.')}</CommandEmpty>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{options.map((option) => {
|
{options.map((option) => {
|
||||||
const isSelected = selectedValues.has(option.value)
|
const isSelected = selectedValues.has(option.value)
|
||||||
let optionIcon: ReactNode = null
|
|
||||||
if (option.iconNode) {
|
|
||||||
optionIcon = (
|
|
||||||
<span className='text-muted-foreground flex size-4 items-center justify-center'>
|
|
||||||
{option.iconNode}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
} else if (option.icon) {
|
|
||||||
optionIcon = (
|
|
||||||
<option.icon className='text-muted-foreground size-4' />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const optionCount =
|
|
||||||
typeof option.count === 'number'
|
|
||||||
? option.count
|
|
||||||
: facets?.get(option.value)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={option.value}
|
key={option.value}
|
||||||
onSelect={() => handleOptionSelect(option.value)}
|
onSelect={() => handleOptionSelect(option.value)}
|
||||||
aria-selected={isSelected}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -166,23 +141,30 @@ export function DataTableFacetedFilter<TData, TValue>({
|
|||||||
: 'opacity-50 [&_svg]:invisible'
|
: 'opacity-50 [&_svg]:invisible'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<CheckIcon
|
<CheckIcon className={cn('text-background h-4 w-4')} />
|
||||||
className={cn('text-background h-4 w-4')}
|
|
||||||
aria-hidden='true'
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{optionIcon}
|
{option.iconNode ? (
|
||||||
|
<span className='text-muted-foreground flex size-4 items-center justify-center'>
|
||||||
|
{option.iconNode}
|
||||||
|
</span>
|
||||||
|
) : option.icon ? (
|
||||||
|
<option.icon className='text-muted-foreground size-4' />
|
||||||
|
) : null}
|
||||||
<span
|
<span
|
||||||
className='min-w-0 flex-1 truncate'
|
className='min-w-0 flex-1 truncate'
|
||||||
title={t(option.label)}
|
title={t(option.label)}
|
||||||
>
|
>
|
||||||
{t(option.label)}
|
{t(option.label)}
|
||||||
</span>
|
</span>
|
||||||
{optionCount != null && (
|
{typeof option.count === 'number' ? (
|
||||||
<span className='text-muted-foreground ms-auto flex h-4 min-w-4 items-center justify-center text-xs tabular-nums'>
|
<span className='text-muted-foreground ms-auto flex h-4 min-w-4 items-center justify-center font-mono text-xs'>
|
||||||
{optionCount}
|
{option.count}
|
||||||
</span>
|
</span>
|
||||||
)}
|
) : facets?.get(option.value) ? (
|
||||||
|
<span className='ms-auto flex h-4 w-4 items-center justify-center font-mono text-xs'>
|
||||||
|
{facets.get(option.value)}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@@ -207,6 +189,10 @@ export function DataTableFacetedFilter<TData, TValue>({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DataTableFacetedFilter = React.memo(
|
||||||
|
DataTableFacetedFilterInner
|
||||||
|
) as typeof DataTableFacetedFilterInner
|
||||||
|
|
||||||
function getNextSelectedValues(
|
function getNextSelectedValues(
|
||||||
selectedValues: Set<string>,
|
selectedValues: Set<string>,
|
||||||
optionValue: string,
|
optionValue: string,
|
||||||
@@ -223,5 +209,5 @@ function getNextSelectedValues(
|
|||||||
nextSelectedValues.add(optionValue)
|
nextSelectedValues.add(optionValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
return [...nextSelectedValues]
|
return Array.from(nextSelectedValues)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,314 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright (C) 2023-2026 QuantumNous
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as
|
|
||||||
published by the Free Software Foundation, either version 3 of the
|
|
||||||
License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
|
||||||
*/
|
|
||||||
import type { Table } from '@tanstack/react-table'
|
|
||||||
import { ChevronDown, Loader2 } from 'lucide-react'
|
|
||||||
import { useState, type ComponentProps, type ReactNode } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
import { Button } from '@/components/design-system/button'
|
|
||||||
import { Input } from '@/components/design-system/input'
|
|
||||||
import { Badge } from '@/components/ui/badge'
|
|
||||||
import {
|
|
||||||
Drawer,
|
|
||||||
DrawerContent,
|
|
||||||
DrawerDescription,
|
|
||||||
DrawerFooter,
|
|
||||||
DrawerHeader,
|
|
||||||
DrawerTitle,
|
|
||||||
DrawerTrigger,
|
|
||||||
} from '@/components/ui/drawer'
|
|
||||||
import { useMediaQuery } from '@/hooks'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
import { DataTableViewOptions } from './view-options'
|
|
||||||
|
|
||||||
export interface DataTableFilterPanelProps<TData> {
|
|
||||||
table: Table<TData>
|
|
||||||
primaryFilters: ReactNode
|
|
||||||
advancedFilters?: ReactNode
|
|
||||||
mobilePinnedFilters?: ReactNode
|
|
||||||
mobileFilters?: ReactNode
|
|
||||||
mobileFilterCount?: number
|
|
||||||
stats?: ReactNode
|
|
||||||
actionStart?: ReactNode
|
|
||||||
viewToggle?: ReactNode
|
|
||||||
hideViewOptions?: boolean
|
|
||||||
hasActiveFilters: boolean
|
|
||||||
hasAdvancedActiveFilters?: boolean
|
|
||||||
advancedFilterCount?: number
|
|
||||||
searchLoading?: boolean
|
|
||||||
onReset: () => void
|
|
||||||
onSearch?: () => void
|
|
||||||
inlineActions?: boolean
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DataTableFilterFieldProps {
|
|
||||||
children: ReactNode
|
|
||||||
wide?: boolean
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DataTableFilterField(props: DataTableFilterFieldProps) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'min-w-0 [&>button]:w-full [&>input]:w-full [&_[data-slot=select-trigger]]:w-full [&_[data-slot=select-trigger]]:text-sm [&_[data-slot=select-value]]:leading-5',
|
|
||||||
props.wide && 'sm:col-span-2',
|
|
||||||
props.className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DataTableFilterInput(props: ComponentProps<typeof Input>) {
|
|
||||||
return (
|
|
||||||
<Input
|
|
||||||
{...props}
|
|
||||||
className={cn('min-w-0 text-sm leading-5', props.className)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DataTableFilterPanel<TData>(
|
|
||||||
props: DataTableFilterPanelProps<TData>
|
|
||||||
) {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const [advancedOpen, setAdvancedOpen] = useState(false)
|
|
||||||
const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false)
|
|
||||||
const isMobile = useMediaQuery('(max-width: 640px)')
|
|
||||||
|
|
||||||
const hasAdvancedFilters = props.advancedFilters != null
|
|
||||||
const hasMobileFilters =
|
|
||||||
props.mobileFilters != null || props.advancedFilters != null
|
|
||||||
const activeAdvancedCount =
|
|
||||||
props.advancedFilterCount ?? (props.hasAdvancedActiveFilters ? 1 : 0)
|
|
||||||
const activeMobileFilterCount = props.mobileFilterCount ?? activeAdvancedCount
|
|
||||||
|
|
||||||
const handleMobileReset = () => {
|
|
||||||
props.onReset()
|
|
||||||
setMobileFiltersOpen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMobileSubmit = () => {
|
|
||||||
props.onSearch?.()
|
|
||||||
setMobileFiltersOpen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const advancedToggle = hasAdvancedFilters ? (
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
variant='ghost'
|
|
||||||
onClick={() => setAdvancedOpen((open) => !open)}
|
|
||||||
aria-expanded={advancedOpen}
|
|
||||||
className={cn(
|
|
||||||
'text-muted-foreground hover:text-foreground gap-1 px-2',
|
|
||||||
props.hasAdvancedActiveFilters &&
|
|
||||||
!advancedOpen &&
|
|
||||||
'text-primary hover:text-primary'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{advancedOpen ? t('Collapse') : t('Expand')}
|
|
||||||
{activeAdvancedCount > 0 && (
|
|
||||||
<Badge variant='secondary' className='ml-0.5'>
|
|
||||||
{activeAdvancedCount}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
<ChevronDown
|
|
||||||
className={cn(
|
|
||||||
'size-3.5 transition-transform duration-200',
|
|
||||||
advancedOpen && 'rotate-180'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
) : null
|
|
||||||
|
|
||||||
const viewOptions = !props.hideViewOptions ? (
|
|
||||||
<DataTableViewOptions table={props.table} />
|
|
||||||
) : null
|
|
||||||
|
|
||||||
const desktopActions = (
|
|
||||||
<div className='ms-auto flex shrink-0 flex-wrap items-center justify-end gap-1.5 sm:gap-2'>
|
|
||||||
{props.actionStart}
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
variant={props.onSearch ? 'outline' : 'ghost'}
|
|
||||||
onClick={props.onReset}
|
|
||||||
disabled={!props.hasActiveFilters}
|
|
||||||
className={cn(
|
|
||||||
!props.onSearch && 'text-muted-foreground hover:text-foreground px-2'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{t('Reset')}
|
|
||||||
</Button>
|
|
||||||
{props.onSearch && (
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
onClick={props.onSearch}
|
|
||||||
disabled={props.searchLoading}
|
|
||||||
>
|
|
||||||
{props.searchLoading && <Loader2 className='animate-spin' />}
|
|
||||||
{t('Search')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{props.viewToggle}
|
|
||||||
{viewOptions}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
if (isMobile && props.mobilePinnedFilters != null) {
|
|
||||||
return (
|
|
||||||
<Drawer open={mobileFiltersOpen} onOpenChange={setMobileFiltersOpen}>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'bg-card/50 min-w-0 rounded-lg border p-2.5',
|
|
||||||
props.className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className='grid min-w-0 gap-2'>{props.mobilePinnedFilters}</div>
|
|
||||||
|
|
||||||
<div className='mt-2 flex min-w-0 flex-col gap-2'>
|
|
||||||
{props.stats}
|
|
||||||
<div className='flex flex-wrap items-center justify-end gap-1.5'>
|
|
||||||
{props.actionStart}
|
|
||||||
{hasMobileFilters && (
|
|
||||||
<DrawerTrigger
|
|
||||||
render={
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
variant='ghost'
|
|
||||||
className={cn(
|
|
||||||
'text-muted-foreground hover:text-foreground gap-1 px-2',
|
|
||||||
activeMobileFilterCount > 0 &&
|
|
||||||
'text-primary hover:text-primary'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{t('Filter')}
|
|
||||||
{activeMobileFilterCount > 0 && (
|
|
||||||
<Badge variant='secondary' className='ml-0.5'>
|
|
||||||
{activeMobileFilterCount}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{!hasMobileFilters && (
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
variant='ghost'
|
|
||||||
onClick={props.onReset}
|
|
||||||
disabled={!props.hasActiveFilters}
|
|
||||||
className='text-muted-foreground hover:text-foreground px-2'
|
|
||||||
>
|
|
||||||
{t('Reset')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{props.onSearch && (
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
onClick={props.onSearch}
|
|
||||||
disabled={props.searchLoading}
|
|
||||||
>
|
|
||||||
{props.searchLoading && <Loader2 className='animate-spin' />}
|
|
||||||
{t('Search')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{props.viewToggle}
|
|
||||||
{viewOptions}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{hasMobileFilters && (
|
|
||||||
<DrawerContent className='max-h-[85dvh] p-0'>
|
|
||||||
<div className='mx-auto flex w-full max-w-md flex-1 flex-col overflow-hidden'>
|
|
||||||
<DrawerHeader className='border-border/70 border-b px-4 py-3 text-left'>
|
|
||||||
<DrawerTitle>{t('Filter')}</DrawerTitle>
|
|
||||||
<DrawerDescription>{t('Filters')}</DrawerDescription>
|
|
||||||
</DrawerHeader>
|
|
||||||
<div className='flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto px-4 py-3'>
|
|
||||||
{props.mobileFilters ?? (
|
|
||||||
<>
|
|
||||||
{props.primaryFilters}
|
|
||||||
{props.advancedFilters}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<DrawerFooter className='border-border/70 grid grid-cols-2 gap-2 border-t px-4 py-3'>
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
variant='outline'
|
|
||||||
onClick={handleMobileReset}
|
|
||||||
disabled={!props.hasActiveFilters}
|
|
||||||
>
|
|
||||||
{t('Reset')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
onClick={handleMobileSubmit}
|
|
||||||
disabled={props.searchLoading}
|
|
||||||
>
|
|
||||||
{props.searchLoading && <Loader2 className='animate-spin' />}
|
|
||||||
{props.onSearch ? t('Search') : t('Done')}
|
|
||||||
</Button>
|
|
||||||
</DrawerFooter>
|
|
||||||
</div>
|
|
||||||
</DrawerContent>
|
|
||||||
)}
|
|
||||||
</Drawer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'bg-card/50 min-w-0 rounded-lg border p-2.5 sm:p-3',
|
|
||||||
props.className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className='flex min-w-0 flex-wrap items-start gap-2'>
|
|
||||||
<div className='grid min-w-0 flex-1 grid-cols-1 gap-2 sm:grid-cols-[repeat(auto-fit,minmax(10rem,1fr))] [&>input]:w-full'>
|
|
||||||
{props.primaryFilters}
|
|
||||||
</div>
|
|
||||||
{advancedToggle && (
|
|
||||||
<div className='flex shrink-0 items-center justify-end'>
|
|
||||||
{advancedToggle}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{props.inlineActions && desktopActions}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{advancedOpen && props.advancedFilters && (
|
|
||||||
<div className='mt-2 grid grid-cols-1 gap-2 sm:grid-cols-[repeat(auto-fit,minmax(10rem,1fr))] [&>input]:w-full'>
|
|
||||||
{props.advancedFilters}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(!props.inlineActions || props.stats != null) && (
|
|
||||||
<div className='mt-2 flex min-w-0 flex-wrap items-center gap-2'>
|
|
||||||
{props.stats}
|
|
||||||
{!props.inlineActions && desktopActions}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
+135
-67
@@ -17,15 +17,18 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
For commercial licensing, please contact support@quantumnous.com
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
import type { Table } from '@tanstack/react-table'
|
import type { Table } from '@tanstack/react-table'
|
||||||
|
import { ChevronDown, Loader2, X as Cross2Icon } from 'lucide-react'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useState, type ReactNode } from 'react'
|
import { useState, type ReactNode } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { Input } from '@/components/design-system/input'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
import { useDebounce } from '@/hooks'
|
import { useDebounce } from '@/hooks'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
import { DataTableFacetedFilter } from './faceted-filter'
|
import { DataTableFacetedFilter } from './faceted-filter'
|
||||||
import { DataTableFilterField, DataTableFilterPanel } from './filter-panel'
|
import { DataTableViewOptions } from './view-options'
|
||||||
|
|
||||||
type FilterDef = {
|
type FilterDef = {
|
||||||
columnId: string
|
columnId: string
|
||||||
@@ -150,6 +153,7 @@ export type DataTableToolbarProps<TData> = {
|
|||||||
*/
|
*/
|
||||||
export function DataTableToolbar<TData>(props: DataTableToolbarProps<TData>) {
|
export function DataTableToolbar<TData>(props: DataTableToolbarProps<TData>) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const [expanded, setExpanded] = useState(false)
|
||||||
const [isSearchComposing, setIsSearchComposing] = useState(false)
|
const [isSearchComposing, setIsSearchComposing] = useState(false)
|
||||||
|
|
||||||
const filters = props.filters ?? []
|
const filters = props.filters ?? []
|
||||||
@@ -242,29 +246,32 @@ export function DataTableToolbar<TData>(props: DataTableToolbarProps<TData>) {
|
|||||||
const searchInput = (
|
const searchInput = (
|
||||||
<Input
|
<Input
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
aria-label={placeholder}
|
|
||||||
value={searchValue}
|
value={searchValue}
|
||||||
onChange={handleSearchChange}
|
onChange={handleSearchChange}
|
||||||
onCompositionStart={handleSearchCompositionStart}
|
onCompositionStart={handleSearchCompositionStart}
|
||||||
onCompositionEnd={handleSearchCompositionEnd}
|
onCompositionEnd={handleSearchCompositionEnd}
|
||||||
className='w-full'
|
className='w-full sm:w-[200px] lg:w-[240px]'
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
const filterChips = filters.map((filter) => {
|
const filterChips = React.useMemo(
|
||||||
const column = props.table.getColumn(filter.columnId)
|
() =>
|
||||||
if (!column) return null
|
filters.map((filter) => {
|
||||||
return (
|
const column = props.table.getColumn(filter.columnId)
|
||||||
<DataTableFilterField key={filter.columnId}>
|
if (!column) return null
|
||||||
<DataTableFacetedFilter
|
return (
|
||||||
column={column}
|
<DataTableFacetedFilter
|
||||||
title={filter.title}
|
key={filter.columnId}
|
||||||
options={filter.options}
|
column={column}
|
||||||
singleSelect={filter.singleSelect}
|
title={filter.title}
|
||||||
/>
|
options={filter.options}
|
||||||
</DataTableFilterField>
|
singleSelect={filter.singleSelect}
|
||||||
)
|
/>
|
||||||
})
|
)
|
||||||
|
}),
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[props.filters, props.table]
|
||||||
|
)
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
setIsSearchComposing(false)
|
setIsSearchComposing(false)
|
||||||
@@ -274,57 +281,118 @@ export function DataTableToolbar<TData>(props: DataTableToolbarProps<TData>) {
|
|||||||
props.onReset?.()
|
props.onReset?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
const primarySearch =
|
// Reset: outline text-only for form mode (always visible, disabled when
|
||||||
props.customSearch !== undefined ? props.customSearch : searchInput
|
// nothing to reset); ghost text + X for filter-as-you-type mode (only
|
||||||
const additionalFilterCount =
|
// visible when active filters exist).
|
||||||
filters.length + (props.additionalSearch != null ? 1 : 0)
|
let resetButton: ReactNode = null
|
||||||
const inlineActions =
|
if (hasSearch) {
|
||||||
additionalFilterCount <= 3 && !hasExpandable && props.leftActions == null
|
resetButton = (
|
||||||
const useWidePrimarySearch = !inlineActions && additionalFilterCount <= 3
|
<Button variant='outline' onClick={handleReset} disabled={!isFiltered}>
|
||||||
const secondaryMobileFilters =
|
{t('Reset')}
|
||||||
props.additionalSearch != null ||
|
</Button>
|
||||||
filterChips.some(Boolean) ||
|
)
|
||||||
hasExpandable ? (
|
} else if (isFiltered) {
|
||||||
<>
|
resetButton = (
|
||||||
{props.additionalSearch}
|
<Button
|
||||||
{filterChips}
|
variant='ghost'
|
||||||
{props.expandable}
|
onClick={handleReset}
|
||||||
</>
|
className='text-muted-foreground hover:text-foreground gap-1 px-2'
|
||||||
) : undefined
|
>
|
||||||
const nonSearchColumnFilterCount = props.table
|
{t('Reset')}
|
||||||
.getState()
|
<Cross2Icon />
|
||||||
.columnFilters.filter((filter) => filter.id !== props.searchKey).length
|
</Button>
|
||||||
const mobileFilterCount =
|
)
|
||||||
nonSearchColumnFilterCount + (props.hasExpandedActiveFilters ? 1 : 0)
|
}
|
||||||
|
|
||||||
return (
|
const searchButton = hasSearch ? (
|
||||||
<DataTableFilterPanel
|
<Button onClick={props.onSearch} disabled={props.searchLoading}>
|
||||||
table={props.table}
|
{props.searchLoading && <Loader2 className='animate-spin' />}
|
||||||
primaryFilters={
|
{t('Search')}
|
||||||
<>
|
</Button>
|
||||||
<DataTableFilterField wide={useWidePrimarySearch}>
|
) : null
|
||||||
{primarySearch}
|
|
||||||
</DataTableFilterField>
|
const viewOptionsNode = !props.hideViewOptions ? (
|
||||||
|
<DataTableViewOptions table={props.table} />
|
||||||
|
) : null
|
||||||
|
|
||||||
|
const viewToggleNode = props.viewToggle ?? null
|
||||||
|
|
||||||
|
const expandToggle = hasExpandable ? (
|
||||||
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
onClick={() => setExpanded((p) => !p)}
|
||||||
|
aria-expanded={expanded}
|
||||||
|
className={cn(
|
||||||
|
'text-muted-foreground hover:text-foreground gap-1 px-2',
|
||||||
|
props.hasExpandedActiveFilters &&
|
||||||
|
!expanded &&
|
||||||
|
'text-primary hover:text-primary'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{expanded ? t('Collapse') : t('Expand')}
|
||||||
|
<ChevronDown
|
||||||
|
className={cn(
|
||||||
|
'size-3.5 transition-transform duration-200',
|
||||||
|
expanded && 'rotate-180'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
) : null
|
||||||
|
|
||||||
|
const hasLeftActions = props.leftActions != null
|
||||||
|
|
||||||
|
if (hasLeftActions) {
|
||||||
|
return (
|
||||||
|
<div className={cn('flex flex-col gap-2', props.className)}>
|
||||||
|
<div className='flex flex-wrap items-center gap-2 sm:gap-3'>
|
||||||
|
{props.customSearch !== undefined ? props.customSearch : searchInput}
|
||||||
{props.additionalSearch}
|
{props.additionalSearch}
|
||||||
{filterChips}
|
{filterChips}
|
||||||
</>
|
<div className='ms-auto flex shrink-0 items-center gap-1.5 sm:gap-2'>
|
||||||
}
|
{expandToggle}
|
||||||
advancedFilters={props.expandable}
|
</div>
|
||||||
mobilePinnedFilters={primarySearch}
|
</div>
|
||||||
mobileFilters={secondaryMobileFilters}
|
|
||||||
mobileFilterCount={mobileFilterCount}
|
{expanded && hasExpandable && (
|
||||||
stats={props.leftActions}
|
<div className='flex flex-wrap items-center gap-2 sm:gap-3'>
|
||||||
actionStart={props.preActions}
|
{props.expandable}
|
||||||
viewToggle={props.viewToggle}
|
</div>
|
||||||
hideViewOptions={props.hideViewOptions}
|
)}
|
||||||
hasActiveFilters={isFiltered}
|
|
||||||
hasAdvancedActiveFilters={props.hasExpandedActiveFilters}
|
<div className='flex flex-wrap items-center gap-2 sm:gap-3'>
|
||||||
advancedFilterCount={props.hasExpandedActiveFilters ? 1 : 0}
|
{props.leftActions}
|
||||||
searchLoading={props.searchLoading}
|
<div className='ms-auto flex shrink-0 items-center gap-1.5 sm:gap-2'>
|
||||||
onReset={handleReset}
|
{props.preActions}
|
||||||
onSearch={hasSearch ? props.onSearch : undefined}
|
{resetButton}
|
||||||
inlineActions={inlineActions}
|
{searchButton}
|
||||||
className={props.className}
|
{viewToggleNode}
|
||||||
/>
|
{viewOptionsNode}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex flex-wrap items-center gap-2 sm:gap-3',
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{props.customSearch !== undefined ? props.customSearch : searchInput}
|
||||||
|
{props.additionalSearch}
|
||||||
|
{filterChips}
|
||||||
|
{expanded && hasExpandable && props.expandable}
|
||||||
|
|
||||||
|
<div className='ms-auto flex shrink-0 items-center gap-1.5 sm:gap-2'>
|
||||||
|
{props.preActions}
|
||||||
|
{resetButton}
|
||||||
|
{searchButton}
|
||||||
|
{viewToggleNode}
|
||||||
|
{viewOptionsNode}
|
||||||
|
{expandToggle}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,12 +19,12 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
import { Grid2X2, Table2 } from 'lucide-react'
|
import { Grid2X2, Table2 } from 'lucide-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@/components/design-system/tabs'
|
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '@/components/ui/tooltip'
|
} from '@/components/ui/tooltip'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DATA_TABLE_VIEW_MODES,
|
DATA_TABLE_VIEW_MODES,
|
||||||
@@ -37,6 +37,12 @@ export type DataTableViewModeToggleProps = {
|
|||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Segment = {
|
||||||
|
value: DataTableViewMode
|
||||||
|
icon: React.ComponentType<{ className?: string }>
|
||||||
|
tooltip: string
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reusable icon segmented control for switching a data table between table and
|
* Reusable icon segmented control for switching a data table between table and
|
||||||
* card views. Shared, accessible version of the local control used by the
|
* card views. Shared, accessible version of the local control used by the
|
||||||
@@ -45,42 +51,56 @@ export type DataTableViewModeToggleProps = {
|
|||||||
export function DataTableViewModeToggle(props: DataTableViewModeToggleProps) {
|
export function DataTableViewModeToggle(props: DataTableViewModeToggleProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const segments: Segment[] = [
|
||||||
|
{
|
||||||
|
value: DATA_TABLE_VIEW_MODES.CARD,
|
||||||
|
icon: Grid2X2,
|
||||||
|
tooltip: t('Card view'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: DATA_TABLE_VIEW_MODES.TABLE,
|
||||||
|
icon: Table2,
|
||||||
|
tooltip: t('Table view'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<div
|
||||||
value={props.value}
|
role='group'
|
||||||
onValueChange={(value) => props.onChange(value as DataTableViewMode)}
|
aria-label={t('View mode')}
|
||||||
className={props.className}
|
className={cn(
|
||||||
|
'bg-muted/60 inline-flex h-8 items-center rounded-lg border p-0.5',
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<TabsList aria-label={t('View mode')}>
|
{segments.map((segment) => {
|
||||||
<Tooltip>
|
const Icon = segment.icon
|
||||||
<TooltipTrigger
|
const isActive = segment.value === props.value
|
||||||
render={
|
return (
|
||||||
<TabsTrigger
|
<Tooltip key={segment.value}>
|
||||||
value={DATA_TABLE_VIEW_MODES.TABLE}
|
<TooltipTrigger
|
||||||
className='px-2'
|
render={
|
||||||
/>
|
<button
|
||||||
}
|
type='button'
|
||||||
>
|
onClick={() => props.onChange(segment.value)}
|
||||||
<Table2 aria-hidden='true' className='size-3.5' />
|
aria-pressed={isActive}
|
||||||
<span className='sr-only'>{t('Table view')}</span>
|
className={cn(
|
||||||
</TooltipTrigger>
|
'inline-flex h-full w-7 items-center justify-center rounded-md text-xs font-medium transition-all',
|
||||||
<TooltipContent side='bottom'>{t('Table view')}</TooltipContent>
|
isActive
|
||||||
</Tooltip>
|
? 'bg-primary text-primary-foreground shadow-sm'
|
||||||
<Tooltip>
|
: 'text-muted-foreground hover:text-foreground'
|
||||||
<TooltipTrigger
|
)}
|
||||||
render={
|
>
|
||||||
<TabsTrigger
|
<Icon className='size-3.5' />
|
||||||
value={DATA_TABLE_VIEW_MODES.CARD}
|
</button>
|
||||||
className='px-2'
|
}
|
||||||
/>
|
/>
|
||||||
}
|
<TooltipContent side='bottom' className='text-xs'>
|
||||||
>
|
{segment.tooltip}
|
||||||
<Grid2X2 aria-hidden='true' className='size-3.5' />
|
</TooltipContent>
|
||||||
<span className='sr-only'>{t('Card view')}</span>
|
</Tooltip>
|
||||||
</TooltipTrigger>
|
)
|
||||||
<TooltipContent side='bottom'>{t('Card view')}</TooltipContent>
|
})}
|
||||||
</Tooltip>
|
</div>
|
||||||
</TabsList>
|
|
||||||
</Tabs>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { type Table } from '@tanstack/react-table'
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { Button } from '@/components/design-system/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuCheckboxItem,
|
DropdownMenuCheckboxItem,
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ import { Calendar as CalendarIcon } from 'lucide-react'
|
|||||||
import { enUS, fr, ja, ru, vi, zhCN } from 'react-day-picker/locale'
|
import { enUS, fr, ja, ru, vi, zhCN } from 'react-day-picker/locale'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { Button } from '@/components/design-system/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Calendar } from '@/components/ui/calendar'
|
import { Calendar } from '@/components/ui/calendar'
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
|
|||||||
+2
-2
@@ -21,9 +21,9 @@ import * as React from 'react'
|
|||||||
import { enUS, fr, ja, ru, vi, zhCN } from 'react-day-picker/locale'
|
import { enUS, fr, ja, ru, vi, zhCN } from 'react-day-picker/locale'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { Button } from '@/components/design-system/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/design-system/input'
|
|
||||||
import { Calendar } from '@/components/ui/calendar'
|
import { Calendar } from '@/components/ui/calendar'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
# Design-system adapters
|
|
||||||
|
|
||||||
This directory is the product-policy boundary above the shadcn source in
|
|
||||||
`src/components/ui/`.
|
|
||||||
|
|
||||||
## Import rule
|
|
||||||
|
|
||||||
- Application and feature code imports policy-bearing controls from
|
|
||||||
`@/components/design-system/*`.
|
|
||||||
- Files in this directory may import `@/components/ui/*`.
|
|
||||||
- Do not make `components/ui` depend on this directory.
|
|
||||||
- Do not proxy a shadcn component unless it carries a stable cross-application
|
|
||||||
policy. Components without product policy continue to be imported directly
|
|
||||||
from `components/ui`.
|
|
||||||
|
|
||||||
The lint configuration enforces this rule for the managed controls.
|
|
||||||
|
|
||||||
## Responsive sizing
|
|
||||||
|
|
||||||
Default controls are 28px below the `sm` breakpoint and 32px at `sm` and above.
|
|
||||||
Dense and micro sizes remain fixed:
|
|
||||||
|
|
||||||
- `sm` / `icon-sm`: 28px
|
|
||||||
- `xs` / `icon-xs`: 24px
|
|
||||||
- `xl`: 40px below `sm`, 44px at `sm` and above
|
|
||||||
|
|
||||||
Call sites should select a semantic size and use `className` only for layout.
|
|
||||||
They must not pin a managed control to a fixed `h-*` or `size-*` value.
|
|
||||||
|
|
||||||
## Updating shadcn
|
|
||||||
|
|
||||||
Keep `components.json` pointed at `@/components/ui`. Preview upstream changes
|
|
||||||
before merging:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
bunx --bun shadcn@latest add <component> --dry-run
|
|
||||||
bunx --bun shadcn@latest add <component> --diff <file>
|
|
||||||
```
|
|
||||||
|
|
||||||
Merge the upstream source into `components/ui`, then verify that the adapter's
|
|
||||||
prop forwarding and class overrides still match the upstream API. Never use
|
|
||||||
`--overwrite` without explicit approval.
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright (C) 2023-2026 QuantumNous
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as
|
|
||||||
published by the Free Software Foundation, either version 3 of the
|
|
||||||
License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
|
||||||
*/
|
|
||||||
import { AlertDialog as AlertDialogPrimitive } from '@base-ui/react/alert-dialog'
|
|
||||||
import * as React from 'react'
|
|
||||||
|
|
||||||
import { Button } from '@/components/design-system/button'
|
|
||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogMedia,
|
|
||||||
AlertDialogOverlay,
|
|
||||||
AlertDialogPortal,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogTrigger,
|
|
||||||
} from '@/components/ui/alert-dialog'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
function AlertDialogAction({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof Button>) {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
data-slot='alert-dialog-action'
|
|
||||||
className={cn(className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AlertDialogCancel({
|
|
||||||
className,
|
|
||||||
variant = 'outline',
|
|
||||||
size = 'default',
|
|
||||||
...props
|
|
||||||
}: AlertDialogPrimitive.Close.Props &
|
|
||||||
Pick<React.ComponentProps<typeof Button>, 'variant' | 'size'>) {
|
|
||||||
return (
|
|
||||||
<AlertDialogPrimitive.Close
|
|
||||||
data-slot='alert-dialog-cancel'
|
|
||||||
className={cn(className)}
|
|
||||||
render={<Button variant={variant} size={size} />}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogMedia,
|
|
||||||
AlertDialogOverlay,
|
|
||||||
AlertDialogPortal,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogTrigger,
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright (C) 2023-2026 QuantumNous
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as
|
|
||||||
published by the Free Software Foundation, either version 3 of the
|
|
||||||
License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
|
||||||
*/
|
|
||||||
import { cva, type VariantProps } from 'class-variance-authority'
|
|
||||||
import * as React from 'react'
|
|
||||||
|
|
||||||
import { Button as ShadcnButton } from '@/components/ui/button'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
const responsiveButtonSizeVariants = cva('', {
|
|
||||||
variants: {
|
|
||||||
size: {
|
|
||||||
default:
|
|
||||||
"h-7 gap-1 px-2.5 text-[0.8rem] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5 sm:h-8 sm:gap-1.5 sm:text-sm sm:has-data-[icon=inline-end]:pr-2 sm:has-data-[icon=inline-start]:pl-2 sm:[&_svg:not([class*='size-'])]:size-4",
|
|
||||||
xs: null,
|
|
||||||
sm: null,
|
|
||||||
lg: 'h-8 sm:h-9',
|
|
||||||
xl: 'h-10 gap-2 px-4 sm:h-11 sm:px-5',
|
|
||||||
icon: 'size-7 sm:size-8',
|
|
||||||
'icon-xs': null,
|
|
||||||
'icon-sm': null,
|
|
||||||
'icon-lg': 'size-8 sm:size-9',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
size: 'default',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
type ButtonSize = NonNullable<
|
|
||||||
VariantProps<typeof responsiveButtonSizeVariants>['size']
|
|
||||||
>
|
|
||||||
|
|
||||||
type ButtonProps = Omit<React.ComponentProps<typeof ShadcnButton>, 'size'> & {
|
|
||||||
size?: ButtonSize
|
|
||||||
}
|
|
||||||
|
|
||||||
function Button({ className, size = 'default', ...props }: ButtonProps) {
|
|
||||||
return (
|
|
||||||
<ShadcnButton
|
|
||||||
data-control-size={size}
|
|
||||||
size={size === 'xl' ? 'lg' : size}
|
|
||||||
className={cn(responsiveButtonSizeVariants({ size }), className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Button }
|
|
||||||
export type { ButtonProps, ButtonSize }
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright (C) 2023-2026 QuantumNous
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as
|
|
||||||
published by the Free Software Foundation, either version 3 of the
|
|
||||||
License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
|
||||||
*/
|
|
||||||
import * as React from 'react'
|
|
||||||
|
|
||||||
import {
|
|
||||||
ComboboxInput as ShadcnComboboxInput,
|
|
||||||
type ComboboxInputOption,
|
|
||||||
} from '@/components/ui/combobox-input'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
function ComboboxInput({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ShadcnComboboxInput>) {
|
|
||||||
return (
|
|
||||||
<ShadcnComboboxInput className={cn('h-7 sm:h-8', className)} {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { ComboboxInput }
|
|
||||||
export type { ComboboxInputOption }
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright (C) 2023-2026 QuantumNous
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as
|
|
||||||
published by the Free Software Foundation, either version 3 of the
|
|
||||||
License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
|
||||||
*/
|
|
||||||
import { Combobox as ComboboxPrimitive } from '@base-ui/react'
|
|
||||||
import * as React from 'react'
|
|
||||||
|
|
||||||
import {
|
|
||||||
ComboboxInput as LegacyComboboxInput,
|
|
||||||
type ComboboxInputOption,
|
|
||||||
} from '@/components/design-system/combobox-input'
|
|
||||||
import {
|
|
||||||
ComboboxChip,
|
|
||||||
ComboboxChips as ShadcnComboboxChips,
|
|
||||||
ComboboxChipsInput,
|
|
||||||
ComboboxCollection,
|
|
||||||
ComboboxContent as ShadcnComboboxContent,
|
|
||||||
ComboboxEmpty,
|
|
||||||
ComboboxGroup,
|
|
||||||
ComboboxInput as ShadcnComboboxInput,
|
|
||||||
ComboboxItem,
|
|
||||||
ComboboxLabel,
|
|
||||||
ComboboxList,
|
|
||||||
ComboboxSeparator,
|
|
||||||
ComboboxTrigger,
|
|
||||||
ComboboxValue,
|
|
||||||
useComboboxAnchor,
|
|
||||||
} from '@/components/ui/combobox'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
type LegacyComboboxProps = {
|
|
||||||
options: ComboboxInputOption[]
|
|
||||||
value?: string
|
|
||||||
onValueChange?: (value: string | null) => void
|
|
||||||
placeholder?: string
|
|
||||||
searchPlaceholder?: string
|
|
||||||
emptyText?: string
|
|
||||||
allowCustomValue?: boolean
|
|
||||||
className?: string
|
|
||||||
id?: string
|
|
||||||
openOnFocus?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
function Combobox(props: LegacyComboboxProps): React.ReactElement
|
|
||||||
function Combobox<Value, Multiple extends boolean | undefined = false>(
|
|
||||||
props: ComboboxPrimitive.Root.Props<Value, Multiple>
|
|
||||||
): React.ReactElement
|
|
||||||
function Combobox(
|
|
||||||
props:
|
|
||||||
| ComboboxPrimitive.Root.Props<unknown, boolean | undefined>
|
|
||||||
| LegacyComboboxProps
|
|
||||||
) {
|
|
||||||
if ('options' in props) {
|
|
||||||
return (
|
|
||||||
<LegacyComboboxInput
|
|
||||||
id={props.id}
|
|
||||||
options={props.options}
|
|
||||||
value={props.value ?? ''}
|
|
||||||
onValueChange={(value) => props.onValueChange?.(value)}
|
|
||||||
placeholder={props.searchPlaceholder ?? props.placeholder}
|
|
||||||
emptyText={props.emptyText}
|
|
||||||
className={props.className}
|
|
||||||
allowCustomValue={props.allowCustomValue}
|
|
||||||
openOnFocus={props.openOnFocus}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return <ComboboxPrimitive.Root {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxInput({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ShadcnComboboxInput>) {
|
|
||||||
return (
|
|
||||||
<ShadcnComboboxInput className={cn('h-7 sm:h-8', className)} {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxContent({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ShadcnComboboxContent>) {
|
|
||||||
return (
|
|
||||||
<ShadcnComboboxContent
|
|
||||||
className={cn(
|
|
||||||
'*:data-[slot=input-group]:h-7 sm:*:data-[slot=input-group]:h-8',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxChips({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ShadcnComboboxChips>) {
|
|
||||||
return (
|
|
||||||
<ShadcnComboboxChips
|
|
||||||
className={cn('min-h-7 sm:min-h-8', className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Combobox,
|
|
||||||
ComboboxChip,
|
|
||||||
ComboboxChips,
|
|
||||||
ComboboxChipsInput,
|
|
||||||
ComboboxCollection,
|
|
||||||
ComboboxContent,
|
|
||||||
ComboboxEmpty,
|
|
||||||
ComboboxGroup,
|
|
||||||
ComboboxInput,
|
|
||||||
ComboboxItem,
|
|
||||||
ComboboxLabel,
|
|
||||||
ComboboxList,
|
|
||||||
ComboboxSeparator,
|
|
||||||
ComboboxTrigger,
|
|
||||||
ComboboxValue,
|
|
||||||
useComboboxAnchor,
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright (C) 2023-2026 QuantumNous
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as
|
|
||||||
published by the Free Software Foundation, either version 3 of the
|
|
||||||
License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
|
||||||
*/
|
|
||||||
import { SearchIcon } from '@hugeicons/core-free-icons'
|
|
||||||
import { HugeiconsIcon } from '@hugeicons/react'
|
|
||||||
import { Command as CommandPrimitive } from 'cmdk'
|
|
||||||
import * as React from 'react'
|
|
||||||
|
|
||||||
import {
|
|
||||||
InputGroup,
|
|
||||||
InputGroupAddon,
|
|
||||||
} from '@/components/design-system/input-group'
|
|
||||||
import {
|
|
||||||
Command,
|
|
||||||
CommandDialog,
|
|
||||||
CommandEmpty,
|
|
||||||
CommandGroup,
|
|
||||||
CommandItem,
|
|
||||||
CommandList,
|
|
||||||
CommandSeparator,
|
|
||||||
CommandShortcut,
|
|
||||||
} from '@/components/ui/command'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
function CommandInput({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
|
||||||
return (
|
|
||||||
<div data-slot='command-input-wrapper' className='p-1 pb-0'>
|
|
||||||
<InputGroup className='border-input/30 bg-input/30 rounded-lg! shadow-none! *:data-[slot=input-group-addon]:pl-2!'>
|
|
||||||
<CommandPrimitive.Input
|
|
||||||
data-slot='command-input'
|
|
||||||
className={cn(
|
|
||||||
'w-full text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
<InputGroupAddon>
|
|
||||||
<HugeiconsIcon
|
|
||||||
icon={SearchIcon}
|
|
||||||
strokeWidth={2}
|
|
||||||
className='size-4 shrink-0 opacity-50'
|
|
||||||
/>
|
|
||||||
</InputGroupAddon>
|
|
||||||
</InputGroup>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Command,
|
|
||||||
CommandDialog,
|
|
||||||
CommandEmpty,
|
|
||||||
CommandGroup,
|
|
||||||
CommandInput,
|
|
||||||
CommandItem,
|
|
||||||
CommandList,
|
|
||||||
CommandSeparator,
|
|
||||||
CommandShortcut,
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright (C) 2023-2026 QuantumNous
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as
|
|
||||||
published by the Free Software Foundation, either version 3 of the
|
|
||||||
License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
|
||||||
*/
|
|
||||||
import { Dialog as DialogPrimitive } from '@base-ui/react/dialog'
|
|
||||||
import * as React from 'react'
|
|
||||||
|
|
||||||
import { Button } from '@/components/design-system/button'
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogOverlay,
|
|
||||||
DialogPortal,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
function DialogFooter({
|
|
||||||
className,
|
|
||||||
showCloseButton = false,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<'div'> & {
|
|
||||||
showCloseButton?: boolean
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot='dialog-footer'
|
|
||||||
className={cn(
|
|
||||||
'bg-muted/50 -mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t p-4 sm:flex-row sm:justify-end',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
{showCloseButton && (
|
|
||||||
<DialogPrimitive.Close render={<Button variant='outline' />}>
|
|
||||||
Close
|
|
||||||
</DialogPrimitive.Close>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogOverlay,
|
|
||||||
DialogPortal,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright (C) 2023-2026 QuantumNous
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as
|
|
||||||
published by the Free Software Foundation, either version 3 of the
|
|
||||||
License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
|
||||||
*/
|
|
||||||
import * as React from 'react'
|
|
||||||
|
|
||||||
import {
|
|
||||||
InputGroup as ShadcnInputGroup,
|
|
||||||
InputGroupAddon,
|
|
||||||
InputGroupButton as ShadcnInputGroupButton,
|
|
||||||
InputGroupInput as ShadcnInputGroupInput,
|
|
||||||
InputGroupText,
|
|
||||||
InputGroupTextarea,
|
|
||||||
} from '@/components/ui/input-group'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
function InputGroup({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ShadcnInputGroup>) {
|
|
||||||
return (
|
|
||||||
<ShadcnInputGroup
|
|
||||||
data-control-size='default'
|
|
||||||
className={cn('h-7 sm:h-8', className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function InputGroupButton({
|
|
||||||
className,
|
|
||||||
size = 'xs',
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ShadcnInputGroupButton>) {
|
|
||||||
return (
|
|
||||||
<ShadcnInputGroupButton
|
|
||||||
data-control-size={size}
|
|
||||||
size={size}
|
|
||||||
className={cn(
|
|
||||||
size === 'sm' && 'h-7 sm:h-8',
|
|
||||||
size === 'icon-sm' && 'size-7 sm:size-8',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function InputGroupInput({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ShadcnInputGroupInput>) {
|
|
||||||
return (
|
|
||||||
<ShadcnInputGroupInput className={cn('h-full', className)} {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
InputGroup,
|
|
||||||
InputGroupAddon,
|
|
||||||
InputGroupButton,
|
|
||||||
InputGroupInput,
|
|
||||||
InputGroupText,
|
|
||||||
InputGroupTextarea,
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright (C) 2023-2026 QuantumNous
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as
|
|
||||||
published by the Free Software Foundation, either version 3 of the
|
|
||||||
License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
|
||||||
*/
|
|
||||||
import * as React from 'react'
|
|
||||||
|
|
||||||
import { Input as ShadcnInput } from '@/components/ui/input'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
function Input({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ShadcnInput>) {
|
|
||||||
return (
|
|
||||||
<ShadcnInput
|
|
||||||
data-control-size='default'
|
|
||||||
className={cn('h-7 sm:h-8', className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Input }
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright (C) 2023-2026 QuantumNous
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as
|
|
||||||
published by the Free Software Foundation, either version 3 of the
|
|
||||||
License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
|
||||||
*/
|
|
||||||
import * as React from 'react'
|
|
||||||
|
|
||||||
import {
|
|
||||||
Pagination as ShadcnPagination,
|
|
||||||
PaginationContent,
|
|
||||||
PaginationEllipsis,
|
|
||||||
PaginationItem,
|
|
||||||
PaginationLink,
|
|
||||||
PaginationNext,
|
|
||||||
PaginationPrevious,
|
|
||||||
} from '@/components/ui/pagination'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
function Pagination({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ShadcnPagination>) {
|
|
||||||
return (
|
|
||||||
<ShadcnPagination
|
|
||||||
className={cn(
|
|
||||||
'[&_[data-slot=pagination-link]]:h-7 [&_[data-slot=pagination-link]:not(:has(span))]:w-7 [&_[data-slot=pagination-ellipsis]]:size-7 sm:[&_[data-slot=pagination-link]]:h-8 sm:[&_[data-slot=pagination-link]:not(:has(span))]:w-8 sm:[&_[data-slot=pagination-ellipsis]]:size-8',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Pagination,
|
|
||||||
PaginationContent,
|
|
||||||
PaginationEllipsis,
|
|
||||||
PaginationItem,
|
|
||||||
PaginationLink,
|
|
||||||
PaginationNext,
|
|
||||||
PaginationPrevious,
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright (C) 2023-2026 QuantumNous
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as
|
|
||||||
published by the Free Software Foundation, either version 3 of the
|
|
||||||
License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
|
||||||
*/
|
|
||||||
import * as React from 'react'
|
|
||||||
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectGroup,
|
|
||||||
SelectItem,
|
|
||||||
SelectLabel,
|
|
||||||
SelectScrollDownButton,
|
|
||||||
SelectScrollUpButton,
|
|
||||||
SelectSeparator,
|
|
||||||
SelectTrigger as ShadcnSelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/components/ui/select'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
function SelectTrigger({
|
|
||||||
className,
|
|
||||||
size = 'default',
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ShadcnSelectTrigger>) {
|
|
||||||
return (
|
|
||||||
<ShadcnSelectTrigger
|
|
||||||
data-control-size={size}
|
|
||||||
size={size}
|
|
||||||
className={cn(
|
|
||||||
size === 'default' && 'h-7 text-[0.8rem] sm:h-8 sm:text-sm',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectGroup,
|
|
||||||
SelectItem,
|
|
||||||
SelectLabel,
|
|
||||||
SelectScrollDownButton,
|
|
||||||
SelectScrollUpButton,
|
|
||||||
SelectSeparator,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright (C) 2023-2026 QuantumNous
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as
|
|
||||||
published by the Free Software Foundation, either version 3 of the
|
|
||||||
License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
|
||||||
*/
|
|
||||||
import * as React from 'react'
|
|
||||||
|
|
||||||
import {
|
|
||||||
Sidebar,
|
|
||||||
SidebarContent,
|
|
||||||
SidebarFooter,
|
|
||||||
SidebarGroup,
|
|
||||||
SidebarGroupAction,
|
|
||||||
SidebarGroupContent,
|
|
||||||
SidebarGroupLabel,
|
|
||||||
SidebarHeader,
|
|
||||||
SidebarInput as ShadcnSidebarInput,
|
|
||||||
SidebarInset,
|
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuAction,
|
|
||||||
SidebarMenuBadge,
|
|
||||||
SidebarMenuButton,
|
|
||||||
SidebarMenuItem,
|
|
||||||
SidebarMenuSkeleton,
|
|
||||||
SidebarMenuSub,
|
|
||||||
SidebarMenuSubButton,
|
|
||||||
SidebarMenuSubItem,
|
|
||||||
SidebarProvider,
|
|
||||||
SidebarRail,
|
|
||||||
SidebarSeparator,
|
|
||||||
SidebarTrigger as ShadcnSidebarTrigger,
|
|
||||||
useSidebar,
|
|
||||||
} from '@/components/ui/sidebar'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
function SidebarTrigger({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ShadcnSidebarTrigger>) {
|
|
||||||
return (
|
|
||||||
<ShadcnSidebarTrigger
|
|
||||||
className={cn('size-7 sm:size-8', className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarInput({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ShadcnSidebarInput>) {
|
|
||||||
return (
|
|
||||||
<ShadcnSidebarInput className={cn('h-7 sm:h-8', className)} {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Sidebar,
|
|
||||||
SidebarContent,
|
|
||||||
SidebarFooter,
|
|
||||||
SidebarGroup,
|
|
||||||
SidebarGroupAction,
|
|
||||||
SidebarGroupContent,
|
|
||||||
SidebarGroupLabel,
|
|
||||||
SidebarHeader,
|
|
||||||
SidebarInput,
|
|
||||||
SidebarInset,
|
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuAction,
|
|
||||||
SidebarMenuBadge,
|
|
||||||
SidebarMenuButton,
|
|
||||||
SidebarMenuItem,
|
|
||||||
SidebarMenuSkeleton,
|
|
||||||
SidebarMenuSub,
|
|
||||||
SidebarMenuSubButton,
|
|
||||||
SidebarMenuSubItem,
|
|
||||||
SidebarProvider,
|
|
||||||
SidebarRail,
|
|
||||||
SidebarSeparator,
|
|
||||||
SidebarTrigger,
|
|
||||||
useSidebar,
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright (C) 2023-2026 QuantumNous
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as
|
|
||||||
published by the Free Software Foundation, either version 3 of the
|
|
||||||
License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
|
||||||
*/
|
|
||||||
import * as React from 'react'
|
|
||||||
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCaption,
|
|
||||||
TableCell,
|
|
||||||
TableFooter,
|
|
||||||
TableHead as ShadcnTableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from '@/components/ui/table'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
function TableHead({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ShadcnTableHead>) {
|
|
||||||
return <ShadcnTableHead className={cn('h-9 sm:h-10', className)} {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCaption,
|
|
||||||
TableCell,
|
|
||||||
TableFooter,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright (C) 2023-2026 QuantumNous
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as
|
|
||||||
published by the Free Software Foundation, either version 3 of the
|
|
||||||
License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
|
||||||
*/
|
|
||||||
import * as React from 'react'
|
|
||||||
|
|
||||||
import {
|
|
||||||
Tabs,
|
|
||||||
TabsContent,
|
|
||||||
TabsList as ShadcnTabsList,
|
|
||||||
TabsTrigger as ShadcnTabsTrigger,
|
|
||||||
} from '@/components/ui/tabs'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
function TabsList({
|
|
||||||
className,
|
|
||||||
variant = 'default',
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ShadcnTabsList>) {
|
|
||||||
return (
|
|
||||||
<ShadcnTabsList
|
|
||||||
data-control-size='default'
|
|
||||||
variant={variant}
|
|
||||||
className={cn(
|
|
||||||
variant === 'line'
|
|
||||||
? 'group-data-horizontal/tabs:h-auto sm:group-data-horizontal/tabs:h-auto'
|
|
||||||
: 'group-data-horizontal/tabs:h-7 sm:group-data-horizontal/tabs:h-8',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TabsTrigger({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ShadcnTabsTrigger>) {
|
|
||||||
return (
|
|
||||||
<ShadcnTabsTrigger
|
|
||||||
className={cn('text-[0.8rem] sm:text-sm', className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Tabs, TabsContent, TabsList, TabsTrigger }
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright (C) 2023-2026 QuantumNous
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as
|
|
||||||
published by the Free Software Foundation, either version 3 of the
|
|
||||||
License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
|
||||||
*/
|
|
||||||
import * as React from 'react'
|
|
||||||
|
|
||||||
import {
|
|
||||||
ToggleGroup as ShadcnToggleGroup,
|
|
||||||
ToggleGroupItem,
|
|
||||||
} from '@/components/ui/toggle-group'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
function ToggleGroup({
|
|
||||||
className,
|
|
||||||
size = 'default',
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ShadcnToggleGroup>) {
|
|
||||||
return (
|
|
||||||
<ShadcnToggleGroup
|
|
||||||
data-control-size={size}
|
|
||||||
size={size}
|
|
||||||
className={cn(
|
|
||||||
size === 'default' &&
|
|
||||||
"[&_[data-slot=toggle-group-item]]:h-7 [&_[data-slot=toggle-group-item]]:min-w-7 [&_[data-slot=toggle-group-item]]:text-[0.8rem] [&_[data-slot=toggle-group-item]_svg:not([class*='size-'])]:size-3.5 sm:[&_[data-slot=toggle-group-item]]:h-8 sm:[&_[data-slot=toggle-group-item]]:min-w-8 sm:[&_[data-slot=toggle-group-item]]:text-sm sm:[&_[data-slot=toggle-group-item]_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { ToggleGroup, ToggleGroupItem }
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright (C) 2023-2026 QuantumNous
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as
|
|
||||||
published by the Free Software Foundation, either version 3 of the
|
|
||||||
License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
|
||||||
*/
|
|
||||||
import { cva, type VariantProps } from 'class-variance-authority'
|
|
||||||
import * as React from 'react'
|
|
||||||
|
|
||||||
import { Toggle as ShadcnToggle } from '@/components/ui/toggle'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
const responsiveToggleSizeVariants = cva('', {
|
|
||||||
variants: {
|
|
||||||
size: {
|
|
||||||
default:
|
|
||||||
"h-7 min-w-7 text-[0.8rem] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5 sm:h-8 sm:min-w-8 sm:text-sm sm:has-data-[icon=inline-end]:pr-2 sm:has-data-[icon=inline-start]:pl-2 sm:[&_svg:not([class*='size-'])]:size-4",
|
|
||||||
sm: null,
|
|
||||||
lg: 'h-8 min-w-8 sm:h-9 sm:min-w-9',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
size: 'default',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
type ToggleSize = NonNullable<
|
|
||||||
VariantProps<typeof responsiveToggleSizeVariants>['size']
|
|
||||||
>
|
|
||||||
|
|
||||||
type ToggleProps = Omit<React.ComponentProps<typeof ShadcnToggle>, 'size'> & {
|
|
||||||
size?: ToggleSize
|
|
||||||
}
|
|
||||||
|
|
||||||
function Toggle({ className, size = 'default', ...props }: ToggleProps) {
|
|
||||||
return (
|
|
||||||
<ShadcnToggle
|
|
||||||
data-control-size={size}
|
|
||||||
size={size}
|
|
||||||
className={cn(responsiveToggleSizeVariants({ size }), className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Toggle }
|
|
||||||
export type { ToggleProps, ToggleSize }
|
|
||||||
+1
-3
@@ -26,7 +26,7 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from '@/components/design-system/dialog'
|
} from '@/components/ui/dialog'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
type DialogProps = React.ComponentProps<typeof DialogRoot> & {
|
type DialogProps = React.ComponentProps<typeof DialogRoot> & {
|
||||||
@@ -71,8 +71,6 @@ export function Dialog({
|
|||||||
{trigger ? <DialogTrigger render={trigger} /> : null}
|
{trigger ? <DialogTrigger render={trigger} /> : null}
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className={cn(
|
className={cn(
|
||||||
// Default width is sm:max-w-2xl. Override with `sm:max-w-*` in
|
|
||||||
// contentClassName (bare `max-w-*` will not replace the sm: default).
|
|
||||||
'flex max-h-[calc(100vh-2rem)] w-full flex-col gap-4 overflow-hidden p-4 sm:max-w-2xl sm:p-6',
|
'flex max-h-[calc(100vh-2rem)] w-full flex-col gap-4 overflow-hidden p-4 sm:max-w-2xl sm:p-6',
|
||||||
contentClassName,
|
contentClassName,
|
||||||
dialogContentMotionClassName
|
dialogContentMotionClassName
|
||||||
|
|||||||
+2
-6
@@ -22,17 +22,13 @@ import { cn } from '@/lib/utils'
|
|||||||
|
|
||||||
export const sideDrawerContentClassName = (className?: string) =>
|
export const sideDrawerContentClassName = (className?: string) =>
|
||||||
cn(
|
cn(
|
||||||
// Width: pass `sm:max-w-*` (or `sm:max-w-none`) in className. SheetContent
|
'bg-background text-foreground flex h-dvh w-full flex-col gap-0 overflow-hidden p-0 shadow-none',
|
||||||
// defaults to 75% width on mobile and `sm:max-w-sm` for left/right.
|
|
||||||
// Match its side variants so product drawers occupy the full mobile viewport.
|
|
||||||
'bg-background text-foreground flex h-dvh w-full flex-col gap-0 overflow-hidden p-0 shadow-none data-[side=left]:w-full data-[side=right]:w-full',
|
|
||||||
className
|
className
|
||||||
)
|
)
|
||||||
|
|
||||||
export const sideDrawerHeaderClassName = (className?: string) =>
|
export const sideDrawerHeaderClassName = (className?: string) =>
|
||||||
cn(
|
cn(
|
||||||
// pr-12 reserves space for SheetContent's absolute close button
|
'border-border/70 bg-background/95 border-b px-4 py-3 text-start backdrop-blur supports-[backdrop-filter]:bg-background/80 sm:px-6 sm:py-4',
|
||||||
'border-border/70 bg-background/95 border-b px-4 py-3 pr-12 text-start backdrop-blur supports-[backdrop-filter]:bg-background/80 sm:px-6 sm:py-4 sm:pr-14',
|
|
||||||
className
|
className
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -20,8 +20,8 @@ import { AlertTriangle, type LucideIcon } from 'lucide-react'
|
|||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { Button } from '@/components/design-system/button'
|
|
||||||
import { FadeIn } from '@/components/page-transition'
|
import { FadeIn } from '@/components/page-transition'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
Empty,
|
Empty,
|
||||||
EmptyContent,
|
EmptyContent,
|
||||||
@@ -61,7 +61,7 @@ export function ErrorState(props: ErrorStateProps) {
|
|||||||
</EmptyHeader>
|
</EmptyHeader>
|
||||||
<EmptyContent>
|
<EmptyContent>
|
||||||
{props.onRetry != null && (
|
{props.onRetry != null && (
|
||||||
<Button variant='outline' onClick={props.onRetry}>
|
<Button variant='outline' size='sm' onClick={props.onRetry}>
|
||||||
{t('Retry')}
|
{t('Retry')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+38
-28
@@ -18,23 +18,27 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
*/
|
*/
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { getIdentityTextColorClass } from '@/lib/colors'
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
import { StatusBadge, type StatusBadgeProps } from './status-badge'
|
import { StatusBadge, type StatusBadgeProps } from './status-badge'
|
||||||
|
|
||||||
type GroupBadgeProps = Omit<StatusBadgeProps, 'children' | 'variant'> & {
|
type GroupBadgeProps = Omit<
|
||||||
|
StatusBadgeProps,
|
||||||
|
'autoColor' | 'label' | 'variant'
|
||||||
|
> & {
|
||||||
group?: string | null
|
group?: string | null
|
||||||
label?: string
|
label?: string
|
||||||
ratio?: number | null
|
ratio?: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGroupRatioVariant(
|
function getGroupRatioClassName(ratio: number): string {
|
||||||
ratio: number
|
if (ratio > 1) {
|
||||||
): NonNullable<StatusBadgeProps['variant']> {
|
return 'bg-warning/10 text-warning'
|
||||||
if (ratio > 1) return 'warning'
|
}
|
||||||
if (ratio < 1) return 'info'
|
if (ratio < 1) {
|
||||||
return 'neutral'
|
return 'bg-info/10 text-info'
|
||||||
|
}
|
||||||
|
return 'bg-muted text-muted-foreground'
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGroupLabel(params: {
|
function getGroupLabel(params: {
|
||||||
@@ -52,11 +56,19 @@ function getGroupLabel(params: {
|
|||||||
|
|
||||||
export function GroupBadge(props: GroupBadgeProps) {
|
export function GroupBadge(props: GroupBadgeProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { group, label: labelOverride, ratio, className, ...badgeProps } = props
|
const {
|
||||||
|
group,
|
||||||
|
label: labelOverride,
|
||||||
|
ratio,
|
||||||
|
copyable = false,
|
||||||
|
showDot,
|
||||||
|
className,
|
||||||
|
...badgeProps
|
||||||
|
} = props
|
||||||
const groupName = group?.trim()
|
const groupName = group?.trim()
|
||||||
const isAutoGroup = groupName === 'auto'
|
const isAutoGroup = groupName === 'auto'
|
||||||
const isEmptyGroup = !groupName
|
const isEmptyGroup = !groupName
|
||||||
const colorKey = groupName || labelOverride || 'group'
|
const isSpecialGroup = isAutoGroup || isEmptyGroup
|
||||||
const label = getGroupLabel({
|
const label = getGroupLabel({
|
||||||
labelOverride,
|
labelOverride,
|
||||||
groupName,
|
groupName,
|
||||||
@@ -68,16 +80,13 @@ export function GroupBadge(props: GroupBadgeProps) {
|
|||||||
const badge = (
|
const badge = (
|
||||||
<StatusBadge
|
<StatusBadge
|
||||||
{...badgeProps}
|
{...badgeProps}
|
||||||
variant='neutral'
|
copyable={copyable}
|
||||||
appearance='plain'
|
label={label}
|
||||||
className={cn(
|
showDot={showDot ?? (isSpecialGroup ? false : undefined)}
|
||||||
'shrink-0 overflow-visible',
|
variant={isSpecialGroup ? 'neutral' : undefined}
|
||||||
getIdentityTextColorClass(colorKey),
|
autoColor={isSpecialGroup ? undefined : groupName}
|
||||||
className
|
className={cn('min-w-0 shrink overflow-hidden', className)}
|
||||||
)}
|
/>
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</StatusBadge>
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (ratio == null) {
|
if (ratio == null) {
|
||||||
@@ -85,15 +94,16 @@ export function GroupBadge(props: GroupBadgeProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className='inline-flex w-max shrink-0 items-center gap-2 text-xs whitespace-nowrap'>
|
<span className='inline-flex max-w-full min-w-0 items-center gap-2 text-xs'>
|
||||||
<span className='inline-flex shrink-0'>{badge}</span>
|
<span className='max-w-full min-w-0 overflow-hidden'>{badge}</span>
|
||||||
<StatusBadge
|
<span
|
||||||
variant={getGroupRatioVariant(ratio)}
|
className={cn(
|
||||||
appearance='plain'
|
'inline-flex h-5 shrink-0 items-center rounded-full px-1.5 font-mono text-xs leading-none font-medium tabular-nums',
|
||||||
className='shrink-0 tabular-nums'
|
getGroupRatioClassName(ratio)
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{ratio}x
|
<span>{ratio}x</span>
|
||||||
</StatusBadge>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-2
@@ -97,7 +97,10 @@ function hardenIsolatedHtml(html: string): string {
|
|||||||
|
|
||||||
template.content.querySelectorAll('a[target="_blank"]').forEach((link) => {
|
template.content.querySelectorAll('a[target="_blank"]').forEach((link) => {
|
||||||
const rel = new Set(
|
const rel = new Set(
|
||||||
link.getAttribute('rel')?.split(/\s+/).filter(Boolean) ?? []
|
link
|
||||||
|
.getAttribute('rel')
|
||||||
|
?.split(/\s+/)
|
||||||
|
.filter(Boolean) ?? []
|
||||||
)
|
)
|
||||||
|
|
||||||
rel.add('noopener')
|
rel.add('noopener')
|
||||||
@@ -179,7 +182,10 @@ function IsolatedHtmlContent(props: {
|
|||||||
}, [props.html])
|
}, [props.html])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className={cn('block w-full', props.className)} />
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={cn('block w-full', props.className)}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-3
@@ -26,7 +26,7 @@ import {
|
|||||||
} from 'react'
|
} from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { Button } from '@/components/design-system/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
@@ -229,7 +229,7 @@ export function JsonCodeEditor({
|
|||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-1 text-xs',
|
'flex items-center gap-1 text-xs',
|
||||||
jsonStatus.valid ? 'text-success' : 'text-destructive'
|
jsonStatus.valid ? 'text-emerald-600' : 'text-destructive'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{jsonStatus.valid ? (
|
{jsonStatus.valid ? (
|
||||||
@@ -242,7 +242,8 @@ export function JsonCodeEditor({
|
|||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
size='xs'
|
size='sm'
|
||||||
|
className='h-6 px-2 text-xs'
|
||||||
onClick={formatJson}
|
onClick={formatJson}
|
||||||
disabled={disabled || !jsonStatus.valid || !value.trim()}
|
disabled={disabled || !jsonStatus.valid || !value.trim()}
|
||||||
>
|
>
|
||||||
|
|||||||
+7
-3
@@ -20,8 +20,8 @@ import { Code, Table, Plus, Trash2 } from 'lucide-react'
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { Button } from '@/components/design-system/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/design-system/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
@@ -192,6 +192,7 @@ export function JsonEditor({
|
|||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
|
size='sm'
|
||||||
onClick={toggleMode}
|
onClick={toggleMode}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
@@ -211,7 +212,8 @@ export function JsonEditor({
|
|||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
variant='link'
|
variant='link'
|
||||||
className='h-auto p-0 sm:h-auto'
|
size='sm'
|
||||||
|
className='h-auto p-0'
|
||||||
onClick={handleFillTemplate}
|
onClick={handleFillTemplate}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
@@ -258,6 +260,7 @@ export function JsonEditor({
|
|||||||
aria-label='Delete row'
|
aria-label='Delete row'
|
||||||
onClick={() => handleDeleteRow(row.id)}
|
onClick={() => handleDeleteRow(row.id)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
className='h-10 w-10'
|
||||||
>
|
>
|
||||||
<Trash2 className='h-4 w-4' aria-hidden='true' />
|
<Trash2 className='h-4 w-4' aria-hidden='true' />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -272,6 +275,7 @@ export function JsonEditor({
|
|||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
|
size='sm'
|
||||||
onClick={handleAddRow}
|
onClick={handleAddRow}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className='w-full'
|
className='w-full'
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user