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 Osyrin.Markdown@1; model "model/**/*.osy"; tests "tests/**/*.test.osy"; }
model/auth.osy44 lines
// Minimal auth, because the DATA path never inherits anonymity. [Role] enum AppRole { Authenticator } 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; Markdown Notes; security { deny read PasswordHash when !IsAuthenticator; allow read when IsAnonymous || IsAuthenticated; 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/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.osy68 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 Osyrin.Ui;` — the app declares the DEPENDENCY once in `app.osy` and imports the names where it uses them. using Osyrin.Markdown; [Page("/")] [Render(CSR)] [Title("Markdown")] 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"; } 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) { Osyrin.Button("Find", onClick: ShowFind); } else { Input(value: query, label: "Find", placeholder: "Find", onEnter: c.FindNext); Text(matches == 0 ? (query == "" ? "" : "none") : hit + "/" + matches); Osyrin.Button("‹", onClick: c.FindPrev); Osyrin.Button("›", onClick: c.FindNext); Input(value: replacement, label: "Replace with", placeholder: "Replace with"); Osyrin.Button("Replace", onClick: c.ReplaceOne); Osyrin.Button("All", onClick: c.ReplaceAll); Osyrin.Button("✕", onClick: c.ClearFind); Osyrin.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"); Input(value: email, label: "Email", placeholder: "email"); Input(value: password, label: "Password", type: "password", placeholder: "password"); Osyrin.Button("Sign in", onClick: SignIn); Osyrin.Button("Create this account", onClick: Register); } } }