dialog-demo
THE CANONICAL DIALOG SAMPLE (D60/D212). Everything a dialog can be, in one small app:
5 source files1 test file

Get it
$ osy init dialog-demo $ osy launch
The app
app.osy6 lines
// THE CANONICAL DIALOG SAMPLE (D60/D212). Everything a dialog can be, in one small app: app DialogDemo { model "model/**/*.osy"; tests "tests/**/*.test.osy"; }
model/auth.osy66 lines
// The smallest real login, on the `demo/dropdown-demo` shape. This demo WRITES (that is the whole subject), and the // deny-all endpoint gate correctly refuses an anonymous write — so a login is not incidental scaffolding here. [Role] enum AppRole { Authenticator, Member } entity RoleGrant { User Grantee; [Required] AppRole Level; security { allow create when IsAuthenticator; allow read when IsAuthenticated; } } policy IsAuthenticator => RoleGrant.Any(g => g.Grantee == user && g.Level == AppRole.Authenticator); [Principal] entity User { [Required, MaxLength(200), Unique] string Email; [MaxLength(200)] string PasswordHash; security { deny read PasswordHash when !IsAuthenticator; allow read, create when IsAuthenticator; allow read when IsAuthenticated; } } [AuthMethod] string Signup(string email, string password) { var u = new User { Email = email, PasswordHash = Security.HashPassword(password) }; var g = new RoleGrant { Grantee = u, Level = AppRole.Member }; 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, }; [Page("/login")] [Render(CSR)] [AllowAnonymous] [Title("Sign in")] component LoginPage() { string email = ""; string password = ""; action CreateAccount() { Session.SignIn(Signup(email, password)); } render { Row(align: Align.Center, justify: Justify.Center, w: "100%", h: "100vh", bg: Colors.Bg) { Stack(gap: 3, w: "320px", p: 5, bg: Colors.Surface, rounded: Radius.Card, borderW: 1, border: Colors.Border, shadow: Shadow.Dialog) { Text("Dialog demo", fontSize: FontSize.Display, fontWeight: FontWeight.Medium, color: Colors.OnSurface); Text("Any email and password — this is a sample.", fontSize: FontSize.Caption, color: Colors.Subtle); Input(value: email, label: "Email", placeholder: "you@example.com"); Input(value: password, label: "Password", type: "password", placeholder: "password"); Osyrin.Button("Create account", onClick: CreateAccount); } } } }
model/dialogs.osy134 lines
// ── THE DIALOGS ────────────────────────────────────────────────────────────────────────────────────────────────── // A scrim + centred panel, so the three dialogs below do not each restate it. This is app code — you are meant to // change it, or not use it at all. // ⚑ The width is a BOOL, not a string. A style prop's unit is applied when the component compiles, so a width // arriving as a per-render value has no unit to apply — the compiler says so rather than emitting a broken rule. // Two literal token references is the honest way to say "two sizes". // ⚑ `role: UiRole.Dialog` + `label:` ARE THE PANEL'S JOB, and without them a modal is announced as nothing at all: someone // not looking at the screen is never told a dialog opened, never told what it is about, and has no boundary telling // them where it ends. The overlay and the scrim are visual facts only. [Composable] component Panel(bool wide, string title) { render { Row(align: Align.Center, justify: Justify.Center, w: "100%", h: "100%", bg: Colors.Scrim, p: 4) { if (wide) { Stack(gap: 4, w: Length.Wide, p: 5, bg: Colors.Surface, rounded: Radius.Card, borderW: 1, border: Colors.Border, shadow: Shadow.Dialog, role: UiRole.Dialog, label: title) { Text(title, fontSize: FontSize.Section, fontWeight: FontWeight.Medium, color: Colors.OnSurface); Slot(); } } else { Stack(gap: 4, w: Length.Narrow, p: 5, bg: Colors.Surface, rounded: Radius.Card, borderW: 1, border: Colors.Border, shadow: Shadow.Dialog, role: UiRole.Dialog, label: title) { Text(title, fontSize: FontSize.Section, fontWeight: FontWeight.Medium, color: Colors.OnSurface); Slot(); } } } } } [Render(CSR)] component EditProject(Project project) { action Ok() { Dialog.Confirm(); } action Cancel() { Dialog.Discard(); } render { Panel(wide: false, title: "Edit project") { Text("OK hands these changes to the page. Nothing is saved until you press Save there.", fontSize: FontSize.Caption, color: Colors.Subtle); Stack(gap: 2) { Text("Name", fontSize: FontSize.Caption, color: Colors.Subtle); Input(value: project.Name, label: "Name", placeholder: "Project name"); } Stack(gap: 2) { Text("Notes", fontSize: FontSize.Caption, color: Colors.Subtle); Input(value: project.Notes, label: "Notes", placeholder: "What is it for?"); } Row(gap: 2, justify: Justify.End) { Osyrin.Button("Cancel", onClick: Cancel); Osyrin.Button("OK", onClick: Ok); } } } } [Render(CSR)] component ConfirmDelete(Project project) { action Yes() { Dialog.Confirm(Answer.Delete); } action No() { Dialog.Confirm(Answer.Keep); } render { Panel(wide: false, title: "Delete this project?") { Text("'" + project.Name + "' will be removed. This one really does save immediately.", fontSize: FontSize.Caption, color: Colors.Subtle); Row(gap: 2, justify: Justify.End) { Osyrin.Button("Keep it", onClick: No); Osyrin.Button("Delete", onClick: Yes); } } } } [Render(CSR)] component Settings(Preferences prefs) { string pane = "Profile"; action ShowProfile() { pane = "Profile"; } action ShowDisplay() { pane = "Display"; } action Save() { Dialog.Confirm(); } action Close() { Dialog.Discard(); } string reset = ""; action ResetName() { if (Dialog.Ask<Answer>(ConfirmReset(), unitOfWork: Inherit) == Answer.Delete) { prefs.DisplayName = ""; reset = "cleared"; } } render { Panel(wide: true, title: "Settings") { Text("Its own unit of work — Save here persists on its own, without touching the page behind.", fontSize: FontSize.Caption, color: Colors.Subtle); Row(gap: 4) { Stack(gap: 1, w: Length.Rail) { Osyrin.Button("Profile", onClick: ShowProfile); Osyrin.Button("Display", onClick: ShowDisplay); } Stack(gap: 3, grow: 1) { if (pane == "Profile") { Text("Display name", fontSize: FontSize.Caption, color: Colors.Subtle); Input(value: prefs.DisplayName, label: "Display name", placeholder: "How you appear"); Osyrin.Button("Reset name…", onClick: ResetName); if (reset != "") { Text("Name cleared — still not saved until you press Save.", fontSize: FontSize.Caption, color: Colors.Subtle); } } if (pane == "Display") { Text("Density", fontSize: FontSize.Caption, color: Colors.Subtle); Input(value: prefs.Density, label: "Density", placeholder: "comfortable / compact"); } } } Row(gap: 2, justify: Justify.End) { Osyrin.Button("Close", onClick: Close); Osyrin.Button("Save", onClick: Save); } } } } [Render(CSR)] component ConfirmReset() { action Yes() { Dialog.Confirm(Answer.Delete); } action No() { Dialog.Confirm(Answer.Keep); } render { Panel(wide: false, title: "Clear your display name?") { Text("This is a dialog opened from a dialog. It savepoints off the SETTINGS sheet, so closing settings " + "without saving discards this too.", fontSize: FontSize.Caption, color: Colors.Subtle); Row(gap: 2, justify: Justify.End) { Osyrin.Button("Keep it", onClick: No); Osyrin.Button("Clear", onClick: Yes); } } } }
model/home.osy97 lines
// Seeds the sample rows on first run. void SeedIfEmpty() { if (Project.Any()) { return; } new Project { Name = "Apollo", Notes = "Re-entry guidance", Priority = Priority.High }; new Project { Name = "Gemini", Notes = "Rendezvous practice", Priority = Priority.Normal }; new Project { Name = "Voyager", Notes = "Grand tour, no return", Priority = Priority.Low }; UnitOfWork.Commit(); } [Page("/")] [Layout(Shell)] [Render(CSR)] [Title("Projects")] component Home() { live var projects = Project.OrderBy(p => p.Name).ToList(); string note = ""; action Edit(Project p) { Dialog.Open(EditProject(project: p), unitOfWork: Inherit); note = ""; } action Save() { UnitOfWork.Commit(); note = "Saved."; } action Revert() { UnitOfWork.Discard(); note = "Reverted — the dialog's edits went with it."; } // DELIBERATELY the odd one out on this page, which is the third of its three unit-of-work stories: `Save` and // `Revert` above hold the page's edits until you press them, and this one persists the moment you confirm. The // warning is right that pressing a per-row control writes on its own — here that IS the lesson, so it is // suppressed rather than designed away. ⚠ Confirming in a dialog does NOT make it exempt: "the user confirmed" is // another proxy for agency, and the rule takes none. action Remove(Project p) { if (Dialog.Ask<Answer>(ConfirmDelete(project: p), unitOfWork: Inherit) == Answer.Delete) { p.Delete(); UnitOfWork.Commit(); note = "Deleted."; } else { note = "Kept it."; } } render { Stack(gap: 4, p: 6, maxW: "820px") { Text("Projects", fontSize: FontSize.Display, fontWeight: FontWeight.Medium, color: Colors.OnBg); Text("Three dialogs, three unit-of-work stories. Edit one and watch nothing save until you press Save.", fontSize: FontSize.Caption, color: Colors.Subtle); Stack(gap: 2) { foreach (var p in projects) { Row(gap: 3, align: Align.Center, p: 3, bg: Colors.Surface, rounded: Radius.Card, borderW: 1, border: Colors.Border) { Stack(gap: 1, grow: 1) { Text(p.Name, fontWeight: FontWeight.Medium, color: Colors.OnSurface); Text(p.Notes, fontSize: FontSize.Caption, color: Colors.Subtle); } Osyrin.Button("Edit…", onClick: () => Edit(p)); Osyrin.Button("Delete…", onClick: () => Remove(p)); } } } Row(gap: 2) { Osyrin.Button("Save", onClick: Save); Osyrin.Button("Revert", onClick: Revert); } if (note != "") { Text(note, fontSize: FontSize.Caption, color: Colors.Subtle); } } } } [Layout] component Shell() { live var prefs = Preferences.FirstOrDefault(); on mount { SeedIfEmpty(); } action OpenSettings() { if (prefs == null) { new Preferences { DisplayName = "", Density = "comfortable", ShowNotes = true }; UnitOfWork.Commit(); } Dialog.Open(Settings(prefs: prefs), unitOfWork: Root); } render { Stack(w: "100%", minH: "100vh", bg: Colors.Bg) { Row(align: Align.Center, p: 4, gap: 3, borderW: 1, border: Colors.Border, bg: Colors.Surface) { Text("Dialog demo", fontWeight: FontWeight.Medium, color: Colors.OnSurface); Box(grow: 1); Osyrin.Button("Settings…", onClick: OpenSettings); } Outlet(); } } }
model/theme.osy42 lines
// Every colour, radius and shadow the dialogs use, named once. A dialog is mostly LOOK, and a sample whose look is // hard-coded hex teaches the wrong thing — `osy lint` says so too (`ui-raw-style-literal-repeated`). theme Studio { Colors { Bg = Modes.Of(light: "#F6F7F9", dark: "#0E1116"); OnBg = Modes.Of(light: "#16181D", dark: "#E6EAF0"); Surface = Modes.Of(light: "#FFFFFF", dark: "#171B21"); OnSurface = Modes.Of(light: "#1A1F26", dark: "#E6EAF0"); Border = Modes.Of(light: "#DDE1E6", dark: "#2A2F37"); Muted = Modes.Of(light: "#EFF1F5", dark: "#232833"); Subtle = Modes.Of(light: "#6B7280", dark: "#8A94A6"); Accent = "#4F46E5"; OnAccent = "#FFFFFF"; Danger = "#B42318"; Scrim = Modes.Of(light: "rgba(15,23,42,0.55)", dark: "rgba(2,6,12,0.66)"); } Radius { Control = "8px"; Card = "14px"; Pill = "999px"; } Shadow { Dialog = "0 12px 32px rgba(15,23,42,0.18)"; } ZIndex { Overlay = "50"; } FontSize { Caption = "13px"; Body = "15px"; Section = "18px"; Display = "24px"; } FontWeight { Medium = "600"; } Length { Narrow = "420px"; Wide = "560px"; Rail = "150px"; } } enum Priority { Low, Normal, High } enum Answer { Delete, Keep } entity Project { [MaxLength(80)] string Name; [MaxLength(200)] string Notes; [Required] Priority Priority; security { allow read, create, update, delete when IsAuthenticated; } } entity Preferences { [MaxLength(40)] string DisplayName; [MaxLength(40)] string Density; bool ShowNotes; security { allow read, create, update when IsAuthenticated; } }