A CLI for scaffolding ATProto web applications

feat: add vanilla javascript template

authored by besaid.zone and committed by tangled.org 76b0d1a0 9e9a4ce9

+282 -7
+2
src/commands/init.ts
··· 231 231 return ["README.md", "eslint.config.js", "public", "src", "package.json"]; 232 232 case "svelte-ts": 233 233 return ["README.md", "src", "package.json", "svelte.config.js"]; 234 + case "vanilla": 235 + return ['src'] 234 236 default: 235 237 return []; 236 238 }
+3 -6
src/constants.ts
··· 24 24 ], 25 25 }, 26 26 { 27 - value: "vanilla", 27 + value: "Vanilla", 28 28 label: "Vanilla", 29 - disabled: true, 30 - hint: "coming soon", 31 29 variants: [ 32 30 { 33 - value: "javascript", 34 - label: "JavaScript (With OAuth)", 35 - docs: "https://github.com/bluesky-social/cookbook/tree/main/vanillajs-oauth-web-app", 31 + value: "vanilla", 32 + label: "JavaScript", 36 33 }, 37 34 ], 38 35 },
+35
templates/vanilla/README.md
··· 1 + # HTML + JavaScript + Vite + AT Protocol 2 + 3 + This is a minimal project with just enough to get you going with developing an AT Protocol web application using the public APIs. 4 + 5 + ## Resources 6 + 7 + - [Introduction to the AT Protocol](https://atproto.com/articles/atproto-ethos) 8 + - [Bluesky Developer Documentation](https://docs.bsky.app) 9 + - [@atproto/lex documentation](https://github.com/bluesky-social/atproto/tree/HEAD/packages/lex/lex#quick-start) 10 + 11 + ## Getting Started 12 + 13 + You'll need to install some lexicons before starting development. In your terminal, run: 14 + 15 + ```bash 16 + npx @atproto/lex install app.bsky.actor.getProfile 17 + ``` 18 + 19 + Then generate the TypeScript files. You can change the destination to where these files are generated but remember to update your ``.gitignore`` file so you don't commit them. 20 + 21 + ```bash 22 + npx @atproto/lex build --out ./src/__generated__ 23 + ``` 24 + 25 + Next, install the package dependencies: 26 + 27 + ```bash 28 + npm install 29 + ``` 30 + 31 + Finally, run the development server: 32 + 33 + ```bash 34 + npm run dev 35 + ```
+20
templates/vanilla/package.json
··· 1 + { 2 + "name": "vanilla", 3 + "private": true, 4 + "version": "0.0.0", 5 + "type": "module", 6 + "scripts": { 7 + "dev": "vite", 8 + "build": "vite build", 9 + "preview": "vite preview", 10 + "update-lexicons": "lex install --update --save", 11 + "postinstall": "lex install --ci", 12 + "prebuild": "lex build --out ./src/__generated__" 13 + }, 14 + "dependencies": { 15 + "@atproto/lex": "^0.0.16" 16 + }, 17 + "devDependencies": { 18 + "vite": "^7.3.1" 19 + } 20 + }
+92
templates/vanilla/src /main.js
··· 1 + import "./style.css"; 2 + import { Client } from "@atproto/lex"; 3 + 4 + import * as app from "./__generated__/app.js"; 5 + 6 + const client = new Client("https://public.api.bsky.app"); 7 + 8 + function escapeHtml(str) { 9 + if (!str) return ""; 10 + return String(str) 11 + .replace(/&/g, "&amp;") 12 + .replace(/</g, "&lt;") 13 + .replace(/>/g, "&gt;") 14 + .replace(/"/g, "&quot;") 15 + .replace(/'/g, "&#039;"); 16 + } 17 + 18 + function formatStat(stat) { 19 + if (!stat || typeof stat !== "number") { 20 + return; 21 + } 22 + 23 + return new Intl.NumberFormat(window.navigator.language).format(stat); 24 + } 25 + 26 + document.addEventListener("DOMContentLoaded", async () => { 27 + const app = document.querySelector("#app"); 28 + 29 + app.innerHTML = `<p class="loading">Loading profile...</p>`; 30 + 31 + const people = [ 32 + "pfrazee.com", 33 + "rude1.blacksky.team", 34 + "jcsalterego.bsky.social", 35 + "byarielm.fyi", 36 + "vicwalker.dev.br", 37 + "nonbinary.computer", 38 + ]; 39 + 40 + const randomPerson = Math.floor(Math.random() * people.length); 41 + 42 + const { 43 + avatar, 44 + displayName, 45 + handle, 46 + followersCount, 47 + followsCount, 48 + description, 49 + postsCount, 50 + } = await client.call(app.bsky.actor.getProfile, { 51 + actor: people[randomPerson], 52 + }); 53 + 54 + app.innerHTML = ` 55 + <article class="profile"> 56 + <header class="header"> 57 + <img 58 + src="${escapeHtml(avatar)}" 59 + alt="" 60 + height="100" 61 + width="100" 62 + class="avatar" 63 + /> 64 + <div> 65 + <h2>${escapeHtml(displayName)}</h2> 66 + <p>${escapeHtml(handle)}</p> 67 + </div> 68 + </header> 69 + <section class="stats"> 70 + <p> 71 + <span class="number"> 72 + ${formatStat(followersCount)} 73 + </span> 74 + followers 75 + </p> 76 + <p> 77 + <span class="number"> 78 + ${formatStat(followsCount)} 79 + </span> 80 + following 81 + </p> 82 + <p> 83 + <span class="number"> 84 + ${formatStat(postsCount)} 85 + </span> 86 + posts 87 + </p> 88 + </section> 89 + <footer>${escapeHtml(description)}</footer> 90 + </article> 91 + `; 92 + });
+128
templates/vanilla/src /style.css
··· 1 + @import "https://unpkg.com/open-props/colors.min.css"; 2 + @import "https://unpkg.com/open-props/shadows.min.css"; 3 + 4 + @layer reset { 5 + *, 6 + *::before, 7 + *::after { 8 + box-sizing: border-box; 9 + } 10 + 11 + * { 12 + margin: 0; 13 + padding: 0; 14 + } 15 + 16 + html { 17 + -webkit-font-smoothing: antialiased; 18 + text-rendering: optimizespeed; 19 + text-size-adjust: none; 20 + tab-size: 2; 21 + scrollbar-gutter: stable; 22 + interpolate-size: allow-keywords; 23 + line-height: 1.5; 24 + height: 100%; 25 + background-color: #f8f9fa; 26 + } 27 + 28 + body { 29 + margin: 0; 30 + /* https://systemfontstack.com */ 31 + font-family: 32 + Menlo, 33 + Consolas, 34 + Monaco, 35 + Adwaita Mono, 36 + Liberation Mono, 37 + Lucida Console, 38 + monospace; 39 + font-synthesis: none; 40 + 41 + height: 100%; 42 + display: flex; 43 + justify-content: center; 44 + align-items: center; 45 + } 46 + 47 + ul[role="list"], 48 + ol[role="list"] { 49 + list-style: none; 50 + padding: 0; 51 + } 52 + 53 + ::marker { 54 + line-height: 0; 55 + } 56 + 57 + :focus-visible { 58 + outline-offset: 2px; 59 + } 60 + 61 + @media (prefers-reduced-motion: no-preference) { 62 + html:focus-within { 63 + scroll-behavior: smooth; 64 + } 65 + } 66 + 67 + a { 68 + color: inherit; 69 + text-underline-offset: 0.2ex; 70 + } 71 + 72 + h1, 73 + h2, 74 + h3, 75 + h4 { 76 + text-wrap: balance; 77 + } 78 + 79 + a[href] { 80 + -webkit-tap-highlight-color: transparent; 81 + } 82 + 83 + p, 84 + h1, 85 + h2, 86 + h3, 87 + h4, 88 + h5, 89 + h6 { 90 + overflow-wrap: break-word; 91 + } 92 + 93 + p { 94 + text-wrap: pretty; 95 + } 96 + } 97 + 98 + .avatar { 99 + border-radius: 100%; 100 + border: 1px solid grey; 101 + } 102 + 103 + .header { 104 + display: flex; 105 + align-items: center; 106 + gap: 1rem; 107 + } 108 + 109 + .number { 110 + font-weight: bold; 111 + } 112 + 113 + .stats { 114 + display: flex; 115 + gap: 0.5rem; 116 + } 117 + 118 + .profile { 119 + width: 80ch; 120 + display: flex; 121 + flex-direction: column; 122 + gap: 1rem; 123 + background-color: #ffffff; 124 + border-radius: 0.5rem; 125 + padding: 2rem; 126 + box-shadow: var(--shadow-1); 127 + border: 1px solid var(--stone-0); 128 + }
+2 -1
tsconfig.json
··· 8 8 "skipLibCheck": true, 9 9 "noEmit": true 10 10 }, 11 - "include": ["src", "__tests__"] 11 + "include": ["src", "__tests__"], 12 + "exclude": ["templates/**"] 12 13 }