markdown-demo
Markdown demo — the built-in markdown control, running against real sectioned storage.
5 source files1 test file

Get it
$ osy init markdown-demo $ osy launch
The app
app.osy8 lines
// Markdown demo — the built-in markdown control, running against real sectioned storage. app MarkdownDemo { use Osysharp.Markdown@1; model "model/**/*.osy"; tests "tests/**/*.test.osy"; }
model/auth.osy66 lines
// Minimal auth, because the DATA path never inherits anonymity. [Role] enum AppRole { Authenticator } entity RoleGrant { User Grantee; // No `[Required]`: a bare `AppRole` is already required by its spelling (`AppRole?` is the optional one), so the // attribute would carry nothing but the platform's own wording for a field no person ever types. 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; Markdown Notes; security { deny read PasswordHash when !IsAuthenticator; allow read when IsAnonymous || IsAuthenticated; allow create when IsAuthenticator; allow update when IsAuthenticator; // ⚠ NOT optional for an editor over `me.Notes`. A `Markdown` property is stored as a document the platform // governs by THIS entity's rules — owner `update` is what confers every write on it — so without a self-row // grant a signed-in user could open the editor, type, and have every save refused as "Create of // 'Osysharp.MarkdownDocument' denied". Measured 2026-09-04 through the app's own MCP `markdown_import`. allow update where Id == user.Id; } } // ⛔ THE LENGTH CHECK IS NOT CEREMONY. Hashing strength does not answer a weak password: an attacker does not need // to break the KDF when the first hundred guesses cover a real share of the accounts. Without this line `""` and // `"a"` are accepted passwords — `security-signup-no-password-policy` (SHOULD) says so. [AuthMethod] string Signup(string email, string password) { if (password.Length < 8) { return ""; } 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` names the FUNCTIONS the app's own login page calls; `app.Auth` names the FIELDS, which is // what anything outside the app needs — `osy user add`, the console login, `osy run --as`. Both, or an operator // cannot create an account on a deployment that has no admin screen. app.Auth = new PasswordAuth { LoginField = Email, PasswordField = PasswordHash }; // Audit redaction is EXPLICIT: a credential not named here is written to the entity-change trail verbatim, and // that trail is durable and exportable. The `deny read` above governs READERS; the audit writer is not one. app.Audit = new AuditConfig { Redact = new AuditRedaction { Properties = [User.PasswordHash] }, }; app.AuthBootstrap = new AuthBootstrap { Role = AppRole.Authenticator, Login = Login, Signup = Signup, LoginPage = LoginPage, };
model/theme.osy32 lines
// markdown-demo — ITS OWN LOOK, and deliberately so: a prose tool reads in a serif. // // ⚑ Not the shared demo theme (`demo/_shared/theme.osy`), and `DemoSharedThemeTests` records why. // The demos are not meant to look uniform — several of them exist to show that a different // palette and a different face are a few lines of `theme`. // // ⚠ This restates the ORIGINAL intent with the mechanism corrected. The original spelled it // `Fonts { Body / Heading / Ui }` — a group name no control reads — so the Hedvig this app // shipped and pinned never reached a single control. `Sans` shadows the KIT's own token, which // is what makes a face apply without touching a page. theme Doc { Colors { Bg = Modes.Of(light: "#FAFAF8", dark: "#0E1116"); // paper, very slightly warm OnBg = Modes.Of(light: "#16181D", dark: "#E6EAF0"); Border = Modes.Of(light: "#E6E4DE", dark: "#2A2F37"); Muted = Modes.Of(light: "#F0EEE8", dark: "#232833"); Surface = Modes.Of(light: "#FFFFFF", dark: "#171B21"); OnSurface = Modes.Of(light: "#1A1F26", dark: "#E6EAF0"); Primary = "#4F46E5"; OnPrimary = "#FFFFFF"; Danger = "#DC2626"; } Radius { Sm = "6px"; Md = "10px"; Lg = "16px"; } Font { Sans = "\"Hedvig Letters Serif\", ui-serif, Charter, Georgia, Cambria, serif"; Mono = "ui-monospace, SFMono-Regular, Menlo, monospace"; } }
model/pages/editor.osy86 lines
// The one page: the markdown control over a real `Markdown` property. // `MarkdownEditor` and `MenuEntry` come from the kit the manifest opted into. An ordinary import, exactly like // `using Osysharp.Ui;` — the app declares the DEPENDENCY once in `app.osy` and imports the names where it uses them. using Osysharp.Markdown; // ⚑ THE TWO `Input`s IN THE FIND BAR ARE ATOMS ON PURPOSE, AND THAT IS WHAT THE SUPPRESSION RECORDS. `Field` // renders its label above the box, which is right on a form and wrong in a one-line toolbar sitting over the // document — the `label:` is still there and is still what a screen reader announces, which is the accessibility // half the rule exists for. Recorded with the attribute rather than a comment so the linter knows it was a // decision; `osy docs ui-atom-where-the-kit-has-a-control` is the rule. [Page("/")] [Render(CSR)] [Title("Markdown")] [SuppressWarning("ui-atom-where-the-kit-has-a-control")] component EditorPage() { live var me = User.FirstOrDefault(); bool finding = false; string query = ""; string replacement = ""; int hit = 0; int matches = 0; action Find(string selection) { finding = true; if (selection != "") { query = selection; } } action Matches(int current, int total) { hit = current; matches = total; } action ShowFind() { finding = true; } action HideFind() { finding = false; query = ""; } string note = ""; action Archive() { note = "archived"; } // ⛔ WHAT THE PAGE LOOKS LIKE WHILE `me` IS STILL ON ITS WAY. Without this the page renders NOTHING until the // server read answers, and a blank screen is indistinguishable from one that hung — which is how it gets // reported. It draws the SHAPE, never the data: it cannot read `me`, which is the whole point of it. skeleton { Stack(gap: 3, p: 3) { Skeleton(shape: SkeletonShape.Control); Skeleton(); Skeleton(); Skeleton(shape: SkeletonShape.HalfLine); } } render { MarkdownEditor( ownerType: "User", ownerId: me.Id, property: "Notes", face: editorial, outline: true, findQuery: query, replaceWith: replacement, findRequested: Find, matchesChanged: Matches, extraItems: [ new MenuEntry { Label = "Archive this doc", Run = Archive }, new MenuEntry { Label = "Delete block", Run = MarkdownEditor.DeleteBlock }, new MenuEntry { Label = "Make it a heading", Run = () => MarkdownEditor.TurnIntoHeading(2) } ] ) { slot Toolbar { c => Row(gap: 1) { if (!finding) { Osysharp.Button("Find", onClick: ShowFind); } else { Input(value: query, label: "Find", placeholder: "Find", onEnter: c.FindNext); Text(matches == 0 ? (query == "" ? "" : "none") : hit + "/" + matches); Osysharp.Button("‹", onClick: c.FindPrev); Osysharp.Button("›", onClick: c.FindNext); Input(value: replacement, label: "Replace with", placeholder: "Replace with"); Osysharp.Button("Replace", onClick: c.ReplaceOne); Osysharp.Button("All", onClick: c.ReplaceAll); Osysharp.Button("✕", onClick: c.ClearFind); Osysharp.Button("Close", onClick: HideFind); } Text(note); } } } } }
model/pages/login.osy24 lines
// Sign-in, prefilled. This is a demo whose point is the editor, not the credential flow, so the fields carry // working values and signing in is one click. `Signup` doubles as "create it if it isn't there yet". [Page("/login")] [AllowAnonymous] [Render(CSR)] [Title("Sign in")] component LoginPage() { string email = "demo@local"; string password = "demo1234"; action SignIn() { Session.SignIn(Login(email, password)); } action Register() { Session.SignIn(Signup(email, password)); } render { Stack(gap: 3) { Text("Markdown demo"); Field("Email", value: email, placeholder: "[email protected]", type: "email"); Field("Password", value: password, type: "password", hint: "At least eight characters."); Osysharp.Button("Sign in", onClick: SignIn); Osysharp.Button("Create this account", onClick: Register); } } }