🪻 distributed transcription service thistle.dunkirk.sh

feat: init base

dunkirk.sh 69133d91

verified
+1
.gitignore
···
+
node_modules
+350
CRUSH.md
···
+
# Thistle - Project Guidelines
+
+
This is a Bun-based transcription service using the [Bun fullstack pattern](https://bun.com/docs/bundler/fullstack) for routing and bundled HTML.
+
+
## Project Info
+
+
- Name: Thistle
+
- Purpose: Transcription service
+
- Runtime: Bun (NOT Node.js)
+
- Language: TypeScript with strict mode
+
- Frontend: Vanilla HTML/CSS/JS with lightweight helpers on top of web components
+
+
## NO FRAMEWORKS
+
+
NEVER use React, Vue, Svelte, or any heavy framework.
+
+
This project prioritizes:
+
- Speed: Minimal JavaScript, fast load times
+
- Small bundle sizes: Keep bundles tiny
+
- Native web platform: Use web standards (Web Components, native DOM APIs)
+
- Simplicity: Vanilla HTML, CSS, and JavaScript
+
+
Allowed lightweight helpers:
+
- Lit (~8-10KB gzipped) for reactive web components
+
- Native Web Components
+
- Plain JavaScript/TypeScript
+
+
Explicitly forbidden:
+
- React, React DOM
+
- Vue
+
- Svelte
+
- Angular
+
- Any framework with a virtual DOM or large runtime
+
+
## Commands
+
+
```bash
+
# Install dependencies
+
bun install
+
+
# Development server with hot reload
+
bun dev
+
+
# Run tests
+
bun test
+
+
# Build files
+
bun build <file.html|file.ts|file.css>
+
```
+
+
Development workflow: `bun dev` runs the server with hot module reloading. Changes to TypeScript, HTML, or CSS files automatically reload.
+
+
**IMPORTANT**: NEVER run `bun dev` yourself - the user always has it running already.
+
+
## Bun Usage
+
+
Default to using Bun instead of Node.js.
+
+
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
+
- Use `bun test` instead of `jest` or `vitest`
+
- Use `bun build <file>` instead of `webpack` or `esbuild`
+
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
+
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>`
+
- Bun automatically loads .env, so don't use dotenv
+
+
## Bun APIs
+
+
Use Bun's built-in APIs instead of npm packages:
+
+
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
+
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
+
- `Bun.redis` for Redis. Don't use `ioredis`.
+
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
+
- `WebSocket` is built-in. Don't use `ws`.
+
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
+
- `Bun.$\`ls\`` instead of execa
+
+
## Server Setup
+
+
Use `Bun.serve()` with the routes pattern:
+
+
```ts
+
import index from "./index.html"
+
+
Bun.serve({
+
routes: {
+
"/": index,
+
"/api/users/:id": {
+
GET: (req) => {
+
return new Response(JSON.stringify({ id: req.params.id }));
+
},
+
},
+
},
+
// optional websocket support
+
websocket: {
+
open: (ws) => {
+
ws.send("Hello, world!");
+
},
+
message: (ws, message) => {
+
ws.send(message);
+
},
+
close: (ws) => {
+
// handle close
+
}
+
},
+
development: {
+
hmr: true,
+
console: true,
+
}
+
})
+
```
+
+
## Frontend Pattern
+
+
Don't use Vite or any build tools. Use HTML imports with `Bun.serve()`.
+
+
HTML files can directly import `.ts` or `.js` files:
+
+
```html
+
<!DOCTYPE html>
+
<html>
+
<head>
+
<link rel="stylesheet" href="./styles.css">
+
</head>
+
<body>
+
<h1>Hello, world!</h1>
+
<my-component></my-component>
+
<script type="module" src="./frontend.ts"></script>
+
</body>
+
</html>
+
```
+
+
Bun's bundler will transpile and bundle automatically. `<link>` tags pointing to stylesheets work with Bun's CSS bundler.
+
+
Frontend TypeScript (vanilla or with Lit web components):
+
+
```ts
+
import { LitElement, html, css } from 'lit';
+
import { customElement, property } from 'lit/decorators.js';
+
+
// Define a Lit web component
+
@customElement('my-component')
+
export class MyComponent extends LitElement {
+
@property({ type: String }) name = 'World';
+
+
// Scoped styles using css tagged template
+
static styles = css`
+
:host {
+
display: block;
+
padding: 1rem;
+
}
+
.greeting {
+
color: blue;
+
}
+
`;
+
+
// Render using html tagged template
+
render() {
+
return html`
+
<div class="greeting">
+
Hello, ${this.name}!
+
</div>
+
`;
+
}
+
}
+
+
// Or use plain DOM manipulation for simple interactions
+
document.querySelector('h1')?.addEventListener('click', () => {
+
console.log('Clicked!');
+
});
+
```
+
+
**When to use Lit:**
+
- Components with reactive properties (auto-updates when data changes)
+
- Complex components needing scoped styles
+
- Form controls with internal state
+
- Components with lifecycle needs
+
+
**When to skip Lit:**
+
- Static content (use plain HTML)
+
- Simple one-off interactions (use vanilla JS)
+
- Anything without reactive state
+
+
Lit provides:
+
- `@customElement` decorator to register components
+
- `@property` decorator for reactive properties
+
- `html` tagged template for declarative rendering
+
- `css` tagged template for scoped styles
+
- Automatic re-rendering when properties change
+
- Size: ~8-10KB minified+gzipped
+
+
## Testing
+
+
Use `bun test` to run tests.
+
+
```ts
+
import { test, expect } from "bun:test";
+
+
test("hello world", () => {
+
expect(1).toBe(1);
+
});
+
```
+
+
## TypeScript Configuration
+
+
Strict mode is enabled with these settings:
+
+
```json
+
{
+
"strict": true,
+
"noFallthroughCasesInSwitch": true,
+
"noUncheckedIndexedAccess": true,
+
"noImplicitOverride": true
+
}
+
```
+
+
Deliberately disabled:
+
- `noUnusedLocals`: false
+
- `noUnusedParameters`: false
+
- `noPropertyAccessFromIndexSignature`: false
+
+
Module system:
+
- `moduleResolution`: "bundler"
+
- `module`: "Preserve"
+
- JSX: `preserve` (NOT react-jsx - we don't use React)
+
- Allows importing `.ts` extensions directly
+
+
## Frontend Technologies
+
+
Core (always use):
+
- Vanilla HTML, CSS, JavaScript/TypeScript
+
- Native Web Components API
+
- Native DOM APIs (querySelector, addEventListener, etc.)
+
+
Lightweight helpers:
+
- Lit (~8-10KB gzipped): For reactive web components with state management
+
+
Bundle size philosophy:
+
- Start with vanilla JS
+
- Add helpers only when they significantly reduce complexity
+
- Measure bundle size impact before adding any library
+
- Target: Keep total JS bundle under 50KB
+
+
## Project Structure
+
+
Based on Bun fullstack pattern:
+
- `src/index.ts`: Server imports HTML files as modules
+
- `src/pages/`: HTML files (route entry points)
+
- `src/components/`: Lit web components
+
- `src/styles/`: CSS files
+
- `public/`: Static assets (images, fonts, etc.)
+
+
**File flow:**
+
1. Server imports HTML: `import indexHTML from "./pages/index.html"`
+
2. HTML imports components: `<script type="module" src="../components/counter.ts"></script>`
+
3. HTML links styles: `<link rel="stylesheet" href="../styles/main.css">`
+
4. Components self-register as custom elements
+
5. Bun bundles everything automatically
+
+
## File Organization
+
+
- `src/index.ts`: Main server entry point with `Bun.serve()` routes
+
- `src/pages/*.html`: Route entry points (imported as modules)
+
- `src/components/*.ts`: Lit web components
+
- `src/styles/*.css`: Stylesheets (linked from HTML)
+
- `public/`: Static assets directory
+
- Tests: `*.test.ts` files
+
+
**Current structure example:**
+
```
+
src/
+
index.ts # Imports HTML, defines routes
+
pages/
+
index.html # Imports components via <script type="module">
+
components/
+
counter.ts # Lit component with @customElement
+
styles/
+
main.css # Linked from HTML with <link>
+
```
+
+
## Naming Conventions
+
+
Follow TypeScript conventions:
+
- PascalCase for components and classes
+
- camelCase for functions and variables
+
- kebab-case for file names
+
+
## Development Workflow
+
+
1. Make changes to `.ts`, `.html`, or `.css` files
+
2. Bun's HMR automatically reloads changes
+
3. Write tests in `*.test.ts` files
+
4. Run `bun test` to verify
+
+
## IDE Setup
+
+
Biome LSP is configured in `crush.json` for linting and formatting support.
+
+
## Common Tasks
+
+
### Adding a new route
+
Add to the `routes` object in `Bun.serve()` configuration
+
+
### Adding a new page
+
Create an HTML file, import it in the server, add to routes
+
+
### Adding frontend functionality
+
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.
+
+
### Adding WebSocket support
+
Add `websocket` configuration to `Bun.serve()`
+
+
## Important Notes
+
+
1. No npm scripts needed: Bun is fast enough to run commands directly
+
2. Private package: `package.json` has `"private": true`
+
3. No build step for development: Hot reload handles everything
+
4. Module type: Package uses `"type": "module"` (ESM)
+
5. Bun types: Available via `@types/bun` (check `node_modules/bun-types/docs/**.md` for API docs)
+
+
## Gotchas
+
+
1. Don't use Node.js commands: Use `bun` instead of `node`, `npm`, `npx`, etc.
+
2. Don't install Express/Vite/other tools: Bun has built-in equivalents
+
3. NEVER EVER use React: This project is vanilla JS/TS with web components only. React is explicitly forbidden.
+
4. Import .ts extensions: Bun allows importing `.ts` files directly
+
5. No dotenv needed: Bun loads `.env` automatically
+
6. HTML imports are special: They trigger Bun's bundler, don't treat them as static files
+
7. Bundle size matters: Always consider the size impact before adding any library
+
+
## Documentation Lookup
+
+
Use Context7 MCP for looking up official documentation for libraries and frameworks.
+
+
## Resources
+
+
- [Bun Fullstack Documentation](https://bun.com/docs/bundler/fullstack)
+
- [Lit Documentation](https://lit.dev/)
+
- [Web Components MDN](https://developer.mozilla.org/en-US/docs/Web/Web_Components)
+
- Bun API docs in `node_modules/bun-types/docs/**.md`
+
+
## Future Additions
+
+
As the codebase grows, document:
+
- Database schema and migrations
+
- API endpoint patterns
+
- Authentication/authorization approach
+
- Transcription service integration details
+
- Deployment process
+
- Environment variables needed
+107
README.md
···
+
# Thistle
+
+
> [!IMPORTANT]
+
> This is crazy pre-alpha and is changing really rapidly. Stuff should stabilize eventually but probably not for a month or two.
+
+
```bash
+
.
+
├── public
+
└── src
+
├── components
+
├── pages
+
└── styles
+
+
6 directories
+
```
+
+
## What's this?
+
+
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.
+
+
## How do I hack on it?
+
+
### Development
+
+
I'm just running this locally for now but getting started is super straightforward.
+
+
```bash
+
bun install
+
bun dev
+
```
+
+
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.
+
+
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.
+
+
## How does it work?
+
+
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.
+
+
```typescript
+
// src/index.ts - Server imports HTML as routes
+
import indexHTML from "./pages/index.html";
+
+
Bun.serve({
+
port: 3000,
+
routes: {
+
"/": indexHTML,
+
},
+
development: {
+
hmr: true,
+
console: true,
+
},
+
});
+
```
+
+
```html
+
<!-- src/pages/index.html -->
+
<!DOCTYPE html>
+
<html lang="en">
+
<head>
+
<link rel="stylesheet" href="../styles/main.css" />
+
</head>
+
<body>
+
<counter-component></counter-component>
+
<script type="module" src="../components/counter.ts"></script>
+
</body>
+
</html>
+
```
+
+
```typescript
+
// src/components/counter.ts
+
import { LitElement, html, css } from "lit";
+
import { customElement, property } from "lit/decorators.js";
+
+
@customElement("counter-component")
+
export class CounterComponent extends LitElement {
+
@property({ type: Number }) count = 0;
+
+
static styles = css`
+
:host {
+
display: block;
+
padding: 1rem;
+
}
+
`;
+
+
render() {
+
return html`
+
<div>${this.count}</div>
+
<button @click=${() => this.count++}>+</button>
+
`;
+
}
+
}
+
```
+
+
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)
+
+
<p align="center">
+
<img src="https://raw.githubusercontent.com/taciturnaxolotl/carriage/master/.github/images/line-break.svg" />
+
</p>
+
+
<p align="center">
+
&copy 2025-present <a href="https://github.com/taciturnaxolotl">Kieran Klukas</a>
+
</p>
+
+
<p align="center">
+
<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>
+
</p>
+34
biome.json
···
+
{
+
"$schema": "https://biomejs.dev/schemas/2.2.7/schema.json",
+
"vcs": {
+
"enabled": false,
+
"clientKind": "git",
+
"useIgnoreFile": false
+
},
+
"files": {
+
"ignoreUnknown": false
+
},
+
"formatter": {
+
"enabled": true,
+
"indentStyle": "tab"
+
},
+
"linter": {
+
"enabled": true,
+
"rules": {
+
"recommended": true
+
}
+
},
+
"javascript": {
+
"formatter": {
+
"quoteStyle": "double"
+
}
+
},
+
"assist": {
+
"enabled": true,
+
"actions": {
+
"source": {
+
"organizeImports": "on"
+
}
+
}
+
}
+
}
+63
bun.lock
···
+
{
+
"lockfileVersion": 1,
+
"workspaces": {
+
"": {
+
"name": "inky",
+
"dependencies": {
+
"lit": "^3.3.1",
+
},
+
"devDependencies": {
+
"@biomejs/biome": "^2.3.2",
+
"@types/bun": "latest",
+
},
+
"peerDependencies": {
+
"typescript": "^5",
+
},
+
},
+
},
+
"packages": {
+
"@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=="],
+
+
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4LECm4kc3If0JISai4c3KWQzukoUdpxy4fRzlrPcrdMSRFksR9ZoXK7JBcPuLBmd2SoT4/d7CQS33VnZpgBjew=="],
+
+
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-jNMnfwHT4N3wi+ypRfMTjLGnDmKYGzxVr1EYAPBcauRcDnICFXN81wD6wxJcSUrLynoyyYCdfW6vJHS/IAoTDA=="],
+
+
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-amnqvk+gWybbQleRRq8TMe0rIv7GHss8mFJEaGuEZYWg1Tw14YKOkeo8h6pf1c+d3qR+JU4iT9KXnBKGON4klw=="],
+
+
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-2Zz4usDG1GTTPQnliIeNx6eVGGP2ry5vE/v39nT73a3cKN6t5H5XxjcEoZZh62uVZvED7hXXikclvI64vZkYqw=="],
+
+
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-8BG/vRAhFz1pmuyd24FQPhNeueLqPtwvZk6yblABY2gzL2H8fLQAF/Z2OPIc+BPIVPld+8cSiKY/KFh6k81xfA=="],
+
+
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-gzB19MpRdTuOuLtPpFBGrV3Lq424gHyq2lFj8wfX9tvLMLdmA/R9C7k/mqBp/spcbWuHeIEKgEs3RviOPcWGBA=="],
+
+
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCruqQlfWjhMlOdyf5pDHOxoNm4WoyY2vZ4YN33/nuZBRstVDuqPPjS0yBkbUlLEte11FbpW+wWSlfnZfSIZvg=="],
+
+
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-6Ee9P26DTb4D8sN9nXxgbi9Dw5vSOfH98M7UlmkjKB2vtUbrRqCbZiNfryGiwnPIpd6YUoTl7rLVD2/x1CyEHQ=="],
+
+
"@lit-labs/ssr-dom-shim": ["@lit-labs/ssr-dom-shim@1.4.0", "", {}, "sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw=="],
+
+
"@lit/reactive-element": ["@lit/reactive-element@2.1.1", "", { "dependencies": { "@lit-labs/ssr-dom-shim": "^1.4.0" } }, "sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg=="],
+
+
"@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="],
+
+
"@types/node": ["@types/node@24.9.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA=="],
+
+
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
+
+
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
+
+
"bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="],
+
+
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
+
+
"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=="],
+
+
"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=="],
+
+
"lit-html": ["lit-html@3.3.1", "", { "dependencies": { "@types/trusted-types": "^2.0.2" } }, "sha512-S9hbyDu/vs1qNrithiNyeyv64c9yqiW9l+DBgI18fL+MTvOtWoFR0FWiyq1TxaYef5wNlpEmzlXoBlZEO+WjoA=="],
+
+
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
+
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
+
}
+
}
+19
crush.json
···
+
{
+
"$schema": "https://charm.land/crush.json",
+
"lsp": {
+
"biome": {
+
"command": "bunx",
+
"args": [
+
"biome",
+
"lsp-proxy"
+
]
+
},
+
"typescript": {
+
"command": "bunx",
+
"args": [
+
"typescript-language-server",
+
"--stdio"
+
]
+
}
+
}
+
}
+19
package.json
···
+
{
+
"name": "thistle",
+
"module": "src/index.ts",
+
"type": "module",
+
"private": true,
+
"scripts": {
+
"dev": "bun run src/index.ts --hot"
+
},
+
"devDependencies": {
+
"@biomejs/biome": "^2.3.2",
+
"@types/bun": "latest"
+
},
+
"peerDependencies": {
+
"typescript": "^5"
+
},
+
"dependencies": {
+
"lit": "^3.3.1"
+
}
+
}
+77
src/components/counter.ts
···
+
import { css, html, LitElement } from "lit";
+
import { customElement, property } from "lit/decorators.js";
+
+
// Simple counter web component using Lit
+
@customElement("counter-component")
+
export class CounterComponent extends LitElement {
+
@property({ type: Number }) count = 10;
+
+
static override styles = css`
+
:host {
+
display: block;
+
margin: 2rem 0;
+
padding: 2rem;
+
border: 1px solid var(--secondary);
+
border-radius: 8px;
+
text-align: center;
+
background: white;
+
}
+
+
.counter-display {
+
font-size: 3rem;
+
font-weight: bold;
+
margin: 1rem 0;
+
font-family: 'Charter', 'Bitstream Charter', 'Sitka Text', Cambria, serif;
+
color: var(--primary);
+
}
+
+
button {
+
font-family: 'Charter', 'Bitstream Charter', 'Sitka Text', Cambria, serif;
+
font-size: 1rem;
+
padding: 0.75rem 1.5rem;
+
margin: 0 0.5rem;
+
border: 2px solid var(--primary);
+
background: var(--background);
+
color: var(--text);
+
cursor: pointer;
+
border-radius: 4px;
+
transition: all 0.2s ease;
+
}
+
+
button:hover {
+
background: var(--primary);
+
color: var(--background);
+
}
+
+
button:active {
+
transform: scale(0.95);
+
}
+
+
button.reset {
+
border-color: var(--accent);
+
}
+
+
button.reset:hover {
+
background: var(--accent);
+
color: var(--background);
+
}
+
`;
+
+
private resetCounter() {
+
this.count = 0;
+
}
+
+
override render() {
+
return html`
+
<div>
+
<h3>Counter</h3>
+
<div class="counter-display">${this.count}</div>
+
<div>
+
<button @click=${() => this.count--}>-</button>
+
<button class="reset" @click=${this.resetCounter}>Reset</button>
+
<button @click=${() => this.count++}>+</button>
+
</div>
+
</div>
+
`;
+
}
+
}
+14
src/index.ts
···
+
import indexHTML from "./pages/index.html";
+
+
const server = Bun.serve({
+
port: 3000,
+
routes: {
+
"/": indexHTML,
+
},
+
development: {
+
hmr: true,
+
console: true,
+
},
+
});
+
+
console.log(`🪻 Thistle running at http://localhost:${server.port}`);
+24
src/pages/index.html
···
+
<!DOCTYPE html>
+
<html lang="en">
+
+
<head>
+
<meta charset="UTF-8">
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
+
<title>Thistle</title>
+
<link rel="icon"
+
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>">
+
<link rel="stylesheet" href="../styles/main.css">
+
</head>
+
+
<body>
+
<main>
+
<h1>Thistle</h1>
+
<p>Here is a basic counter to figure out the basics of web components</p>
+
+
<counter-component></counter-component>
+
</main>
+
+
<script type="module" src="../components/counter.ts"></script>
+
</body>
+
+
</html>
+46
src/styles/main.css
···
+
:root {
+
--text: #5b6971;
+
--background: #fefbf1;
+
--primary: #8fa668;
+
--secondary: #d0cdf9;
+
--accent: #e59976;
+
}
+
+
body {
+
font-family: 'Charter', 'Bitstream Charter', 'Sitka Text', Cambria, serif;
+
font-weight: 400;
+
margin: 0;
+
padding: 2rem;
+
line-height: 1.6;
+
background: var(--background);
+
color: var(--text);
+
}
+
+
h1, h2, h3, h4, h5 {
+
font-family: 'Charter', 'Bitstream Charter', 'Sitka Text', Cambria, serif;
+
font-weight: 600;
+
line-height: 1.2;
+
color: var(--text);
+
}
+
+
html {font-size: 100%;} /* 16px */
+
+
h1 {
+
font-size: 4.210rem; /* 67.36px */
+
margin-top: 0;
+
}
+
+
h2 {font-size: 3.158rem; /* 50.56px */}
+
+
h3 {font-size: 2.369rem; /* 37.92px */}
+
+
h4 {font-size: 1.777rem; /* 28.48px */}
+
+
h5 {font-size: 1.333rem; /* 21.28px */}
+
+
small {font-size: 0.750rem; /* 12px */}
+
+
main {
+
max-width: 800px;
+
margin: 0 auto;
+
}
+33
tsconfig.json
···
+
{
+
"compilerOptions": {
+
// Environment setup & latest features
+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
+
"target": "ESNext",
+
"module": "Preserve",
+
"moduleDetection": "force",
+
"jsx": "preserve",
+
"allowJs": true,
+
+
// Bundler mode
+
"moduleResolution": "bundler",
+
"allowImportingTsExtensions": true,
+
"verbatimModuleSyntax": true,
+
"noEmit": true,
+
+
// Decorators
+
"experimentalDecorators": true,
+
"useDefineForClassFields": false,
+
+
// Best practices
+
"strict": true,
+
"skipLibCheck": true,
+
"noFallthroughCasesInSwitch": true,
+
"noUncheckedIndexedAccess": true,
+
"noImplicitOverride": true,
+
+
// Some stricter flags (disabled by default)
+
"noUnusedLocals": false,
+
"noUnusedParameters": false,
+
"noPropertyAccessFromIndexSignature": false
+
}
+
}