Compare commits
18
Commits
73dc856821
..
v0.0.9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fab51878f | ||
|
|
8e3abca9d7 | ||
|
|
c7e7e615c2 | ||
|
|
7fb87fbb96 | ||
|
|
aa647ee869 | ||
|
|
97bda53e9b | ||
|
|
8105f57be9 | ||
|
|
15565b7208 | ||
|
|
d1c46cdee1 | ||
|
|
d33b9c5467 | ||
|
|
df0e78d9ba | ||
|
|
cc596e632f | ||
|
|
7923514c5c | ||
|
|
7cd943cced | ||
|
|
0016e2104c | ||
|
|
752ca147ff | ||
|
|
56b57aa8de | ||
|
|
ae823b755e |
@@ -0,0 +1,43 @@
|
||||
name: Publish npm package
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: local
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
# Stage-only tokens require `npm stage publish` (npm >= 11.15.0).
|
||||
- name: Update npm
|
||||
run: npm install -g npm@^11.15.0
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Check tag matches package version
|
||||
run: |
|
||||
PACKAGE_VERSION="v$(node -p "require('./package.json').version")"
|
||||
TAG_NAME="${GITHUB_REF_NAME:-${GITHUB_REF#refs/tags/}}"
|
||||
test "$PACKAGE_VERSION" = "$TAG_NAME"
|
||||
|
||||
- name: Build
|
||||
run: bun run build
|
||||
|
||||
- name: Stage publish
|
||||
run: npm stage publish --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
@@ -1,8 +1,16 @@
|
||||
# create-glstack
|
||||
|
||||
Scaffold a fullstack application with a Go backend, SvelteKit frontend, and PostgreSQL database, connected via Connect RPC.
|
||||
Scaffold a fullstack application with a Go backend, PostgreSQL database, and your choice of web and mobile frontends, connected via Connect RPC.
|
||||
|
||||
> **Work in progress** -- additional frontend frameworks and features are planned for future releases.
|
||||
Supported web frontends:
|
||||
|
||||
- [SvelteKit](https://kit.svelte.dev/)
|
||||
- [Solid Start](https://start.solidjs.com/)
|
||||
- [Nuxt](https://nuxt.com/)
|
||||
|
||||
Supported mobile frontends:
|
||||
|
||||
- [React Native + Expo](https://expo.dev/)
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -10,29 +18,71 @@ Scaffold a fullstack application with a Go backend, SvelteKit frontend, and Post
|
||||
bun create glstack
|
||||
```
|
||||
|
||||
The CLI asks for:
|
||||
|
||||
- **Project name** -- lowercase letters, digits and dashes; used for the directory, the database and the `@<name>/rpc` package. An existing non-empty directory is never overwritten
|
||||
- **Web frontend** -- SvelteKit, Solid Start, Nuxt or none
|
||||
- **Mobile frontend** -- React Native + Expo or none
|
||||
- **Bundle identifier** (Expo only) -- reverse-DNS id used as both the iOS bundle identifier and the Android package, e.g. `com.example.app`
|
||||
- **Go package prefix** -- e.g. `github.com/you`, giving the module path `github.com/you/<name>`
|
||||
- **Install dependencies** -- runs `bun install` in the new project
|
||||
|
||||
## What you get
|
||||
|
||||
A monorepo with the following structure:
|
||||
|
||||
- **`services/api`** -- Go backend using [Cobra](https://github.com/spf13/cobra) for CLI, [pgx](https://github.com/jackc/pgx) for Postgres, and [Connect RPC](https://connectrpc.com/) for the API layer
|
||||
- **`apps/web`** -- SvelteKit frontend with [shadcn-svelte](https://shadcn-svelte.com/), TailwindCSS, [TanStack Query](https://tanstack.com/query), and [Paraglide](https://inlang.com/m/gerre34r/library-inlang-paraglideJs) for i18n
|
||||
- **`apps/web`** -- Web frontend app (SvelteKit, Solid Start or Nuxt)
|
||||
- **`apps/mobile`** -- Optional [React Native + Expo](https://expo.dev/) mobile app sharing the same RPC client
|
||||
- **`packages/proto`** -- Protobuf service definitions with [Buf](https://buf.build/) for codegen (Go + TypeScript)
|
||||
- **`packages/rpc`** -- Generated TypeScript Connect RPC client shared across frontend apps
|
||||
- **`scripts`** -- Codegen helpers, e.g. building the `packages/rpc` index from the generated client
|
||||
|
||||
### Dev environment
|
||||
|
||||
The template includes a [devenv.nix](https://devenv.sh/) configuration that sets up:
|
||||
The template includes a [devenv.nix](https://devenv.sh/) configuration for macOS and Linux that sets up:
|
||||
|
||||
- Go, TypeScript, Bun
|
||||
- Go, TypeScript, Bun, Node.js
|
||||
- PostgreSQL with an auto-provisioned database
|
||||
- File watchers for protobuf, SQL, and frontend hot reload
|
||||
- File watchers for protobuf, SQL, and frontend hot reload with proper process dependency ordering
|
||||
- [Air](https://github.com/air-verse/air) for Go live reload
|
||||
- [sqlc](https://sqlc.dev/) for type-safe SQL
|
||||
- [dbmate](https://github.com/amacneil/dbmate) for database migrations
|
||||
- With Expo: the Android SDK and an emulator, plus the iOS simulator on macOS
|
||||
|
||||
Start everything with:
|
||||
|
||||
```sh
|
||||
cd <name>
|
||||
devenv up
|
||||
```
|
||||
|
||||
### Mobile (Expo)
|
||||
|
||||
`devenv up` starts Metro as its own process, creates an Android emulator matching your CPU (arm64 or x86_64), waits up to 5 minutes for it to boot and runs the app on it. On macOS it also builds and runs the app on the iOS simulator. Both apps use the same Metro bundler, so saving a file reloads both, and one platform failing to build doesn't stop the other.
|
||||
|
||||
The app reaches the API at `127.0.0.1:8080` everywhere: the iOS simulator shares the Mac's network, and `devenv up` runs `adb reverse` for the emulator and any Android phone connected over USB. Physical iPhones aren't covered, since the API only listens on localhost.
|
||||
|
||||
The example apps poll the API every 2 seconds, so a change made on one device or in the web app shows up on the others.
|
||||
|
||||
The native `ios/` and `android/` folders are generated from `app.json` and not committed. After changing native config or upgrading Expo, delete them and they are regenerated on the next run. The same applies when a native build can't find files under `node_modules`: the folders hold absolute paths into `node_modules` from when they were generated.
|
||||
|
||||
#### iOS on macOS
|
||||
|
||||
Nix can't provide Xcode, so the iOS simulator needs a few things installed on the Mac. Entering the devenv shell checks them and lists anything missing (once they pass, at most once a day); run `ios-check` to check again. Until they are met, `devenv up` skips iOS and runs everything else. Set `SKIP_IOS_CHECK=1` to turn the check off, e.g. in `devenv.local.nix`.
|
||||
|
||||
| Requirement | How to fix |
|
||||
| --- | --- |
|
||||
| Xcode | Install it from the [App Store](https://apps.apple.com/app/xcode/id497799835) |
|
||||
| Xcode license and first-launch setup | `sudo xcodebuild -runFirstLaunch` |
|
||||
| An iOS simulator runtime | `xcodebuild -downloadPlatform iOS` |
|
||||
| An iPhone simulator | Create one in Xcode › Window › Devices and Simulators |
|
||||
|
||||
CocoaPods comes from Nix. The iOS build uses `/Applications/Xcode.app` (or the Xcode chosen with `xcode-select`) outside the Nix compiler environment, so the Command Line Tools being selected is fine.
|
||||
|
||||
### Starter example
|
||||
|
||||
The generated project includes a working Todo CRUD example wired end-to-end: protobuf schema, Go service implementation, SQL queries, and a SvelteKit UI.
|
||||
The generated project includes a working Todo CRUD example wired end-to-end: protobuf schema, Go service implementation, SQL queries, and frontend UI. When a todo is checked off and the list re-sorts, rows animate into place: with [Reanimated](https://docs.swmansion.com/react-native-reanimated/) in the mobile app and the [GSAP](https://gsap.com/) Flip plugin in the web apps.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"name": "glstack",
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^1.2.0",
|
||||
"@gregorlohaus/tdir": "^0.1.2",
|
||||
"@gregorlohaus/tdir": "^0.3.0",
|
||||
"zod": "^4.3.6",
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
"@clack/prompts": ["@clack/prompts@1.2.0", "", { "dependencies": { "@clack/core": "1.2.0", "fast-string-width": "^1.1.0", "fast-wrap-ansi": "^0.1.3", "sisteransi": "^1.0.5" } }, "sha512-4jmztR9fMqPMjz6H/UZXj0zEmE43ha1euENwkckKKel4XpSfokExPo5AiVStdHSAlHekz4d0CA/r45Ok1E4D3w=="],
|
||||
|
||||
"@gregorlohaus/tdir": ["@gregorlohaus/tdir@0.1.2", "", { "peerDependencies": { "zod": "^4" } }, "sha512-gEZg1+kwhMQQqEhMdyVBnHBrIWFMlvHt8C6sbPtY/PbbFo02odTBkpclfovIs2p4Awo0Hagzq3B6rpePGD/03w=="],
|
||||
"@gregorlohaus/tdir": ["@gregorlohaus/tdir@0.3.0", "", { "peerDependencies": { "zod": "^4" }, "bin": { "tdir": "dist/cli.js" } }, "sha512-b6l+yXdyFpPI573iuMCdVHkJjD3j+fN7LRHnHzorU0QGRh6OVrOZsiiqMA5/9v+RiAF5SDBrZckj8zp5WoqLtA=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
|
||||
|
||||
|
||||
@@ -1,20 +1,34 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import * as p from "@clack/prompts";
|
||||
import { initRenderer } from '@gregorlohaus/tdir'
|
||||
import { initRenderer, TargetExistsError } from '@gregorlohaus/tdir'
|
||||
import { z } from 'zod'
|
||||
import path from "node:path";
|
||||
import { existsSync, readdirSync, statSync } from "node:fs";
|
||||
|
||||
p.intro(`create-glstack ${process.env.GLSTACK_DEV && 'isDev'}`);
|
||||
p.intro(`create-glstack${process.env.GLSTACK_DEV === 'true' ? ' (dev)' : ''}`);
|
||||
|
||||
const defaultName = "glstack-test"
|
||||
// Java and Kotlin keywords can't be Android package segments.
|
||||
const reservedWords = new Set(["abstract", "as", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue", "default", "do", "double", "else", "enum", "extends", "false", "final", "finally", "float", "for", "fun", "goto", "if", "implements", "import", "in", "instanceof", "int", "interface", "is", "long", "native", "new", "null", "object", "package", "private", "protected", "public", "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this", "throw", "throws", "transient", "true", "try", "typealias", "typeof", "val", "var", "void", "volatile", "when", "while"])
|
||||
|
||||
const isNonEmptyPath = (target: string) =>
|
||||
existsSync(target) && (!statSync(target).isDirectory() || readdirSync(target).length > 0)
|
||||
|
||||
const project = await p.group(
|
||||
{
|
||||
name: () =>
|
||||
p.text({
|
||||
message: "What would you like to name your project?",
|
||||
placeholder: "glstack-test",
|
||||
defaultValue: "glstack-test",
|
||||
placeholder: defaultName,
|
||||
defaultValue: defaultName,
|
||||
validate: (value) => {
|
||||
const name = value || defaultName
|
||||
// Used as directory, npm scope, Go module path segment and database name.
|
||||
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
||||
return "Use lowercase letters, digits and dashes, starting with a letter"
|
||||
}
|
||||
if (isNonEmptyPath(name)) return `./${name} already exists and is not empty`
|
||||
return undefined
|
||||
},
|
||||
}),
|
||||
@@ -23,19 +37,56 @@ const project = await p.group(
|
||||
message: 'Pick a frontend framework.',
|
||||
options: [
|
||||
{ value: "svelte-kit", label: "SvelteKit" },
|
||||
{value: "solid-start", label:"SolidStart"}
|
||||
{ value: "solid-start", label: "SolidStart" },
|
||||
{ value: "nuxt", label: "Nuxt" },
|
||||
{ value: "none", label: "None" }
|
||||
]
|
||||
})
|
||||
,
|
||||
mobile: async () =>
|
||||
await p.select({
|
||||
message: 'Pick a mobile framework.',
|
||||
options: [
|
||||
{ value: "expo", label: "ReactNative + Expo" },
|
||||
{ value: "none", label: "None" }
|
||||
]
|
||||
})
|
||||
,
|
||||
bundleid: async ({ results }) => {
|
||||
if (results.mobile !== "expo") return ""
|
||||
// Valid as both an iOS bundle identifier and an Android package name.
|
||||
const nameSegment = String(results.name).replace(/[^a-z0-9]/g, "")
|
||||
const fallback = `com.${reservedWords.has(nameSegment) ? `${nameSegment}app` : nameSegment}.mobile`
|
||||
return await p.text({
|
||||
message: "What bundle identifier should the mobile app use?",
|
||||
placeholder: fallback,
|
||||
defaultValue: fallback,
|
||||
validate: (value) => {
|
||||
if (!value) return undefined
|
||||
if (!/^[a-zA-Z][a-zA-Z0-9]*(\.[a-zA-Z][a-zA-Z0-9]*)+$/.test(value)) {
|
||||
return "Use reverse-DNS segments of letters and digits, e.g. com.example.app"
|
||||
}
|
||||
const reserved = value.split(".").find((segment) => reservedWords.has(segment))
|
||||
if (reserved) return `"${reserved}" is a Java/Kotlin keyword and can't be part of an Android package`
|
||||
return undefined
|
||||
},
|
||||
})
|
||||
},
|
||||
goprefix: () =>
|
||||
p.text({
|
||||
message: "What would you like to use as a go package prefix?",
|
||||
placeholder: "github.com/glstack-test",
|
||||
defaultValue: "github.com/glstack-test",
|
||||
validate: (value) => {
|
||||
if (value && !/^[a-zA-Z0-9._~-]+(\/[a-zA-Z0-9._~-]+)*$/.test(value)) {
|
||||
return "Use a module path without scheme or trailing slash, e.g. github.com/you"
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
}),
|
||||
installDeps: async () => {
|
||||
return await p.confirm({message: "Install dependencies?", })
|
||||
}
|
||||
},
|
||||
{
|
||||
onCancel: () => {
|
||||
@@ -52,17 +103,30 @@ const render = createRenderer(z.object({
|
||||
name: z.string(),
|
||||
goprefix: z.string(),
|
||||
frontend: z.string(),
|
||||
mobile: z.string(),
|
||||
bundleid: z.string()
|
||||
})
|
||||
}))
|
||||
const destDir = path.join("./", project.name);
|
||||
render(destDir,{project})
|
||||
// TODO: template rendering — copy/generate files into destDir
|
||||
try {
|
||||
// `results` in the bundleid prompt makes group() infer it as unknown.
|
||||
render(destDir, { project: { ...project, bundleid: project.bundleid as string } }, {
|
||||
reverseMap: true,
|
||||
overwrite: false,
|
||||
// npm never publishes .gitignore/.npmrc, so the template stores them as _gitignore/_npmrc.
|
||||
rename: { _gitignore: ".gitignore", _npmrc: ".npmrc" },
|
||||
})
|
||||
} catch (error) {
|
||||
if (!(error instanceof TargetExistsError)) throw error
|
||||
p.cancel(`./${project.name} already exists and is not empty.`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const s = p.spinner();
|
||||
if (project.installDeps) {
|
||||
s.start("Installing dependencies");
|
||||
await Bun.$`bun install`.cwd(path.join(destDir)).quiet();
|
||||
await Bun.$`bun install`.cwd(path.join(destDir,'packages','rpc')).quiet();
|
||||
await Bun.$`bun install`.cwd(path.join(destDir,'apps','web')).quiet();
|
||||
s.stop("Dependencies installed.");
|
||||
}
|
||||
|
||||
p.outro("You're all set!");
|
||||
|
||||
+4
-7
@@ -1,15 +1,12 @@
|
||||
{
|
||||
"name": "create-glstack",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.9",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"create-glstack": "./dist/index.js"
|
||||
"create-glstack": "dist/index.js"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"template"
|
||||
],
|
||||
"files": ["dist", "template"],
|
||||
"scripts": {
|
||||
"build": "bun build ./index.ts --outdir ./dist --target bun",
|
||||
"prepublishOnly": "bun run build"
|
||||
@@ -22,7 +19,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^1.2.0",
|
||||
"@gregorlohaus/tdir": "^0.1.2",
|
||||
"@gregorlohaus/tdir": "^0.3.0",
|
||||
"zod": "^4.3.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,3 +10,6 @@ devenv.local.yaml
|
||||
.pre-commit-config.yaml
|
||||
node_modules
|
||||
scripts/node_modules
|
||||
|
||||
# Android emulator state
|
||||
.android
|
||||
@@ -0,0 +1,24 @@
|
||||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.data
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.fleet
|
||||
.idea
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
@@ -0,0 +1,83 @@
|
||||
<script setup lang="ts">
|
||||
import { gsap } from "gsap"
|
||||
import { Flip } from "gsap/Flip"
|
||||
import { createRouter, type Todo } from "@<@var(context.project.name)>/rpc"
|
||||
|
||||
gsap.registerPlugin(Flip)
|
||||
const router = createRouter("http://127.0.0.1:8080")
|
||||
// Changes made elsewhere (another tab, the mobile app) show up within this interval.
|
||||
const syncIntervalMs = 2000
|
||||
const todos = ref<Todo[]>([])
|
||||
const offline = ref(false)
|
||||
const todoToCreateTask = ref("")
|
||||
|
||||
// Like the mobile app: rows spring into their new place when the list re-sorts, new rows fade in.
|
||||
const showTodos = async (next: Todo[]) => {
|
||||
const rows = Flip.getState(".todo-row")
|
||||
todos.value = next
|
||||
await nextTick()
|
||||
Flip.from(rows, {
|
||||
targets: ".todo-row",
|
||||
duration: 0.5,
|
||||
ease: "back.out(1.4)",
|
||||
onEnter: (entering) => gsap.fromTo(entering, { opacity: 0 }, { opacity: 1, duration: 0.3 }),
|
||||
})
|
||||
}
|
||||
|
||||
const fetchTodos = () => {
|
||||
router.todos.listTodos({}).then((r) => {
|
||||
offline.value = false
|
||||
// Polling returns the same list most of the time; skip re-rendering it.
|
||||
if (JSON.stringify(todos.value) !== JSON.stringify(r.todos)) showTodos(r.todos)
|
||||
}).catch(() => offline.value = true)
|
||||
}
|
||||
let interval: ReturnType<typeof setInterval> | undefined
|
||||
onMounted(() => {
|
||||
fetchTodos()
|
||||
interval = setInterval(fetchTodos, syncIntervalMs)
|
||||
})
|
||||
onUnmounted(() => clearInterval(interval))
|
||||
|
||||
const setTodoDone = (todo: Todo, event: Event) => {
|
||||
const done = (event.target as HTMLInputElement).checked
|
||||
router.todos.updateTodo({ todo: { id: todo.id, task: todo.task, done } }).then(fetchTodos, () => offline.value = true)
|
||||
}
|
||||
|
||||
const deleteTodo = async (todo: Todo, event: Event) => {
|
||||
await gsap.to((event.currentTarget as HTMLElement).closest(".todo-row"), { opacity: 0, duration: 0.3 })
|
||||
router.todos.deleteTodo({ todo: { id: todo.id } }).then(fetchTodos, () => offline.value = true)
|
||||
}
|
||||
|
||||
const createTodo = () => {
|
||||
const task = todoToCreateTask.value.trim()
|
||||
if (!task) return
|
||||
router.todos.createTodo({ todo: { id: crypto.randomUUID(), task } }).then(() => {
|
||||
todoToCreateTask.value = ""
|
||||
fetchTodos()
|
||||
}, () => offline.value = true)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<h1>Create Todo</h1>
|
||||
<p v-if="offline" class="offline">Can't reach the API, retrying…</p>
|
||||
<form @submit.prevent="createTodo">
|
||||
<input v-model="todoToCreateTask" type="text" placeholder="What needs to be done?">
|
||||
<button>create Todo</button>
|
||||
</form>
|
||||
<div v-for="todo in todos" :key="todo.id" class="todo-row" :data-flip-id="todo.id">
|
||||
<span>{{ todo.task }}</span>
|
||||
<input type="checkbox" :checked="todo.done" @change="setTodoDone(todo, $event)">
|
||||
<button @click="deleteTodo(todo, $event)">delete</button>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
main { max-width: 32rem; margin: 2rem auto; font-family: system-ui, sans-serif; }
|
||||
form, .todo-row { display: flex; gap: 0.75rem; align-items: center; margin-bottom: 0.75rem; }
|
||||
input[type="text"] { flex: 1; padding: 0.5rem; border: 1px solid #8e8e93; border-radius: 8px; }
|
||||
.todo-row span { flex: 1; }
|
||||
.offline { color: #d32f2f; }
|
||||
</style>
|
||||
@@ -0,0 +1,7 @@
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: '2025-07-15',
|
||||
devtools: { enabled: true },
|
||||
// Same port as the other web templates, which the API allows via CORS.
|
||||
devServer: { port: 5173 },
|
||||
})
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "web",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare",
|
||||
"check": "nuxt typecheck"
|
||||
},
|
||||
"dependencies": {
|
||||
"@<@var(context.project.name)>/rpc": "workspace:*",
|
||||
"gsap": "^3.15.0",
|
||||
"nuxt": "^4.6.0",
|
||||
"vue": "^3.5.43",
|
||||
"vue-router": "^5.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^6.0.3",
|
||||
"vue-tsc": "^3.3.12"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,2 @@
|
||||
User-Agent: *
|
||||
Disallow:
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.server.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.shared.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
node_modules
|
||||
@@ -1,32 +0,0 @@
|
||||
# SolidStart
|
||||
|
||||
Everything you need to build a Solid project, powered by [`solid-start`](https://start.solidjs.com);
|
||||
|
||||
## Creating a project
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npm init solid@latest
|
||||
|
||||
# create a new project in my-app
|
||||
npm init solid@latest my-app
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
Solid apps are built with _presets_, which optimise your project for deployment to different environments.
|
||||
|
||||
By default, `npm run build` will generate a Node app that you can run with `npm start`. To use a different preset, add it to the `devDependencies` in `package.json` and specify in your `app.config.js`.
|
||||
|
||||
## This project was created with the [Solid CLI](https://github.com/solidjs-community/solid-cli)
|
||||
@@ -1,28 +1,25 @@
|
||||
{
|
||||
"name": "example-basic",
|
||||
"name": "web",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"start": "vite start",
|
||||
"preview": "vite preview"
|
||||
"start": "node .output/server/index.mjs",
|
||||
"preview": "vite preview",
|
||||
"check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@<@var(context.project.name)>/rpc": "workspace:*",
|
||||
"@solidjs/meta": "^0.29.4",
|
||||
"@solidjs/router": "^0.15.0",
|
||||
"@solidjs/start": "2.0.0-alpha.2",
|
||||
"@solidjs/vite-plugin-nitro-2": "^0.1.0",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@tanstack/query-db-collection": "^1.0.35",
|
||||
"@tanstack/solid-db": "^0.2.18",
|
||||
"@tanstack/solid-query": "^5.96.2",
|
||||
"solid-js": "^1.9.5",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"vite": "^7.0.0"
|
||||
"@solidjs/start": "^2.0.0",
|
||||
"solid-js": "^1.9.14",
|
||||
"vite": "^8.0.0",
|
||||
"nitro": "^3.0.260610-beta",
|
||||
"gsap": "^3.15.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
"node": ">=24"
|
||||
},
|
||||
"devDependencies": {}
|
||||
"devDependencies": {
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
body {
|
||||
font-family: Gordita, Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||
}
|
||||
main { max-width: 32rem; margin: 2rem auto; font-family: system-ui, sans-serif; }
|
||||
form, .todo-row { display: flex; gap: 0.75rem; align-items: center; margin-bottom: 0.75rem; }
|
||||
input[type="text"] { flex: 1; padding: 0.5rem; border: 1px solid #8e8e93; border-radius: 8px; }
|
||||
.todo-row span { flex: 1; }
|
||||
.offline { color: #d32f2f; }
|
||||
|
||||
@@ -1,58 +1,84 @@
|
||||
import { MetaProvider, Title } from "@solidjs/meta";
|
||||
import { Router } from "@solidjs/router";
|
||||
import { FileRoutes } from "@solidjs/start/router";
|
||||
import { Suspense } from "solid-js";
|
||||
import { createCollection } from '@tanstack/solid-db'
|
||||
import { queryCollectionOptions } from '@tanstack/query-db-collection'
|
||||
import type { Todo, ExtractPayload } from '@<@var(context.project.name)>/rpc'
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
|
||||
import { getRouter } from "./lib/getRouter"
|
||||
import { createSignal, For, onCleanup, onMount, Show } from "solid-js";
|
||||
import { gsap } from "gsap";
|
||||
import { Flip } from "gsap/Flip";
|
||||
import { createRouter, Todo } from "@<@var(context.project.name)>/rpc"
|
||||
import "./app.css";
|
||||
import { TodoCollectionProvider } from "./context/todocollection/provider";
|
||||
const queryClient = new QueryClient({
|
||||
|
||||
})
|
||||
const router = getRouter();
|
||||
gsap.registerPlugin(Flip)
|
||||
const router = createRouter("http://127.0.0.1:8080")
|
||||
// Changes made elsewhere (another tab, the mobile app) show up within this interval.
|
||||
const syncIntervalMs = 2000
|
||||
|
||||
export default function App() {
|
||||
const todosCollection = createCollection(
|
||||
queryCollectionOptions({
|
||||
queryKey: ["todos"],
|
||||
queryFn: async () => {
|
||||
const todos = await router.todos.listTodos({})
|
||||
return todos.todos as ExtractPayload<Todo>[];
|
||||
},
|
||||
queryClient,
|
||||
getKey: (item) => item.id ? item.id : crypto.randomUUID(),
|
||||
onInsert: async ({ transaction }) => {
|
||||
Promise.all(transaction.mutations.map((m) => {
|
||||
router.todos.createTodo({ todo: m.modified })
|
||||
}))
|
||||
},
|
||||
onDelete: async ({ transaction }) => {
|
||||
Promise.all(transaction.mutations.map((m) => {
|
||||
router.todos.deleteTodo({ todo: m.modified })
|
||||
}))
|
||||
},
|
||||
onUpdate: async ({ transaction }) => {
|
||||
Promise.all(transaction.mutations.map((m) => {
|
||||
router.todos.updateTodo({ todo: m.modified })
|
||||
}))
|
||||
}
|
||||
const [todos, setTodos] = createSignal<Todo[]>([]);
|
||||
const [offline, setOffline] = createSignal(false);
|
||||
const [todoToCreateTask, setTodoToCreateTask] = createSignal<string>("");
|
||||
|
||||
// Like the mobile app: rows spring into their new place when the list re-sorts, new rows fade in.
|
||||
// Solid recreates rows for new objects; data-flip-id lets Flip match them to the old ones.
|
||||
const showTodos = (next: Todo[]) => {
|
||||
const rows = Flip.getState(".todo-row")
|
||||
setTodos(next)
|
||||
Flip.from(rows, {
|
||||
targets: ".todo-row",
|
||||
duration: 0.5,
|
||||
ease: "back.out(1.4)",
|
||||
onEnter: (entering) => gsap.fromTo(entering, { opacity: 0 }, { opacity: 1, duration: 0.3 }),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const fetchTodos = () => {
|
||||
router.todos.listTodos({}).then((r) => {
|
||||
setOffline(false)
|
||||
// Polling returns the same list most of the time; skip re-rendering it.
|
||||
if (JSON.stringify(todos()) !== JSON.stringify(r.todos)) showTodos(r.todos)
|
||||
}).catch(() => setOffline(true))
|
||||
}
|
||||
onMount(() => {
|
||||
fetchTodos()
|
||||
const interval = setInterval(fetchTodos, syncIntervalMs)
|
||||
onCleanup(() => clearInterval(interval))
|
||||
})
|
||||
|
||||
const setTodoDone = (id: string, task: string) => {
|
||||
return (event: Event & { currentTarget: HTMLInputElement }) => {
|
||||
router.todos.updateTodo({ todo: { id, task, done: event.currentTarget.checked } }).then(fetchTodos, () => setOffline(true))
|
||||
}
|
||||
}
|
||||
|
||||
const deleteTodo = (id: string) => {
|
||||
return async (event: Event & { currentTarget: HTMLElement }) => {
|
||||
await gsap.to(event.currentTarget.closest(".todo-row"), { opacity: 0, duration: 0.3 })
|
||||
router.todos.deleteTodo({ todo: { id } }).then(fetchTodos, () => setOffline(true))
|
||||
}
|
||||
}
|
||||
|
||||
const createTodo = () => {
|
||||
const task = todoToCreateTask().trim()
|
||||
if (!task) return
|
||||
router.todos.createTodo({ todo: { id: crypto.randomUUID(), task } }).then(() => {
|
||||
setTodoToCreateTask("")
|
||||
fetchTodos()
|
||||
}, () => setOffline(true))
|
||||
}
|
||||
|
||||
return (
|
||||
<Router
|
||||
root={props => (
|
||||
<MetaProvider>
|
||||
<QueryClientProvider client={queryClient} >
|
||||
<TodoCollectionProvider value={todosCollection}>
|
||||
<Suspense>{props.children}</Suspense>
|
||||
</TodoCollectionProvider>
|
||||
</QueryClientProvider>
|
||||
</MetaProvider>
|
||||
)}
|
||||
>
|
||||
<FileRoutes/>
|
||||
</Router>
|
||||
<main>
|
||||
<h1>Create Todo</h1>
|
||||
<Show when={offline()}><p class="offline">Can't reach the API, retrying…</p></Show>
|
||||
<form onSubmit={(event) => { event.preventDefault(); createTodo() }}>
|
||||
<input value={todoToCreateTask()} onInput={(e) => setTodoToCreateTask(e.currentTarget.value)} type="text" placeholder="What needs to be done?"/>
|
||||
<button>create Todo</button>
|
||||
</form>
|
||||
<For each={todos()}>
|
||||
{(todo: Todo) =>
|
||||
<div class="todo-row" data-flip-id={todo.id}>
|
||||
<span>{todo.task}</span>
|
||||
<input type="checkbox" checked={todo.done} onChange={setTodoDone(todo.id || "", todo.task)}/>
|
||||
<button onClick={deleteTodo(todo.id || "")}>delete</button>
|
||||
</div>
|
||||
}
|
||||
</For>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
import { ExtractPayload, type Todo } from '@<@var(context.project.name)>/rpc'
|
||||
import { createSignal } from 'solid-js';
|
||||
import { useTodoCollection } from '~/context/todocollection/create';
|
||||
|
||||
export const CreateTodo = () => {
|
||||
const [todoState, setTodoState] = createSignal<ExtractPayload<Todo>>({
|
||||
id: crypto.randomUUID(),
|
||||
task: "",
|
||||
done: false,
|
||||
})
|
||||
|
||||
let inputRef!: HTMLInputElement
|
||||
|
||||
const setTask = () => {
|
||||
const todo = {...todoState()}
|
||||
todo.task = inputRef.value;
|
||||
setTodoState(todo)
|
||||
}
|
||||
|
||||
const todoCollection = useTodoCollection()
|
||||
|
||||
const insertTodo = () => {
|
||||
todoCollection.insert(todoState())
|
||||
setTodoState({
|
||||
id: crypto.randomUUID(),
|
||||
task: "",
|
||||
done: false
|
||||
})
|
||||
inputRef.value = "";
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<input class="border rounded-md px-3 py-1" ref={inputRef} type='text' onInput={setTask} />
|
||||
<button class="bg-teal-800 text-teal-100 p-1 rounded-md cursor-pointer" onClick={insertTodo}> create </button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
import { useLiveQuery } from "@tanstack/solid-db";
|
||||
import { useTodoCollection } from "~/context/todocollection/create";
|
||||
import { Todo } from './Todo'
|
||||
import { For } from 'solid-js'
|
||||
|
||||
export const ListTodos = () => {
|
||||
const todoCollection = useTodoCollection()
|
||||
const data = useLiveQuery((q) =>
|
||||
q.from({todos: todoCollection})
|
||||
)
|
||||
const todos = () => {
|
||||
const items = Array.from(data.state.values());
|
||||
return items.sort((a: any, b: any) => {
|
||||
if (a.done != b.done) return a.done ? -1 : 1
|
||||
if (!a.done && !b.done) {
|
||||
const adate = a.createdAt ? new Date(a.createdAt) : new Date()
|
||||
const bdate = b.createdAt ? new Date(b.createdAt) : new Date()
|
||||
return bdate.getTime() - adate.getTime()
|
||||
}
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<For each={todos()} fallback={<div>Loading...</div>}>
|
||||
{(item) => <Todo todo={item} />}
|
||||
</For>
|
||||
)
|
||||
}
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
import { ExtractPayload, type Todo as RpcTodo } from '@<@var(context.project.name)>/rpc'
|
||||
import { createSignal, type JSX } from 'solid-js';
|
||||
import { useTodoCollection } from '~/context/todocollection/create';
|
||||
|
||||
export const Todo = ({ todo }: { todo: ExtractPayload<RpcTodo> }) => {
|
||||
const [todoState, setTodoState] = createSignal(todo)
|
||||
const todoCollection = useTodoCollection()
|
||||
let commitUpdateTimeoutId: NodeJS.Timeout|null = null;
|
||||
const updateTask: JSX.EventHandlerUnion<HTMLInputElement, Event> = (e) => {
|
||||
if (commitUpdateTimeoutId != null) {
|
||||
clearTimeout(commitUpdateTimeoutId)
|
||||
}
|
||||
const todo = { ...todoState() };
|
||||
todo.task = e.currentTarget.value
|
||||
setTodoState(todo)
|
||||
commitUpdateTimeoutId = setTimeout(() => {
|
||||
todoCollection.update(todoState().id, (draft) => {
|
||||
draft.task = todoState().task
|
||||
})
|
||||
},3000)
|
||||
|
||||
}
|
||||
const updateDone: JSX.EventHandlerUnion<HTMLInputElement, Event> = (e) => {
|
||||
const todo = { ...todoState() }
|
||||
todo.done = e.currentTarget.checked
|
||||
setTodoState(todo)
|
||||
todoCollection.update(todoState().id, (draft) => {
|
||||
draft.done = todoState().done
|
||||
})
|
||||
}
|
||||
const del = () => {
|
||||
todoCollection.delete(todoState().id || "")
|
||||
}
|
||||
return (
|
||||
<div class="flex flex-col items-center border rounded-md p-5 gap-2">
|
||||
<input class="text-center border rounded-md px-3 py-1" oninput={updateTask} type="text" value={todoState().task} />
|
||||
<span> {(new Date(todoState().createdAt || "")).toLocaleString()}</span>
|
||||
<span> {(new Date(todoState().updatesAt || "")).toLocaleString()}</span>
|
||||
<input type="checkbox" onchange={updateDone} checked={todoState().done} />
|
||||
<button class="bg-amber-800 text-amber-100 p-1 rounded-md cursor-pointer" onclick={del} class=""> delete </button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
import { createContext,useContext } from 'solid-js'
|
||||
import { type CollectionImpl, type CollectionLike } from '@tanstack/solid-db'
|
||||
import type { ExtractPayload, Todo } from '@<@var(context.project.name)>/rpc';
|
||||
export type TodoCollection = CollectionImpl<ExtractPayload<Todo>>
|
||||
export const TodoCollectionContext = createContext<TodoCollection>()
|
||||
export const useTodoCollection = () => {
|
||||
const col = useContext(TodoCollectionContext)
|
||||
if (col === undefined) {
|
||||
throw new Error("Todo collection has not been initialized")
|
||||
}
|
||||
return col;
|
||||
}
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
import { JSXElement } from 'solid-js';
|
||||
import { TodoCollection, TodoCollectionContext } from './create'
|
||||
|
||||
export const TodoCollectionProvider = (props : {children:JSXElement, value: TodoCollection}) => {
|
||||
return (
|
||||
<TodoCollectionContext.Provider value={props.value}> {props.children} </TodoCollectionContext.Provider>
|
||||
)
|
||||
}
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
import { createRouter } from "@<@var(context.project.name)>/rpc"
|
||||
|
||||
const router = createRouter("http://127.0.0.1:8080")
|
||||
|
||||
export const getRouter = () => {
|
||||
return router;
|
||||
}
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
import { Title } from "@solidjs/meta";
|
||||
import { HttpStatusCode } from "@solidjs/start";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<main>
|
||||
<Title>Not Found</Title>
|
||||
<HttpStatusCode code={404} />
|
||||
<h1>Page Not Found</h1>
|
||||
<p>
|
||||
Visit{" "}
|
||||
<a href="https://start.solidjs.com" target="_blank">
|
||||
start.solidjs.com
|
||||
</a>{" "}
|
||||
to learn how to build SolidStart apps.
|
||||
</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
import { Title } from "@solidjs/meta";
|
||||
import { CreateTodo } from "~/components/CreateTodo";
|
||||
import { ListTodos } from "~/components/ListTodos"
|
||||
export default function Home() {
|
||||
return (
|
||||
<main class="flex flex-col items-center gap-2 py-5">
|
||||
<Title>Todos</Title>
|
||||
<h1 class="text-5xl"> Todos </h1>
|
||||
<CreateTodo/>
|
||||
<ListTodos/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -10,10 +10,15 @@
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"types": ["vite/client"],
|
||||
"types": [
|
||||
"vite/client"
|
||||
],
|
||||
"isolatedModules": true,
|
||||
"paths": {
|
||||
"~/*": ["./src/*"]
|
||||
}
|
||||
"~/*": [
|
||||
"./src/*"
|
||||
]
|
||||
},
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { defineConfig } from "vite";
|
||||
import { nitroV2Plugin as nitro } from "@solidjs/vite-plugin-nitro-2";
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { nitro } from "nitro/vite";
|
||||
|
||||
import { solidStart } from "@solidjs/start/config";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
solidStart(),
|
||||
nitro(),
|
||||
tailwindcss()
|
||||
]
|
||||
plugins: [solidStart(),
|
||||
nitro()
|
||||
],
|
||||
// Same port as the other web templates, which the API allows via CORS.
|
||||
server: { port: 5173 },
|
||||
// SolidStart's dev toolbar imports this CommonJS package in the browser, which fails unless prebundled.
|
||||
optimizeDeps: { include: ["@solidjs/start > @jridgewell/trace-mapping"] },
|
||||
});
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
node_modules
|
||||
|
||||
# Output
|
||||
.output
|
||||
.vercel
|
||||
.netlify
|
||||
.wrangler
|
||||
/.svelte-kit
|
||||
/build
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Env
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.test
|
||||
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
# Paraglide
|
||||
src/lib/paraglide
|
||||
project.inlang/cache/
|
||||
@@ -1,9 +0,0 @@
|
||||
# Package Managers
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
bun.lock
|
||||
bun.lockb
|
||||
|
||||
# Miscellaneous
|
||||
/static/
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.svelte",
|
||||
"options": {
|
||||
"parser": "svelte"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tailwindStylesheet": "./src/routes/layout.css"
|
||||
}
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"recommendations": ["svelte.svelte-vscode", "esbenp.prettier-vscode", "bradlc.vscode-tailwindcss"]
|
||||
"recommendations": ["svelte.svelte-vscode"]
|
||||
}
|
||||
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"files.associations": {
|
||||
"*.css": "tailwindcss"
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
# sv
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
||||
|
||||
## Creating a project
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```sh
|
||||
# create a new project
|
||||
npx sv create my-app
|
||||
```
|
||||
|
||||
To recreate this project with the same configuration:
|
||||
|
||||
```sh
|
||||
# recreate this project
|
||||
bun x sv@0.14.0 create --template minimal --types ts --add prettier tailwindcss="plugins:typography" paraglide="languageTags:en, de-De+demo:no" --install bun web
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
||||
-3
@@ -21,6 +21,3 @@ Thumbs.db
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
# Paraglide
|
||||
src/lib/paraglide
|
||||
project.inlang/cache/
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||
"tailwind": {
|
||||
"css": "src/routes/layout.css",
|
||||
"baseColor": "mist"
|
||||
},
|
||||
"aliases": {
|
||||
"components": "$lib/components",
|
||||
"utils": "$lib/utils",
|
||||
"ui": "$lib/components/ui",
|
||||
"hooks": "$lib/hooks",
|
||||
"lib": "$lib"
|
||||
},
|
||||
"typescript": true,
|
||||
"registry": "https://shadcn-svelte.com/registry",
|
||||
"style": "lyra",
|
||||
"iconLibrary": "lucide",
|
||||
"menuColor": "default",
|
||||
"menuAccent": "subtle"
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"hello_world": "Hello, {name} from de-de!"
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"hello_world": "Hello, {name} from en!"
|
||||
}
|
||||
@@ -9,51 +9,23 @@
|
||||
"preview": "vite preview",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check .",
|
||||
"format": "prettier --write ."
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fontsource-variable/roboto": "^5.2.10",
|
||||
"@inlang/paraglide-js": "^2.10.0",
|
||||
"@internationalized/date": "^3.12.0",
|
||||
"@lucide/svelte": "^0.577.0",
|
||||
"@sveltejs/adapter-auto": "^7.0.0",
|
||||
"@sveltejs/kit": "^2.50.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tanstack/table-core": "^8.21.3",
|
||||
"bits-ui": "^2.16.3",
|
||||
"clsx": "^2.1.1",
|
||||
"embla-carousel-svelte": "^8.6.0",
|
||||
"formsnap": "^2.0.1",
|
||||
"layerchart": "2.0.0-next.48",
|
||||
"mode-watcher": "^1.1.0",
|
||||
"paneforge": "^1.0.2",
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-svelte": "^3.4.1",
|
||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||
"shadcn-svelte": "^1.2.7",
|
||||
"svelte": "^5.54.0",
|
||||
"svelte-check": "^4.4.2",
|
||||
"svelte-sonner": "^1.1.0",
|
||||
"sveltekit-superforms": "^2.30.0",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwind-variants": "^3.2.2",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vaul-svelte": "^1.0.0-next.7",
|
||||
"vite": "^7.3.1"
|
||||
"@sveltejs/adapter-auto": "^8.0.0",
|
||||
"@sveltejs/kit": "^3.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^7.2.0",
|
||||
"svelte": "^5.57.1",
|
||||
"svelte-check": "^4.6.0",
|
||||
"typescript": "^6.0.3",
|
||||
"vite": "^8.3.0"
|
||||
},
|
||||
"imports": {
|
||||
"#lib": "./src/lib/index.js",
|
||||
"#lib/*": "./src/lib/*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^2.11.0",
|
||||
"@connectrpc/connect": "^2.1.1",
|
||||
"@connectrpc/connect-web": "^2.1.1",
|
||||
"@<@var(context.project.name)>/rpc": "workspace:*",
|
||||
"@tanstack/query-db-collection": "^1.0.33",
|
||||
"@tanstack/svelte-db": "^0.1.79",
|
||||
"@tanstack/svelte-query": "^6.1.13"
|
||||
"gsap": "^3.15.0"
|
||||
}
|
||||
}
|
||||
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/project-settings",
|
||||
"modules": [
|
||||
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js",
|
||||
"https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js"
|
||||
],
|
||||
"plugin.inlang.messageFormat": {
|
||||
"pathPattern": "./messages/{locale}.json"
|
||||
},
|
||||
"baseLocale": "en",
|
||||
"locales": ["en", "de-de"]
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -1,15 +1,11 @@
|
||||
<!doctype html>
|
||||
|
||||
<html lang="%paraglide.lang%" dir="%paraglide.dir%">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<meta name="text-scale" content="scale" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { Handle } from '@sveltejs/kit';
|
||||
import { getTextDirection } from '$lib/paraglide/runtime';
|
||||
import { paraglideMiddleware } from '$lib/paraglide/server';
|
||||
|
||||
const handleParaglide: Handle = ({ event, resolve }) =>
|
||||
paraglideMiddleware(event.request, ({ request, locale }) => {
|
||||
event.request = request;
|
||||
|
||||
return resolve(event, {
|
||||
transformPageChunk: ({ html }) =>
|
||||
html
|
||||
.replace('%paraglide.lang%', locale)
|
||||
.replace('%paraglide.dir%', getTextDirection(locale))
|
||||
});
|
||||
});
|
||||
|
||||
export const handle: Handle = handleParaglide;
|
||||
@@ -1,4 +0,0 @@
|
||||
import type { Reroute } from '@sveltejs/kit';
|
||||
import { deLocalizeUrl } from '$lib/paraglide/runtime';
|
||||
|
||||
export const reroute: Reroute = (request) => deLocalizeUrl(request.url).pathname;
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { Todo, ExtractPayload } from "@<@var(context.project.name)>/rpc";
|
||||
import Input from "$lib/components/ui/input/input.svelte";
|
||||
import Button from "$lib/components/ui/button/button.svelte"
|
||||
import * as Field from "$lib/components/ui/field/index"
|
||||
import { getTodoCollection } from "$lib/todocollectionscontext";
|
||||
const todoCollection = getTodoCollection();
|
||||
let todo = $state<ExtractPayload<Todo>>({id: crypto.randomUUID() ,task: "",done:false})
|
||||
let create = () => {
|
||||
todoCollection.insert(todo)
|
||||
todo = { id: crypto.randomUUID(), task: "", done:false }
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<Field.FieldGroup class="w-50">
|
||||
<Field.Field>
|
||||
<Field.Label>
|
||||
Todo
|
||||
</Field.Label>
|
||||
<Input bind:value={todo.task}/>
|
||||
</Field.Field>
|
||||
<Button variant="outline" onclick={create} > Create </Button>
|
||||
</Field.FieldGroup>
|
||||
</div>
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { getTodoCollection } from '$lib/todocollectionscontext';
|
||||
import { useLiveQuery } from '@tanstack/svelte-db';
|
||||
import { tick } from 'svelte';
|
||||
import { ScrollArea } from '../ui/scroll-area/index';
|
||||
import type { Todo as TodoRpc } from '@<@var(context.project.name)>/rpc';
|
||||
import { browser } from '$app/environment';
|
||||
import Todo from './Todo.svelte';
|
||||
|
||||
let { initialTodos }: { initialTodos: TodoRpc[] } = $props();
|
||||
const todoCollection = getTodoCollection();
|
||||
const data = useLiveQuery((q) => q.from({ todos: todoCollection }));
|
||||
const todos = $derived.by(() => {
|
||||
const res = data.data.toSorted((a, b) => {
|
||||
if (a.done != b.done) {
|
||||
return a.done ? -1 : 1;
|
||||
}
|
||||
if (!a.done && !b.done) {
|
||||
let adate = a.createdAt ? new Date(a.createdAt) : new Date(Date.now());
|
||||
let bdate = b.createdAt ? new Date(b.createdAt) : new Date(Date.now());
|
||||
return bdate.getTime() - adate.getTime();
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
return res;
|
||||
});
|
||||
let scrollerRef = $state<HTMLElement | null>(null);
|
||||
$effect(() => {
|
||||
todos;
|
||||
tick().then(() => {
|
||||
scrollerRef?.scrollTo({ top: scrollerRef.scrollHeight, behavior: 'smooth' });
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<ScrollArea
|
||||
bind:viewportRef={scrollerRef}
|
||||
id="scroller"
|
||||
class="flex h-[80%] w-full flex-col items-center justify-center lg:w-[30%]"
|
||||
>
|
||||
{#if browser}
|
||||
{#each todos as todo (todo.id)}
|
||||
<Todo {todo} />
|
||||
{/each}
|
||||
{:else}
|
||||
{#each initialTodos as todo}
|
||||
<Todo {todo} />
|
||||
{/each}
|
||||
{/if}
|
||||
</ScrollArea>
|
||||
-69
@@ -1,69 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { Todo, ExtractPayload } from "@<@var(context.project.name)>/rpc";
|
||||
import { getTodoCollection } from "$lib/todocollectionscontext";
|
||||
import * as Card from "../ui/card/index";
|
||||
import * as Field from "../ui/field/index";
|
||||
import Input from "../ui/input/input.svelte";
|
||||
import Checkbox from "../ui/checkbox/checkbox.svelte";
|
||||
import Button from "../ui/button/button.svelte";
|
||||
import { DeleteIcon } from "@lucide/svelte"
|
||||
let { todo } : { todo:ExtractPayload<Todo> } = $props();
|
||||
let todoState = $state(todo)
|
||||
const todoCollection = getTodoCollection();
|
||||
const update = () => {
|
||||
todoCollection.update(todoState.id,(draft) => {
|
||||
draft.done = todoState.done;
|
||||
draft.task = todoState.task;
|
||||
})
|
||||
}
|
||||
const del = () => {
|
||||
if (todoState.id) {
|
||||
todoCollection.delete(todoState.id)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card.Card class="p-5" onkeydown={()=>update()}>
|
||||
<Card.CardContent>
|
||||
<Field.FieldSet>
|
||||
<Field.Field>
|
||||
<Field.Label>
|
||||
task
|
||||
</Field.Label>
|
||||
<Field.FieldContent>
|
||||
<Input bind:value={todoState.task}/>
|
||||
</Field.FieldContent>
|
||||
</Field.Field>
|
||||
<Field.Field>
|
||||
<Field.Label>
|
||||
done
|
||||
</Field.Label>
|
||||
<Field.FieldContent onclick={()=>update()}>
|
||||
<Checkbox bind:checked={todoState.done}/>
|
||||
</Field.FieldContent>
|
||||
</Field.Field>
|
||||
<div class="flex flex-row">
|
||||
<Field.Field>
|
||||
<Field.Label>
|
||||
Created
|
||||
</Field.Label>
|
||||
<Field.FieldContent>
|
||||
{(new Date(todoState.createdAt||"now")).toLocaleString()}
|
||||
</Field.FieldContent>
|
||||
</Field.Field>
|
||||
<Field.Field>
|
||||
<Field.Label>
|
||||
Updated
|
||||
</Field.Label>
|
||||
<Field.FieldContent>
|
||||
{(new Date(todoState.updatesAt||"now")).toLocaleString()}
|
||||
</Field.FieldContent>
|
||||
</Field.Field>
|
||||
<Button variant='destructive' onclick={del}>
|
||||
<DeleteIcon/>
|
||||
</Button>
|
||||
</div>
|
||||
</Field.FieldSet>
|
||||
</Card.CardContent>
|
||||
</Card.Card>
|
||||
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Accordion as AccordionPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChild<AccordionPrimitive.ContentProps> = $props();
|
||||
</script>
|
||||
|
||||
<AccordionPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="accordion-content"
|
||||
class="data-open:animate-accordion-down data-closed:animate-accordion-up text-xs overflow-hidden"
|
||||
{...restProps}
|
||||
>
|
||||
<div
|
||||
class={cn(
|
||||
"pt-0 pb-2.5 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3 [&_p:not(:last-child)]:mb-4",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</AccordionPrimitive.Content>
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Accordion as AccordionPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: AccordionPrimitive.ItemProps = $props();
|
||||
</script>
|
||||
|
||||
<AccordionPrimitive.Item
|
||||
bind:ref
|
||||
data-slot="accordion-item"
|
||||
class={cn("not-last:border-b", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Accordion as AccordionPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
|
||||
import ChevronUpIcon from '@lucide/svelte/icons/chevron-up';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
level = 3,
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChild<AccordionPrimitive.TriggerProps> & {
|
||||
level?: AccordionPrimitive.HeaderProps["level"];
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<AccordionPrimitive.Header {level} class="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
data-slot="accordion-trigger"
|
||||
bind:ref
|
||||
class={cn(
|
||||
"focus-visible:ring-ring/50 focus-visible:border-ring focus-visible:after:border-ring **:data-[slot=accordion-trigger-icon]:text-muted-foreground rounded-none py-2.5 text-left text-xs font-medium hover:underline focus-visible:ring-1 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 group/accordion-trigger relative flex flex-1 items-start justify-between border border-transparent transition-all outline-none disabled:pointer-events-none disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
<ChevronDownIcon data-slot="accordion-trigger-icon" class="cn-accordion-trigger-icon pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden" />
|
||||
<ChevronUpIcon data-slot="accordion-trigger-icon" class="cn-accordion-trigger-icon pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Accordion as AccordionPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
class: className,
|
||||
...restProps
|
||||
}: AccordionPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<AccordionPrimitive.Root
|
||||
bind:ref
|
||||
bind:value={value as never}
|
||||
data-slot="accordion"
|
||||
class={cn("cn-accordion flex w-full flex-col", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
import Root from "./accordion.svelte";
|
||||
import Content from "./accordion-content.svelte";
|
||||
import Item from "./accordion-item.svelte";
|
||||
import Trigger from "./accordion-trigger.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Item,
|
||||
Trigger,
|
||||
//
|
||||
Root as Accordion,
|
||||
Content as AccordionContent,
|
||||
Item as AccordionItem,
|
||||
Trigger as AccordionTrigger,
|
||||
};
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
import {
|
||||
buttonVariants,
|
||||
type ButtonVariant,
|
||||
type ButtonSize,
|
||||
} from "$lib/components/ui/button/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
...restProps
|
||||
}: AlertDialogPrimitive.ActionProps & {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<AlertDialogPrimitive.Action
|
||||
bind:ref
|
||||
data-slot="alert-dialog-action"
|
||||
class={cn(buttonVariants({ variant, size }), "cn-alert-dialog-action", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
import {
|
||||
buttonVariants,
|
||||
type ButtonVariant,
|
||||
type ButtonSize,
|
||||
} from "$lib/components/ui/button/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
variant = "outline",
|
||||
size = "default",
|
||||
...restProps
|
||||
}: AlertDialogPrimitive.CancelProps & {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<AlertDialogPrimitive.Cancel
|
||||
bind:ref
|
||||
data-slot="alert-dialog-cancel"
|
||||
class={cn(buttonVariants({ variant, size }), "cn-alert-dialog-cancel", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
import AlertDialogPortal from "./alert-dialog-portal.svelte";
|
||||
import AlertDialogOverlay from "./alert-dialog-overlay.svelte";
|
||||
import { cn, type WithoutChild, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
size = "default",
|
||||
portalProps,
|
||||
...restProps
|
||||
}: WithoutChild<AlertDialogPrimitive.ContentProps> & {
|
||||
size?: "default" | "sm";
|
||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof AlertDialogPortal>>;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<AlertDialogPortal {...portalProps}>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="alert-dialog-content"
|
||||
data-size={size}
|
||||
class={cn(
|
||||
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 bg-popover text-popover-foreground ring-foreground/10 gap-4 rounded-none p-4 ring-1 duration-100 data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 outline-none",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: AlertDialogPrimitive.DescriptionProps = $props();
|
||||
</script>
|
||||
|
||||
<AlertDialogPrimitive.Description
|
||||
bind:ref
|
||||
data-slot="alert-dialog-description"
|
||||
class={cn("text-muted-foreground *:[a]:hover:text-foreground text-xs/relaxed text-balance md:text-pretty *:[a]:underline *:[a]:underline-offset-3", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="alert-dialog-footer"
|
||||
class={cn(
|
||||
"cn-alert-dialog-footer flex flex-col-reverse gap-2 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="alert-dialog-header"
|
||||
class={cn("grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="alert-dialog-media"
|
||||
class={cn("bg-muted mb-2 inline-flex size-10 items-center justify-center rounded-none sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-6", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: AlertDialogPrimitive.OverlayProps = $props();
|
||||
</script>
|
||||
|
||||
<AlertDialogPrimitive.Overlay
|
||||
bind:ref
|
||||
data-slot="alert-dialog-overlay"
|
||||
class={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 z-50", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
|
||||
let { ...restProps }: AlertDialogPrimitive.PortalProps = $props();
|
||||
</script>
|
||||
|
||||
<AlertDialogPrimitive.Portal {...restProps} />
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: AlertDialogPrimitive.TitleProps = $props();
|
||||
</script>
|
||||
|
||||
<AlertDialogPrimitive.Title
|
||||
bind:ref
|
||||
data-slot="alert-dialog-title"
|
||||
class={cn("text-sm font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: AlertDialogPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<AlertDialogPrimitive.Trigger bind:ref data-slot="alert-dialog-trigger" {...restProps} />
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
|
||||
let { open = $bindable(false), ...restProps }: AlertDialogPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<AlertDialogPrimitive.Root bind:open {...restProps} />
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
import Root from "./alert-dialog.svelte";
|
||||
import Portal from "./alert-dialog-portal.svelte";
|
||||
import Trigger from "./alert-dialog-trigger.svelte";
|
||||
import Title from "./alert-dialog-title.svelte";
|
||||
import Action from "./alert-dialog-action.svelte";
|
||||
import Cancel from "./alert-dialog-cancel.svelte";
|
||||
import Footer from "./alert-dialog-footer.svelte";
|
||||
import Header from "./alert-dialog-header.svelte";
|
||||
import Overlay from "./alert-dialog-overlay.svelte";
|
||||
import Content from "./alert-dialog-content.svelte";
|
||||
import Description from "./alert-dialog-description.svelte";
|
||||
import Media from "./alert-dialog-media.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Title,
|
||||
Action,
|
||||
Cancel,
|
||||
Portal,
|
||||
Footer,
|
||||
Header,
|
||||
Trigger,
|
||||
Overlay,
|
||||
Content,
|
||||
Description,
|
||||
Media,
|
||||
//
|
||||
Root as AlertDialog,
|
||||
Title as AlertDialogTitle,
|
||||
Action as AlertDialogAction,
|
||||
Cancel as AlertDialogCancel,
|
||||
Portal as AlertDialogPortal,
|
||||
Footer as AlertDialogFooter,
|
||||
Header as AlertDialogHeader,
|
||||
Trigger as AlertDialogTrigger,
|
||||
Overlay as AlertDialogOverlay,
|
||||
Content as AlertDialogContent,
|
||||
Description as AlertDialogDescription,
|
||||
Media as AlertDialogMedia,
|
||||
};
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="alert-action"
|
||||
class={cn("absolute top-[calc(--spacing(1.25))] right-[calc(--spacing(1.25))]", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="alert-description"
|
||||
class={cn(
|
||||
"text-muted-foreground text-xs/relaxed text-balance md:text-pretty [&_p:not(:last-child)]:mb-2 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="alert-title"
|
||||
class={cn(
|
||||
"font-medium group-has-[>svg]/alert:col-start-2 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
<script lang="ts" module>
|
||||
import { type VariantProps, tv } from "tailwind-variants";
|
||||
|
||||
export const alertVariants = tv({
|
||||
base: "grid gap-0.5 rounded-none border px-2.5 py-2 text-left text-xs has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4 group/alert relative w-full",
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-card text-card-foreground",
|
||||
destructive: "text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
});
|
||||
|
||||
export type AlertVariant = VariantProps<typeof alertVariants>["variant"];
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
variant = "default",
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||
variant?: AlertVariant;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="alert"
|
||||
role="alert"
|
||||
class={cn(alertVariants({ variant }), className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
import Root from "./alert.svelte";
|
||||
import Description from "./alert-description.svelte";
|
||||
import Title from "./alert-title.svelte";
|
||||
import Action from "./alert-action.svelte";
|
||||
export { alertVariants, type AlertVariant } from "./alert.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Description,
|
||||
Title,
|
||||
Action,
|
||||
//
|
||||
Root as Alert,
|
||||
Description as AlertDescription,
|
||||
Title as AlertTitle,
|
||||
Action as AlertAction,
|
||||
};
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { AspectRatio as AspectRatioPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: AspectRatioPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<AspectRatioPrimitive.Root bind:ref data-slot="aspect-ratio" {...restProps} />
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
import Root from "./aspect-ratio.svelte";
|
||||
|
||||
export { Root, Root as AspectRatio };
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
|
||||
</script>
|
||||
|
||||
<span
|
||||
bind:this={ref}
|
||||
data-slot="avatar-badge"
|
||||
class={cn(
|
||||
"bg-primary text-primary-foreground ring-background absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-blend-color ring-2 select-none",
|
||||
"group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
|
||||
"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
|
||||
"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</span>
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Avatar as AvatarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: AvatarPrimitive.FallbackProps = $props();
|
||||
</script>
|
||||
|
||||
<AvatarPrimitive.Fallback
|
||||
bind:ref
|
||||
data-slot="avatar-fallback"
|
||||
class={cn(
|
||||
"bg-muted text-muted-foreground rounded-full flex size-full items-center justify-center text-sm group-data-[size=sm]/avatar:text-xs",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="avatar-group-count"
|
||||
class={cn(
|
||||
"bg-muted text-muted-foreground size-8 rounded-full text-xs group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3 ring-background relative flex shrink-0 items-center justify-center ring-2",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="avatar-group"
|
||||
class={cn(
|
||||
"cn-avatar-group *:data-[slot=avatar]:ring-background group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Avatar as AvatarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: AvatarPrimitive.ImageProps = $props();
|
||||
</script>
|
||||
|
||||
<AvatarPrimitive.Image
|
||||
bind:ref
|
||||
data-slot="avatar-image"
|
||||
class={cn("rounded-full aspect-square size-full object-cover", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Avatar as AvatarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
loadingStatus = $bindable("loading"),
|
||||
size = "default",
|
||||
class: className,
|
||||
...restProps
|
||||
}: AvatarPrimitive.RootProps & {
|
||||
size?: "default" | "sm" | "lg";
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<AvatarPrimitive.Root
|
||||
bind:ref
|
||||
bind:loadingStatus
|
||||
data-slot="avatar"
|
||||
data-size={size}
|
||||
class={cn(
|
||||
"size-8 rounded-full after:rounded-full data-[size=lg]:size-10 data-[size=sm]:size-6 after:border-border group/avatar relative flex shrink-0 select-none after:absolute after:inset-0 after:border after:mix-blend-darken dark:after:mix-blend-lighten",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
import Root from "./avatar.svelte";
|
||||
import Image from "./avatar-image.svelte";
|
||||
import Fallback from "./avatar-fallback.svelte";
|
||||
import Badge from "./avatar-badge.svelte";
|
||||
import Group from "./avatar-group.svelte";
|
||||
import GroupCount from "./avatar-group-count.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Image,
|
||||
Fallback,
|
||||
Badge,
|
||||
Group,
|
||||
GroupCount,
|
||||
//
|
||||
Root as Avatar,
|
||||
Image as AvatarImage,
|
||||
Fallback as AvatarFallback,
|
||||
Badge as AvatarBadge,
|
||||
Group as AvatarGroup,
|
||||
GroupCount as AvatarGroupCount,
|
||||
};
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
<script lang="ts" module>
|
||||
import { type VariantProps, tv } from "tailwind-variants";
|
||||
|
||||
export const badgeVariants = tv({
|
||||
base: "h-5 gap-1 rounded-none border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive group/badge inline-flex w-fit shrink-0 items-center justify-center overflow-hidden whitespace-nowrap transition-colors focus-visible:ring-[3px] [&>svg]:pointer-events-none",
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||
secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
||||
destructive: "bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20",
|
||||
outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
||||
ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
});
|
||||
|
||||
export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { HTMLAnchorAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
href,
|
||||
class: className,
|
||||
variant = "default",
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAnchorAttributes> & {
|
||||
variant?: BadgeVariant;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<svelte:element
|
||||
this={href ? "a" : "span"}
|
||||
bind:this={ref}
|
||||
data-slot="badge"
|
||||
{href}
|
||||
class={cn(badgeVariants({ variant }), className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</svelte:element>
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
export { default as Badge } from "./badge.svelte";
|
||||
export { badgeVariants, type BadgeVariant } from "./badge.svelte";
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef, type WithoutChildren } from "$lib/utils.js";
|
||||
import MoreHorizontalIcon from '@lucide/svelte/icons/more-horizontal';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLSpanElement>>> = $props();
|
||||
</script>
|
||||
|
||||
<span
|
||||
bind:this={ref}
|
||||
data-slot="breadcrumb-ellipsis"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
class={cn("size-5 [&>svg]:size-4 flex items-center justify-center", className)}
|
||||
{...restProps}
|
||||
>
|
||||
<MoreHorizontalIcon />
|
||||
<span class="sr-only">More</span>
|
||||
</span>
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLLiAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLLiAttributes> = $props();
|
||||
</script>
|
||||
|
||||
<li
|
||||
bind:this={ref}
|
||||
data-slot="breadcrumb-item"
|
||||
class={cn("gap-1 inline-flex items-center", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</li>
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAnchorAttributes } from "svelte/elements";
|
||||
import type { Snippet } from "svelte";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
href = undefined,
|
||||
child,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAnchorAttributes> & {
|
||||
child?: Snippet<[{ props: HTMLAnchorAttributes }]>;
|
||||
} = $props();
|
||||
|
||||
const attrs = $derived({
|
||||
"data-slot": "breadcrumb-link",
|
||||
class: cn("hover:text-foreground transition-colors", className),
|
||||
href,
|
||||
...restProps,
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: attrs })}
|
||||
{:else}
|
||||
<a bind:this={ref} {...attrs}>
|
||||
{@render children?.()}
|
||||
</a>
|
||||
{/if}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLOlAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLOlAttributes> = $props();
|
||||
</script>
|
||||
|
||||
<ol
|
||||
bind:this={ref}
|
||||
data-slot="breadcrumb-list"
|
||||
class={cn("text-muted-foreground gap-1.5 text-xs flex flex-wrap items-center wrap-break-word", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</ol>
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
|
||||
</script>
|
||||
|
||||
<span
|
||||
bind:this={ref}
|
||||
data-slot="breadcrumb-page"
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
class={cn("text-foreground font-normal", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</span>
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLLiAttributes } from "svelte/elements";
|
||||
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLLiAttributes> = $props();
|
||||
</script>
|
||||
|
||||
<li
|
||||
bind:this={ref}
|
||||
data-slot="breadcrumb-separator"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
class={cn("[&>svg]:size-3.5", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{#if children}
|
||||
{@render children?.()}
|
||||
{:else}
|
||||
<ChevronRightIcon />
|
||||
{/if}
|
||||
</li>
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||
</script>
|
||||
|
||||
<nav
|
||||
bind:this={ref}
|
||||
data-slot="breadcrumb"
|
||||
aria-label="breadcrumb"
|
||||
class={cn("cn-breadcrumb", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</nav>
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
import Root from "./breadcrumb.svelte";
|
||||
import Ellipsis from "./breadcrumb-ellipsis.svelte";
|
||||
import Item from "./breadcrumb-item.svelte";
|
||||
import Separator from "./breadcrumb-separator.svelte";
|
||||
import Link from "./breadcrumb-link.svelte";
|
||||
import List from "./breadcrumb-list.svelte";
|
||||
import Page from "./breadcrumb-page.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Ellipsis,
|
||||
Item,
|
||||
Separator,
|
||||
Link,
|
||||
List,
|
||||
Page,
|
||||
//
|
||||
Root as Breadcrumb,
|
||||
Ellipsis as BreadcrumbEllipsis,
|
||||
Item as BreadcrumbItem,
|
||||
Separator as BreadcrumbSeparator,
|
||||
Link as BreadcrumbLink,
|
||||
List as BreadcrumbList,
|
||||
Page as BreadcrumbPage,
|
||||
};
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils.js";
|
||||
import type { ComponentProps } from "svelte";
|
||||
import { Separator } from "$lib/components/ui/separator/index.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
orientation = "vertical",
|
||||
...restProps
|
||||
}: ComponentProps<typeof Separator> = $props();
|
||||
</script>
|
||||
|
||||
<Separator
|
||||
bind:ref
|
||||
data-slot="button-group-separator"
|
||||
{orientation}
|
||||
class={cn(
|
||||
"bg-input relative self-stretch data-[orientation=horizontal]:mx-px data-[orientation=horizontal]:w-auto data-[orientation=vertical]:my-px data-[orientation=vertical]:h-auto",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
child,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||
child?: Snippet<[{ props: Record<string, unknown> }]>;
|
||||
} = $props();
|
||||
|
||||
const mergedProps = $derived({
|
||||
...restProps,
|
||||
class: cn("bg-muted gap-2 rounded-none border px-2.5 text-xs font-medium [&_svg:not([class*='size-'])]:size-4 flex items-center [&_svg]:pointer-events-none", className),
|
||||
"data-slot": "button-group-text",
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<div bind:this={ref} {...mergedProps}>
|
||||
{@render mergedProps.children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
<script lang="ts" module>
|
||||
import { tv, type VariantProps } from "tailwind-variants";
|
||||
|
||||
export const buttonGroupVariants = tv({
|
||||
base: "rounded-none has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-none flex w-fit items-stretch [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
|
||||
variants: {
|
||||
orientation: {
|
||||
horizontal:
|
||||
"cn-button-group-orientation-horizontal [&>[data-slot]]:rounded-r-none [&>[data-slot]~[data-slot]]:rounded-l-none [&>[data-slot]~[data-slot]]:border-l-0",
|
||||
vertical:
|
||||
"cn-button-group-orientation-vertical flex-col [&>[data-slot]]:rounded-b-none [&>[data-slot]~[data-slot]]:rounded-t-none [&>[data-slot]~[data-slot]]:border-t-0",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
orientation: "horizontal",
|
||||
},
|
||||
});
|
||||
|
||||
export type ButtonGroupOrientation = VariantProps<typeof buttonGroupVariants>["orientation"];
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
orientation = "horizontal",
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||
orientation?: ButtonGroupOrientation;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
role="group"
|
||||
data-slot="button-group"
|
||||
data-orientation={orientation}
|
||||
class={cn(buttonGroupVariants({ orientation }), className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
import Root, { buttonGroupVariants, type ButtonGroupOrientation } from "./button-group.svelte";
|
||||
import Text from "./button-group-text.svelte";
|
||||
import Separator from "./button-group-separator.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Text,
|
||||
Separator,
|
||||
buttonGroupVariants,
|
||||
type ButtonGroupOrientation,
|
||||
//
|
||||
Root as ButtonGroup,
|
||||
Text as ButtonGroupText,
|
||||
Separator as ButtonGroupSeparator,
|
||||
};
|
||||
-82
@@ -1,82 +0,0 @@
|
||||
<script lang="ts" module>
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
|
||||
import { type VariantProps, tv } from "tailwind-variants";
|
||||
|
||||
export const buttonVariants = tv({
|
||||
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-none border border-transparent bg-clip-padding text-xs font-medium focus-visible:ring-1 active:not-aria-[haspopup]:translate-y-px aria-invalid:ring-1 [&_svg:not([class*='size-'])]:size-4 group/button inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-all outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||
outline: "border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
||||
ghost: "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground",
|
||||
destructive: "bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||
xs: "h-6 gap-1 rounded-none px-2 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: "h-7 gap-1 rounded-none px-2.5 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||
icon: "size-8",
|
||||
"icon-xs": "size-6 rounded-none [&_svg:not([class*='size-'])]:size-3",
|
||||
"icon-sm": "size-7 rounded-none",
|
||||
"icon-lg": "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
});
|
||||
|
||||
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
|
||||
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
|
||||
|
||||
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||
WithElementRef<HTMLAnchorAttributes> & {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
let {
|
||||
class: className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
ref = $bindable(null),
|
||||
href = undefined,
|
||||
type = "button",
|
||||
disabled,
|
||||
children,
|
||||
...restProps
|
||||
}: ButtonProps = $props();
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<a
|
||||
bind:this={ref}
|
||||
data-slot="button"
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
href={disabled ? undefined : href}
|
||||
aria-disabled={disabled}
|
||||
role={disabled ? "link" : undefined}
|
||||
tabindex={disabled ? -1 : undefined}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
bind:this={ref}
|
||||
data-slot="button"
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
{type}
|
||||
{disabled}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
{/if}
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
import Root, {
|
||||
type ButtonProps,
|
||||
type ButtonSize,
|
||||
type ButtonVariant,
|
||||
buttonVariants,
|
||||
} from "./button.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
type ButtonProps as Props,
|
||||
//
|
||||
Root as Button,
|
||||
buttonVariants,
|
||||
type ButtonProps,
|
||||
type ButtonSize,
|
||||
type ButtonVariant,
|
||||
};
|
||||
-76
@@ -1,76 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { ComponentProps } from "svelte";
|
||||
import type Calendar from "./calendar.svelte";
|
||||
import CalendarMonthSelect from "./calendar-month-select.svelte";
|
||||
import CalendarYearSelect from "./calendar-year-select.svelte";
|
||||
import { DateFormatter, getLocalTimeZone, type DateValue } from "@internationalized/date";
|
||||
|
||||
let {
|
||||
captionLayout,
|
||||
months,
|
||||
monthFormat,
|
||||
years,
|
||||
yearFormat,
|
||||
month,
|
||||
locale,
|
||||
placeholder = $bindable(),
|
||||
monthIndex = 0,
|
||||
}: {
|
||||
captionLayout: ComponentProps<typeof Calendar>["captionLayout"];
|
||||
months: ComponentProps<typeof CalendarMonthSelect>["months"];
|
||||
monthFormat: ComponentProps<typeof CalendarMonthSelect>["monthFormat"];
|
||||
years: ComponentProps<typeof CalendarYearSelect>["years"];
|
||||
yearFormat: ComponentProps<typeof CalendarYearSelect>["yearFormat"];
|
||||
month: DateValue;
|
||||
placeholder: DateValue | undefined;
|
||||
locale: string;
|
||||
monthIndex: number;
|
||||
} = $props();
|
||||
|
||||
function formatYear(date: DateValue) {
|
||||
const dateObj = date.toDate(getLocalTimeZone());
|
||||
if (typeof yearFormat === "function") return yearFormat(dateObj.getFullYear());
|
||||
return new DateFormatter(locale, { year: yearFormat }).format(dateObj);
|
||||
}
|
||||
|
||||
function formatMonth(date: DateValue) {
|
||||
const dateObj = date.toDate(getLocalTimeZone());
|
||||
if (typeof monthFormat === "function") return monthFormat(dateObj.getMonth() + 1);
|
||||
return new DateFormatter(locale, { month: monthFormat }).format(dateObj);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#snippet MonthSelect()}
|
||||
<CalendarMonthSelect
|
||||
{months}
|
||||
{monthFormat}
|
||||
value={month.month}
|
||||
onchange={(e) => {
|
||||
if (!placeholder) return;
|
||||
const v = Number.parseInt(e.currentTarget.value);
|
||||
const newPlaceholder = placeholder.set({ month: v });
|
||||
placeholder = newPlaceholder.subtract({ months: monthIndex });
|
||||
}}
|
||||
/>
|
||||
{/snippet}
|
||||
|
||||
{#snippet YearSelect()}
|
||||
<CalendarYearSelect {years} {yearFormat} value={month.year} />
|
||||
{/snippet}
|
||||
|
||||
{#if captionLayout === "dropdown"}
|
||||
{@render MonthSelect()}
|
||||
{@render YearSelect()}
|
||||
{:else if captionLayout === "dropdown-months"}
|
||||
{@render MonthSelect()}
|
||||
{#if placeholder}
|
||||
{formatYear(placeholder)}
|
||||
{/if}
|
||||
{:else if captionLayout === "dropdown-years"}
|
||||
{#if placeholder}
|
||||
{formatMonth(placeholder)}
|
||||
{/if}
|
||||
{@render YearSelect()}
|
||||
{:else}
|
||||
{formatMonth(month)} {formatYear(month)}
|
||||
{/if}
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CalendarPrimitive.CellProps = $props();
|
||||
</script>
|
||||
|
||||
<CalendarPrimitive.Cell
|
||||
bind:ref
|
||||
class={cn(
|
||||
"relative size-(--cell-size) p-0 text-center text-sm focus-within:z-20 [&:first-child[data-selected]_[data-bits-day]]:rounded-s-(--cell-radius) [&:last-child[data-selected]_[data-bits-day]]:rounded-e-(--cell-radius)",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CalendarPrimitive.DayProps = $props();
|
||||
</script>
|
||||
|
||||
<CalendarPrimitive.Day
|
||||
bind:ref
|
||||
class={cn(
|
||||
"flex size-(--cell-size) flex-col items-center justify-center gap-1 rounded-(--cell-radius) p-0 leading-none font-normal whitespace-nowrap select-none",
|
||||
"[&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius)",
|
||||
"not-data-selected:hover:bg-accent/50 not-data-selected:hover:text-accent-foreground",
|
||||
"[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground [&[data-today][data-disabled]]:text-muted-foreground",
|
||||
"data-[selected]:bg-primary data-[selected]:text-primary-foreground data-[selected]:hover:text-foreground",
|
||||
// Outside months
|
||||
"[&[data-outside-month]:not([data-selected])]:text-muted-foreground [&[data-outside-month]:not([data-selected])]:hover:text-accent-foreground",
|
||||
// Disabled
|
||||
"data-[disabled]:text-muted-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
// Unavailable
|
||||
"data-[unavailable]:text-muted-foreground data-[unavailable]:line-through",
|
||||
// focus
|
||||
"focus:border-ring focus:ring-ring/50 focus:relative",
|
||||
// inner spans
|
||||
"[&>span]:text-xs [&>span]:opacity-70",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user