mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-19 13:55:40 -04:00
Refactor ListView to use table-based row rendering with useTable hook
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -2023,14 +2023,6 @@
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||
"type": "string"
|
||||
},
|
||||
"cmd": {
|
||||
"description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
|
||||
"type": "string"
|
||||
},
|
||||
"args": {
|
||||
"description": "The allowed arguments for the command execution.",
|
||||
"allOf": [
|
||||
@@ -2038,6 +2030,14 @@
|
||||
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
|
||||
}
|
||||
]
|
||||
},
|
||||
"cmd": {
|
||||
"description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -2049,10 +2049,6 @@
|
||||
"sidecar"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||
"type": "string"
|
||||
},
|
||||
"args": {
|
||||
"description": "The allowed arguments for the command execution.",
|
||||
"allOf": [
|
||||
@@ -2061,6 +2057,10 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||
"type": "string"
|
||||
},
|
||||
"sidecar": {
|
||||
"description": "If this command is a sidecar command.",
|
||||
"type": "boolean"
|
||||
@@ -2083,14 +2083,6 @@
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||
"type": "string"
|
||||
},
|
||||
"cmd": {
|
||||
"description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
|
||||
"type": "string"
|
||||
},
|
||||
"args": {
|
||||
"description": "The allowed arguments for the command execution.",
|
||||
"allOf": [
|
||||
@@ -2098,6 +2090,14 @@
|
||||
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
|
||||
}
|
||||
]
|
||||
},
|
||||
"cmd": {
|
||||
"description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -2109,10 +2109,6 @@
|
||||
"sidecar"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||
"type": "string"
|
||||
},
|
||||
"args": {
|
||||
"description": "The allowed arguments for the command execution.",
|
||||
"allOf": [
|
||||
@@ -2121,6 +2117,10 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||
"type": "string"
|
||||
},
|
||||
"sidecar": {
|
||||
"description": "If this command is a sidecar command.",
|
||||
"type": "boolean"
|
||||
@@ -6290,6 +6290,34 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"ShellScopeEntryAllowedArg": {
|
||||
"description": "A command argument allowed to be executed by the webview API.",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "A non-configurable argument that is passed to the command in the order it was specified.",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "A variable that is set while calling the command from the webview API.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"validator"
|
||||
],
|
||||
"properties": {
|
||||
"raw": {
|
||||
"description": "Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.",
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"validator": {
|
||||
"description": "[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: <https://docs.rs/regex/latest/regex/#syntax>",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"ShellScopeEntryAllowedArgs": {
|
||||
"description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.",
|
||||
"anyOf": [
|
||||
@@ -6305,34 +6333,6 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"ShellScopeEntryAllowedArg": {
|
||||
"description": "A command argument allowed to be executed by the webview API.",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "A non-configurable argument that is passed to the command in the order it was specified.",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "A variable that is set while calling the command from the webview API.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"validator"
|
||||
],
|
||||
"properties": {
|
||||
"validator": {
|
||||
"description": "[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: <https://docs.rs/regex/latest/regex/#syntax>",
|
||||
"type": "string"
|
||||
},
|
||||
"raw": {
|
||||
"description": "Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.",
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2023,14 +2023,6 @@
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||
"type": "string"
|
||||
},
|
||||
"cmd": {
|
||||
"description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
|
||||
"type": "string"
|
||||
},
|
||||
"args": {
|
||||
"description": "The allowed arguments for the command execution.",
|
||||
"allOf": [
|
||||
@@ -2038,6 +2030,14 @@
|
||||
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
|
||||
}
|
||||
]
|
||||
},
|
||||
"cmd": {
|
||||
"description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -2049,10 +2049,6 @@
|
||||
"sidecar"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||
"type": "string"
|
||||
},
|
||||
"args": {
|
||||
"description": "The allowed arguments for the command execution.",
|
||||
"allOf": [
|
||||
@@ -2061,6 +2057,10 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||
"type": "string"
|
||||
},
|
||||
"sidecar": {
|
||||
"description": "If this command is a sidecar command.",
|
||||
"type": "boolean"
|
||||
@@ -2083,14 +2083,6 @@
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||
"type": "string"
|
||||
},
|
||||
"cmd": {
|
||||
"description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
|
||||
"type": "string"
|
||||
},
|
||||
"args": {
|
||||
"description": "The allowed arguments for the command execution.",
|
||||
"allOf": [
|
||||
@@ -2098,6 +2090,14 @@
|
||||
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
|
||||
}
|
||||
]
|
||||
},
|
||||
"cmd": {
|
||||
"description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -2109,10 +2109,6 @@
|
||||
"sidecar"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||
"type": "string"
|
||||
},
|
||||
"args": {
|
||||
"description": "The allowed arguments for the command execution.",
|
||||
"allOf": [
|
||||
@@ -2121,6 +2117,10 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||
"type": "string"
|
||||
},
|
||||
"sidecar": {
|
||||
"description": "If this command is a sidecar command.",
|
||||
"type": "boolean"
|
||||
@@ -6290,6 +6290,34 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"ShellScopeEntryAllowedArg": {
|
||||
"description": "A command argument allowed to be executed by the webview API.",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "A non-configurable argument that is passed to the command in the order it was specified.",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "A variable that is set while calling the command from the webview API.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"validator"
|
||||
],
|
||||
"properties": {
|
||||
"raw": {
|
||||
"description": "Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.",
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"validator": {
|
||||
"description": "[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: <https://docs.rs/regex/latest/regex/#syntax>",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"ShellScopeEntryAllowedArgs": {
|
||||
"description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.",
|
||||
"anyOf": [
|
||||
@@ -6305,34 +6333,6 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"ShellScopeEntryAllowedArg": {
|
||||
"description": "A command argument allowed to be executed by the webview API.",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "A non-configurable argument that is passed to the command in the order it was specified.",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "A variable that is set while calling the command from the webview API.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"validator"
|
||||
],
|
||||
"properties": {
|
||||
"validator": {
|
||||
"description": "[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: <https://docs.rs/regex/latest/regex/#syntax>",
|
||||
"type": "string"
|
||||
},
|
||||
"raw": {
|
||||
"description": "Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.",
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
11
bun.lock
11
bun.lock
@@ -190,6 +190,7 @@
|
||||
"@sd/ui": "workspace:*",
|
||||
"@tanstack/react-query": "^5.90.7",
|
||||
"@tanstack/react-query-devtools": "^5.90.2",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tanstack/react-virtual": "^3.13.12",
|
||||
"@types/d3": "^7.4.3",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
@@ -1525,8 +1526,12 @@
|
||||
|
||||
"@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.90.2", "", { "dependencies": { "@tanstack/query-devtools": "5.90.1" }, "peerDependencies": { "@tanstack/react-query": "^5.90.2", "react": "^18 || ^19" } }, "sha512-vAXJzZuBXtCQtrY3F/yUNJCV4obT/A/n81kb3+YqLbro5Z2+phdAbceO+deU3ywPw8B42oyJlp4FhO0SoivDFQ=="],
|
||||
|
||||
"@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.13.12", "", { "dependencies": { "@tanstack/virtual-core": "3.13.12" }, "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-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA=="],
|
||||
|
||||
"@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="],
|
||||
|
||||
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.12", "", {}, "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA=="],
|
||||
|
||||
"@taplo/cli": ["@taplo/cli@0.7.0", "", { "bin": { "taplo": "dist/cli.js" } }, "sha512-Ck3zFhQhIhi02Hl6T4ZmJsXdnJE+wXcJz5f8klxd4keRYgenMnip3JDPMGDRLbnC/2iGd8P0sBIQqI3KxfVjBg=="],
|
||||
@@ -5303,6 +5308,8 @@
|
||||
|
||||
"@sd/ui/@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="],
|
||||
|
||||
"@sd/ui/postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||
|
||||
"@sd/ui/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"@sd/web/@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
|
||||
@@ -8775,6 +8782,10 @@
|
||||
|
||||
"@sd/ui/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@sd/ui/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"@sd/ui/postcss/picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"@sd/web/vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
||||
|
||||
"@sd/web/vite/postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||
|
||||
1
docs/workbench
Submodule
1
docs/workbench
Submodule
Submodule docs/workbench added at 10256fd2d6
@@ -26,6 +26,7 @@
|
||||
"@sd/ui": "workspace:*",
|
||||
"@tanstack/react-query": "^5.90.7",
|
||||
"@tanstack/react-query-devtools": "^5.90.2",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tanstack/react-virtual": "^3.13.12",
|
||||
"@types/d3": "^7.4.3",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
import clsx from "clsx";
|
||||
import type { File } from "@sd/ts-client";
|
||||
import { File as FileComponent } from "../../File";
|
||||
import { useExplorer } from "../../context";
|
||||
import { useSelection } from "../../SelectionContext";
|
||||
import { formatBytes, formatRelativeTime } from "../../utils";
|
||||
import { TagPill } from "../../../Tags";
|
||||
|
||||
interface FileRowProps {
|
||||
file: File;
|
||||
fileIndex: number;
|
||||
allFiles: File[];
|
||||
}
|
||||
|
||||
export function FileRow({ file, fileIndex, allFiles }: FileRowProps) {
|
||||
const { setCurrentPath } = useExplorer();
|
||||
const { selectFile, isSelected } = useSelection();
|
||||
|
||||
const selected = isSelected(file.id);
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
const multi = e.metaKey || e.ctrlKey;
|
||||
const range = e.shiftKey;
|
||||
selectFile(file, allFiles, multi, range);
|
||||
};
|
||||
|
||||
const handleDoubleClick = () => {
|
||||
if (file.kind === "Directory") {
|
||||
setCurrentPath(file.sd_path);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={handleClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
className="flex items-center px-2 py-1.5 group"
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
"rounded-lg p-1.5 transition-colors mr-3",
|
||||
selected ? "bg-app-box" : "bg-transparent"
|
||||
)}
|
||||
>
|
||||
<FileComponent.Thumb file={file} size={16} />
|
||||
</div>
|
||||
<div className="flex-1 flex items-center gap-2 min-w-0">
|
||||
<div
|
||||
className={clsx(
|
||||
"text-sm truncate px-2 py-0.5 rounded-md transition-colors inline-block",
|
||||
selected ? "bg-accent text-white" : "text-ink"
|
||||
)}
|
||||
>
|
||||
{file.name}
|
||||
</div>
|
||||
|
||||
{/* Tag Pills (compact) */}
|
||||
{file.tags && file.tags.length > 0 && (
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
{file.tags.slice(0, 2).map((tag) => (
|
||||
<TagPill
|
||||
key={tag.id}
|
||||
color={tag.color || '#3B82F6'}
|
||||
size="xs"
|
||||
>
|
||||
{tag.canonical_name}
|
||||
</TagPill>
|
||||
))}
|
||||
{file.tags.length > 2 && (
|
||||
<span className="text-[10px] text-ink-faint">
|
||||
+{file.tags.length - 2}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-24 text-sm text-ink-dull">
|
||||
{file.size > 0 ? formatBytes(file.size) : "—"}
|
||||
</div>
|
||||
<div className="w-32 text-sm text-ink-dull">
|
||||
{formatRelativeTime(file.modified_at)}
|
||||
</div>
|
||||
<div className="w-24 text-sm text-ink-dull">
|
||||
{file.kind === "File" ? file.extension || "—" : "Folder"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,30 @@
|
||||
import { useExplorer } from "../../context";
|
||||
import { useNormalizedCache } from "../../../../context";
|
||||
import { FileRow } from "./FileRow";
|
||||
import type { DirectorySortBy } from "@sd/ts-client";
|
||||
import { useCallback, useRef, useEffect, memo } from "react";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { flexRender } from "@tanstack/react-table";
|
||||
import { CaretDown } from "@phosphor-icons/react";
|
||||
import clsx from "clsx";
|
||||
|
||||
export function ListView() {
|
||||
const { currentPath, sortBy } = useExplorer();
|
||||
import type { File, DirectorySortBy } from "@sd/ts-client";
|
||||
|
||||
import { useExplorer } from "../../context";
|
||||
import { useSelection } from "../../SelectionContext";
|
||||
import { useNormalizedCache } from "../../../../context";
|
||||
import { TableRow } from "./TableRow";
|
||||
import {
|
||||
useTable,
|
||||
ROW_HEIGHT,
|
||||
TABLE_PADDING_X,
|
||||
TABLE_PADDING_Y,
|
||||
TABLE_HEADER_HEIGHT,
|
||||
} from "./useTable";
|
||||
|
||||
export const ListView = memo(function ListView() {
|
||||
const { currentPath, sortBy, setSortBy } = useExplorer();
|
||||
const { focusedIndex, setFocusedIndex, selectedFiles, selectFile, moveFocus } = useSelection();
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const headerScrollRef = useRef<HTMLDivElement>(null);
|
||||
const bodyScrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const directoryQuery = useNormalizedCache({
|
||||
wireMethod: "query:files.directory_listing",
|
||||
@@ -22,25 +42,202 @@ export function ListView() {
|
||||
});
|
||||
|
||||
const files = directoryQuery.data?.files || [];
|
||||
const { table } = useTable(files);
|
||||
const { rows } = table.getRowModel();
|
||||
|
||||
// Virtual row rendering - uses the container as scroll element
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: rows.length,
|
||||
getScrollElement: useCallback(() => containerRef.current, []),
|
||||
estimateSize: useCallback(() => ROW_HEIGHT, []),
|
||||
paddingStart: TABLE_HEADER_HEIGHT + TABLE_PADDING_Y,
|
||||
paddingEnd: TABLE_PADDING_Y,
|
||||
overscan: 15,
|
||||
});
|
||||
|
||||
const virtualRows = rowVirtualizer.getVirtualItems();
|
||||
|
||||
// Sync horizontal scroll between header and body
|
||||
const handleBodyScroll = useCallback(() => {
|
||||
if (bodyScrollRef.current && headerScrollRef.current) {
|
||||
headerScrollRef.current.scrollLeft = bodyScrollRef.current.scrollLeft;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Keyboard navigation
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
const direction = e.key === "ArrowDown" ? "down" : "up";
|
||||
|
||||
const currentIndex = focusedIndex >= 0 ? focusedIndex : 0;
|
||||
const newIndex =
|
||||
direction === "down"
|
||||
? Math.min(currentIndex + 1, files.length - 1)
|
||||
: Math.max(currentIndex - 1, 0);
|
||||
|
||||
if (e.shiftKey) {
|
||||
// Range selection with shift
|
||||
if (newIndex !== focusedIndex && files[newIndex]) {
|
||||
selectFile(files[newIndex], files, false, true);
|
||||
setFocusedIndex(newIndex);
|
||||
}
|
||||
} else {
|
||||
moveFocus(direction, files);
|
||||
}
|
||||
|
||||
// Scroll to keep selection visible
|
||||
rowVirtualizer.scrollToIndex(newIndex, { align: "auto" });
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [focusedIndex, files, selectFile, setFocusedIndex, moveFocus, rowVirtualizer]);
|
||||
|
||||
// Column sorting handler
|
||||
const handleHeaderClick = useCallback(
|
||||
(columnId: string) => {
|
||||
const sortMap: Record<string, DirectorySortBy> = {
|
||||
name: "name",
|
||||
size: "size",
|
||||
modified: "modified",
|
||||
type: "type",
|
||||
};
|
||||
const newSort = sortMap[columnId];
|
||||
if (newSort) {
|
||||
setSortBy(newSort);
|
||||
}
|
||||
},
|
||||
[setSortBy]
|
||||
);
|
||||
|
||||
// Calculate total width for table
|
||||
const headerGroups = table.getHeaderGroups();
|
||||
const totalWidth = table.getTotalSize() + TABLE_PADDING_X * 2;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col p-6">
|
||||
<div className="flex items-center px-2 py-1 text-xs font-semibold text-ink-dull border-b border-app-line mb-2">
|
||||
<div className="w-10"></div>
|
||||
<div className="flex-1">Name</div>
|
||||
<div className="w-24">Size</div>
|
||||
<div className="w-32">Modified</div>
|
||||
<div className="w-24">Type</div>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="h-full overflow-auto"
|
||||
>
|
||||
{/* Sticky Header */}
|
||||
<div
|
||||
className="sticky top-0 z-10 border-b border-app-line bg-app/90 backdrop-blur-lg"
|
||||
style={{ height: TABLE_HEADER_HEIGHT }}
|
||||
>
|
||||
<div
|
||||
ref={headerScrollRef}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div
|
||||
className="flex"
|
||||
style={{
|
||||
width: totalWidth,
|
||||
paddingLeft: TABLE_PADDING_X,
|
||||
paddingRight: TABLE_PADDING_X,
|
||||
}}
|
||||
>
|
||||
{headerGroups.map((headerGroup) =>
|
||||
headerGroup.headers.map((header) => {
|
||||
const isSorted = sortBy === header.id;
|
||||
const canResize = header.column.getCanResize();
|
||||
|
||||
return (
|
||||
<div
|
||||
key={header.id}
|
||||
className={clsx(
|
||||
"relative flex select-none items-center gap-1 px-2 py-2 text-xs font-medium",
|
||||
isSorted ? "text-ink" : "text-ink-dull",
|
||||
"cursor-pointer hover:text-ink"
|
||||
)}
|
||||
style={{ width: header.getSize() }}
|
||||
onClick={() => handleHeaderClick(header.id)}
|
||||
>
|
||||
<span className="truncate">
|
||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</span>
|
||||
|
||||
{isSorted && (
|
||||
<CaretDown className="size-3 flex-shrink-0 text-ink-faint" />
|
||||
)}
|
||||
|
||||
{/* Resize handle */}
|
||||
{canResize && (
|
||||
<div
|
||||
onMouseDown={header.getResizeHandler()}
|
||||
onTouchStart={header.getResizeHandler()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className={clsx(
|
||||
"absolute right-0 top-1/2 h-4 w-1 -translate-y-1/2 cursor-col-resize rounded-full",
|
||||
header.column.getIsResizing()
|
||||
? "bg-accent"
|
||||
: "bg-transparent hover:bg-ink-faint/50"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{files.map((file, index) => (
|
||||
<FileRow
|
||||
key={file.id}
|
||||
file={file}
|
||||
fileIndex={index}
|
||||
allFiles={files}
|
||||
/>
|
||||
))}
|
||||
{/* Virtual List Body */}
|
||||
<div
|
||||
ref={bodyScrollRef}
|
||||
className="overflow-x-auto"
|
||||
onScroll={handleBodyScroll}
|
||||
>
|
||||
<div
|
||||
className="relative"
|
||||
style={{
|
||||
height: rowVirtualizer.getTotalSize() - TABLE_HEADER_HEIGHT,
|
||||
width: totalWidth,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute left-0 top-0 w-full"
|
||||
style={{
|
||||
transform: `translateY(${(virtualRows[0]?.start ?? 0) - TABLE_HEADER_HEIGHT - TABLE_PADDING_Y}px)`,
|
||||
}}
|
||||
>
|
||||
{virtualRows.map((virtualRow) => {
|
||||
const row = rows[virtualRow.index];
|
||||
if (!row) return null;
|
||||
|
||||
const file = row.original;
|
||||
const isSelected = selectedFiles.some((f) => f.id === file.id);
|
||||
const isFocused = focusedIndex === virtualRow.index;
|
||||
const previousRow = rows[virtualRow.index - 1];
|
||||
const nextRow = rows[virtualRow.index + 1];
|
||||
const isPreviousSelected = previousRow
|
||||
? selectedFiles.some((f) => f.id === previousRow.original.id)
|
||||
: false;
|
||||
const isNextSelected = nextRow
|
||||
? selectedFiles.some((f) => f.id === nextRow.original.id)
|
||||
: false;
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
row={row}
|
||||
file={file}
|
||||
files={files}
|
||||
index={virtualRow.index}
|
||||
isSelected={isSelected}
|
||||
isFocused={isFocused}
|
||||
isPreviousSelected={isPreviousSelected}
|
||||
isNextSelected={isNextSelected}
|
||||
measureRef={rowVirtualizer.measureElement}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
import { memo, useCallback } from "react";
|
||||
import { flexRender, type Row } from "@tanstack/react-table";
|
||||
import clsx from "clsx";
|
||||
|
||||
import type { File } from "@sd/ts-client";
|
||||
|
||||
import { File as FileComponent } from "../../File";
|
||||
import { useExplorer } from "../../context";
|
||||
import { useSelection } from "../../SelectionContext";
|
||||
import { TagPill } from "../../../Tags";
|
||||
import { ROW_HEIGHT, TABLE_PADDING_X } from "./useTable";
|
||||
|
||||
interface TableRowProps {
|
||||
row: Row<File>;
|
||||
file: File;
|
||||
files: File[];
|
||||
index: number;
|
||||
isSelected: boolean;
|
||||
isFocused: boolean;
|
||||
isPreviousSelected: boolean;
|
||||
isNextSelected: boolean;
|
||||
measureRef: (node: HTMLElement | null) => void;
|
||||
}
|
||||
|
||||
export const TableRow = memo(function TableRow({
|
||||
row,
|
||||
file,
|
||||
files,
|
||||
index,
|
||||
isSelected,
|
||||
isFocused,
|
||||
isPreviousSelected,
|
||||
isNextSelected,
|
||||
measureRef,
|
||||
}: TableRowProps) {
|
||||
const { setCurrentPath } = useExplorer();
|
||||
const { selectFile } = useSelection();
|
||||
|
||||
const handleClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
const multi = e.metaKey || e.ctrlKey;
|
||||
const range = e.shiftKey;
|
||||
selectFile(file, files, multi, range);
|
||||
},
|
||||
[file, files, selectFile]
|
||||
);
|
||||
|
||||
const handleDoubleClick = useCallback(() => {
|
||||
if (file.kind === "Directory") {
|
||||
setCurrentPath(file.sd_path);
|
||||
}
|
||||
}, [file, setCurrentPath]);
|
||||
|
||||
const cells = row.getVisibleCells();
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={measureRef}
|
||||
data-index={index}
|
||||
className="relative"
|
||||
style={{ height: ROW_HEIGHT }}
|
||||
onClick={handleClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
>
|
||||
{/* Background layer for alternating colors and selection */}
|
||||
<div
|
||||
className={clsx(
|
||||
"absolute inset-0 rounded-md border transition-colors",
|
||||
// Alternating background
|
||||
index % 2 === 0 && !isSelected && "bg-app-darkBox/50",
|
||||
// Selection styling
|
||||
isSelected
|
||||
? "border-accent bg-accent/10"
|
||||
: "border-transparent",
|
||||
// Connect adjacent selected rows
|
||||
isSelected && isPreviousSelected && "rounded-t-none border-t-0",
|
||||
isSelected && isNextSelected && "rounded-b-none border-b-0"
|
||||
)}
|
||||
style={{
|
||||
left: TABLE_PADDING_X,
|
||||
right: TABLE_PADDING_X,
|
||||
}}
|
||||
>
|
||||
{/* Subtle separator between connected selected rows */}
|
||||
{isSelected && isPreviousSelected && (
|
||||
<div className="absolute inset-x-3 top-0 h-px bg-accent/20" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Row content */}
|
||||
<div
|
||||
className="relative flex h-full items-center"
|
||||
style={{
|
||||
paddingLeft: TABLE_PADDING_X,
|
||||
paddingRight: TABLE_PADDING_X,
|
||||
}}
|
||||
>
|
||||
{cells.map((cell) => {
|
||||
const isNameColumn = cell.column.id === "name";
|
||||
|
||||
return (
|
||||
<div
|
||||
key={cell.id}
|
||||
className={clsx(
|
||||
"flex h-full items-center px-2 text-sm",
|
||||
isNameColumn ? "min-w-0 flex-1" : "text-ink-dull"
|
||||
)}
|
||||
style={{ width: cell.column.getSize() }}
|
||||
>
|
||||
{isNameColumn ? (
|
||||
<NameCell file={file} isSelected={isSelected} />
|
||||
) : (
|
||||
<span className="truncate">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
// Name cell with icon and tags
|
||||
const NameCell = memo(function NameCell({
|
||||
file,
|
||||
isSelected,
|
||||
}: {
|
||||
file: File;
|
||||
isSelected: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
{/* File icon */}
|
||||
<div
|
||||
className={clsx(
|
||||
"flex-shrink-0 rounded-md p-1 transition-colors",
|
||||
isSelected ? "bg-app-box/50" : "bg-transparent"
|
||||
)}
|
||||
>
|
||||
<FileComponent.Thumb file={file} size={20} />
|
||||
</div>
|
||||
|
||||
{/* File name */}
|
||||
<span
|
||||
className={clsx(
|
||||
"truncate rounded px-1.5 py-0.5 text-sm transition-colors",
|
||||
isSelected ? "bg-accent text-white" : "text-ink"
|
||||
)}
|
||||
>
|
||||
{file.name}
|
||||
</span>
|
||||
|
||||
{/* Tags (inline, compact) */}
|
||||
{file.tags && file.tags.length > 0 && (
|
||||
<div className="flex flex-shrink-0 items-center gap-1">
|
||||
{file.tags.slice(0, 2).map((tag) => (
|
||||
<TagPill
|
||||
key={tag.id}
|
||||
color={tag.color || "#3B82F6"}
|
||||
size="xs"
|
||||
>
|
||||
{tag.canonical_name}
|
||||
</TagPill>
|
||||
))}
|
||||
{file.tags.length > 2 && (
|
||||
<span className="text-[10px] text-ink-faint">
|
||||
+{file.tags.length - 2}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -1,2 +1,2 @@
|
||||
export { ListView } from "./ListView";
|
||||
export { FileRow } from "./FileRow";
|
||||
export { TableRow } from "./TableRow";
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type ColumnDef,
|
||||
type ColumnSizingState,
|
||||
} from "@tanstack/react-table";
|
||||
import type { File } from "@sd/ts-client";
|
||||
|
||||
import { formatBytes, formatRelativeTime } from "../../utils";
|
||||
|
||||
export const ROW_HEIGHT = 36;
|
||||
export const TABLE_PADDING_X = 16;
|
||||
export const TABLE_PADDING_Y = 12;
|
||||
export const TABLE_HEADER_HEIGHT = 32;
|
||||
|
||||
// Column definitions for the list view
|
||||
export function useTable(files: File[]) {
|
||||
const columns = useMemo<ColumnDef<File>[]>(
|
||||
() => [
|
||||
{
|
||||
id: "name",
|
||||
header: "Name",
|
||||
minSize: 200,
|
||||
size: 300,
|
||||
maxSize: 800,
|
||||
accessorFn: (row) => row.name,
|
||||
},
|
||||
{
|
||||
id: "size",
|
||||
header: "Size",
|
||||
size: 80,
|
||||
minSize: 60,
|
||||
maxSize: 120,
|
||||
accessorFn: (row) => (row.size > 0 ? formatBytes(row.size) : "—"),
|
||||
},
|
||||
{
|
||||
id: "modified",
|
||||
header: "Modified",
|
||||
size: 120,
|
||||
minSize: 80,
|
||||
maxSize: 180,
|
||||
accessorFn: (row) => formatRelativeTime(row.modified_at),
|
||||
},
|
||||
{
|
||||
id: "type",
|
||||
header: "Type",
|
||||
size: 80,
|
||||
minSize: 60,
|
||||
maxSize: 120,
|
||||
accessorFn: (row) =>
|
||||
row.kind === "File" ? row.extension?.toUpperCase() || "—" : "Folder",
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const table = useReactTable({
|
||||
data: files,
|
||||
columns,
|
||||
defaultColumn: {
|
||||
minSize: 60,
|
||||
maxSize: 500,
|
||||
},
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
columnResizeMode: "onChange",
|
||||
getRowId: (row) => row.id,
|
||||
});
|
||||
|
||||
return { table, columns };
|
||||
}
|
||||
|
||||
export type { ColumnSizingState };
|
||||
@@ -7,6 +7,9 @@ import {
|
||||
Folder,
|
||||
HardDrive,
|
||||
Tag as TagIcon,
|
||||
FolderOpen,
|
||||
MagnifyingGlass,
|
||||
Trash,
|
||||
} from "@phosphor-icons/react";
|
||||
import { Location } from "@sd/assets/icons";
|
||||
import type {
|
||||
@@ -15,6 +18,9 @@ import type {
|
||||
File,
|
||||
} from "@sd/ts-client";
|
||||
import { Thumb } from "../Explorer/File/Thumb";
|
||||
import { useContextMenu } from "../../hooks/useContextMenu";
|
||||
import { usePlatform } from "../../platform";
|
||||
import { useLibraryMutation } from "../../context";
|
||||
|
||||
interface SpaceItemProps {
|
||||
item: SpaceItemType;
|
||||
@@ -107,6 +113,8 @@ export function SpaceItem({
|
||||
}: SpaceItemProps) {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const platform = usePlatform();
|
||||
const deleteItem = useLibraryMutation("spaces.delete_item");
|
||||
|
||||
// Check if this is a raw location object (has 'name' and 'sd_path' but no 'item_type')
|
||||
const isRawLocation =
|
||||
@@ -151,9 +159,75 @@ export function SpaceItem({
|
||||
}
|
||||
};
|
||||
|
||||
// Context menu for space items
|
||||
const contextMenu = useContextMenu({
|
||||
items: [
|
||||
{
|
||||
icon: FolderOpen,
|
||||
label: "Open",
|
||||
onClick: () => {
|
||||
if (path) navigate(path);
|
||||
},
|
||||
condition: () => !!path,
|
||||
},
|
||||
{
|
||||
icon: MagnifyingGlass,
|
||||
label: "Show in Finder",
|
||||
onClick: async () => {
|
||||
// For Path items, get the physical path
|
||||
if (typeof item.item_type === "object" && "Path" in item.item_type) {
|
||||
const sdPath = item.item_type.Path.sd_path;
|
||||
if (typeof sdPath === "object" && "Physical" in sdPath) {
|
||||
const physicalPath = sdPath.Physical.path;
|
||||
if (platform.revealFile) {
|
||||
try {
|
||||
await platform.revealFile(physicalPath);
|
||||
} catch (err) {
|
||||
console.error("Failed to reveal file:", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
keybind: "⌘⇧R",
|
||||
condition: () => {
|
||||
if (typeof item.item_type === "object" && "Path" in item.item_type) {
|
||||
const sdPath = item.item_type.Path.sd_path;
|
||||
return typeof sdPath === "object" && "Physical" in sdPath && !!platform.revealFile;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
icon: Trash,
|
||||
label: "Remove from Space",
|
||||
onClick: async () => {
|
||||
if (confirm(`Remove "${label}" from this space?`)) {
|
||||
try {
|
||||
await deleteItem.mutateAsync({ item_id: item.id });
|
||||
} catch (err) {
|
||||
console.error("Failed to remove item:", err);
|
||||
}
|
||||
}
|
||||
},
|
||||
variant: "danger" as const,
|
||||
// Can only remove custom Path items, not built-in items
|
||||
condition: () => typeof item.item_type === "object" && "Path" in item.item_type,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const handleContextMenu = async (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
await contextMenu.show(e);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
className={clsx(
|
||||
"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm font-medium",
|
||||
className ||
|
||||
|
||||
Submodule workbench deleted from 678ef4e295
Reference in New Issue
Block a user