Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions internal/mcpapp/css/input.css
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,27 @@ body {
@apply w-full px-2.5 py-2 border border-border rounded-md bg-background-deep text-foreground font-sans placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-accent;
}

/* ---- File picker (MCP upload apps) --------------------------------------- */
/* The native <input type="file"> cannot be styled, so pin it invisible atop a
styled chrome (a "Choose file" button + the picked name). Clicking anywhere
on the field opens the native picker; the name is refreshed from JS on
change. */
@utility file-field {
@apply relative cursor-pointer;
}
@utility file-input {
@apply absolute inset-0 w-full h-full opacity-0 cursor-pointer;
}
@utility file-picker {
@apply flex items-center gap-2 px-2.5 py-2 border border-border rounded-md bg-background-deep;
}
@utility file-btn {
@apply shrink-0 inline-flex items-center px-3 py-1 rounded border border-border bg-card text-foreground text-sm font-medium cursor-pointer select-none transition-colors hover:bg-card/80;
}
@utility file-name {
@apply truncate min-w-0 text-sm text-muted-foreground;
}

/* ---- Tables (MCP pin-list + vault) --------------------------------------- */
@utility table {
@apply w-full text-sm text-foreground;
Expand Down
8 changes: 7 additions & 1 deletion internal/mcpapp/ipfs_upload_app.templ
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ templ IPFSUploadAppForm() {
<h1 class="app-title">Upload to IPFS</h1>
<form id="ipfs-upload-form" class="mt-2">
<label for="file" class="form-label">File</label>
<input id="file" name="file" class="form-input" type="file"/>
<div class="file-field">
<input id="file" name="file" type="file" class="file-input"/>
<div class="file-picker">
<span class="file-btn">Choose file</span>
<span id="file-name" class="file-name">No file chosen</span>
</div>
</div>
<label for="name" class="form-label">Name (optional)</label>
<input id="name" name="name" class="form-input" placeholder="my-file" autocomplete="off"/>
<button type="submit" class="btn-primary" id="start">Upload</button>
Expand Down
8 changes: 7 additions & 1 deletion internal/mcpapp/vault_upload_app.templ
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ templ VaultUploadAppForm() {
<h1 class="app-title">Upload to Vault</h1>
<form id="vault-upload-form" class="mt-2">
<label for="vfile" class="form-label">File</label>
<input id="vfile" name="vfile" class="form-input" type="file"/>
<div class="file-field">
<input id="vfile" name="vfile" type="file" class="file-input"/>
<div class="file-picker">
<span class="file-btn">Choose file</span>
<span id="vfile-name" class="file-name">No file chosen</span>
</div>
</div>
<label for="vault-path" class="form-label">Vault path</label>
<input id="vault-path" name="vault-path" class="form-input" placeholder="vault:/uploads/document.pdf" autocomplete="off" spellcheck="false"/>
<button type="submit" class="btn-primary" id="vstart">Upload</button>
Expand Down
1 change: 1 addition & 0 deletions packages/apps/src/entries/ipfs-upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const def: IPFSUploadAppEntry = {
ids: {
form: "ipfs-upload-form",
file: "file",
fileName: "file-name",
name: "name",
status: "ipfs-upload-status",
outCid: "out-cid",
Expand Down
1 change: 1 addition & 0 deletions packages/apps/src/entries/vault-upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const def: VaultUploadAppEntry = {
ids: {
form: "vault-upload-form",
file: "vfile",
fileName: "vfile-name",
vaultPath: "vault-path",
status: "vault-upload-status",
outPath: "out-path",
Expand Down
16 changes: 14 additions & 2 deletions packages/apps/src/ipfs-upload-bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
//
// Element contract:
// #ipfs-upload-form the <form> that starts the upload.
// #file the file <input type="file">.
// #file the file <input type="file"> (styled composite picker).
// #file-name the picked-file label span ("No file chosen").
// #name the upload-name <input>.
// #ipfs-upload-status the status element (class "status <state>").
// #out-cid the result CID <code>.
Expand Down Expand Up @@ -38,6 +39,7 @@ import { byId, setStatus, StatusClass } from "@/dom";
export type IPFSUploadElementIds = {
form: string;
file: string;
fileName: string;
name: string;
status: string;
outCid: string;
Expand All @@ -49,7 +51,8 @@ export type IPFSUploadAppEntry = AppDefinition<IPFSUploadConfig, IPFSUploadEleme

export interface IPFSUploadElements {
form: { addEventListener(type: "submit", listener: (ev: SubmitEvent) => void): void };
fileInput: { files: FileList | null };
fileInput: HTMLInputElement;
fileNameEl: HTMLElement;
nameInput: { value: string };
statusEl: HTMLElement;
outCid: HTMLElement;
Expand Down Expand Up @@ -135,6 +138,14 @@ export function runIPFSUploadEntry(opts: IPFSUploadEntryOptions) {
if (r.setCid) opts.elements.outCid.textContent = ctx.outCid || opts.elements.outCid.textContent;
});

// Reflect the picked file's name in the styled picker chrome. The native
// input is invisible (see file-field/file-input in the theme), so without
// this the user has no way to see what was selected.
opts.elements.fileInput.addEventListener("change", () => {
const picked = opts.elements.fileInput.files?.[0];
opts.elements.fileNameEl.textContent = picked ? picked.name : "No file chosen";
});

const submit = (file: File | null, name: string) => {
if (!file) {
setStatus(opts.elements.statusEl, StatusClass.Error, opts.config.noFileMsg);
Expand Down Expand Up @@ -176,6 +187,7 @@ export function mountIPFSUploadApp(def: IPFSUploadAppEntry, root: Document, call
elements: {
form: byId<HTMLFormElement>(root, def.ids.form)!,
fileInput: byId<HTMLInputElement>(root, def.ids.file)!,
fileNameEl: byId<HTMLElement>(root, def.ids.fileName)!,
nameInput: byId<HTMLInputElement>(root, def.ids.name)!,
statusEl: statusEl!,
outCid: byId<HTMLElement>(root, def.ids.outCid)!,
Expand Down
16 changes: 14 additions & 2 deletions packages/apps/src/vault-upload-bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
//
// Element contract:
// #vault-upload-form the <form> that submits the upload.
// #vfile the file <input type="file">.
// #vfile the file <input type="file"> (styled composite picker).
// #vfile-name the picked-file label span ("No file chosen").
// #vault-path the vault destination path <input>.
// #vault-upload-status the status element (class "status <state>").
// #out-path the result stored-path <code>.
Expand All @@ -31,6 +32,7 @@ import { byId, setStatus, StatusClass } from "@/dom";
export type VaultUploadElementIds = {
form: string;
file: string;
fileName: string;
vaultPath: string;
status: string;
outPath: string;
Expand All @@ -42,7 +44,8 @@ export type VaultUploadAppEntry = AppDefinition<VaultUploadConfig, VaultUploadEl

export interface VaultUploadElements {
form: { addEventListener(type: "submit", listener: (ev: SubmitEvent) => void): void };
fileInput: { files: FileList | null };
fileInput: HTMLInputElement;
fileNameEl: HTMLElement;
vaultPathInput: { value: string };
statusEl: HTMLElement;
outPath: HTMLElement;
Expand Down Expand Up @@ -133,6 +136,14 @@ export function runVaultUploadEntry(opts: VaultUploadEntryOptions) {
if (r.setOutPath && ctx.outPath) opts.elements.outPath.textContent = ctx.outPath;
});

// Reflect the picked file's name in the styled picker chrome. The native
// input is invisible (see file-field/file-input in the theme), so without
// this the user has no way to see what was selected.
opts.elements.fileInput.addEventListener("change", () => {
const picked = opts.elements.fileInput.files?.[0];
opts.elements.fileNameEl.textContent = picked ? picked.name : "No file chosen";
});

const submit = async (file: File | null, vaultPath: string) => {
if (!file) {
setStatus(opts.elements.statusEl, StatusClass.Error, opts.config.noFileMsg);
Expand Down Expand Up @@ -179,6 +190,7 @@ export function mountVaultUploadApp(def: VaultUploadAppEntry, root: Document, ca
elements: {
form: byId<HTMLFormElement>(root, def.ids.form)!,
fileInput: byId<HTMLInputElement>(root, def.ids.file)!,
fileNameEl: byId<HTMLElement>(root, def.ids.fileName)!,
vaultPathInput: byId<HTMLInputElement>(root, def.ids.vaultPath)!,
statusEl: statusEl!,
outPath: byId<HTMLElement>(root, def.ids.outPath)!,
Expand Down
40 changes: 40 additions & 0 deletions tests/sunpeak/tests/inspector-apps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,43 @@ test('upload_file app boots (ipfs-upload bundle has no unmet imports)', async ({
test('vault_put_file app boots (vault-upload bundle has no unmet imports)', async ({ inspector }) => {
await assertUploadAppBoots('vault_put_file', 'Upload to Vault', inspector);
});

/**
* The upload forms use a styled composite file picker: the native
* <input type="file"> is pinned invisible under a "Choose file" button + a
* picked-file label, because the browser's native "Choose file/No file chosen"
* chrome cannot be themed. Verify the chrome renders and that picking a file
* updates the label (the bootstraps wire a change listener for that).
*/
async function assertStyledFilePicker(
tool: string,
fileId: string,
labelId: string,
inspector: { renderTool: (n: string, i: unknown) => Promise<{ app(): FrameLocator }> },
) {
const result = await inspector.renderTool(tool, {});
const app = result.app();
const body = await app.locator('body').innerText();
expect(body).toContain('Choose file');
expect(body).toContain('No file chosen');
expect(body).toContain('Upload');

// Actually select a file through the hidden native input and assert the
// picker label adopts the filename (proves the change listener fired and
// the machine is wired, not just the static HTML shell).
const fileInput = app.locator(`#${fileId}`);
await fileInput.setInputFiles({
name: 'report.pdf',
mimeType: 'application/pdf',
buffer: Buffer.from('pdf-bytes'),
});
await expect(app.locator(`#${labelId}`)).toHaveText('report.pdf');
}

test('upload_file app renders the styled file picker and reflects the picked name', async ({ inspector }) => {
await assertStyledFilePicker('upload_file', 'file', 'file-name', inspector);
});

test('vault_put_file app renders the styled file picker and reflects the picked name', async ({ inspector }) => {
await assertStyledFilePicker('vault_put_file', 'vfile', 'vfile-name', inspector);
});
Loading