Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Samples

file-manager

Files and folders — upload, download, move and delete, with a folder tree.

8 source files2 test files

The file-manager sample, running.
Running, signed in, compiled from the source below.

Get it

$ osy init file-manager
$ osy launch

The app

app.osy13 lines
// Files and folders — upload, download, move and delete, with a folder tree.
// A dogfood demo of the FileAsset model (parity F4 slice 7). A folder tree (a vendored headless-tree
// FOREIGN CONTROL), file upload + public download, per-file metadata, folder create / file move / delete, and the
// "pick the hero" re-point flow (a Product points at a FileAsset — which is what makes some files IsReferenced vs
// orphaned). Exercises FileAsset + Folder + Osyrin.Images + IsReferenced + a foreign control in one artifact.
app FileManager {
  use Osyrin.Storage;                          // FileAsset / FileGrant / Folder + the File.* path store
  use Osyrin.Images;                           // the Image.* thumbnail surface
  use Osyrin.Ui;
  model "model/**/*.osy";
  tests "tests/**/*.test.osy";
}
model/auth.osy46 lines
// Auth for the file manager (the security model: a [Principal] + AuthBootstrap, secure-by-default). A real principal
// is what makes the file model work — FileAsset/Folder are creator-owns, so "your files" needs a `user` to own them.
// Adapted from demo/auth-demo. See Docs/reference/security/index.md.

[Role] enum AppRole { Authenticator, Member }

entity RoleGrant {
  User Grantee;
  [Required] AppRole Level;
  security { }
}

policy IsAuthenticator => RoleGrant.Any(g => g.Grantee == user && g.Level == AppRole.Authenticator);

[Principal] entity User {
  [MaxLength(200), Unique] string Email;
  [MaxLength(200)] string PasswordHash;
  security {
    deny read PasswordHash when !IsAuthenticator;
    allow read when IsAuthenticator;
    allow create when IsAuthenticator;
    allow update when IsAuthenticator;
  }
}

[AuthMethod]
string Signup(string email, string password) {
  var u = new User { Email = email, PasswordHash = Security.HashPassword(password) };
  return Security.IssueJwt(u.Id, u.Email);
}

[AuthMethod]
string Login(string email, string password) {
  var u = User.Where(x => x.Email == email).FirstOrDefault();
  if (u == null) { Security.VerifyPassword(password); return ""; }
  if (Security.VerifyPassword(password, u.PasswordHash)) { return Security.IssueJwt(u.Id, u.Email); }
  return "";
}

app.AuthBootstrap = new AuthBootstrap {
  Role      = AppRole.Authenticator,
  Login     = Login,
  Signup    = Signup,
  LoginPage = LoginPage,
};
model/functions.osy71 lines
using Osyrin.Storage;
using Osyrin.Images;

void CreateRootFolder(string name) {
  new Folder { Name = name };
  UnitOfWork.Commit();
}

void CreateSubfolder(string name, Guid parentId) {
  if (name == "") { return; }
  var parent = parentId == Guid.Empty ? null : Folder.Single(x => x.Id == parentId);
  new Folder { Name = name, Parent = parent };
  UnitOfWork.Commit();
}

void SeedFolders() {
  var photos = new Folder { Name = "Photos" };
  var docs = new Folder { Name = "Documents" };
  var y2024 = new Folder { Name = "2024", Parent = photos };
  new Folder { Name = "2025", Parent = photos };
  var invoices = new Folder { Name = "Invoices", Parent = docs };
  var contracts = new Folder { Name = "Contracts", Parent = docs };

  new FileAsset { Name = "welcome.txt", MimeType = "text/plain", Size = 1240, StorageMode = "inline", IsPublic = true };
  new FileAsset { Name = "getting-started.pdf", MimeType = "application/pdf", Size = 48210, StorageMode = "inline" };

  new FileAsset { Name = "sunset.jpg", MimeType = "image/jpeg", Size = 2841600, StorageMode = "inline", IsPublic = true, Folder = photos };
  new FileAsset { Name = "portrait.png", MimeType = "image/png", Size = 845000, StorageMode = "inline", Folder = photos };
  new FileAsset { Name = "birthday.jpg", MimeType = "image/jpeg", Size = 1920000, StorageMode = "inline", Folder = y2024 };

  new FileAsset { Name = "invoice-jan.pdf", MimeType = "application/pdf", Size = 128400, StorageMode = "inline", Folder = invoices };
  new FileAsset { Name = "invoice-feb.pdf", MimeType = "application/pdf", Size = 131900, StorageMode = "inline", Folder = invoices };
  new FileAsset { Name = "nda-signed.pdf", MimeType = "application/pdf", Size = 512000, StorageMode = "inline", IsPublic = false, Folder = contracts };

  UnitOfWork.Commit();
}

void RemoveFile(Guid fileId) {
  var f = FileAsset.Single(x => x.Id == fileId);
  f.Delete();
  UnitOfWork.Commit();
}

void Ingest(UploadedFile f, Guid folderId) {
  var bytes = File.ReadAllBytes(f.Path);
  var folder = folderId == Guid.Empty ? null : Folder.Single(x => x.Id == folderId);
  new FileAsset {
    Name = f.FileName,
    MimeType = f.ContentType,
    StorageMode = "inline",
    InlineData = bytes,
    Folder = folder
  };
  UnitOfWork.Commit();
}

FileAsset Thumbnail(FileAsset source) {
  return Image.Thumbnail(source, 128);
}

string MimeFor(string name) {
  if (name.EndsWith(".png")) { return "image/png"; }
  if (name.EndsWith(".jpg")) { return "image/jpeg"; }
  if (name.EndsWith(".jpeg")) { return "image/jpeg"; }
  if (name.EndsWith(".gif")) { return "image/gif"; }
  if (name.EndsWith(".webp")) { return "image/webp"; }
  if (name.EndsWith(".svg")) { return "image/svg+xml"; }
  if (name.EndsWith(".pdf")) { return "application/pdf"; }
  return "application/octet-stream";
}
model/model.osy10 lines
using Osyrin.Storage;   // FileAsset, FileGrant, Folder — the first-class file model
using Osyrin.Images;    // Image.* — server-side thumbnails

entity Product {
  [Required, MaxLength(120)] string Name;
  FileAsset Hero;
  security { allow read, create, update, delete when IsAuthenticated; }
}

model/theme.osy74 lines
// File Manager — the design tokens (forked from the New Admin design system: osyrin cerulean, warm-cream / cool-slate,
// light + dark). Components read the PURPOSE layer only (`Bg = Colors.Surface1`), never a raw hex, so the whole app follows
// light/dark for free. See the original for the authoring rules (length tokens carry their unit; a token name denotes
// one value app-wide).

theme FileManager {

  Colors {
    Surface0 = Modes.Of(light: "#f5f4f0", dark: "#0d1a24");   // page canvas — warm cream / near-black navy
    Surface1 = Modes.Of(light: "#faf9f5", dark: "#0f2130");   // in-flow card / sidebar / rail
    Surface2 = Modes.Of(light: "#ffffff", dark: "#0f2535");   // panel / working area / raised card
    Surface3 = Modes.Of(light: "#ffffff", dark: "#14304a");   // popover / menu / dropdown

    TextPrimary   = Modes.Of(light: "#1a1a1a", dark: "#d0eaf7");
    TextSecondary = Modes.Of(light: "#64748b", dark: "#89acc6");   // slate-500 / cerulean-slate
    TextMuted     = Modes.Of(light: "#94a3b8", dark: "#5b7d96");   // slate-400
    TextDisabled  = Modes.Of(light: "rgba(26, 26, 26, 0.32)", dark: "rgba(208, 234, 247, 0.32)");

    Border         = Modes.Of(light: "#e2e8f0", dark: "#1a3a52");
    BorderStrong   = Modes.Of(light: "#cbd5e1", dark: "#244a66");
    BorderStronger = Modes.Of(light: "#94a3b8", dark: "#2f5a7a");

    BgAccent     = Modes.Of(light: "#e8f4fb", dark: "#10283b");   // cerulean tint
    TextAccent   = Modes.Of(light: "#005f92", dark: "#8fceeb");   // readable cerulean on the tint
    FillAccent   = Modes.Of(light: "#0077b6", dark: "#48b4e0");   // the osyrin cerulean
    BorderAccent = Modes.Of(light: "#a9d3ec", dark: "#2b5b7a");
    OnAccent     = "#ffffff";

    BgSuccess     = Modes.Of(light: "#eaf3de", dark: "#182410");
    TextSuccess   = Modes.Of(light: "#3b6d11", dark: "#a5cf6c");
    FillSuccess   = Modes.Of(light: "#639922", dark: "#5f9220");
    BorderSuccess = Modes.Of(light: "#c0dd97", dark: "#35521c");

    BgWarning     = Modes.Of(light: "#faeeda", dark: "#2a1e0c");
    TextWarning   = Modes.Of(light: "#854f0b", dark: "#e3b167");
    FillWarning   = Modes.Of(light: "#ba7517", dark: "#b06f16");
    BorderWarning = Modes.Of(light: "#fac775", dark: "#5b4014");

    BgDanger     = Modes.Of(light: "#fcebeb", dark: "#2b1413");
    TextDanger   = Modes.Of(light: "#a32d2d", dark: "#ef8d8c");
    FillDanger   = Modes.Of(light: "#e24b4a", dark: "#d64645");
    BorderDanger = Modes.Of(light: "#f7c1c1", dark: "#5d2a29");

    FillPrimary = Modes.Of(light: "#1a1a1a", dark: "#d0eaf7");
    OnPrimary   = Modes.Of(light: "#ffffff", dark: "#0f2535");

    Bg      = Colors.Surface0;     // <body> background
    OnBg    = Colors.TextPrimary;  // <body> colour
    Surface = Colors.Surface2;     // an unstyled Input / the theme toggle sits on a panel
    OnSurface = Colors.TextPrimary;
    Muted   = Modes.Of(light: "#eef1f5", dark: "#14304a");   // an unstyled Button's neutral fill

    Primary = Palette.From("#0077b6");   // the osyrin cerulean; also the focus ring (§2: focus is an accent role)
    Danger  = Palette.From("#e24b4a");
    Success = Palette.From("#639922");
    Warning = Palette.From("#ba7517");
  }

  Radius { Sm = "6px"; Md = "8px"; Lg = "12px"; Pill = "999px"; }

  Font { Sans = "\"Helvetica Neue\", Helvetica, Arial, sans-serif";
         Mono = "ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, monospace"; }
  FontSize   { Caption = "12px"; Label = "13px"; Body = "13px"; Subhead = "16px"; Heading = "18px"; Title = "22px"; }
  FontWeight { Light = 300; Regular = 400; Medium = 500; Semibold = 500; }

  Density { ControlHeight = "32px"; RowHeight = "34px"; NavHeight = "32px"; CellPadY = "7px"; }

  Breakpoints { Compact = 640; Cozy = 960; }
  Touch       { Min = "44px"; Dense = "36px"; }

  ZIndex { Drawer = 20; Fab = 30; Overlay = 40; }
  Motion { Fade = "opacity 0.12s ease"; Tint = "background 0.12s ease, color 0.12s ease"; }
}
model/controls/tree.osy27 lines
// The folder-tree FOREIGN CONTROL (parity F4 slice 7) — a headless adapter over `@headless-tree/core`, bundled into
// the sibling shim `tree.js` with esbuild.
// HEADLESS: the library computes the tree state (expand/collapse/selection) and the shim renders the DOM THROUGH the
// app's design tokens (host.tokens), so the tree inherits the app's theme + dark mode. The app hands in a FLAT list of
// nodes (each carrying its parent's id) and the shim builds the hierarchy — the same "flatten the entity to a plain
// record" contract the DataGrid uses. Selecting a folder emits its IDENTITY (the Folder's id), never a display index.

// The control's OWN shape type (never the app's `Folder` entity directly — a control stays reusable across apps). The
// page projects each `Folder` into one of these. `ParentId` is `Guid.Empty` for a root; the shim roots those.
class FolderNode {
  public Guid FolderId;   // the Folder's id — emitted on select, and the node's stable key
  public string Name;     // the folder's display label
  public Guid ParentId;   // the parent Folder's id, or Guid.Empty for a top-level folder
}

control FolderTree {
  contractVersion "1.0"
  version "1.0.0"
  participation headless
  props {
    FolderNode[] nodes;
  }
  events {
    folderSelected(Guid id);
  }
}
model/pages/login.osy98 lines
// Login + signup — the two public routes ([AllowAnonymous]; not under any layout, so they render full-viewport). Each
// is a single card in the app's design language (cerulean on a warm-cream canvas, flat + hairline). Their Submit calls
// the Side=Server credential [AuthMethod] and hands the ticket to Session.SignIn, which stores it and navigates on.
// AuthBootstrap routes an anonymous visit to a protected page here.

using Osyrin.Ui;

[Page("/login")]
[AllowAnonymous]
[Title("Sign in")]
[Render(CSR)]
component LoginPage() {
  string email = "";
  string password = "";
  action Submit() { Session.SignIn(Login(email, password)); }
  render {
    AuthShell {
      AuthCard {
        AuthBrand();
        Stack(gap: 1, align: Align.Center) {
          Text("Sign in", fontSize: FontSize.Title, fontWeight: FontWeight.Medium, color: Colors.TextPrimary);
          Hint("Welcome back to your files.");
        }
        Stack(gap: 3) {
          Field("Email",    value: email,    placeholder: "you@company.com");
          Field("Password", value: password, placeholder: "••••••••", type: "password");
        }
        Pressable(onClick: Submit) { AuthPrimary("Sign in"); }
        AuthSwitch("New here?", "Create an account", "/signup");
      }
    }
  }
}

[Page("/signup")]
[AllowAnonymous]
[Title("Create your account")]
[Render(CSR)]
component SignupPage() {
  string email = "";
  string password = "";
  action Submit() { Session.SignIn(Signup(email, password)); }
  render {
    AuthShell {
      AuthCard {
        AuthBrand();
        Stack(gap: 1, align: Align.Center) {
          Text("Create your account", fontSize: FontSize.Title, fontWeight: FontWeight.Medium, color: Colors.TextPrimary);
          Hint("Sign up to start organizing your files.");
        }
        Stack(gap: 3) {
          Field("Email",    value: email,    placeholder: "you@company.com");
          Field("Password", value: password, placeholder: "••••••••", type: "password");
        }
        Pressable(onClick: Submit) { AuthPrimary("Create account"); }
        AuthSwitch("Already have an account?", "Sign in", "/login");
      }
    }
  }
}

/// The full-viewport canvas that centers the card.
[Composable] component AuthShell() {
  variants { base { MinH = "100vh"; Bg = Colors.Surface0; Color = Colors.TextPrimary; P = 4; } }
  render { Row(align: Align.Center, justify: Justify.Center) { Slot; } }
}
/// The auth card — a Surface2 hairline panel, ~380px wide.
[Composable] component AuthCard() {
  variants { base { W = "380px"; MaxW = "100%"; Bg = Colors.Surface2; BorderW = 1; Border = Colors.Border; Rounded = Radius.Lg; P = 5; } }
  render { Stack(gap: 4) { Slot; } }
}
/// The brand lockup — the cerulean mark over the app name.
[Composable] component AuthBrand() {
  variants { base { Display = Display.Flex; } }
  render {
    Stack(gap: 2, align: Align.Center) {
      Icon(Icons.Logo, size: 30, color: Colors.TextAccent);
      Text("File Manager", fontSize: FontSize.Heading, fontWeight: FontWeight.Medium, color: Colors.TextPrimary, letterSpacing: "0.01em");
    }
  }
}
/// The one cerulean action — a full-width primary button.
[Composable] component AuthPrimary(string label) {
  variants { base { Display = Display.Flex; H = "40px"; W = "100%"; Bg = Colors.FillAccent; Color = Colors.OnAccent; Rounded = Radius.Md;
                    FontSize = FontSize.Body; FontWeight = FontWeight.Medium; Transition = Motion.Tint; } }
  render { Row(align: Align.Center, justify: Justify.Center) { Text(label); } }
}
/// The mode switch line under the button — a hint + an accent link to the other page.
[Composable] component AuthSwitch(string prompt, string linkText, string href) {
  variants { base { Display = Display.Flex; } }
  render {
    Row(gap: 1, align: Align.Center, justify: Justify.Center) {
      Hint(prompt);
      Link(href: href) { Text(linkText, color: Colors.TextAccent, fontSize: FontSize.Body, fontWeight: FontWeight.Medium); }
    }
  }
}
model/pages/manager.osy233 lines
using Osyrin.Ui;
using Osyrin.Storage;

[Page("/")]
[Title("File Manager")]
[Render(CSR)]
component FileManagerPage() {
  Guid selectedId = Guid.Empty;                  // the open folder (Guid.Empty = the "All files" root view)
  string folderName = "All files";               // the open folder's display name (for the file-panel header)
  string newFolderName = "";
  live var loadedNodes = Folder.Select(f => new FolderNode {
    FolderId = f.Id,
    Name = f.Name,
    ParentId = f.Parent?.Id ?? Guid.Empty
  }).ToList();

  live var loadedFiles = FileAsset.Where(f => selectedId == Guid.Empty || f.Folder.Id == selectedId).ToList();

  action SelectFolder(Guid id) {
    selectedId = id;
    folderName = "All files";
    foreach (var n in loadedNodes) {
      if (n.FolderId == id) { folderName = n.Name; }
    }
  }
  action NewFolder() { CreateSubfolder(newFolderName, selectedId); newFolderName = ""; }
  action Seed() { SeedFolders(); }
  action Uploaded(UploadedFile f) { Ingest(f, selectedId); }
  action DeleteFile(Guid id) { RemoveFile(id); }
  action OpenFile(FileAsset f) { Navigation.Go("/file/" + f.Id); }

  render {
    FmShell {
      CommandBar {
        Icon(Icons.Logo, size: 22, color: Colors.TextAccent);
        Stack(gap: 0) {
          Text("File Manager", fontSize: FontSize.Subhead, fontWeight: FontWeight.Medium, color: Colors.TextPrimary);
          FieldLabel("Osy# storage demo");
        }
        Spacer();
        Row(gap: 2, align: Align.Center, wrap: Wrapping.Wrap) {
          Input(value: newFolderName, label: "New folder name", placeholder: "New folder name…", w: "190px");
          Button("New folder", onPress: NewFolder) { Icon(Icons.Plus, size: 18); }
          UploadButton(Uploaded);
          Button("Seed", onPress: Seed) { Icon(Icons.Folder, size: 18); }
          Sep2();
          ThemeToggle();
          SignOutButton();
        }
      }

      BodyRow {
        if (Layout.AtLeast(Compact)) {
          TreePanel {
            PanelHead { SectionLabel("Folders"); }
            TreeScroll {
              FolderTree(nodes: loadedNodes, folderSelected: SelectFolder);
            }
          }
        }

        FilePanel {
          FilePanelHead {
            Stack(gap: 0) {
              Text(folderName, fontSize: FontSize.Heading, fontWeight: FontWeight.Medium, color: Colors.TextPrimary);
              Text(loadedFiles.Count == 1 ? "1 file" : loadedFiles.Count + " files", fontSize: FontSize.Caption, color: Colors.TextMuted);
            }
            Spacer();
            UploadButton(Uploaded);
          }
          FileBody {
            if (loadedFiles.Count == 0) {
              EmptyFiles {
                Icon(Icons.File, size: 34, color: Colors.TextMuted);
                Text("No files here yet", fontSize: FontSize.Subhead, fontWeight: FontWeight.Medium, color: Colors.TextSecondary);
                Hint("Upload a file, or pick another folder on the left.");
              }
            } else {
              DataGrid(
            label: "Files",
                rows: loadedFiles,
                columns: [
                  new GridColumn<FileAsset> { Name = "Name", Label = "Name", Value = f => f.Name, Title = true, Width = 300, Fill = true },
                  new GridColumn<FileAsset> { Name = "MimeType", Label = "Type", Value = f => f.MimeType, Secondary = true, Width = 200 },
                  new GridColumn<FileAsset> { Name = "Size", Label = "Size", Value = f => Text.ByteSize(f.Size), Align = "right", Width = 120 },
                  new GridColumn<FileAsset> { Name = "IsPublic", Label = "Visibility", Value = f => f.IsPublic ? "Public" : "Private", Width = 140 },
                  new GridColumn<FileAsset> { Name = "Actions", Label = "", Value = f => "", Width = 80, Align = "right", Sortable = false }
                ],
                rowSelected: OpenFile
              ) { f =>
                Stack(gap: 0) { Strong(f.Name); FieldLabel(f.MimeType); }

                slot IsPublic { f =>
                  Row(align: Align.Center) {
                    if (f.IsPublic) { Badge("Public", tone: Tone.Success); }
                    else { Badge("Private", tone: Tone.Ghost); }
                  }
                }
                slot Actions { f =>
                  Row(align: Align.Center, justify: Justify.End) {
                    IconButton("Delete " + f.Name, () => DeleteFile(f.Id), tone: Tone.Danger) {
                      Icon(Icons.Trash, size: 16);
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

/// The canvas + floating card: a Surface0 gutter (Cozy) around a Surface2 hairline card that clips its children, so
/// the tree's right edge and the grid's scroll live inside the rounded corners.
// region: shell
[Composable] component FmShell() {
  variants { base { Display = Display.Flex; MinH = "100vh"; Bg = Colors.Surface0; Color = Colors.OnBg; Cozy { P = "16px"; } } }
  render { Row(align: Align.Stretch) { FmCard { Slot; } } }
}
// endregion
[Composable] component FmCard() {
  variants { base { Grow = 1; MinW = "0"; Bg = Colors.Surface2; Overflow = Overflow.Hidden; Cozy { BorderW = 1; Border = Colors.Border; Rounded = Radius.Lg; } } }
  render { Stack(gap: 0) { Slot; } }   // column: command bar on top, body row below
}

/// The top command bar — brand + title on the left, the app's actions on the right, over a bottom hairline.
[Composable] component CommandBar() {
  variants { base { Display = Display.Flex; Px = "20px"; MinH = "60px"; Py = "10px"; Wrap = Wrapping.Wrap; Bg = Colors.Surface1; BorderBW = 1; Border = Colors.Border; } }
  render { Row(gap: 3, align: Align.Center, wrap: Wrapping.Wrap) { Slot; } }
}

/// The body — fills the card below the command bar; a flex row of the two panels, each free to scroll (MinH 0).
[Composable] component BodyRow() {
  variants { base { Grow = 1; MinH = "0"; Display = Display.Flex; } }
  render { Row(align: Align.Stretch) { Slot; } }
}

/// The left folder panel — a fixed-width Surface1 column with a right hairline.
[Composable] component TreePanel() {
  variants { base { W = "268px"; MinW = "268px"; MinH = "0"; Bg = Colors.Surface1; BorderRW = 1; Border = Colors.Border; Display = Display.Flex; } }
  render { Stack(gap: 0, role: UiRole.Region, label: "Folders") { Slot; } }
}
/// A panel's small padded header row (the "FOLDERS" eyebrow).
[Composable] component PanelHead() {
  variants { base { Px = "16px"; Pt = "16px"; Pb = "8px"; } }
  render { Row(align: Align.Center) { Slot; } }
}
/// The scrolling tree region.
[Composable] component TreeScroll() {
  variants { base { Grow = 1; MinH = "0"; OverflowY = Overflow.Auto; Px = "8px"; Pb = "12px"; } }
  render { Stack(gap: 0) { Slot; } }
}

/// The right file panel — grows to fill, a flex column of its header + scrolling body.
[Composable] component FilePanel() {
  variants { base { Grow = 1; MinW = "0"; MinH = "0"; Bg = Colors.Surface2; Display = Display.Flex; } }
  render { Stack(gap: 0, role: UiRole.Region, label: "Files") { Slot; } }
}
/// The file panel's header — the open folder's name + file count on the left, an upload on the right, over a hairline.
[Composable] component FilePanelHead() {
  variants { base { Px = "24px"; Py = "14px"; BorderBW = 1; Border = Colors.Border; } }
  render { Row(gap: 3, align: Align.Center) { Slot; } }
}
/// The scrolling file region.
[Composable] component FileBody() {
  variants { base { Grow = 1; MinH = "0"; OverflowY = Overflow.Auto; Px = "24px"; Py = "18px"; } }
  render { Stack(gap: 0) { Slot; } }
}

/// The empty-state placeholder shown when the open folder has no files — a centered glyph + hint.
[Composable] component EmptyFiles() {
  variants { base { Display = Display.Flex; Py = "56px"; Gap = "10px"; } }
  render { Stack(gap: 2, align: Align.Center, justify: Justify.Center) { Slot; } }
}

/// A short vertical hairline divider for the command bar.
[Composable] component Sep2() {
  variants { base { W = "1px"; H = "24px"; Bg = Colors.Border; Mx = "2px"; } }
  render { Box(); }
}

/// Theme flip — the platform's `Theme.Toggle()` client effect (flips + persists `data-theme`).
[Composable] component ThemeToggle() {
  action Flip() { Theme.Toggle(); }
  variants { base { W = "32px"; H = "32px"; Rounded = Radius.Md; Color = Colors.TextMuted; Transition = Motion.Tint; Hover { Bg = Colors.Surface1; Color = Colors.TextPrimary; } } }
  render { Pressable(onClick: Flip) { Row(align: Align.Center, justify: Justify.Center) { Icon(Icons.Gear, size: 18); } } }
}

/// Sign out — the platform's `Session.SignOut()` client effect (drops the ticket + returns to /login).
[Composable] component SignOutButton() {
  action SignOut() { Session.SignOut(); }
  variants { base { Px = 2; H = "32px"; Rounded = Radius.Md; Color = Colors.TextMuted; FontSize = FontSize.Caption; WhiteSpace = WhiteSpace.Nowrap;
                    Transition = Motion.Tint; Hover { Bg = Colors.Surface1; Color = Colors.TextPrimary; } } }
  render { Pressable(onClick: SignOut) { Row(align: Align.Center) { Text("Sign out"); } } }
}

/// The upload affordance. `Upload` owns its own chrome — it renders as a `<label>` carrying THIS content, with the
/// real file picker hidden inside it — so the button is styled here like any other and there is no overlay to contain.
[Composable] component UploadButton(Action<UploadedFile> onUploaded) {
  variants {
    base { Display = Display.InlineFlex; H = "32px"; Px = 3; Bg = Colors.Primary; Color = Colors.OnPrimary; Rounded = Radius.Md;
           FontSize = FontSize.Body; FontWeight = FontWeight.Semibold; WhiteSpace = WhiteSpace.Nowrap; Transition = Motion.Fast; }
  }
  render {
    Upload(onUploaded: onUploaded) {
      Row(gap: 2, align: Align.Center, justify: Justify.Center) { Icon(Icons.Upload, size: 18); Text("Upload"); }
    }
  }
}

/// The row-click destination — the page `rowSelected` navigates to. It exists so "clicking a row navigates" is a
/// fact the drive script can ASSERT, rather than a handler that fires into nothing.
[Page("/file/{id}")]
component FileDetail(Guid id) {
  live var file = FileAsset.Where(f => f.Id == id).FirstOrDefault();
  action Back() { Navigation.Go("/"); }
  render {
    FmShell {
      Stack(gap: 4, p: 8) {
        Pressable(onClick: Back) { Text("← All files", color: Colors.TextAccent); }
        if (file == null) { Text("No such file.", fontSize: FontSize.Subhead); }
        else {
          Text(file.Name, fontSize: FontSize.Subhead, fontWeight: FontWeight.Medium);
          Text("Type " + file.MimeType, color: Colors.TextMuted);
          Text("Size " + Text.ByteSize(file.Size), color: Colors.TextMuted);
        }
      }
    }
  }
}