🪻 distributed transcription service thistle.dunkirk.sh
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: init base

Kieran Klukas 69133d91

+787
+1
.gitignore
··· 1 + node_modules
+350
CRUSH.md
··· 1 + # Thistle - Project Guidelines 2 + 3 + This is a Bun-based transcription service using the [Bun fullstack pattern](https://bun.com/docs/bundler/fullstack) for routing and bundled HTML. 4 + 5 + ## Project Info 6 + 7 + - Name: Thistle 8 + - Purpose: Transcription service 9 + - Runtime: Bun (NOT Node.js) 10 + - Language: TypeScript with strict mode 11 + - Frontend: Vanilla HTML/CSS/JS with lightweight helpers on top of web components 12 + 13 + ## NO FRAMEWORKS 14 + 15 + NEVER use React, Vue, Svelte, or any heavy framework. 16 + 17 + This project prioritizes: 18 + - Speed: Minimal JavaScript, fast load times 19 + - Small bundle sizes: Keep bundles tiny 20 + - Native web platform: Use web standards (Web Components, native DOM APIs) 21 + - Simplicity: Vanilla HTML, CSS, and JavaScript 22 + 23 + Allowed lightweight helpers: 24 + - Lit (~8-10KB gzipped) for reactive web components 25 + - Native Web Components 26 + - Plain JavaScript/TypeScript 27 + 28 + Explicitly forbidden: 29 + - React, React DOM 30 + - Vue 31 + - Svelte 32 + - Angular 33 + - Any framework with a virtual DOM or large runtime 34 + 35 + ## Commands 36 + 37 + ```bash 38 + # Install dependencies 39 + bun install 40 + 41 + # Development server with hot reload 42 + bun dev 43 + 44 + # Run tests 45 + bun test 46 + 47 + # Build files 48 + bun build <file.html|file.ts|file.css> 49 + ``` 50 + 51 + Development workflow: `bun dev` runs the server with hot module reloading. Changes to TypeScript, HTML, or CSS files automatically reload. 52 + 53 + **IMPORTANT**: NEVER run `bun dev` yourself - the user always has it running already. 54 + 55 + ## Bun Usage 56 + 57 + Default to using Bun instead of Node.js. 58 + 59 + - Use `bun <file>` instead of `node <file>` or `ts-node <file>` 60 + - Use `bun test` instead of `jest` or `vitest` 61 + - Use `bun build <file>` instead of `webpack` or `esbuild` 62 + - Use `bun install` instead of `npm install` or `yarn install` or `pnpm install` 63 + - Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` 64 + - Bun automatically loads .env, so don't use dotenv 65 + 66 + ## Bun APIs 67 + 68 + Use Bun's built-in APIs instead of npm packages: 69 + 70 + - `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`. 71 + - `bun:sqlite` for SQLite. Don't use `better-sqlite3`. 72 + - `Bun.redis` for Redis. Don't use `ioredis`. 73 + - `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`. 74 + - `WebSocket` is built-in. Don't use `ws`. 75 + - Prefer `Bun.file` over `node:fs`'s readFile/writeFile 76 + - `Bun.$\`ls\`` instead of execa 77 + 78 + ## Server Setup 79 + 80 + Use `Bun.serve()` with the routes pattern: 81 + 82 + ```ts 83 + import index from "./index.html" 84 + 85 + Bun.serve({ 86 + routes: { 87 + "/": index, 88 + "/api/users/:id": { 89 + GET: (req) => { 90 + return new Response(JSON.stringify({ id: req.params.id })); 91 + }, 92 + }, 93 + }, 94 + // optional websocket support 95 + websocket: { 96 + open: (ws) => { 97 + ws.send("Hello, world!"); 98 + }, 99 + message: (ws, message) => { 100 + ws.send(message); 101 + }, 102 + close: (ws) => { 103 + // handle close 104 + } 105 + }, 106 + development: { 107 + hmr: true, 108 + console: true, 109 + } 110 + }) 111 + ``` 112 + 113 + ## Frontend Pattern 114 + 115 + Don't use Vite or any build tools. Use HTML imports with `Bun.serve()`. 116 + 117 + HTML files can directly import `.ts` or `.js` files: 118 + 119 + ```html 120 + <!DOCTYPE html> 121 + <html> 122 + <head> 123 + <link rel="stylesheet" href="./styles.css"> 124 + </head> 125 + <body> 126 + <h1>Hello, world!</h1> 127 + <my-component></my-component> 128 + <script type="module" src="./frontend.ts"></script> 129 + </body> 130 + </html> 131 + ``` 132 + 133 + Bun's bundler will transpile and bundle automatically. `<link>` tags pointing to stylesheets work with Bun's CSS bundler. 134 + 135 + Frontend TypeScript (vanilla or with Lit web components): 136 + 137 + ```ts 138 + import { LitElement, html, css } from 'lit'; 139 + import { customElement, property } from 'lit/decorators.js'; 140 + 141 + // Define a Lit web component 142 + @customElement('my-component') 143 + export class MyComponent extends LitElement { 144 + @property({ type: String }) name = 'World'; 145 + 146 + // Scoped styles using css tagged template 147 + static styles = css` 148 + :host { 149 + display: block; 150 + padding: 1rem; 151 + } 152 + .greeting { 153 + color: blue; 154 + } 155 + `; 156 + 157 + // Render using html tagged template 158 + render() { 159 + return html` 160 + <div class="greeting"> 161 + Hello, ${this.name}! 162 + </div> 163 + `; 164 + } 165 + } 166 + 167 + // Or use plain DOM manipulation for simple interactions 168 + document.querySelector('h1')?.addEventListener('click', () => { 169 + console.log('Clicked!'); 170 + }); 171 + ``` 172 + 173 + **When to use Lit:** 174 + - Components with reactive properties (auto-updates when data changes) 175 + - Complex components needing scoped styles 176 + - Form controls with internal state 177 + - Components with lifecycle needs 178 + 179 + **When to skip Lit:** 180 + - Static content (use plain HTML) 181 + - Simple one-off interactions (use vanilla JS) 182 + - Anything without reactive state 183 + 184 + Lit provides: 185 + - `@customElement` decorator to register components 186 + - `@property` decorator for reactive properties 187 + - `html` tagged template for declarative rendering 188 + - `css` tagged template for scoped styles 189 + - Automatic re-rendering when properties change 190 + - Size: ~8-10KB minified+gzipped 191 + 192 + ## Testing 193 + 194 + Use `bun test` to run tests. 195 + 196 + ```ts 197 + import { test, expect } from "bun:test"; 198 + 199 + test("hello world", () => { 200 + expect(1).toBe(1); 201 + }); 202 + ``` 203 + 204 + ## TypeScript Configuration 205 + 206 + Strict mode is enabled with these settings: 207 + 208 + ```json 209 + { 210 + "strict": true, 211 + "noFallthroughCasesInSwitch": true, 212 + "noUncheckedIndexedAccess": true, 213 + "noImplicitOverride": true 214 + } 215 + ``` 216 + 217 + Deliberately disabled: 218 + - `noUnusedLocals`: false 219 + - `noUnusedParameters`: false 220 + - `noPropertyAccessFromIndexSignature`: false 221 + 222 + Module system: 223 + - `moduleResolution`: "bundler" 224 + - `module`: "Preserve" 225 + - JSX: `preserve` (NOT react-jsx - we don't use React) 226 + - Allows importing `.ts` extensions directly 227 + 228 + ## Frontend Technologies 229 + 230 + Core (always use): 231 + - Vanilla HTML, CSS, JavaScript/TypeScript 232 + - Native Web Components API 233 + - Native DOM APIs (querySelector, addEventListener, etc.) 234 + 235 + Lightweight helpers: 236 + - Lit (~8-10KB gzipped): For reactive web components with state management 237 + 238 + Bundle size philosophy: 239 + - Start with vanilla JS 240 + - Add helpers only when they significantly reduce complexity 241 + - Measure bundle size impact before adding any library 242 + - Target: Keep total JS bundle under 50KB 243 + 244 + ## Project Structure 245 + 246 + Based on Bun fullstack pattern: 247 + - `src/index.ts`: Server imports HTML files as modules 248 + - `src/pages/`: HTML files (route entry points) 249 + - `src/components/`: Lit web components 250 + - `src/styles/`: CSS files 251 + - `public/`: Static assets (images, fonts, etc.) 252 + 253 + **File flow:** 254 + 1. Server imports HTML: `import indexHTML from "./pages/index.html"` 255 + 2. HTML imports components: `<script type="module" src="../components/counter.ts"></script>` 256 + 3. HTML links styles: `<link rel="stylesheet" href="../styles/main.css">` 257 + 4. Components self-register as custom elements 258 + 5. Bun bundles everything automatically 259 + 260 + ## File Organization 261 + 262 + - `src/index.ts`: Main server entry point with `Bun.serve()` routes 263 + - `src/pages/*.html`: Route entry points (imported as modules) 264 + - `src/components/*.ts`: Lit web components 265 + - `src/styles/*.css`: Stylesheets (linked from HTML) 266 + - `public/`: Static assets directory 267 + - Tests: `*.test.ts` files 268 + 269 + **Current structure example:** 270 + ``` 271 + src/ 272 + index.ts # Imports HTML, defines routes 273 + pages/ 274 + index.html # Imports components via <script type="module"> 275 + components/ 276 + counter.ts # Lit component with @customElement 277 + styles/ 278 + main.css # Linked from HTML with <link> 279 + ``` 280 + 281 + ## Naming Conventions 282 + 283 + Follow TypeScript conventions: 284 + - PascalCase for components and classes 285 + - camelCase for functions and variables 286 + - kebab-case for file names 287 + 288 + ## Development Workflow 289 + 290 + 1. Make changes to `.ts`, `.html`, or `.css` files 291 + 2. Bun's HMR automatically reloads changes 292 + 3. Write tests in `*.test.ts` files 293 + 4. Run `bun test` to verify 294 + 295 + ## IDE Setup 296 + 297 + Biome LSP is configured in `crush.json` for linting and formatting support. 298 + 299 + ## Common Tasks 300 + 301 + ### Adding a new route 302 + Add to the `routes` object in `Bun.serve()` configuration 303 + 304 + ### Adding a new page 305 + Create an HTML file, import it in the server, add to routes 306 + 307 + ### Adding frontend functionality 308 + Import TS/JS files directly from HTML using `<script type="module" src="../components/my-component.ts"></script>`. Use Lit for reactive components or vanilla JS for simple interactions. Never React. 309 + 310 + ### Adding WebSocket support 311 + Add `websocket` configuration to `Bun.serve()` 312 + 313 + ## Important Notes 314 + 315 + 1. No npm scripts needed: Bun is fast enough to run commands directly 316 + 2. Private package: `package.json` has `"private": true` 317 + 3. No build step for development: Hot reload handles everything 318 + 4. Module type: Package uses `"type": "module"` (ESM) 319 + 5. Bun types: Available via `@types/bun` (check `node_modules/bun-types/docs/**.md` for API docs) 320 + 321 + ## Gotchas 322 + 323 + 1. Don't use Node.js commands: Use `bun` instead of `node`, `npm`, `npx`, etc. 324 + 2. Don't install Express/Vite/other tools: Bun has built-in equivalents 325 + 3. NEVER EVER use React: This project is vanilla JS/TS with web components only. React is explicitly forbidden. 326 + 4. Import .ts extensions: Bun allows importing `.ts` files directly 327 + 5. No dotenv needed: Bun loads `.env` automatically 328 + 6. HTML imports are special: They trigger Bun's bundler, don't treat them as static files 329 + 7. Bundle size matters: Always consider the size impact before adding any library 330 + 331 + ## Documentation Lookup 332 + 333 + Use Context7 MCP for looking up official documentation for libraries and frameworks. 334 + 335 + ## Resources 336 + 337 + - [Bun Fullstack Documentation](https://bun.com/docs/bundler/fullstack) 338 + - [Lit Documentation](https://lit.dev/) 339 + - [Web Components MDN](https://developer.mozilla.org/en-US/docs/Web/Web_Components) 340 + - Bun API docs in `node_modules/bun-types/docs/**.md` 341 + 342 + ## Future Additions 343 + 344 + As the codebase grows, document: 345 + - Database schema and migrations 346 + - API endpoint patterns 347 + - Authentication/authorization approach 348 + - Transcription service integration details 349 + - Deployment process 350 + - Environment variables needed
+107
README.md
··· 1 + # Thistle 2 + 3 + > [!IMPORTANT] 4 + > This is crazy pre-alpha and is changing really rapidly. Stuff should stabilize eventually but probably not for a month or two. 5 + 6 + ```bash 7 + . 8 + ├── public 9 + └── src 10 + ├── components 11 + ├── pages 12 + └── styles 13 + 14 + 6 directories 15 + ``` 16 + 17 + ## What's this? 18 + 19 + Thistle is a transcription service I'm building for Cedarville's startup competition! I'm also using it as an opportunity to become more familar with web components and full stack applications. 20 + 21 + ## How do I hack on it? 22 + 23 + ### Development 24 + 25 + I'm just running this locally for now but getting started is super straightforward. 26 + 27 + ```bash 28 + bun install 29 + bun dev 30 + ``` 31 + 32 + Your server will be running at `http://localhost:3000` with hot module reloading. Just edit any `.ts`, `.html`, or `.css` file and watch it update in the browser. 33 + 34 + The tech stack is pretty minimal on purpose. Lit components (~8-10KB gzipped) for things that need reactivity, vanilla JS for simple stuff, and CSS variables for theming. The goal is to keep the total JS bundle as small as possible. 35 + 36 + ## How does it work? 37 + 38 + The development flow is really nice in my opinion. The server imports HTML files as route handlers. Those HTML files import TypeScript components using `<script type="module">`. The components are just Lit web components that self-register as custom elements. Bun sees all this and bundles everything automatically including linked images or assets from the public directory. 39 + 40 + ```typescript 41 + // src/index.ts - Server imports HTML as routes 42 + import indexHTML from "./pages/index.html"; 43 + 44 + Bun.serve({ 45 + port: 3000, 46 + routes: { 47 + "/": indexHTML, 48 + }, 49 + development: { 50 + hmr: true, 51 + console: true, 52 + }, 53 + }); 54 + ``` 55 + 56 + ```html 57 + <!-- src/pages/index.html --> 58 + <!DOCTYPE html> 59 + <html lang="en"> 60 + <head> 61 + <link rel="stylesheet" href="../styles/main.css" /> 62 + </head> 63 + <body> 64 + <counter-component></counter-component> 65 + <script type="module" src="../components/counter.ts"></script> 66 + </body> 67 + </html> 68 + ``` 69 + 70 + ```typescript 71 + // src/components/counter.ts 72 + import { LitElement, html, css } from "lit"; 73 + import { customElement, property } from "lit/decorators.js"; 74 + 75 + @customElement("counter-component") 76 + export class CounterComponent extends LitElement { 77 + @property({ type: Number }) count = 0; 78 + 79 + static styles = css` 80 + :host { 81 + display: block; 82 + padding: 1rem; 83 + } 84 + `; 85 + 86 + render() { 87 + return html` 88 + <div>${this.count}</div> 89 + <button @click=${() => this.count++}>+</button> 90 + `; 91 + } 92 + } 93 + ``` 94 + 95 + Oh last two points. Please please please use standard commits for my sanity and report any issues to [the tangled repo](https://tangled.org/dunkirk.sh/thistle) 96 + 97 + <p align="center"> 98 + <img src="https://raw.githubusercontent.com/taciturnaxolotl/carriage/master/.github/images/line-break.svg" /> 99 + </p> 100 + 101 + <p align="center"> 102 + &copy 2025-present <a href="https://github.com/taciturnaxolotl">Kieran Klukas</a> 103 + </p> 104 + 105 + <p align="center"> 106 + <a href="https://github.com/taciturnaxolotl/thistle/blob/main/LICENSE.md"><img src="https://img.shields.io/static/v1.svg?style=for-the-badge&label=License&message=MIT&logoColor=d9e0ee&colorA=363a4f&colorB=b7bdf8"/></a> 107 + </p>
+34
biome.json
··· 1 + { 2 + "$schema": "https://biomejs.dev/schemas/2.2.7/schema.json", 3 + "vcs": { 4 + "enabled": false, 5 + "clientKind": "git", 6 + "useIgnoreFile": false 7 + }, 8 + "files": { 9 + "ignoreUnknown": false 10 + }, 11 + "formatter": { 12 + "enabled": true, 13 + "indentStyle": "tab" 14 + }, 15 + "linter": { 16 + "enabled": true, 17 + "rules": { 18 + "recommended": true 19 + } 20 + }, 21 + "javascript": { 22 + "formatter": { 23 + "quoteStyle": "double" 24 + } 25 + }, 26 + "assist": { 27 + "enabled": true, 28 + "actions": { 29 + "source": { 30 + "organizeImports": "on" 31 + } 32 + } 33 + } 34 + }
+63
bun.lock
··· 1 + { 2 + "lockfileVersion": 1, 3 + "workspaces": { 4 + "": { 5 + "name": "inky", 6 + "dependencies": { 7 + "lit": "^3.3.1", 8 + }, 9 + "devDependencies": { 10 + "@biomejs/biome": "^2.3.2", 11 + "@types/bun": "latest", 12 + }, 13 + "peerDependencies": { 14 + "typescript": "^5", 15 + }, 16 + }, 17 + }, 18 + "packages": { 19 + "@biomejs/biome": ["@biomejs/biome@2.3.2", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.2", "@biomejs/cli-darwin-x64": "2.3.2", "@biomejs/cli-linux-arm64": "2.3.2", "@biomejs/cli-linux-arm64-musl": "2.3.2", "@biomejs/cli-linux-x64": "2.3.2", "@biomejs/cli-linux-x64-musl": "2.3.2", "@biomejs/cli-win32-arm64": "2.3.2", "@biomejs/cli-win32-x64": "2.3.2" }, "bin": { "biome": "bin/biome" } }, "sha512-8e9tzamuDycx7fdrcJ/F/GDZ8SYukc5ud6tDicjjFqURKYFSWMl0H0iXNXZEGmcmNUmABgGuHThPykcM41INgg=="], 20 + 21 + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4LECm4kc3If0JISai4c3KWQzukoUdpxy4fRzlrPcrdMSRFksR9ZoXK7JBcPuLBmd2SoT4/d7CQS33VnZpgBjew=="], 22 + 23 + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-jNMnfwHT4N3wi+ypRfMTjLGnDmKYGzxVr1EYAPBcauRcDnICFXN81wD6wxJcSUrLynoyyYCdfW6vJHS/IAoTDA=="], 24 + 25 + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-amnqvk+gWybbQleRRq8TMe0rIv7GHss8mFJEaGuEZYWg1Tw14YKOkeo8h6pf1c+d3qR+JU4iT9KXnBKGON4klw=="], 26 + 27 + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-2Zz4usDG1GTTPQnliIeNx6eVGGP2ry5vE/v39nT73a3cKN6t5H5XxjcEoZZh62uVZvED7hXXikclvI64vZkYqw=="], 28 + 29 + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-8BG/vRAhFz1pmuyd24FQPhNeueLqPtwvZk6yblABY2gzL2H8fLQAF/Z2OPIc+BPIVPld+8cSiKY/KFh6k81xfA=="], 30 + 31 + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-gzB19MpRdTuOuLtPpFBGrV3Lq424gHyq2lFj8wfX9tvLMLdmA/R9C7k/mqBp/spcbWuHeIEKgEs3RviOPcWGBA=="], 32 + 33 + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCruqQlfWjhMlOdyf5pDHOxoNm4WoyY2vZ4YN33/nuZBRstVDuqPPjS0yBkbUlLEte11FbpW+wWSlfnZfSIZvg=="], 34 + 35 + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-6Ee9P26DTb4D8sN9nXxgbi9Dw5vSOfH98M7UlmkjKB2vtUbrRqCbZiNfryGiwnPIpd6YUoTl7rLVD2/x1CyEHQ=="], 36 + 37 + "@lit-labs/ssr-dom-shim": ["@lit-labs/ssr-dom-shim@1.4.0", "", {}, "sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw=="], 38 + 39 + "@lit/reactive-element": ["@lit/reactive-element@2.1.1", "", { "dependencies": { "@lit-labs/ssr-dom-shim": "^1.4.0" } }, "sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg=="], 40 + 41 + "@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="], 42 + 43 + "@types/node": ["@types/node@24.9.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA=="], 44 + 45 + "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], 46 + 47 + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], 48 + 49 + "bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="], 50 + 51 + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], 52 + 53 + "lit": ["lit@3.3.1", "", { "dependencies": { "@lit/reactive-element": "^2.1.0", "lit-element": "^4.2.0", "lit-html": "^3.3.0" } }, "sha512-Ksr/8L3PTapbdXJCk+EJVB78jDodUMaP54gD24W186zGRARvwrsPfS60wae/SSCTCNZVPd1chXqio1qHQmu4NA=="], 54 + 55 + "lit-element": ["lit-element@4.2.1", "", { "dependencies": { "@lit-labs/ssr-dom-shim": "^1.4.0", "@lit/reactive-element": "^2.1.0", "lit-html": "^3.3.0" } }, "sha512-WGAWRGzirAgyphK2urmYOV72tlvnxw7YfyLDgQ+OZnM9vQQBQnumQ7jUJe6unEzwGU3ahFOjuz1iz1jjrpCPuw=="], 56 + 57 + "lit-html": ["lit-html@3.3.1", "", { "dependencies": { "@types/trusted-types": "^2.0.2" } }, "sha512-S9hbyDu/vs1qNrithiNyeyv64c9yqiW9l+DBgI18fL+MTvOtWoFR0FWiyq1TxaYef5wNlpEmzlXoBlZEO+WjoA=="], 58 + 59 + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], 60 + 61 + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], 62 + } 63 + }
+19
crush.json
··· 1 + { 2 + "$schema": "https://charm.land/crush.json", 3 + "lsp": { 4 + "biome": { 5 + "command": "bunx", 6 + "args": [ 7 + "biome", 8 + "lsp-proxy" 9 + ] 10 + }, 11 + "typescript": { 12 + "command": "bunx", 13 + "args": [ 14 + "typescript-language-server", 15 + "--stdio" 16 + ] 17 + } 18 + } 19 + }
+19
package.json
··· 1 + { 2 + "name": "thistle", 3 + "module": "src/index.ts", 4 + "type": "module", 5 + "private": true, 6 + "scripts": { 7 + "dev": "bun run src/index.ts --hot" 8 + }, 9 + "devDependencies": { 10 + "@biomejs/biome": "^2.3.2", 11 + "@types/bun": "latest" 12 + }, 13 + "peerDependencies": { 14 + "typescript": "^5" 15 + }, 16 + "dependencies": { 17 + "lit": "^3.3.1" 18 + } 19 + }
+77
src/components/counter.ts
··· 1 + import { css, html, LitElement } from "lit"; 2 + import { customElement, property } from "lit/decorators.js"; 3 + 4 + // Simple counter web component using Lit 5 + @customElement("counter-component") 6 + export class CounterComponent extends LitElement { 7 + @property({ type: Number }) count = 10; 8 + 9 + static override styles = css` 10 + :host { 11 + display: block; 12 + margin: 2rem 0; 13 + padding: 2rem; 14 + border: 1px solid var(--secondary); 15 + border-radius: 8px; 16 + text-align: center; 17 + background: white; 18 + } 19 + 20 + .counter-display { 21 + font-size: 3rem; 22 + font-weight: bold; 23 + margin: 1rem 0; 24 + font-family: 'Charter', 'Bitstream Charter', 'Sitka Text', Cambria, serif; 25 + color: var(--primary); 26 + } 27 + 28 + button { 29 + font-family: 'Charter', 'Bitstream Charter', 'Sitka Text', Cambria, serif; 30 + font-size: 1rem; 31 + padding: 0.75rem 1.5rem; 32 + margin: 0 0.5rem; 33 + border: 2px solid var(--primary); 34 + background: var(--background); 35 + color: var(--text); 36 + cursor: pointer; 37 + border-radius: 4px; 38 + transition: all 0.2s ease; 39 + } 40 + 41 + button:hover { 42 + background: var(--primary); 43 + color: var(--background); 44 + } 45 + 46 + button:active { 47 + transform: scale(0.95); 48 + } 49 + 50 + button.reset { 51 + border-color: var(--accent); 52 + } 53 + 54 + button.reset:hover { 55 + background: var(--accent); 56 + color: var(--background); 57 + } 58 + `; 59 + 60 + private resetCounter() { 61 + this.count = 0; 62 + } 63 + 64 + override render() { 65 + return html` 66 + <div> 67 + <h3>Counter</h3> 68 + <div class="counter-display">${this.count}</div> 69 + <div> 70 + <button @click=${() => this.count--}>-</button> 71 + <button class="reset" @click=${this.resetCounter}>Reset</button> 72 + <button @click=${() => this.count++}>+</button> 73 + </div> 74 + </div> 75 + `; 76 + } 77 + }
+14
src/index.ts
··· 1 + import indexHTML from "./pages/index.html"; 2 + 3 + const server = Bun.serve({ 4 + port: 3000, 5 + routes: { 6 + "/": indexHTML, 7 + }, 8 + development: { 9 + hmr: true, 10 + console: true, 11 + }, 12 + }); 13 + 14 + console.log(`🪻 Thistle running at http://localhost:${server.port}`);
+24
src/pages/index.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + 4 + <head> 5 + <meta charset="UTF-8"> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 + <title>Thistle</title> 8 + <link rel="icon" 9 + href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='0.9em' font-size='90'>🪻</text></svg>"> 10 + <link rel="stylesheet" href="../styles/main.css"> 11 + </head> 12 + 13 + <body> 14 + <main> 15 + <h1>Thistle</h1> 16 + <p>Here is a basic counter to figure out the basics of web components</p> 17 + 18 + <counter-component></counter-component> 19 + </main> 20 + 21 + <script type="module" src="../components/counter.ts"></script> 22 + </body> 23 + 24 + </html>
+46
src/styles/main.css
··· 1 + :root { 2 + --text: #5b6971; 3 + --background: #fefbf1; 4 + --primary: #8fa668; 5 + --secondary: #d0cdf9; 6 + --accent: #e59976; 7 + } 8 + 9 + body { 10 + font-family: 'Charter', 'Bitstream Charter', 'Sitka Text', Cambria, serif; 11 + font-weight: 400; 12 + margin: 0; 13 + padding: 2rem; 14 + line-height: 1.6; 15 + background: var(--background); 16 + color: var(--text); 17 + } 18 + 19 + h1, h2, h3, h4, h5 { 20 + font-family: 'Charter', 'Bitstream Charter', 'Sitka Text', Cambria, serif; 21 + font-weight: 600; 22 + line-height: 1.2; 23 + color: var(--text); 24 + } 25 + 26 + html {font-size: 100%;} /* 16px */ 27 + 28 + h1 { 29 + font-size: 4.210rem; /* 67.36px */ 30 + margin-top: 0; 31 + } 32 + 33 + h2 {font-size: 3.158rem; /* 50.56px */} 34 + 35 + h3 {font-size: 2.369rem; /* 37.92px */} 36 + 37 + h4 {font-size: 1.777rem; /* 28.48px */} 38 + 39 + h5 {font-size: 1.333rem; /* 21.28px */} 40 + 41 + small {font-size: 0.750rem; /* 12px */} 42 + 43 + main { 44 + max-width: 800px; 45 + margin: 0 auto; 46 + }
+33
tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + // Environment setup & latest features 4 + "lib": ["ESNext", "DOM", "DOM.Iterable"], 5 + "target": "ESNext", 6 + "module": "Preserve", 7 + "moduleDetection": "force", 8 + "jsx": "preserve", 9 + "allowJs": true, 10 + 11 + // Bundler mode 12 + "moduleResolution": "bundler", 13 + "allowImportingTsExtensions": true, 14 + "verbatimModuleSyntax": true, 15 + "noEmit": true, 16 + 17 + // Decorators 18 + "experimentalDecorators": true, 19 + "useDefineForClassFields": false, 20 + 21 + // Best practices 22 + "strict": true, 23 + "skipLibCheck": true, 24 + "noFallthroughCasesInSwitch": true, 25 + "noUncheckedIndexedAccess": true, 26 + "noImplicitOverride": true, 27 + 28 + // Some stricter flags (disabled by default) 29 + "noUnusedLocals": false, 30 + "noUnusedParameters": false, 31 + "noPropertyAccessFromIndexSignature": false 32 + } 33 + }