Compare changes

Choose any two refs to compare.

+50
.tangled/workflows/deploy.yml
···
···
+
when:
+
- event: ['push']
+
branch: ['main']
+
- event: ['manual']
+
+
engine: 'nixery'
+
+
clone:
+
skip: false
+
depth: 1
+
+
dependencies:
+
nixpkgs:
+
- coreutils
+
- curl
+
- nodejs
+
- glibc
+
github:NixOS/nixpkgs/nixpkgs-unstable:
+
- bun
+
+
+
environment:
+
SITE_PATH: 'dist' # Copy entire repo
+
SITE_NAME: 'meow'
+
WISP_HANDLE: 'nekomimi.pet'
+
+
steps:
+
- build:
+
command: |
+
export PATH="$HOME/.nix-profile/bin:$PATH"
+
+
rm -rf bun.lock
+
bun install @oven/bun-linux-x64
+
bun install
+
+
bun run build
+
- name: deploy assets to wisp
+
command: |
+
# Download Wisp CLI
+
curl https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-x86_64-linux -o wisp-cli
+
+
chmod +x wisp-cli
+
+
# Deploy to Wisp
+
./wisp-cli \
+
"$WISP_HANDLE" \
+
--path "$SITE_PATH" \
+
--site "$SITE_NAME" \
+
--password "$WISP_APP_PASSWORD"
+
-184
CRUSH.md
···
-
# CRUSH.md
-
-
## Project Overview
-
-
This is a personal portfolio website built with Bun, React, TypeScript, and Tailwind CSS. It uses the shadcn/ui component library and serves as both a portfolio and development environment for AT Protocol-related projects. The project demonstrates modern web development practices with a focus on decentralized technologies.
-
-
## Development Commands
-
-
### Core Commands
-
- `bun install` - Install dependencies
-
- `bun dev` - Start development server with hot reload and HMR
-
- `bun start` - Run production server
-
- `bun run build.ts` - Build for production (outputs to `dist/`)
-
- `bun run build.ts --help` - Show all build options
-
-
### Build System
-
The custom build script (`build.ts`) supports various options:
-
- `--outdir <path>` - Output directory (default: "dist")
-
- `--minify` - Enable minification
-
- `--sourcemap <type>` - Sourcemap type (none|linked|inline|external)
-
- `--external <list>` - External packages (comma separated)
-
-
The build automatically:
-
- Processes all HTML files in `src/` as entrypoints
-
- Copies `public/` folder to dist
-
- Uses Tailwind plugin for CSS processing
-
- Includes linked sourcemaps by default
-
-
## Architecture
-
-
### Project Structure
-
```
-
src/
-
โ”œโ”€โ”€ components/
-
โ”‚ โ”œโ”€โ”€ ui/ # shadcn/ui components (Button, Card, Input, etc.)
-
โ”‚ โ”œโ”€โ”€ sections/ # Main page sections (Header, Work, Connect)
-
โ”‚ โ””โ”€โ”€ ... # Other React components
-
โ”œโ”€โ”€ data/
-
โ”‚ โ””โ”€โ”€ portfolio.ts # Portfolio content and metadata
-
โ”œโ”€โ”€ hooks/ # Custom React hooks
-
โ”œโ”€โ”€ lib/ # Utility functions
-
โ””โ”€โ”€ styles/ # Global CSS and Tailwind config
-
```
-
-
### Server Architecture
-
- Uses Bun's built-in server (`src/index.ts`)
-
- Serves React SPA with API routes
-
- API routes use pattern matching (`/api/hello/:name`)
-
- CORS headers configured for cross-origin requests
-
- Development mode includes HMR and browser console echoing
-
-
### Key Files
-
- `src/index.ts` - Server entry point with API routes
-
- `src/App.tsx` - Main React component with intersection observer animations
-
- `src/data/portfolio.ts` - All portfolio content (personal info, work experience, skills)
-
- `build.ts` - Custom build script with extensive CLI options
-
- `styles/globals.css` - Tailwind imports, CSS variables, and custom animations
-
-
## Code Conventions
-
-
### TypeScript Configuration
-
- Strict mode enabled with `noUncheckedIndexedAccess`
-
- Path aliases: `@/*` maps to `./src/*`
-
- JSX: `react-jsx` transform
-
- Module resolution: `bundler` mode
-
- Target: `ESNext` with DOM libraries
-
-
### Component Patterns
-
- Uses shadcn/ui component library with `class-variance-authority`
-
- Utility function `cn()` combines `clsx` and `tailwind-merge`
-
- Components follow Radix UI patterns for accessibility
-
- File exports: Named exports for components, default for main App
-
-
### Styling
-
- Tailwind CSS v4 with custom CSS variables
-
- Dark theme by default with light mode support
-
- Glassmorphism effects with custom utilities
-
- Custom animations: `fade-in-up`, `bounce-slow`
-
- Fira Code monospace font throughout
-
-
### Import Aliases (from components.json)
-
```typescript
-
"@/components" โ†’ "./src/components"
-
"@/lib/utils" โ†’ "./src/lib/utils"
-
"@/components/ui" โ†’ "./src/components/ui"
-
"@/lib" โ†’ "./src/lib"
-
"@/hooks" โ†’ "./src/hooks"
-
```
-
-
## UI Components
-
-
### shadcn/ui Integration
-
The project uses shadcn/ui with:
-
- Style variant: "new-york"
-
- Base color: "neutral"
-
- Icon library: Lucide React
-
- CSS variables enabled
-
- Custom CSS location: `styles/globals.css`
-
-
### Available UI Components
-
- Button (multiple variants: default, destructive, outline, secondary, ghost, link)
-
- Card
-
- Input
-
- Label
-
- Select
-
- Textarea
-
-
### Custom Components
-
- ThemeToggle (dark/light mode switching)
-
- SectionNav (navigation between portfolio sections)
-
- ProjectCard/WorkExperienceCard (portfolio item displays)
-
- SocialLink (social media links with icons)
-
-
## Content Management
-
-
Portfolio data is centralized in `src/data/portfolio.ts`:
-
- `personalInfo` - Name, title, description, availability, contact
-
- `currentRole` - Current employment status
-
- `skills` - Array of technical skills
-
- `workExperience` - Array of work history with projects
-
- `socialLinks` - Social media profiles
-
- `sections` - Page section identifiers
-
-
The description format supports rich text with bold styling and URLs:
-
```typescript
-
type DescriptionPart = {
-
text: string
-
bold?: boolean
-
url?: string
-
}
-
```
-
-
## Deployment
-
-
### Netlify Configuration
-
- Static site hosting
-
- CORS headers configured in `public/netlify.toml`
-
- AT Protocol DID file at `public/.well-known/atproto-did`
-
-
### Build Output
-
- Production builds output to `dist/`
-
- All HTML files in `src/` become entrypoints
-
- Public assets copied automatically
-
- Source maps linked for debugging
-
-
## Development Notes
-
-
### Hot Module Replacement
-
- Development server includes HMR
-
- Browser console logs echoed to server
-
- Automatic reloading on file changes
-
-
### Performance Features
-
- Intersection Observer for scroll-triggered animations
-
- Code splitting support in build configuration
-
- Minification enabled by default in production
-
- Lazy loading with `react` imports
-
-
### AT Protocol Integration
-
- Project showcases AT Protocol-related work
-
- Uses `atproto-ui` component library
-
- Bluesky and Tangled integration in portfolio
-
-
## Gotchas
-
-
### Build System
-
- Custom build script requires Bun runtime (not Node.js)
-
- HTML files in `src/` automatically become entrypoints
-
- Must use `--external` flag for libraries that shouldn't be bundled
-
-
### Styling
-
- Dark mode is default styling approach
-
- CSS variables are used extensively for theming
-
- Custom glassmorphism effects require SVG filters (defined in CSS)
-
-
### Server Routes
-
- API routes use Bun's pattern matching syntax
-
- All unmatched routes serve the main SPA (catch-all route)
-
- CORS headers pre-configured for API access
-
-
### Content Structure
-
- Portfolio content is TypeScript data, not markdown
-
- Rich text descriptions use specific object structure
-
- Projects support multiple links (live demo, GitHub, etc.)
···
+1 -21
README.md
···
-
# bun-react-tailwind-shadcn-template
-
-
To install dependencies:
-
-
```bash
-
bun install
-
```
-
-
To start a development server:
-
-
```bash
-
bun dev
-
```
-
-
To run for production:
-
-
```bash
-
bun start
-
```
-
-
This project was created using `bun init` in bun v1.3.1. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
···
+
https://nekomimi.pet
+2
bun-env.d.ts
···
// Generated by `bun init`
declare module "*.svg" {
/**
* A path to the SVG file
···
// Generated by `bun init`
+
/// <reference path="src/guestbook.d.ts" />
+
declare module "*.svg" {
/**
* A path to the SVG file
+72 -26
bun.lock
···
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "bun-react-template",
···
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.3",
-
"atproto-ui": "^0.7.2",
"bun-plugin-tailwind": "^0.1.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.545.0",
"react": "^19",
"react-dom": "^19",
···
},
},
"packages": {
-
"@atcute/atproto": ["@atcute/atproto@3.1.8", "", { "dependencies": { "@atcute/lexicons": "^1.2.2" } }, "sha512-Miu+S7RSgAYbmQWtHJKfSFUN5Kliqoo4YH0rILPmBtfmlZieORJgXNj9oO/Uive0/ulWkiRse07ATIcK8JxMnw=="],
-
"@atcute/bluesky": ["@atcute/bluesky@3.2.8", "", { "dependencies": { "@atcute/atproto": "^3.1.8", "@atcute/lexicons": "^1.2.2" } }, "sha512-wxEnSOvX7nLH4sVzX9YFCkaNEWIDrTv3pTs6/x4NgJ3AJ3XJio0OYPM8tR7wAgsklY6BHvlAgt3yoCDK0cl1CA=="],
"@atcute/client": ["@atcute/client@4.0.5", "", { "dependencies": { "@atcute/identity": "^1.1.1", "@atcute/lexicons": "^1.2.2" } }, "sha512-R8Qen8goGmEkynYGg2m6XFlVmz0GTDvQ+9w+4QqOob+XMk8/WDpF4aImev7WKEde/rV2gjcqW7zM8E6W9NShDA=="],
···
"@atcute/lexicons": ["@atcute/lexicons@1.2.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-bgEhJq5Z70/0TbK5sx+tAkrR8FsCODNiL2gUEvS5PuJfPxmFmRYNWaMGehxSPaXWpU2+Oa9ckceHiYbrItDTkA=="],
"@atcute/tangled": ["@atcute/tangled@1.0.10", "", { "dependencies": { "@atcute/atproto": "^3.1.8", "@atcute/lexicons": "^1.2.2" } }, "sha512-DGconZIN5TpLBah+aHGbWI1tMsL7XzyVEbr/fW4CbcLWYKICU6SAUZ0YnZ+5GvltjlORWHUy7hfftvoh4zodIA=="],
"@atcute/util-fetch": ["@atcute/util-fetch@1.0.3", "", { "dependencies": { "@badrap/valita": "^0.4.6" } }, "sha512-f8zzTb/xlKIwv2OQ31DhShPUNCmIIleX6p7qIXwWwEUjX6x8skUtpdISSjnImq01LXpltGV5y8yhV4/Mlb7CRQ=="],
"@badrap/valita": ["@badrap/valita@0.4.6", "", {}, "sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg=="],
···
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
-
"@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7Rap1BHNWqgnexc4wLjjdZeVRQKtk534iGuJ7qZ42i/q1B+cxJZ6zSnrFsYmo+zreH7dUyUXL3AHuXGrl2772Q=="],
-
"@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-wpqmgT/8w+tEr5YMGt1u1sEAMRHhyA2SKZddC6GCPasHxSqkCWOPQvYIHIApnTsoSsxhxP0x6Cpe93+4c7hq/w=="],
-
"@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-mJo715WvwEHmJ6khNymWyxi0QrFzU94wolsUmxolViNHrk+2ugzIkVIJhTnxf7pHnarxxHwyJ/kgatuV//QILQ=="],
-
"@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-ACn038SZL8del+sFnqCjf+haGB02//j2Ez491IMmPTvbv4a/D0iiNz9xiIB3ICbQd3EwQzi+Ut/om3Ba/KoHbQ=="],
-
"@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gKU3Wv3BTG5VMjqMMnRwqU6tipCveE9oyYNt62efy6cQK3Vo1DOBwY2SmjbFw+yzj+Um20YoFOLGxghfQET4Ng=="],
-
"@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-cAUeM3I5CIYlu5Ur52eCOGg9yfqibQd4lzt9G1/rA0ajqcnCBaTuekhUDZETJJf5H9QV+Gm46CqQg2DpdJzJsw=="],
-
"@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-7+2aCrL81mtltZQbKdiPB58UL+Gr3DAIuPyUAKm0Ib/KG/Z8t7nD/eSMRY/q6b+NsAjYnVPiPwqSjC3edpMmmQ=="],
-
"@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-8AgEAHyuJ5Jm9MUo1L53K1SRYu0bNGqV0E0L5rB5DjkteO4GXrnWGBT8qsuwuy7WMuCMY3bj64/pFjlRkZuiXw=="],
-
"@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-tP0WWcAqrMayvkggOHBGBoyyoK+QHAqgRUyj1F6x5/udiqc9vCXmIt1tlydxYV/NvyvUAmJ7MWT0af44Xm2kJw=="],
-
"@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.1", "", { "os": "win32", "cpu": "x64" }, "sha512-xdUjOZRq6PwPbbz4/F2QEMLBZwintGp7AS50cWxgkHnyp7Omz5eJfV6/vWtN4qwZIyR3V3DT/2oXsY1+7p3rtg=="],
-
"@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.1", "", { "os": "win32", "cpu": "x64" }, "sha512-dcA+Kj7hGFrY3G8NWyYf3Lj3/GMViknpttWUf5pI6p6RphltZaoDu0lY5Lr71PkMdRZTwL2NnZopa/x/NWCdKA=="],
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
···
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
-
"@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="],
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="],
"@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="],
-
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="],
-
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
···
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
-
"@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.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg=="],
-
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
-
"@types/react-dom": ["@types/react-dom@19.2.2", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw=="],
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
-
"atproto-ui": ["atproto-ui@0.7.2", "", { "dependencies": { "@atcute/atproto": "^3.1.7", "@atcute/bluesky": "^3.2.3", "@atcute/client": "^4.0.3", "@atcute/identity-resolver": "^1.1.3", "@atcute/tangled": "^1.0.6" }, "peerDependencies": { "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0" }, "optionalPeers": ["react-dom"] }, "sha512-bVHjur5Wh5g+47p8Zaq7iZkd5zpqw5A8xg0z5rsDWkmRvqO8E3kZbL9Svco0qWQM/jg4akG/97Vn1XecATovzg=="],
-
"bun": ["bun@1.3.1", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.1", "@oven/bun-darwin-x64": "1.3.1", "@oven/bun-darwin-x64-baseline": "1.3.1", "@oven/bun-linux-aarch64": "1.3.1", "@oven/bun-linux-aarch64-musl": "1.3.1", "@oven/bun-linux-x64": "1.3.1", "@oven/bun-linux-x64-baseline": "1.3.1", "@oven/bun-linux-x64-musl": "1.3.1", "@oven/bun-linux-x64-musl-baseline": "1.3.1", "@oven/bun-windows-x64": "1.3.1", "@oven/bun-windows-x64-baseline": "1.3.1" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-enqkEb0RhNOgDzHQwv7uvnIhX3uSzmKzz779dL7kdH8SauyTdQvCz4O1UT2rU0UldQp2K9OlrJNdyDHayPEIvw=="],
"bun-plugin-tailwind": ["bun-plugin-tailwind@0.1.2", "", { "peerDependencies": { "bun": ">=1.0.0" } }, "sha512-41jNC1tZRSK3s1o7pTNrLuQG8kL/0vR/JgiTmZAJ1eHwe0w5j6HFPKeqEk0WAD13jfrUC7+ULuewFBBCoADPpg=="],
-
"bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="],
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
···
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
···
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
"lucide-react": ["lucide-react@0.545.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-7r1/yUuflQDSt4f1bpn5ZAocyIxcTyVyBBChSVtBKn5M+392cPmI5YJMWOJKk/HUWGm5wg83chlAZtCcGbEZtw=="],
"react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="],
···
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
-
"tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="],
-
"tailwindcss": ["tailwindcss@4.1.16", "", {}, "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
···
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
}
}
···
{
"lockfileVersion": 1,
+
"configVersion": 1,
"workspaces": {
"": {
"name": "bun-react-template",
···
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.3",
+
"atproto-ui": "0.11.3",
"bun-plugin-tailwind": "^0.1.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+
"cutebook": "0.1.1",
"lucide-react": "^0.545.0",
"react": "^19",
"react-dom": "^19",
···
},
},
"packages": {
+
"@atcute/atproto": ["@atcute/atproto@3.1.9", "", { "dependencies": { "@atcute/lexicons": "^1.2.2" } }, "sha512-DyWwHCTdR4hY2BPNbLXgVmm7lI+fceOwWbE4LXbGvbvVtSn+ejSVFaAv01Ra3kWDha0whsOmbJL8JP0QPpf1+w=="],
+
"@atcute/bluesky": ["@atcute/bluesky@3.2.10", "", { "dependencies": { "@atcute/atproto": "^3.1.9", "@atcute/lexicons": "^1.2.2" } }, "sha512-qwQWTzRf3umnh2u41gdU+xWYkbzGlKDupc3zeOB+YjmuP1N9wEaUhwS8H7vgrqr0xC9SGNDjeUVcjC4m5BPLBg=="],
"@atcute/client": ["@atcute/client@4.0.5", "", { "dependencies": { "@atcute/identity": "^1.1.1", "@atcute/lexicons": "^1.2.2" } }, "sha512-R8Qen8goGmEkynYGg2m6XFlVmz0GTDvQ+9w+4QqOob+XMk8/WDpF4aImev7WKEde/rV2gjcqW7zM8E6W9NShDA=="],
···
"@atcute/lexicons": ["@atcute/lexicons@1.2.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-bgEhJq5Z70/0TbK5sx+tAkrR8FsCODNiL2gUEvS5PuJfPxmFmRYNWaMGehxSPaXWpU2+Oa9ckceHiYbrItDTkA=="],
+
"@atcute/multibase": ["@atcute/multibase@1.1.6", "", { "dependencies": { "@atcute/uint8array": "^1.0.5" } }, "sha512-HBxuCgYLKPPxETV0Rot4VP9e24vKl8JdzGCZOVsDaOXJgbRZoRIF67Lp0H/OgnJeH/Xpva8Z5ReoTNJE5dn3kg=="],
+
+
"@atcute/oauth-browser-client": ["@atcute/oauth-browser-client@2.0.1", "", { "dependencies": { "@atcute/client": "^4.0.5", "@atcute/identity": "^1.1.1", "@atcute/identity-resolver": "^1.1.4", "@atcute/lexicons": "^1.2.2", "@atcute/multibase": "^1.1.6", "@atcute/uint8array": "^1.0.5", "nanoid": "^5.1.5" } }, "sha512-lG021GkeORG06zfFf4bH85egObjBEKHNgAWHvbtY/E2dX4wxo88hf370pJDx8acdnuUJLJ2VKPikJtZwo4Heeg=="],
+
"@atcute/tangled": ["@atcute/tangled@1.0.10", "", { "dependencies": { "@atcute/atproto": "^3.1.8", "@atcute/lexicons": "^1.2.2" } }, "sha512-DGconZIN5TpLBah+aHGbWI1tMsL7XzyVEbr/fW4CbcLWYKICU6SAUZ0YnZ+5GvltjlORWHUy7hfftvoh4zodIA=="],
+
"@atcute/uint8array": ["@atcute/uint8array@1.0.5", "", {}, "sha512-XLWWxoR2HNl2qU+FCr0rp1APwJXci7HnzbOQLxK55OaMNBXZ19+xNC5ii4QCsThsDxa4JS/JTzuiQLziITWf2Q=="],
+
"@atcute/util-fetch": ["@atcute/util-fetch@1.0.3", "", { "dependencies": { "@badrap/valita": "^0.4.6" } }, "sha512-f8zzTb/xlKIwv2OQ31DhShPUNCmIIleX6p7qIXwWwEUjX6x8skUtpdISSjnImq01LXpltGV5y8yhV4/Mlb7CRQ=="],
"@badrap/valita": ["@badrap/valita@0.4.6", "", {}, "sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg=="],
···
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
+
"@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-licBDIbbLP5L5/S0+bwtJynso94XD3KyqSP48K59Sq7Mude6C7dR5ZujZm4Ut4BwZqUFfNOfYNMWBU5nlL7t1A=="],
+
"@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-hn8lLzsYyyh6ULo2E8v2SqtrWOkdQKJwapeVy1rDw7juTTeHY3KDudGWf4mVYteC9riZU6HD88Fn3nGwyX0eIg=="],
+
"@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-UHxdtbyxdtNJUNcXtIrjx3Lmq8ji3KywlXtIHV/0vn9A8W5mulqOcryqUWMFVH9JTIIzmNn6Q/qVmXHTME63Ww=="],
+
"@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5uZzxzvHU/z+3cZwN/A0H8G+enQ+9FkeJVZkE2fwK2XhiJZFUGAuWajCpy7GepvOWlqV7VjPaKi2+Qmr4IX7nQ=="],
+
"@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-OD9DYkjes7WXieBn4zQZGXWhRVZhIEWMDGCetZ3H4vxIuweZ++iul/CNX5jdpNXaJ17myb1ROMvmRbrqW44j3w=="],
+
"@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-EoEuRP9bxAxVKuvi6tZ0ZENjueP4lvjz0mKsMzdG0kwg/2apGKiirH1l0RIcdmvfDGGuDmNiv/XBpkoXq1x8ug=="],
+
"@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-m9Ov9YH8KjRLui87eNtQQFKVnjGsNk3xgbrR9c8d2FS3NfZSxmVjSeBvEsDjzNf1TXLDriHb/NYOlpiMf/QzDg=="],
+
"@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-3TuOsRVoG8K+soQWRo+Cp5ACpRs6rTFSu5tAqc/6WrqwbNWmqjov/eWJPTgz3gPXnC7uNKVG7RxxAmV8r2EYTQ=="],
+
"@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-q8Hto8hcpofPJjvuvjuwyYvhOaAzPw1F5vRUUeOJDmDwZ4lZhANFM0rUwchMzfWUJCD6jg8/EVQ8MiixnZWU0A=="],
+
"@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-nZJUa5NprPYQ4Ii4cMwtP9PzlJJTp1XhxJ+A9eSn1Jfr6YygVWyN2KLjenyI93IcuBouBAaepDAVZZjH2lFBhg=="],
+
"@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-s00T99MjB+xLOWq+t+wVaVBrry+oBOZNiTJijt+bmkp/MJptYS3FGvs7a+nkjLNzoNDoWQcXgKew6AaHES37Bg=="],
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
···
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
+
"@radix-ui/react-label": ["@radix-ui/react-label@2.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A=="],
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="],
"@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="],
+
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
"@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="],
+
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
···
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
+
"@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="],
+
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
+
"@types/react": ["@types/react@19.2.3", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-k5dJVszUiNr1DSe8Cs+knKR6IrqhqdhpUwzqhkS8ecQTSf3THNtbfIp/umqHMpX2bv+9dkx3fwDv/86LcSfvSg=="],
+
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
+
+
"actor-typeahead": ["actor-typeahead@0.1.2", "", {}, "sha512-I97YqqNl7Kar0J/bIJvgY/KmHpssHcDElhfwVTLP7wRFlkxso2ZLBqiS2zol5A8UVUJbQK2JXYaqNpZXz8Uk2A=="],
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
+
"atproto-ui": ["atproto-ui@0.11.3", "", { "dependencies": { "@atcute/atproto": "^3.1.7", "@atcute/bluesky": "^3.2.3", "@atcute/client": "^4.0.3", "@atcute/identity-resolver": "^1.1.3", "@atcute/tangled": "^1.0.10" }, "peerDependencies": { "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0" }, "optionalPeers": ["react-dom"] }, "sha512-NIBsORuo9lpCpr1SNKcKhNvqOVpsEy9IoHqFe1CM9gNTArpQL1hUcoP1Cou9a1O5qzCul9kaiu5xBHnB81I/WQ=="],
+
"bun": ["bun@1.3.2", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.2", "@oven/bun-darwin-x64": "1.3.2", "@oven/bun-darwin-x64-baseline": "1.3.2", "@oven/bun-linux-aarch64": "1.3.2", "@oven/bun-linux-aarch64-musl": "1.3.2", "@oven/bun-linux-x64": "1.3.2", "@oven/bun-linux-x64-baseline": "1.3.2", "@oven/bun-linux-x64-musl": "1.3.2", "@oven/bun-linux-x64-musl-baseline": "1.3.2", "@oven/bun-windows-x64": "1.3.2", "@oven/bun-windows-x64-baseline": "1.3.2" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-x75mPJiEfhO1j4Tfc65+PtW6ZyrAB6yTZInydnjDZXF9u9PRAnr6OK3v0Q9dpDl0dxRHkXlYvJ8tteJxc8t4Sw=="],
"bun-plugin-tailwind": ["bun-plugin-tailwind@0.1.2", "", { "peerDependencies": { "bun": ">=1.0.0" } }, "sha512-41jNC1tZRSK3s1o7pTNrLuQG8kL/0vR/JgiTmZAJ1eHwe0w5j6HFPKeqEk0WAD13jfrUC7+ULuewFBBCoADPpg=="],
+
"bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg=="],
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
···
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
+
"cutebook": ["cutebook@0.1.1", "", { "dependencies": { "actor-typeahead": "^0.1.2" }, "peerDependencies": { "@atcute/client": "^4.0.0", "@atcute/identity-resolver": "^1.0.0", "@atcute/oauth-browser-client": "^2.0.0" } }, "sha512-Wh4fpQUFwVnmKnLA8MOnNRbPstYv2EeC8KG1d9P6MMzupjMP2GRaDnixzg1ADvH2wBuVcpGDbGm4zyhN+h3D8w=="],
+
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
···
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
"lucide-react": ["lucide-react@0.545.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-7r1/yUuflQDSt4f1bpn5ZAocyIxcTyVyBBChSVtBKn5M+392cPmI5YJMWOJKk/HUWGm5wg83chlAZtCcGbEZtw=="],
+
+
"nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="],
"react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="],
···
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
+
"tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="],
+
"tailwindcss": ["tailwindcss@4.1.17", "", {}, "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
···
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
+
+
"@radix-ui/react-arrow/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+
"@radix-ui/react-collection/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+
"@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+
"@radix-ui/react-dismissable-layer/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+
"@radix-ui/react-focus-scope/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+
"@radix-ui/react-popper/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+
"@radix-ui/react-portal/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+
"@radix-ui/react-select/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+
"@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+
"@radix-ui/react-visually-hidden/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+
"@radix-ui/react-arrow/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+
"@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+
"@radix-ui/react-focus-scope/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+
"@radix-ui/react-popper/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+
"@radix-ui/react-portal/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+
"@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
}
}
+2 -1
package.json
···
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.3",
-
"atproto-ui": "^0.7.2",
"bun-plugin-tailwind": "^0.1.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.545.0",
"react": "^19",
"react-dom": "^19",
···
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.3",
+
"atproto-ui": "0.11.3",
"bun-plugin-tailwind": "^0.1.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+
"cutebook": "0.1.1",
"lucide-react": "^0.545.0",
"react": "^19",
"react-dom": "^19",
+13
public/client-metadata.json
···
···
+
{
+
"client_id": "https://nekomimi.pet/client-metadata.json",
+
"client_name": "nekomimi.pet",
+
"client_uri": "https://nekomimi.pet",
+
"redirect_uris": ["https://nekomimi.pet/guestbook"],
+
"scope": "atproto transition:generic",
+
"grant_types": ["authorization_code", "refresh_token"],
+
"response_types": ["code"],
+
"token_endpoint_auth_method": "none",
+
"application_type": "web",
+
"dpop_bound_access_tokens": true
+
}
+
+41 -3
src/App.tsx
···
import { Header } from "./components/sections/Header"
import { Work } from "./components/sections/Work"
import { Connect } from "./components/sections/Connect"
import { sections } from "./data/portfolio"
export function App() {
const [activeSection, setActiveSection] = useState("")
const sectionsRef = useRef<(HTMLElement | null)[]>([])
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
···
})
return () => observer.disconnect()
-
}, [])
-
return (
<div className="min-h-screen dark:bg-background text-foreground relative">
···
<main>
<div className="max-w-4xl mx-auto px-6 sm:px-8 lg:px-16">
-
<Header sectionRef={(el) => (sectionsRef.current[0] = el)} />
</div>
<Work sectionRef={(el) => (sectionsRef.current[1] = el)} />
<Connect sectionRef={(el) => (sectionsRef.current[2] = el)} />
···
import { Header } from "./components/sections/Header"
import { Work } from "./components/sections/Work"
import { Connect } from "./components/sections/Connect"
+
import { GuestbookPage } from "./components/sections/GuestbookPage"
import { sections } from "./data/portfolio"
export function App() {
const [activeSection, setActiveSection] = useState("")
+
const [currentPath, setCurrentPath] = useState(window.location.pathname)
const sectionsRef = useRef<(HTMLElement | null)[]>([])
+
// Handle SPA navigation
useEffect(() => {
+
const handlePopState = () => setCurrentPath(window.location.pathname)
+
window.addEventListener('popstate', handlePopState)
+
return () => window.removeEventListener('popstate', handlePopState)
+
}, [])
+
+
useEffect(() => {
+
if (currentPath === '/guestbook') return // Skip observer on guestbook page
+
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
···
})
return () => observer.disconnect()
+
}, [currentPath])
+
// Guestbook page
+
if (currentPath === '/guestbook') {
+
return (
+
<div className="min-h-screen dark:bg-background text-foreground relative">
+
<div className="fixed top-6 left-6 z-50">
+
<button
+
onClick={() => {
+
window.history.pushState({}, '', '/')
+
setCurrentPath('/')
+
}}
+
className="px-4 py-2 rounded-full text-sm font-medium bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200 shadow-md hover:shadow-lg border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 transition-all flex items-center gap-2"
+
>
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
+
<path strokeLinecap="round" strokeLinejoin="round" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
+
</svg>
+
Back
+
</button>
+
</div>
+
<GuestbookPage />
+
</div>
+
)
+
}
return (
<div className="min-h-screen dark:bg-background text-foreground relative">
···
<main>
<div className="max-w-4xl mx-auto px-6 sm:px-8 lg:px-16">
+
<Header
+
sectionRef={(el) => (sectionsRef.current[0] = el)}
+
onGuestbookClick={() => {
+
window.history.pushState({}, '', '/guestbook')
+
setCurrentPath('/guestbook')
+
}}
+
/>
</div>
<Work sectionRef={(el) => (sectionsRef.current[1] = el)} />
<Connect sectionRef={(el) => (sectionsRef.current[2] = el)} />
+252
src/components/GuestbookEntries.tsx
···
···
+
import { useEffect, useState } from "react"
+
+
interface GuestbookEntry {
+
uri: string
+
author: string
+
authorHandle?: string
+
message: string
+
createdAt: string
+
}
+
+
interface ConstellationRecord {
+
did: string
+
collection: string
+
rkey: string
+
}
+
+
const COLORS = [
+
'#dc2626', // red
+
'#0d9488', // teal
+
'#059669', // emerald
+
'#84cc16', // lime
+
'#ec4899', // pink
+
'#3b82f6', // blue
+
'#8b5cf6', // violet
+
]
+
+
function getColorForIndex(index: number): string {
+
return COLORS[index % COLORS.length]!
+
}
+
+
interface GuestbookEntriesProps {
+
did: string
+
limit?: number
+
onRefresh?: (refresh: () => void) => void
+
}
+
+
export function GuestbookEntries({ did, limit = 50, onRefresh }: GuestbookEntriesProps) {
+
const [entries, setEntries] = useState<GuestbookEntry[]>([])
+
const [loading, setLoading] = useState(true)
+
const [error, setError] = useState<string | null>(null)
+
+
const fetchEntries = async (signal: AbortSignal) => {
+
setLoading(true)
+
setError(null)
+
+
try {
+
const url = new URL('/xrpc/blue.microcosm.links.getBacklinks', 'https://constellation.microcosm.blue')
+
url.searchParams.set('subject', did)
+
url.searchParams.set('source', 'pet.nkp.guestbook.sign:subject')
+
url.searchParams.set('limit', limit.toString())
+
+
const response = await fetch(url.toString(), { signal })
+
if (!response.ok) throw new Error('Failed to fetch signatures')
+
+
const data = await response.json()
+
+
if (!data.records || !Array.isArray(data.records)) {
+
setEntries([])
+
setLoading(false)
+
return
+
}
+
+
// Collect all entries first, then render once
+
const entryPromises = (data.records as ConstellationRecord[]).map(async (record) => {
+
try {
+
const recordUrl = new URL('/xrpc/com.atproto.repo.getRecord', 'https://slingshot.wisp.place')
+
recordUrl.searchParams.set('repo', record.did)
+
recordUrl.searchParams.set('collection', record.collection)
+
recordUrl.searchParams.set('rkey', record.rkey)
+
+
const recordResponse = await fetch(recordUrl.toString(), { signal })
+
if (!recordResponse.ok) return null
+
+
const recordData = await recordResponse.json()
+
+
if (
+
recordData.value &&
+
recordData.value.$type === 'pet.nkp.guestbook.sign' &&
+
typeof recordData.value.message === 'string'
+
) {
+
return {
+
uri: recordData.uri,
+
author: record.did,
+
authorHandle: undefined,
+
message: recordData.value.message,
+
createdAt: recordData.value.createdAt,
+
} as GuestbookEntry
+
}
+
} catch (err) {
+
if (err instanceof Error && err.name === 'AbortError') throw err
+
}
+
return null
+
})
+
+
const results = await Promise.all(entryPromises)
+
const validEntries = results.filter((e): e is GuestbookEntry => e !== null)
+
+
// Sort once and set all entries at once
+
validEntries.sort((a, b) =>
+
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
+
)
+
+
setEntries(validEntries)
+
setLoading(false)
+
+
// Batch fetch profiles asynchronously
+
if (validEntries.length > 0) {
+
const uniqueDids = Array.from(new Set(validEntries.map(e => e.author)))
+
+
// Batch fetch profiles up to 25 at a time (API limit)
+
const profilePromises = []
+
for (let i = 0; i < uniqueDids.length; i += 25) {
+
const batch = uniqueDids.slice(i, i + 25)
+
+
const profileUrl = new URL('/xrpc/app.bsky.actor.getProfiles', 'https://public.api.bsky.app')
+
batch.forEach(d => profileUrl.searchParams.append('actors', d))
+
+
profilePromises.push(
+
fetch(profileUrl.toString(), { signal })
+
.then(profileResponse => profileResponse.ok ? profileResponse.json() : null)
+
.then(profilesData => {
+
if (profilesData?.profiles && Array.isArray(profilesData.profiles)) {
+
const handles = new Map<string, string>()
+
profilesData.profiles.forEach((profile: any) => {
+
if (profile.handle) {
+
handles.set(profile.did, profile.handle)
+
}
+
})
+
return handles
+
}
+
return new Map<string, string>()
+
})
+
.catch((err) => {
+
if (err instanceof Error && err.name === 'AbortError') throw err
+
return new Map<string, string>()
+
})
+
)
+
}
+
+
// Wait for all profile batches, then update once
+
const handleMaps = await Promise.all(profilePromises)
+
const allHandles = new Map<string, string>()
+
handleMaps.forEach(map => {
+
map.forEach((handle, did) => allHandles.set(did, handle))
+
})
+
+
if (allHandles.size > 0) {
+
setEntries(prev => prev.map(entry => {
+
const handle = allHandles.get(entry.author)
+
return handle ? { ...entry, authorHandle: handle } : entry
+
}))
+
}
+
}
+
} catch (err) {
+
if (err instanceof Error && err.name === 'AbortError') return
+
setError(err instanceof Error ? err.message : 'Failed to load entries')
+
setLoading(false)
+
}
+
}
+
+
useEffect(() => {
+
const abortController = new AbortController()
+
fetchEntries(abortController.signal)
+
onRefresh?.(() => {
+
abortController.abort()
+
const newController = new AbortController()
+
fetchEntries(newController.signal)
+
})
+
+
return () => abortController.abort()
+
}, [did, limit])
+
+
const formatDate = (isoString: string) => {
+
const date = new Date(isoString)
+
return date.toLocaleDateString('en-US', { month: 'long', day: 'numeric' })
+
}
+
+
const shortenDid = (did: string) => {
+
if (did.startsWith('did:')) {
+
const afterPrefix = did.indexOf(':', 4)
+
if (afterPrefix !== -1) {
+
return `${did.slice(0, afterPrefix + 9)}...`
+
}
+
}
+
return did
+
}
+
+
if (loading) {
+
return (
+
<div className="text-center py-12 text-gray-500">
+
Loading entries...
+
</div>
+
)
+
}
+
+
if (error) {
+
return (
+
<div className="text-center py-12 text-red-500">
+
{error}
+
</div>
+
)
+
}
+
+
if (entries.length === 0) {
+
return (
+
<div className="text-center py-12 text-gray-500">
+
No entries yet. Be the first to sign!
+
</div>
+
)
+
}
+
+
return (
+
<div className="space-y-4">
+
{entries.map((entry, index) => (
+
<div
+
key={entry.uri}
+
className="bg-gray-100 rounded-lg p-4 border-l-4 transition-colors"
+
style={{ borderLeftColor: getColorForIndex(index) }}
+
>
+
<div className="flex justify-between items-start mb-1">
+
<a
+
href={`https://bsky.app/profile/${entry.authorHandle || entry.author}`}
+
target="_blank"
+
rel="noopener noreferrer"
+
className="font-semibold text-gray-900 hover:underline"
+
>
+
{entry.authorHandle || shortenDid(entry.author)}
+
</a>
+
<a
+
href={`https://bsky.app/profile/${entry.authorHandle || entry.author}`}
+
target="_blank"
+
rel="noopener noreferrer"
+
className="text-gray-400 hover:text-gray-600"
+
style={{ color: getColorForIndex(index) }}
+
>
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
+
<path strokeLinecap="round" strokeLinejoin="round" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
+
</svg>
+
</a>
+
</div>
+
<p className="text-gray-800 mb-2">
+
{entry.message}
+
</p>
+
<span className="text-sm text-gray-500">
+
{formatDate(entry.createdAt)}
+
</span>
+
</div>
+
))}
+
</div>
+
)
+
}
+
+84
src/components/sections/GuestbookPage.tsx
···
···
+
/// <reference path="../../guestbook.d.ts" />
+
import { useEffect, useRef } from "react"
+
import { configureGuestbook } from "cutebook/register"
+
import { GuestbookEntries } from "../GuestbookEntries"
+
+
// Configure guestbook once
+
let configured = false
+
if (!configured) {
+
const isDev = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
+
const port = window.location.port || '3000'
+
+
// For dev, use loopback client format matching the demo
+
// Client ID uses http://localhost, redirect_uri uses 127.0.0.1
+
const scope = 'atproto transition:generic'
+
const redirectUri = isDev ? `http://127.0.0.1:${port}/guestbook` : 'https://nekomimi.pet/guestbook'
+
const clientId = isDev
+
? `http://localhost?redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}`
+
: 'https://nekomimi.pet/client-metadata.json'
+
+
configureGuestbook({
+
oauth: {
+
clientId,
+
redirectUri,
+
scope,
+
},
+
})
+
configured = true
+
}
+
+
export function GuestbookPage() {
+
const refreshRef = useRef<(() => void) | null>(null)
+
+
const handleSignCreated = () => {
+
refreshRef.current?.()
+
}
+
+
useEffect(() => {
+
const signElement = document.querySelector('guestbook-sign')
+
if (signElement) {
+
signElement.addEventListener('sign-created', handleSignCreated)
+
return () => signElement.removeEventListener('sign-created', handleSignCreated)
+
}
+
}, [])
+
+
return (
+
<div className="min-h-screen bg-gradient-to-b from-gray-50 to-gray-100 py-12 px-6">
+
<div className="max-w-xl mx-auto">
+
{/* Header */}
+
<header className="mb-12 text-center">
+
<div className="inline-block mb-4">
+
<span className="text-5xl">๐Ÿ“–</span>
+
</div>
+
<h1 className="text-3xl font-light tracking-tight text-gray-900 mb-3">
+
Ana's Guestbook
+
</h1>
+
<p className="text-gray-500 font-mono text-sm">
+
Leave a message, say hello
+
</p>
+
</header>
+
+
{/* Sign Form */}
+
<div className="mb-12 bg-white rounded-2xl shadow-sm border border-gray-200/50 p-6">
+
<guestbook-sign did="did:plc:ttdrpj45ibqunmfhdsb4zdwq"></guestbook-sign>
+
</div>
+
+
{/* Entries Header */}
+
<div className="flex items-center gap-3 mb-6">
+
<div className="h-px flex-1 bg-gradient-to-r from-transparent via-gray-300 to-transparent"></div>
+
<span className="text-xs font-mono text-gray-400 uppercase tracking-widest">
+
Messages
+
</span>
+
<div className="h-px flex-1 bg-gradient-to-r from-transparent via-gray-300 to-transparent"></div>
+
</div>
+
+
<GuestbookEntries
+
did="did:plc:ttdrpj45ibqunmfhdsb4zdwq"
+
limit={50}
+
onRefresh={(refresh) => { refreshRef.current = refresh }}
+
/>
+
</div>
+
</div>
+
)
+
}
+
+35 -24
src/components/sections/Header.tsx
···
import type { RefObject } from "react"
import { personalInfo, currentRole, skills } from "../../data/portfolio"
interface HeaderProps {
sectionRef: (el: HTMLElement | null) => void
}
-
export function Header({ sectionRef }: HeaderProps) {
const scrollToWork = () => {
document.getElementById('work')?.scrollIntoView({ behavior: 'smooth' })
}
···
<div className="absolute inset-0 bg-background/70"></div>
</div>
-
<div className="grid lg:grid-cols-5 gap-12 sm:gap-16 w-full relative z-10">
<div className="lg:col-span-3 space-y-6 sm:space-y-8">
<div className="space-y-3 sm:space-y-2">
<div className="text-sm text-gray-300 font-mono tracking-wider">PORTFOLIO / 2025</div>
···
</h1>
</div>
-
<div className="space-y-6 max-w-md">
<p className="text-lg sm:text-xl text-stone-200 leading-relaxed">
{personalInfo.description.map((part, i) => {
if (part.url) {
···
})}
</p>
<div className="space-y-4">
<div className="flex items-center gap-2 text-sm text-gray-300">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
···
Read my blog
</a>
</div>
</div>
</div>
</div>
-
<div className="lg:col-span-2 flex flex-col justify-end space-y-6 sm:space-y-8 mt-8 lg:mt-0">
<div className="space-y-4">
-
<div className="text-sm text-gray-300 font-mono">CURRENTLY</div>
-
<div className="space-y-2">
-
<div className="text-white">{currentRole.title}</div>
-
<div className="text-sm text-gray-300">{personalInfo.availability.location}</div>
-
<div className="text-gray-300">@ {currentRole.company}</div>
-
<div className="text-xs text-gray-100">{currentRole.period}</div>
-
</div>
-
</div>
-
-
<div className="space-y-4">
-
<div className="text-sm text-gray-300 font-mono">FOCUS</div>
-
<div className="flex flex-wrap gap-2">
-
{skills.map((skill) => (
-
<span
-
key={skill}
-
className="glass glass-hover px-3 py-1 text-xs rounded-full transition-colors duration-300"
-
>
-
{skill}
-
</span>
-
))}
</div>
</div>
</div>
···
import type { RefObject } from "react"
+
import { CurrentlyPlaying, LastPlayed, type AtProtoStyles } from "atproto-ui"
import { personalInfo, currentRole, skills } from "../../data/portfolio"
interface HeaderProps {
sectionRef: (el: HTMLElement | null) => void
+
onGuestbookClick?: () => void
}
+
export function Header({ sectionRef, onGuestbookClick }: HeaderProps) {
const scrollToWork = () => {
document.getElementById('work')?.scrollIntoView({ behavior: 'smooth' })
}
···
<div className="absolute inset-0 bg-background/70"></div>
</div>
+
<div className="grid lg:grid-cols-5 gap-12 sm:gap-16 w-full relative z-10 items-center">
<div className="lg:col-span-3 space-y-6 sm:space-y-8">
<div className="space-y-3 sm:space-y-2">
<div className="text-sm text-gray-300 font-mono tracking-wider">PORTFOLIO / 2025</div>
···
</h1>
</div>
+
<div className="space-y-6 max-w-md ">
<p className="text-lg sm:text-xl text-stone-200 leading-relaxed">
{personalInfo.description.map((part, i) => {
if (part.url) {
···
})}
</p>
+
<div className="space-y-4">
<div className="flex items-center gap-2 text-sm text-gray-300">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
···
Read my blog
</a>
</div>
+
<button
+
onClick={onGuestbookClick}
+
className="glass glass-hover w-full px-6 py-3 rounded-lg transition-all duration-300 inline-flex items-center justify-center gap-2 text-sm text-gray-300 hover:text-white"
+
>
+
<svg
+
className="w-4 h-4"
+
fill="none"
+
stroke="currentColor"
+
viewBox="0 0 24 24"
+
strokeWidth={2}
+
>
+
<path
+
strokeLinecap="round"
+
strokeLinejoin="round"
+
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
+
/>
+
</svg>
+
Sign my guestbook
+
</button>
</div>
</div>
</div>
+
<div className="hidden lg:flex lg:col-span-2 flex-col justify-end space-y-6 sm:space-y-8 mt-8 lg:mt-0">
<div className="space-y-4">
+
<p className="text-sm text-gray-300 font-mono">IM LISTENING TO:</p>
+
<div className="glass rounded-2xl" style={{
+
'--atproto-color-bg': 'transparent',
+
'--atproto-color-border': 'transparent',
+
'--atproto-color-bg-elevated': 'rgba(255, 255, 255, 0.20)',
+
'--atproto-color-text': 'white',
+
'--atproto-color-text-secondary': 'rgba(255, 255, 255, 0.80)',
+
} as AtProtoStyles }>
+
<CurrentlyPlaying did="nekomimi.pet"/>
</div>
</div>
</div>
+1 -1
src/components/ui/card.tsx
···
return (
<div
data-slot="card"
-
className={cn("glass glass-hover text-card-foreground flex flex-col gap-6 rounded-xl py-6", className)}
{...props}
/>
);
···
return (
<div
data-slot="card"
+
className={cn("text-card-foreground flex flex-col gap-6 rounded-xl py-6", className)}
{...props}
/>
);
+27
src/guestbook.d.ts
···
···
+
import type { GuestbookSignElement, GuestbookDisplayElement } from "cutebook"
+
import type { DetailedHTMLProps, HTMLAttributes } from "react"
+
+
declare module "react" {
+
namespace JSX {
+
interface IntrinsicElements {
+
'guestbook-sign': DetailedHTMLProps<HTMLAttributes<HTMLElement> & {
+
did?: string;
+
}, HTMLElement>;
+
'guestbook-display': DetailedHTMLProps<HTMLAttributes<HTMLElement> & {
+
did?: string;
+
limit?: string;
+
ref?: any;
+
}, HTMLElement>;
+
}
+
}
+
}
+
+
declare global {
+
interface HTMLElementTagNameMap {
+
'guestbook-sign': GuestbookSignElement;
+
'guestbook-display': GuestbookDisplayElement;
+
}
+
}
+
+
export {}
+
+2 -2
src/index.html
···
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
-
<title>Bun + React</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
···
<filter id="frosted" x="0%" y="0%" width="100%" height="100%">
<feTurbulence
type="fractalNoise"
-
baseFrequency="0.008 0.008"
numOctaves="2"
seed="92"
result="noise"
···
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+
<title>nekomimi.pet</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
···
<filter id="frosted" x="0%" y="0%" width="100%" height="100%">
<feTurbulence
type="fractalNoise"
+
baseFrequency="0.012 0.012"
numOctaves="2"
seed="92"
result="noise"
+12
src/index.ts
···
});
},
// Serve static files from public directory
"/nekomata.png": async () => {
try {
···
});
},
+
// Serve client-metadata.json for OAuth
+
"/client-metadata.json": async () => {
+
try {
+
const file = Bun.file("public/client-metadata.json");
+
return new Response(file, {
+
headers: { "Content-Type": "application/json" },
+
});
+
} catch {
+
return new Response("File not found", { status: 404 });
+
}
+
},
+
// Serve static files from public directory
"/nekomata.png": async () => {
try {
+4 -3
styles/globals.css
···
}
.animate-bounce-slow {
-
animation: bounce-slow 2s ease-in-out infinite;
}
/* Glassmorphism utilities */
···
--shadow-color: rgba(255, 255, 255, 0.7);
/* Painted glass */
-
--tint-color: rgba(255, 255, 255, 0.08);
-
--tint-opacity: 0.4;
/* Background frost */
--frost-blur: 2px;
···
box-shadow:
/*0 0 0 2px rgba(255, 255, 255, 0.7),*/
0 20px 40px rgba(0, 0, 0, 0.16);
}
}
···
}
.animate-bounce-slow {
+
animation: bounce-slow 1s ease-in-out infinite;
}
/* Glassmorphism utilities */
···
--shadow-color: rgba(255, 255, 255, 0.7);
/* Painted glass */
+
--tint-color: rgba(255, 255, 255, 0.28);
+
--tint-opacity: 1;
/* Background frost */
--frost-blur: 2px;
···
box-shadow:
/*0 0 0 2px rgba(255, 255, 255, 0.7),*/
0 20px 40px rgba(0, 0, 0, 0.16);
+
transform: scale(1.05); /* Scales to 105% of original size */
}
}