File Picker
A button and an input-style field that open the native file dialog via FileSelection.
Installation
npx kosmesis add file-pickerpnpm dlx kosmesis add file-pickeryarn dlx kosmesis add file-pickerbunx kosmesis add file-pickerInstall the following dependencies:
npm install @praxisjs/composables @morphos/iconspnpm add @praxisjs/composables @morphos/iconsyarn add @praxisjs/composables @morphos/iconsbun add @praxisjs/composables @morphos/iconsThis component builds on other Kosmesis components — install these first (via kosmesis add or this same manual process on their own pages): button.
Copy and paste the following code into your project.
import type { FileSelection } from "@praxisjs/composables";import { StatelessComponent } from "@praxisjs/core";import { Component } from "@praxisjs/decorators";import { Icon } from "@morphos/icons";import { Button, type ButtonProps } from "./button";import { cn } from "@/lib/utils";export interface FilePickerProps { selection: FileSelection; label?: string; variant?: ButtonProps["variant"]; size?: ButtonProps["size"]; disabled?: boolean; class?: string;}@Component()export class FilePicker extends StatelessComponent<FilePickerProps> { render() { const { selection, label = "Choose file", variant = "outline", size, disabled, class: cls } = this.props; return ( <Button variant={variant} size={size} disabled={disabled} class={cls} onClick={() => { selection.open(); }}> <Icon name="Paperclip" size={14} /> {label} </Button> ); }}export interface FileInputProps { selection: FileSelection; placeholder?: string; buttonLabel?: string; disabled?: boolean; class?: string;}@Component()export class FileInput extends StatelessComponent<FileInputProps> { render() { const { selection, placeholder = "No file chosen", buttonLabel = "Browse", disabled, class: cls } = this.props; return ( <button type="button" disabled={disabled} class={cn( "flex h-9 w-full min-w-0 items-center gap-3 rounded-md border border-input bg-transparent pr-3 text-left text-sm shadow-xs outline-none transition-[color,box-shadow]", "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50", "disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50", cls, )} onClick={() => { selection.open(); }} > <span class="flex h-full items-center rounded-l-md border-r border-input bg-muted px-3 font-medium"> {buttonLabel} </span> {() => { const files = selection.files; if (files.length === 0) return <span class="truncate text-muted-foreground">{placeholder}</span>; return <span class="truncate">{files.length === 1 ? files[0].name : `${String(files.length)} files selected`}</span>; }} </button> ); }}Install the following dependencies:
npm install @praxisjs/composables @morphos/icons @praxisjs/csspnpm add @praxisjs/composables @morphos/icons @praxisjs/cssyarn add @praxisjs/composables @morphos/icons @praxisjs/cssbun add @praxisjs/composables @morphos/icons @praxisjs/cssThis component builds on other Kosmesis components — install these first (via kosmesis add or this same manual process on their own pages): button.
Copy and paste the following code into your project.
import type { FileSelection } from "@praxisjs/composables";import { StatelessComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled, tokenVars } from "@praxisjs/css";import { Component } from "@praxisjs/decorators";import { Icon } from "@morphos/icons";import { Button, type ButtonProps } from "./button";import { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);class FilePickerStyles extends Stylesheet { $input = this.css({ display: "flex", height: "2.25rem", width: "100%", minWidth: "0", alignItems: "center", gap: "0.75rem", borderRadius: `calc(${t.radius} - 2px)`, border: `1px solid ${t.input}`, backgroundColor: "transparent", paddingRight: "0.75rem", textAlign: "left", fontSize: "0.875rem", boxShadow: "0 1px 2px 0 rgb(0 0 0 / 0.05)", outline: "none", cursor: "pointer", transition: "color 120ms ease, box-shadow 120ms ease", }) .focusVisible({ borderColor: t.ring, boxShadow: `0 0 0 3px color-mix(in oklab, ${t.ring} 50%, transparent)` }) .disabled({ pointerEvents: "none", cursor: "not-allowed", opacity: 0.5 }); $inputButton = this.css({ display: "flex", height: "100%", alignItems: "center", borderRight: `1px solid ${t.input}`, backgroundColor: t.muted, padding: "0 0.75rem", fontWeight: 500, }); $text = this.css({ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }); $placeholder = this.css({ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", color: t.mutedForeground });}export interface FilePickerProps { selection: FileSelection; label?: string; variant?: ButtonProps["variant"]; size?: ButtonProps["size"]; disabled?: boolean; class?: string;}@Component()export class FilePicker extends StatelessComponent<FilePickerProps> { render() { const { selection, label = "Choose file", variant = "outline", size, disabled, class: cls } = this.props; return ( <Button variant={variant} size={size} disabled={disabled} class={cls} onClick={() => { selection.open(); }}> <Icon name="Paperclip" size={14} /> {label} </Button> ); }}export interface FileInputProps { selection: FileSelection; placeholder?: string; buttonLabel?: string; disabled?: boolean; class?: string;}@Component()export class FileInput extends StatelessComponent<FileInputProps> { @Styled(FilePickerStyles) $s!: FilePickerStyles; render() { const { selection, placeholder = "No file chosen", buttonLabel = "Browse", disabled, class: cls } = this.props; return ( <button type="button" disabled={disabled} class={cx(this.$s.$input, cls)} onClick={() => { selection.open(); }}> <span class={this.$s.$inputButton}>{buttonLabel}</span> {() => { const files = selection.files; if (files.length === 0) return <span class={this.$s.$placeholder}>{placeholder}</span>; return <span class={this.$s.$text}>{files.length === 1 ? files[0].name : `${String(files.length)} files selected`}</span>; }} </button> ); }}Examples
Usage
These components are presentation for the file upload composables in
@praxisjs/composables (installed automatically by the
CLI). The composable is attached with @Compose in your component, so validation options (accept,
maxSize, maxFiles, …) stay yours, and is passed to the Kosmesis component as a prop.
import { FileSelection } from "@praxisjs/composables";
import { FileInput, FilePicker } from "@/components/ui/file-picker";
@Component()
class Form extends StatefulComponent {
@Compose(FileSelection, { accept: ".pdf", maxSize: 2 * 1024 * 1024 })
selection!: FileSelection;
render() {
return (
<>
<FilePicker selection={this.selection} label="Choose file" />
<FileInput selection={this.selection} />
</>
);
}
}FileInput looks like a text input and shows the chosen file name (or a count) in place of its
placeholder. Pair either with File List to show and remove the selection.
Props
FilePicker
| Prop | Type | Default |
|---|---|---|
selection | FileSelection | — required |
label | string | "Choose file" |
variant / size | Button variant / size | "outline" / — |
disabled | boolean | — |
class | string | — |
FileInput
| Prop | Type | Default |
|---|---|---|
selection | FileSelection | — required |
placeholder | string | "No file chosen" |
buttonLabel | string | "Browse" |
disabled | boolean | — |
class | string | — |