Lots of regenerating

This commit is contained in:
Half-Shot 2026-05-14 16:59:19 +01:00
parent 314c57da8c
commit ba78a54c56
19 changed files with 435 additions and 43 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View File

@ -75,6 +75,18 @@ const meta = {
type: "figma",
url: "https://www.figma.com/design/1UCf1hF507QaRus3CUBKGn/Nectcloud-File-Picker?node-id=2138-14865&t=njpHkpdk8tVhp7cr-0",
},
a11y: {
config: {
rules: [
{
// TODO: We need a new folder icon, the current one is a emoji and we
// can't determine the contrast.
id: "color-contrast",
enabled: false,
},
],
},
},
},
} satisfies Meta<typeof FileListWrapper>;

View File

@ -0,0 +1,70 @@
/*
* Copyright 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render } from "@test-utils";
import { composeStories } from "@storybook/react-vite";
import { describe, it, expect } from "vitest";
import { fn } from "storybook/test";
import userEvent from "@testing-library/user-event";
import * as stories from "./GridFileList.stories";
import { BaseViewModel } from "../../core/viewmodel";
import type { FileShareViewModel, FileShareViewSnapshot } from "./Viewmodel";
import { FileListView } from "./FileList";
const { Default } = composeStories(stories);
class MockViewModel extends BaseViewModel<FileShareViewSnapshot, unknown> implements FileShareViewModel {
public constructor(snapshot: FileShareViewSnapshot) {
super(undefined, snapshot);
}
public loadFiles = fn();
public setCurrentDirectory = fn();
public goBackDirectory = fn();
public onFileSelected = fn();
public getThumbnailForFile = fn().mockResolvedValue(null);
public setFileViewSetting = fn();
}
function renderView(initialSnapshot?: Partial<FileShareViewSnapshot>): [ReturnType<typeof render>, MockViewModel] {
const snapshot: FileShareViewSnapshot = {
currentDirectory: [],
directories: [],
files: [
{
id: "a_file",
name: "myfile.txt",
},
],
selectedFiles: [],
loading: false,
sending: false,
viewSetting: "list",
...initialSnapshot,
};
const vm = new MockViewModel(snapshot);
const result = render(<FileListView vm={vm} />);
return [result, vm];
}
describe("<FileListView />", () => {
it("renders Default story", () => {
const { container } = render(<Default />);
expect(container).toMatchSnapshot();
});
it("can select a file in details mode", async () => {
const [{ getByLabelText }, vm] = renderView();
await userEvent.click(getByLabelText("myfile.txt"));
expect(vm.onFileSelected).toHaveBeenCalledWith("a_file");
});
it("can select a file in grid mode", async () => {
const [{ getByLabelText }, vm] = renderView({ viewSetting: "grid" });
await userEvent.click(getByLabelText("myfile.txt"));
expect(vm.onFileSelected).toHaveBeenCalledWith("a_file");
});
});

View File

@ -4,8 +4,17 @@
display: grid;
gap: 0 var(--cpd-space-4x);
padding: var(--cpd-space-4x);
grid-template-columns: repeat(4, minmax(0, 1fr));
grid-template-columns: repeat(auto-fit, minmax(128px, 1fr));
border-radius: var(--cpd-space-2x);
li {
list-style: none;
gap: var(--cpd-space-3x);
> button {
width: fit-content;
}
}
button.fileTile:hover {
background: var(--cpd-color-bg-action-tertiary-hovered);
}
@ -17,7 +26,6 @@
label {
display: block;
}
gap: var(--cpd-space-3x);
border: none;
label {
@ -29,6 +37,8 @@
}
.previewThumb {
width: 128px;
height: 128px;
width: 100%;
aspect-ratio: 1;
background-size: cover;
@ -42,15 +52,16 @@
}
.bigIcon {
width: fit-content;
max-width: 128px;
max-height: 128px;
padding: 4em;
aspect-ratio: 1;
margin: 0 auto;
width: 48px;
height: 48px;
color: var(--cpd-color-icon-secondary);
> span {
line-height: 0.6;
font-size: 36px;
margin-left: auto;
margin-right: auto;
margin: auto;
}
}
}

View File

@ -62,6 +62,23 @@ const meta = {
type: "figma",
url: "https://www.figma.com/design/1UCf1hF507QaRus3CUBKGn/Nectcloud-File-Picker?node-id=2138-14865&t=njpHkpdk8tVhp7cr-0",
},
a11y: {
config: {
rules: [
{
// TODO: We need a new folder icon, the current one is a emoji and we
// can't determine the contrast.
id: "color-contrast",
enabled: false,
},
{
// So that we can hide the button which is just a bigger target for the checkbox.
id: "aria-hidden-focus",
enabled: false,
},
],
},
},
},
} satisfies Meta<typeof GridFileListView>;

View File

@ -38,21 +38,23 @@ function DirectoryItem({
);
return (
<button
data-kind="primary"
className={classNames(styles.fileTile)}
disabled={disabled}
onClick={onTileClick}
id={id}
>
<BigIcon size="md" className={styles.bigIcon}>
<span>🗀</span>
</BigIcon>
<div>
<label htmlFor={id}>{name}</label>
{updatedAt && <span className={styles.timestamp}>{i18n.humanizeTime(updatedAt.getTime())}</span>}
</div>
</button>
<li>
<button
data-kind="primary"
className={classNames(styles.fileTile)}
disabled={disabled}
onClick={onTileClick}
id={id}
>
<BigIcon size="md" className={styles.bigIcon}>
<span>🗀</span>
</BigIcon>
<div>
<label htmlFor={id}>{name}</label>
{updatedAt && <span className={styles.timestamp}>{i18n.humanizeTime(updatedAt.getTime())}</span>}
</div>
</button>
</li>
);
}
@ -83,24 +85,29 @@ function FileItem({
}, [fileId, previewEngine]);
return (
<button
data-kind="primary"
className={classNames(styles.fileTile)}
disabled={disabled}
onClick={() => onChange()}
id={id}
>
<div
className={classNames(styles.previewThumb)}
style={{ backgroundImage: previewUrl ? `url("${previewUrl}")` : undefined }}
<li>
<button
data-kind="primary"
className={classNames(styles.fileTile)}
disabled={disabled}
onClick={() => onChange()}
id={id}
tabIndex={-1}
/* For keyboard nav, use the checkbox. The button is just a bigger target. */
aria-hidden
>
<Checkbox checked={selected} id={id} disabled={disabled} onChange={() => onChange()} />
</div>
<div>
<label htmlFor={id}>{name}</label>
{updatedAt && <span className={styles.timestamp}>{i18n.humanizeTime(updatedAt.getTime())}</span>}
</div>
</button>
<div
className={classNames(styles.previewThumb)}
style={{ backgroundImage: previewUrl ? `url("${previewUrl}")` : undefined }}
>
<Checkbox aria-labelledby={id} checked={selected} />
</div>
<div>
<label htmlFor={id}>{name}</label>
{updatedAt && <span className={styles.timestamp}>{i18n.humanizeTime(updatedAt.getTime())}</span>}
</div>
</button>
</li>
);
}

View File

@ -55,6 +55,18 @@ const meta = {
type: "figma",
url: "https://www.figma.com/design/1UCf1hF507QaRus3CUBKGn/Nectcloud-File-Picker?node-id=2138-14865&t=njpHkpdk8tVhp7cr-0",
},
a11y: {
config: {
rules: [
{
// TODO: We need a new folder icon, the current one is a emoji and we
// can't determine the contrast.
id: "color-contrast",
enabled: false,
},
],
},
},
},
} satisfies Meta<typeof VerticalFileListView>;

View File

@ -62,8 +62,10 @@ function FileItem({
<div className={styles.fileEntryComponentName}>
<Checkbox checked={selected} id={id} disabled={disabled} onChange={() => onChange()} />
<div>
<label htmlFor={id}>{fileName}</label>
{fileExt[0] && <span className={styles.fileEntryNameExtension}>.{fileExt.join(".")}</span>}
<label htmlFor={id}>
{fileName}
{fileExt[0] && <span className={styles.fileEntryNameExtension}>.{fileExt.join(".")}</span>}
</label>
</div>
</div>
{updatedAt && <span className={styles.timestamp}>{i18n.humanizeTime(updatedAt.getTime())}</span>}

View File

@ -5,7 +5,7 @@
*/
import type { ViewModel } from "../../core/viewmodel";
import { GridFileListPreviewEngine } from "./GridFileList";
import type { GridFileListPreviewEngine } from "./GridFileList";
export type FileId = string;
export type FileShareViewSetting = "list" | "grid";
@ -23,7 +23,6 @@ export interface FileShareViewSnapshot {
}
export interface FileShareActions {
loadFiles(): Promise<void>;
setCurrentDirectory(name: string): void;
goBackDirectory(index?: number): void;
onFileSelected(name: string): void;

View File

@ -0,0 +1,262 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<FileListView /> > renders Default story 1`] = `
<div>
<ol
class="GridFileList-module_container"
>
<button
class="GridFileList-module_fileTile"
data-kind="primary"
id="_r_0_"
>
<div
class="_big-icon_1ssbv_8 GridFileList-module_bigIcon"
data-kind="primary"
data-size="md"
>
<span>
🗀
</span>
</div>
<div>
<label
for="_r_0_"
>
A directory
</label>
</div>
</button>
<button
class="GridFileList-module_fileTile"
data-kind="primary"
id="_r_1_"
>
<div
class="GridFileList-module_previewThumb"
>
<div
class="_container_153f2_10"
>
<input
class="_input_153f2_18"
tabindex="-1"
type="checkbox"
/>
<div
class="_ui_153f2_19"
>
<svg
aria-hidden="true"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.55 17.575q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L4.55 13q-.274-.274-.262-.713.012-.437.287-.712a.95.95 0 0 1 .7-.275q.425 0 .7.275L9.55 15.15l8.475-8.475q.274-.275.713-.275.437 0 .712.275.275.274.275.713 0 .437-.275.712l-9.2 9.2q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
/>
</svg>
</div>
</div>
</div>
<div>
<label
for="_r_1_"
>
testfile.txt
</label>
<span
class="GridFileList-module_timestamp"
>
89 days ago
</span>
</div>
</button>
<button
class="GridFileList-module_fileTile"
data-kind="primary"
id="_r_2_"
>
<div
class="GridFileList-module_previewThumb"
>
<div
class="_container_153f2_10"
>
<input
class="_input_153f2_18"
tabindex="-1"
type="checkbox"
/>
<div
class="_ui_153f2_19"
>
<svg
aria-hidden="true"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.55 17.575q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L4.55 13q-.274-.274-.262-.713.012-.437.287-.712a.95.95 0 0 1 .7-.275q.425 0 .7.275L9.55 15.15l8.475-8.475q.274-.275.713-.275.437 0 .712.275.275.274.275.713 0 .437-.275.712l-9.2 9.2q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
/>
</svg>
</div>
</div>
</div>
<div>
<label
for="_r_2_"
>
image.png
</label>
<span
class="GridFileList-module_timestamp"
>
89 days ago
</span>
</div>
</button>
<button
class="GridFileList-module_fileTile"
data-kind="primary"
id="_r_3_"
>
<div
class="GridFileList-module_previewThumb"
>
<div
class="_container_153f2_10"
>
<input
class="_input_153f2_18"
tabindex="-1"
type="checkbox"
/>
<div
class="_ui_153f2_19"
>
<svg
aria-hidden="true"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.55 17.575q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L4.55 13q-.274-.274-.262-.713.012-.437.287-.712a.95.95 0 0 1 .7-.275q.425 0 .7.275L9.55 15.15l8.475-8.475q.274-.275.713-.275.437 0 .712.275.275.274.275.713 0 .437-.275.712l-9.2 9.2q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
/>
</svg>
</div>
</div>
</div>
<div>
<label
for="_r_3_"
>
compressed.tar.gz
</label>
<span
class="GridFileList-module_timestamp"
>
89 days ago
</span>
</div>
</button>
<button
class="GridFileList-module_fileTile"
data-kind="primary"
id="_r_4_"
>
<div
class="GridFileList-module_previewThumb"
>
<div
class="_container_153f2_10"
>
<input
class="_input_153f2_18"
tabindex="-1"
type="checkbox"
/>
<div
class="_ui_153f2_19"
>
<svg
aria-hidden="true"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.55 17.575q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L4.55 13q-.274-.274-.262-.713.012-.437.287-.712a.95.95 0 0 1 .7-.275q.425 0 .7.275L9.55 15.15l8.475-8.475q.274-.275.713-.275.437 0 .712.275.275.274.275.713 0 .437-.275.712l-9.2 9.2q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
/>
</svg>
</div>
</div>
</div>
<div>
<label
for="_r_4_"
>
no-extension
</label>
<span
class="GridFileList-module_timestamp"
>
30 days ago
</span>
</div>
</button>
<button
class="GridFileList-module_fileTile"
data-kind="primary"
id="_r_5_"
>
<div
class="GridFileList-module_previewThumb"
>
<div
class="_container_153f2_10"
>
<input
class="_input_153f2_18"
tabindex="-1"
type="checkbox"
/>
<div
class="_ui_153f2_19"
>
<svg
aria-hidden="true"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.55 17.575q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L4.55 13q-.274-.274-.262-.713.012-.437.287-.712a.95.95 0 0 1 .7-.275q.425 0 .7.275L9.55 15.15l8.475-8.475q.274-.275.713-.275.437 0 .712.275.275.274.275.713 0 .437-.275.712l-9.2 9.2q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
/>
</svg>
</div>
</div>
</div>
<div>
<label
for="_r_5_"
>
no modified time
</label>
</div>
</button>
</ol>
</div>
`;