Compare commits

..

No commits in common. "main" and "v0.0.21" have entirely different histories.

147 changed files with 2436 additions and 2002 deletions

View File

@ -1,21 +0,0 @@
name: "Initialize"
description: "Install nix & use cachix"
inputs:
token:
description: "cachix auth token"
required: true
runs:
using: "composite"
steps:
- name: Install nix
uses: cachix/install-nix-action@v31
with:
nix_path: nixpkgs=channel:nixos-unstable
- name: Use cachix
uses: cachix/cachix-action@v16
with:
name: trevstack
authToken: "${{ inputs.token }}"

View File

@ -1,63 +0,0 @@
name: "Docker Push"
description: "Push to docker registry"
inputs:
server_url:
required: true
repository:
required: true
tag:
required: true
runs:
using: "composite"
steps:
- name: Set env
shell: bash
run: |
REGISTRY=$(basename ${{ inputs.server_url }})
NR=${{ inputs.repository }}
NAMESPACE="${NR%%/*}"
REPOSITORY="${NR##*/}"
TAG=${{ inputs.tag }}
VERSION=${TAG#v}
echo "REGISTRY=${REGISTRY}" >> $GITHUB_ENV
echo "NAMESPACE=${NAMESPACE}" >> $GITHUB_ENV
echo "REPOSITORY=${REPOSITORY}" >> $GITHUB_ENV
echo "VERSION=${VERSION}" >> $GITHUB_ENV
- name: Push images
shell: bash
run: |
docker image tag $REPOSITORY:$VERSION-amd64 $REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION-amd64
docker push $REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION-amd64
docker image tag $REPOSITORY:$VERSION-arm64 $REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION-arm64
docker push $REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION-arm64
docker image tag $REPOSITORY:$VERSION-arm $REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION-arm
docker push $REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION-arm
- name: Push manifest
shell: bash
run: |
docker manifest create $REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION \
$REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION-amd64 \
$REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION-arm64 \
$REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION-arm
docker manifest annotate $REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION $REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION-amd64 --arch amd64
docker manifest annotate $REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION $REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION-arm64 --arch arm64
docker manifest annotate $REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION $REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION-arm --arch arm
docker manifest push $REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION
docker manifest create $REGISTRY/$NAMESPACE/$REPOSITORY:latest \
$REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION-amd64 \
$REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION-arm64 \
$REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION-arm
docker manifest annotate $REGISTRY/$NAMESPACE/$REPOSITORY:latest $REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION-amd64 --arch amd64
docker manifest annotate $REGISTRY/$NAMESPACE/$REPOSITORY:latest $REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION-arm64 --arch arm64
docker manifest annotate $REGISTRY/$NAMESPACE/$REPOSITORY:latest $REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION-arm --arch arm
docker manifest push $REGISTRY/$NAMESPACE/$REPOSITORY:latest

14
.dockerignore Normal file
View File

@ -0,0 +1,14 @@
.env
/docker-compose.*
/result*
/.direnv/
/build/
# Client
/client/node_modules/
/client/.svelte-kit/
# Server
/server/client/
/server/tmp/
/server/build/

View File

@ -1,23 +0,0 @@
name: Check
on:
push:
branches:
- main
pull_request:
types: [opened, reopened, edited, auto_merge_enabled]
jobs:
check:
name: check
runs-on: ubuntu-latest
if: |
contains(github.event.head_commit.message, 'bump:') == false &&
contains(github.event.head_commit.message, 'Merge pull request') == false
steps:
- uses: actions/checkout@v4
- uses: ./.actions/init
with:
token: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- run: nix flake check

View File

@ -1,68 +0,0 @@
name: Release
on:
push:
tags:
- "*"
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.actions/init
with:
token: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- run: nix flake check
release:
runs-on: ubuntu-latest
needs: check
steps:
- uses: actions/checkout@v4
- uses: ./.actions/init
with:
token: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- run: >
nix build
.#trevstack-linux-amd64
.#trevstack-linux-arm64
.#trevstack-linux-arm
.#trevstack-windows-amd64
.#trevstack-darwin-amd64
.#trevstack-darwin-arm64
- uses: akkuman/gitea-release-action@v1
with:
files: |-
result*/bin/*
package:
runs-on: ubuntu-latest
needs: release
steps:
- uses: actions/checkout@v4
- uses: ./.actions/init
with:
token: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- uses: docker/login-action@v3
with:
registry: ${{ github.server_url }}
username: ${{ github.actor }}
password: ${{ secrets.PAT }}
- name: Build & load images
run: |
nix build .#trevstack-linux-amd64-image && ./result | docker load
nix build .#trevstack-linux-arm64-image && ./result | docker load
nix build .#trevstack-linux-arm-image && ./result | docker load
- name: Push images
uses: ./.actions/push
with:
server_url: ${{ github.server_url }}
repository: ${{ github.repository }}
tag: ${{ github.ref_name }}

View File

@ -1,50 +0,0 @@
name: Update
on:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
jobs:
update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.actions/init
with:
token: "${{ secrets.CACHIX_AUTH_TOKEN }}"
# https://github.com/actions/checkout/issues/13
- name: Set git config
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -B update
- run: nix run .#update
- name: Create pull request
env:
PAT: ${{ secrets.PAT }}
run: |
URL="${{ gitea.server_url }}"
REPO_OWNER_SLASH_NAME="${{ gitea.repository }}"
if ! git ls-remote --exit-code origin update; then
git push origin update --force
PR_RESPONSE=$(curl -s -X POST -H "Authorization: token $PAT" \
-H "Content-Type: application/json" \
-d '{"title":"update","body":"automatic update","head":"update","base":"main"}' \
"$URL/api/v1/repos/$REPO_OWNER_SLASH_NAME/pulls")
PR_NUMBER=$(echo "$PR_RESPONSE" | jq -r '.number')
curl -s -X POST -H "Authorization: token $PAT" \
-H "Content-Type: application/json" \
-d '{"Do":"merge","merge_when_checks_succeed":true,"delete_branch_after_merge":true}' \
"$URL/api/v1/repos/$REPO_OWNER_SLASH_NAME/pulls/$PR_NUMBER/merge"
else
git push origin update --force
fi

View File

@ -1,23 +0,0 @@
name: Check
on:
push:
branches:
- main
pull_request:
types: [opened, reopened, edited, auto_merge_enabled]
jobs:
check:
name: check
runs-on: ubuntu-latest
if: |
contains(github.event.head_commit.message, 'bump:') == false &&
contains(github.event.head_commit.message, 'Merge pull request') == false
steps:
- uses: actions/checkout@v4
- uses: ./.actions/init
with:
token: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- run: nix flake check

28
.github/workflows/lint.yaml vendored Normal file
View File

@ -0,0 +1,28 @@
name: Lint Workflow
on:
push:
branches:
- main
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Nix
uses: cachix/install-nix-action@v31
with:
nix_path: nixpkgs=channel:nixos-unstable
- name: Use Cachix
uses: cachix/cachix-action@v16
with:
name: trevstack
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- name: Run checks
run: nix flake check

View File

@ -1,35 +1,34 @@
name: Release
name: Release Workflow
on:
push:
tags:
- "*"
- '*'
permissions:
contents: write
packages: write
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.actions/init
with:
token: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- run: nix flake check
release:
runs-on: ubuntu-latest
needs: check
steps:
- uses: actions/checkout@v4
- uses: ./.actions/init
with:
token: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: Checkout
uses: actions/checkout@v4
- run: >
- name: Install Nix
uses: cachix/install-nix-action@v31
with:
nix_path: nixpkgs=channel:nixos-unstable
- name: Use Cachix
uses: cachix/cachix-action@v16
with:
name: trevstack
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- name: Build
run: >
nix build
.#trevstack-linux-amd64
.#trevstack-linux-arm64
@ -38,36 +37,52 @@ jobs:
.#trevstack-darwin-amd64
.#trevstack-darwin-arm64
- uses: softprops/action-gh-release@v2
- name: Create Release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
files: |-
result*/bin/*
# https://docs.docker.com/build/ci/github-actions/manage-tags-labels/
package:
runs-on: ubuntu-latest
needs: release
needs: release # Wait for binary cache to propagate
steps:
- uses: actions/checkout@v4
- uses: ./.actions/init
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
token: "${{ secrets.CACHIX_AUTH_TOKEN }}"
# list of Docker images to use as base name for tags
images: |
${{ github.repository }}
ghcr.io/${{ github.repository }}
# generate Docker tags based on the following events/attributes
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
- uses: docker/login-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build & load images
run: |
nix build .#trevstack-linux-amd64-image && ./result | docker load
nix build .#trevstack-linux-arm64-image && ./result | docker load
nix build .#trevstack-linux-arm-image && ./result | docker load
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Push images
uses: ./.actions/push
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v6
with:
server_url: ghcr.io
repository: ${{ github.repository }}
tag: ${{ github.ref_name }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@ -1,40 +1,41 @@
name: Update
name: Update Workflow
on:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.actions/init
- name: Checkout
uses: actions/checkout@v4
- name: Install Nix
uses: cachix/install-nix-action@v31
with:
token: "${{ secrets.CACHIX_AUTH_TOKEN }}"
nix_path: nixpkgs=channel:nixos-unstable
- name: Use Cachix
uses: cachix/cachix-action@v16
with:
name: trevstack
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
# https://github.com/actions/checkout/issues/13
- name: Set git config
- name: Set Git Config
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- run: nix run .#update
- name: Update
run: nix run .#update
- name: Create pull request
id: cpr
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7
with:
branch: update
title: update
body: automatic update
delete-branch: true
title: Bump deps
- name: Enable automerge
run: gh pr merge --merge --auto "${{ steps.cpr.outputs.pull-request-number }}"
env:
GH_TOKEN: ${{ secrets.PAT }}

4
.gitignore vendored
View File

@ -1,7 +1,5 @@
.env
/docker-compose.*
/build/
# Nix
/result*
/.direnv/
/build/

View File

@ -3,25 +3,7 @@
git_root=$(git rev-parse --show-toplevel)
git_version=$(git describe --tags --abbrev=0)
version=${git_version#v}
major=$(echo "${version}" | cut -d . -f1)
minor=$(echo "${version}" | cut -d . -f2)
patch=$(echo "${version}" | cut -d . -f3)
case "${1-patch}" in
major) major=$((major + 1)) ;;
minor) minor=$((minor + 1)) ;;
*) patch=$((patch + 1)) ;;
esac
next_version="${major}.${minor}.${patch}"
echo "${version} -> ${next_version}"
echo "bumping openapi"
cd "${git_root}"
sed -i -e "s/${version}/${next_version}/g" openapi.yaml
sed -i -e "s/${version}/${next_version}/g" client/static/openapi/openapi.yaml
git add openapi.yaml
git add client/static/openapi/openapi.yaml
next_version=$(echo "${version}" | awk -F. -v OFS=. '{$NF += 1 ; print}')
echo "bumping client"
cd "${git_root}/client"

View File

@ -6,19 +6,11 @@ updated=false
echo "updating nix flake"
cd "${git_root}"
nix flake update
if ! git diff --exit-code flake.lock; then
git add flake.lock
if ! git diff --exit-code flake.nix; then
git add flake.nix
git commit -m "build(nix): updated nix dependencies"
fi
echo "updating protobuf deps"
cd "${git_root}/proto"
buf dep update
if ! git diff --exit-code buf.lock; then
git add buf.lock
git commit -m "build(buf): updated buf dependencies"
fi
echo "updating client"
cd "${git_root}/client"
npm update --save && npm i
@ -36,7 +28,7 @@ go mod tidy
if ! git diff --exit-code go.mod go.sum; then
git add go.mod
git add go.sum
git commit -m "build(server): updated go dependencies"
git commit -m "build(go): updated go dependencies"
updated=true
fi

View File

@ -39,7 +39,7 @@ apps:
- ts
- svelte
onstart: npx prettier --check .
onchange: npx prettier --check .
onchange: npx prettier --check . || npx prettier --write .
revive:
color: "#89dceb"

View File

@ -1,10 +0,0 @@
{
"recommendations": [
"golang.go",
"dorzey.vscode-sqlfluff",
"bufbuild.vscode-buf",
"dbaeumer.vscode-eslint",
"svelte.svelte-vscode",
"esbenp.prettier-vscode"
]
}

49
.vscode/settings.json vendored
View File

@ -1,46 +1,7 @@
{
"editor.formatOnSave": true,
// Go
"go.lintTool": "revive",
"go.formatTool": "goimports",
"go.buildTags": "dev",
"go.lintFlags": ["--config=server/revive.toml"],
"gopls": { "ui.semanticTokens": true },
"[go]": {
"editor.defaultFormatter": "golang.go"
},
// SQLFluff
"sqlfluff.config": "server/db/.sqlfluff",
"[sql]": {
"editor.defaultFormatter": "dorzey.vscode-sqlfluff"
},
// Proto
"[proto]": {
"editor.defaultFormatter": "bufbuild.vscode-buf"
},
// ESLint
"eslint.workingDirectories": ["./client"],
"eslint.validate": ["svelte", "javascript", "typescript"],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
// Svelte
"svelte.enable-ts-plugin": true,
"[svelte]": {
"editor.defaultFormatter": "svelte.svelte-vscode"
},
// Prettier
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
"go.lintTool": "revive",
"go.lintFlags": [
"--config=server/revive.toml"
],
"sqlfluff.config": "server/db/.sqlfluff",
}

29
Dockerfile Normal file
View File

@ -0,0 +1,29 @@
# Nix builder
FROM nixos/nix:latest AS builder
# Copy our source and setup our working dir.
COPY . /tmp/build
WORKDIR /tmp/build
# Build our Nix environment
RUN nix \
--extra-experimental-features "nix-command flakes" \
--option filter-syscalls false \
--accept-flake-config \
build
# Copy the Nix store closure into a directory. The Nix store closure is the
# entire set of Nix store values that we need for our build.
RUN mkdir /tmp/nix-store-closure
RUN cp -R $(nix-store -qR result/) /tmp/nix-store-closure
# Final image is based on scratch. We copy a bunch of Nix dependencies
# but they're fully self-contained so we don't need Nix anymore.
FROM scratch
WORKDIR /app
# Copy /nix/store
COPY --from=builder /tmp/nix-store-closure /nix/store
COPY --from=builder /tmp/build/result /app
CMD ["/app/bin/trevstack"]

21
LICENSE
View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) Trev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

125
README.md
View File

@ -1,125 +0,0 @@
## TrevStack
This is a CRUD app to use as a template for starting projects
### Features
- **Communicate anywhere**. Define a [protocol buffer](https://protobuf.dev/), and [Connect](https://connectrpc.com/) generates type-safe code to facilitate communication between the server and any client (web, mobile, embedded, etc). The protocol buffers can contain annotations to validate fields on the client and server. For clients that cannot use Connect, an OpenAPI spec is also generated
- **Build anywhere**. The dev environment, testing and building is all declared in a single [Nix](https://nixos.org/) flake. Every developer and server can use the same environment
- **Deploy anywhere**. CI/CD is already set up using github actions. New versions are automatically released for every major platform, along with a docker image. The binaries created require zero run-time dependencies and are relatively small (this app is 26 MiB)
- Can be entirely self-hosted
- Authentication is rolled in, including API key, fingerprint & passkey
- Automatic database migration on startup
- Light & dark modes with the [catppuccin](https://catppuccin.com/palette/) color palette
- Really good at running as a [progressive web app](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps)
While I personally prefer using a Svelte frontend and Go backend, feel free to swap them out with whatever you like, you'll be surprised how easy it is.
## Getting Started
1. [Install Nix](https://nixos.org/download/)
2. Run `nix develop`
3. Create a `server/.env` file that looks something like
```env
KEY=changeme
PORT=8080
URL=http://localhost:5173
DATABASE_URL=sqlite:/home/trev/.config/trevstack/sqlite.db
```
4. Run `treli` to start the server & client
It's that simple. If you're feeling fancy, install [direnv](https://direnv.net/) and the dev environment will load automatically.
### Useful Commands
- `nix run #update`: updates all of the dependencies
- `nix run #bump [major | minor]`: bumps the current version up one. Defaults to "patch" (0.0.1 -> 0.0.2)
- `nix build [#trevstack-(GOOS)-(GOARCH)]`: builds the application. Defaults to building for your current platform, but can be built to many by specifying the GOOS and GOARCH values
- `nix flake check`: runs all validations
- `buf lint proto` & `buf generate`: lints and generates code from protocol buffers
- `sqlc vet` & `sqlc generate`: verifies and generates code from SQL files
- `dbmate new` & `dbmate up`: creates a new migration file and runs pending migrations
### Github Actions
To use github actions for CI/CD, you'll need to create a fine-grained personal access token for the repository with the permissions:
- Contents (read and write)
- Pull requests (read and write)
And change some settings for the repository:
- General -> Allow auto-merge: true
- Rules -> Rulesets -> New ruleset
- Branch targeting criteria: Default
- Branch rules
- Require status checks to pass -> Add checks -> "check"
- Actions -> General -> Workflow permissions
- Read and write permissions: true
- Allow GitHub Actions to create and approve pull requests: true
- Secrets and variables -> Actions -> Repository secrets
- PAT: (personal access token)
### Gitea Actions
To use gitea actions for CI/CD, you'll need to create an [API token](https://docs.gitea.com/development/api-usage) with the scopes:
- write:repository
- write:package
And change some settings for the repository:
- Repository -> Delete pull request branch after merge by default: true
- Branches -> Add New Rule
- Protected Branch Name Pattern: main
- Enable Status Check: true
- Status check patterns: Check / check\*
- Actions -> Secrets
- PAT: (API token)
## Components
### Client
- **svelte 5** [[docs](https://svelte.dev/docs/svelte)] UI framework
- **tailwind 4** [[docs](https://tailwindcss.com/)] CSS framework
- **bits ui** [[docs](https://bits-ui.com/docs/)] headless components
- [components](https://github.com/spotdemo4/trevstack/tree/main/client/src/lib/ui) from **shadcn-svelte** [[docs](https://www.shadcn-svelte.com/docs)] altered to work with tailwind 4, fit the [catppuccin](https://catppuccin.com/palette/) color palette, and use [shallow routing](https://svelte.dev/docs/kit/shallow-routing)
- **connect rpc** [[docs](https://connectrpc.com/docs/web/)] to communicating with the server
- **protovalidate-es** [[docs](https://github.com/bufbuild/protovalidate-es)], along with a function [coolforms](https://github.com/spotdemo4/trevstack/blob/main/client/src/lib/coolforms/) to emulate the library [sveltekit-superforms](https://superforms.rocks/)
- **simplewebauthn** [[docs](https://simplewebauthn.dev/docs/packages/browser)] for passkey authentication
- **scalar** [[docs](https://github.com/scalar/scalar)] for displaying openapi specs
- **tw-animate-css** [[docs](https://github.com/Wombosvideo/tw-animate-css)] to animate with just tailwind classes
- vite [[docs](https://vite.dev/)] for dev server and bundling
- eslint [[docs](https://eslint.org/)] for linting
- prettier [[docs](https://prettier.io/)] for formatting
- prettier-plugin-svelte [[docs](https://github.com/sveltejs/prettier-plugin-svelte)]
- prettier-plugin-tailwindcss [[docs](https://github.com/tailwindlabs/prettier-plugin-tailwindcss)]
- prettier-plugin-sort-imports [[docs](https://github.com/IanVS/prettier-plugin-sort-imports)]
### Server
- **go** [[docs](https://go.dev/doc/)]
- **connect rpc** [[docs](https://connectrpc.com/docs/go/)] to serve gRPC & HTTP requests
- **protovalidate-go** [[docs](https://github.com/bufbuild/protovalidate-go)] for validating those requests
- **sqlc** [[docs](https://docs.sqlc.dev/en/latest/)] because writing the SQL yourself is better than an ORM
- **go-webauthn** [[docs](https://github.com/go-webauthn/webauthn)] because webauthn is hard
- **dbmate** [[docs](https://github.com/amacneil/dbmate)] for database migrations
- revive [[docs](https://github.com/mgechev/revive)] for linting
### Protocol Buffers / gRPC
- **buf** [[docs](https://buf.build/docs/)] CLI for linting & code generation
- **protovalidate** [[docs](https://buf.build/docs/protovalidate/)] provides annotations to validate proto messages & fields
- **protoc-gen-connect-openapi** [[docs](https://github.com/sudorandom/protoc-gen-connect-openapi)] generates openapi specs
- protoc-gen-go [[docs](https://pkg.go.dev/google.golang.org/protobuf)]
- protoc-gen-connect-go [[docs](https://connectrpc.com/docs/go)]
- protoc-gen-es [[docs](https://connectrpc.com/docs/web/)]

View File

@ -1,8 +1,5 @@
version: v2
clean: true
inputs:
- directory: proto
managed:
enabled: true
override:
@ -30,5 +27,5 @@ plugins:
out: client/static/openapi
strategy: all
opt:
- base=openapi.yaml
- base=openapi.base.yaml
- path=openapi.yaml

View File

@ -1,6 +1,6 @@
# For details on buf.yaml configuration, visit https://buf.build/docs/configuration/v2/buf-yaml
version: v2
modules:
- path: .
- path: proto
deps:
- buf.build/bufbuild/protovalidate

View File

@ -3,21 +3,8 @@
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": [
"prettier-plugin-svelte",
"prettier-plugin-tailwindcss",
"@ianvs/prettier-plugin-sort-imports"
],
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"tailwindFunctions": ["tv", "cn", "clsx"],
"importOrder": [
"<TYPES>^(node:)",
"<TYPES>",
"<TYPES>^[.]",
"<BUILTIN_MODULES>",
"^(\\$lib/ui)(/.*)$",
"<THIRD_PARTY_MODULES>",
"^[.]"
],
"overrides": [
{
"files": "*.svelte",

View File

@ -1,12 +1,11 @@
import { fileURLToPath } from 'node:url';
import { includeIgnoreFile } from '@eslint/compat';
import js from '@eslint/js';
import prettier from 'eslint-config-prettier';
import js from '@eslint/js';
import { includeIgnoreFile } from '@eslint/compat';
import svelte from 'eslint-plugin-svelte';
import globals from 'globals';
import { fileURLToPath } from 'node:url';
import ts from 'typescript-eslint';
import svelteConfig from './svelte.config.js';
const gitignorePath = fileURLToPath(new URL('./.prettierignore', import.meta.url));
export default ts.config(

2025
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{
"name": "trevstack",
"private": true,
"version": "0.0.47",
"version": "0.0.21",
"type": "module",
"scripts": {
"dev": "vite dev",
@ -19,33 +19,33 @@
"@connectrpc/connect-web": "^2.0.2",
"@eslint/compat": "^1.2.9",
"@eslint/js": "^9.18.0",
"@ianvs/prettier-plugin-sort-imports": "^4.4.1",
"@lucide/svelte": "^0.479.0",
"@scalar/api-reference": "^1.28.34",
"@scalar/api-reference": "^1.28.32",
"@simplewebauthn/browser": "^13.1.0",
"@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/kit": "^2.21.1",
"@sveltejs/kit": "^2.20.8",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tailwindcss/vite": "^4.1.7",
"bits-ui": "^1.5.3",
"@tailwindcss/vite": "^4.1.6",
"bits-ui": "^1.4.8",
"clsx": "^2.1.1",
"eslint": "^9.27.0",
"eslint": "^9.26.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-svelte": "^3.8.1",
"eslint-plugin-svelte": "^3.5.1",
"fast-deep-equal": "^3.1.3",
"globals": "^16.1.0",
"mode-watcher": "^1.0.7",
"prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.4.0",
"prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"svelte": "^5.31.1",
"svelte-check": "^4.2.1",
"svelte": "^5.28.2",
"svelte-check": "^4.1.7",
"svelte-sonner": "^0.3.28",
"tailwind-merge": "^3.3.0",
"tailwind-variants": "^1.0.0",
"tailwindcss": "^4.0.13",
"tw-animate-css": "^1.3.0",
"tw-animate-css": "^1.2.9",
"typescript": "^5.8.3",
"typescript-eslint": "^8.32.1",
"typescript-eslint": "^8.32.0",
"vite": "^6.3.5"
}
}

View File

@ -1,99 +1,99 @@
@import 'tailwindcss';
@import 'tw-animate-css';
@import "tw-animate-css";
@custom-variant dark (&:where(.dark, .dark *));
@theme inline {
--spacing-body: calc(100vh - 180px);
--spacing-body: calc(100vh - 180px);
--color-accent: var(--sky);
--color-accent: var(--sky);
--color-rosewater: var(--rosewater);
--color-flamingo: var(--flamingo);
--color-pink: var(--pink);
--color-mauve: var(--mauve);
--color-red: var(--red);
--color-maroon: var(--maroon);
--color-peach: var(--peach);
--color-yellow: var(--yellow);
--color-green: var(--green);
--color-teal: var(--teal);
--color-sky: var(--sky);
--color-sapphire: var(--sapphire);
--color-blue: var(--blue);
--color-lavender: var(--lavender);
--color-text: var(--text);
--color-subtext-1: var(--subtext-1);
--color-subtext: var(--subtext);
--color-overlay-2: var(--overlay-2);
--color-overlay-1: var(--overlay-1);
--color-overlay: var(--overlay);
--color-surface-2: var(--surface-2);
--color-surface-1: var(--surface-1);
--color-surface: var(--surface);
--color-based: var(--based);
--color-mantle: var(--mantle);
--color-crust: var(--crust);
--color-rosewater: var(--rosewater);
--color-flamingo: var(--flamingo);
--color-pink: var(--pink);
--color-mauve: var(--mauve);
--color-red: var(--red);
--color-maroon: var(--maroon);
--color-peach: var(--peach);
--color-yellow: var(--yellow);
--color-green: var(--green);
--color-teal: var(--teal);
--color-sky: var(--sky);
--color-sapphire: var(--sapphire);
--color-blue: var(--blue);
--color-lavender: var(--lavender);
--color-text: var(--text);
--color-subtext-1: var(--subtext-1);
--color-subtext: var(--subtext);
--color-overlay-2: var(--overlay-2);
--color-overlay-1: var(--overlay-1);
--color-overlay: var(--overlay);
--color-surface-2: var(--surface-2);
--color-surface-1: var(--surface-1);
--color-surface: var(--surface);
--color-based: var(--based);
--color-mantle: var(--mantle);
--color-crust: var(--crust);
}
@layer base {
:root {
--rosewater: hsl(11deg, 59%, 67%);
--flamingo: hsl(0deg, 60%, 67%);
--pink: hsl(316deg, 73%, 69%);
--mauve: hsl(266deg, 85%, 58%);
--red: hsl(347deg, 87%, 44%);
--maroon: hsl(355deg, 76%, 59%);
--peach: hsl(22deg, 99%, 52%);
--yellow: hsl(35deg, 77%, 49%);
--green: hsl(109deg, 58%, 40%);
--teal: hsl(183deg, 74%, 35%);
--sky: hsl(197deg, 97%, 46%);
--sapphire: hsl(189deg, 70%, 42%);
--blue: hsl(220deg, 91%, 54%);
--lavender: hsl(231deg, 97%, 72%);
--text: hsl(234deg, 16%, 35%);
--subtext-1: hsl(233deg, 13%, 41%);
--subtext: hsl(233deg, 10%, 47%);
--overlay-2: hsl(232deg, 10%, 53%);
--overlay-1: hsl(231deg, 10%, 59%);
--overlay: hsl(228deg, 11%, 65%);
--surface-2: hsl(227deg, 12%, 71%);
--surface-1: hsl(225deg, 14%, 77%);
--surface: hsl(223deg, 16%, 83%);
--based: hsl(220deg, 23%, 95%);
--mantle: hsl(220deg, 22%, 92%);
--crust: hsl(220deg, 21%, 89%);
}
:root {
--rosewater: hsl(11deg, 59%, 67%);
--flamingo: hsl(0deg, 60%, 67%);
--pink: hsl(316deg, 73%, 69%);
--mauve: hsl(266deg, 85%, 58%);
--red: hsl(347deg, 87%, 44%);
--maroon: hsl(355deg, 76%, 59%);
--peach: hsl(22deg, 99%, 52%);
--yellow: hsl(35deg, 77%, 49%);
--green: hsl(109deg, 58%, 40%);
--teal: hsl(183deg, 74%, 35%);
--sky: hsl(197deg, 97%, 46%);
--sapphire: hsl(189deg, 70%, 42%);
--blue: hsl(220deg, 91%, 54%);
--lavender: hsl(231deg, 97%, 72%);
--text: hsl(234deg, 16%, 35%);
--subtext-1: hsl(233deg, 13%, 41%);
--subtext: hsl(233deg, 10%, 47%);
--overlay-2: hsl(232deg, 10%, 53%);
--overlay-1: hsl(231deg, 10%, 59%);
--overlay: hsl(228deg, 11%, 65%);
--surface-2: hsl(227deg, 12%, 71%);
--surface-1: hsl(225deg, 14%, 77%);
--surface: hsl(223deg, 16%, 83%);
--based: hsl(220deg, 23%, 95%);
--mantle: hsl(220deg, 22%, 92%);
--crust: hsl(220deg, 21%, 89%);
}
.dark {
--rosewater: hsl(10deg, 56%, 91%);
--flamingo: hsl(0deg, 59%, 88%);
--pink: hsl(316deg, 72%, 86%);
--mauve: hsl(267deg, 84%, 81%);
--red: hsl(343deg, 81%, 75%);
--maroon: hsl(350deg, 65%, 77%);
--peach: hsl(23deg, 92%, 75%);
--yellow: hsl(41deg, 86%, 83%);
--green: hsl(115deg, 54%, 76%);
--teal: hsl(170deg, 57%, 73%);
--sky: hsl(189deg, 71%, 73%);
--sapphire: hsl(199deg, 76%, 69%);
--blue: hsl(217deg, 92%, 76%);
--lavender: hsl(232deg, 97%, 85%);
--text: hsl(226deg, 64%, 88%);
--subtext-1: hsl(227deg, 35%, 80%);
--subtext: hsl(228deg, 24%, 72%);
--overlay-2: hsl(228deg, 17%, 64%);
--overlay-1: hsl(230deg, 13%, 55%);
--overlay: hsl(231deg, 11%, 47%);
--surface-2: hsl(233deg, 12%, 39%);
--surface-1: hsl(234deg, 13%, 31%);
--surface: hsl(237deg, 16%, 23%);
--based: hsl(240deg, 21%, 15%);
--mantle: hsl(240deg, 21%, 12%);
--crust: hsl(240deg, 23%, 9%);
}
.dark {
--rosewater: hsl(10deg, 56%, 91%);
--flamingo: hsl(0deg, 59%, 88%);
--pink: hsl(316deg, 72%, 86%);
--mauve: hsl(267deg, 84%, 81%);
--red: hsl(343deg, 81%, 75%);
--maroon: hsl(350deg, 65%, 77%);
--peach: hsl(23deg, 92%, 75%);
--yellow: hsl(41deg, 86%, 83%);
--green: hsl(115deg, 54%, 76%);
--teal: hsl(170deg, 57%, 73%);
--sky: hsl(189deg, 71%, 73%);
--sapphire: hsl(199deg, 76%, 69%);
--blue: hsl(217deg, 92%, 76%);
--lavender: hsl(232deg, 97%, 85%);
--text: hsl(226deg, 64%, 88%);
--subtext-1: hsl(227deg, 35%, 80%);
--subtext: hsl(228deg, 24%, 72%);
--overlay-2: hsl(228deg, 17%, 64%);
--overlay-1: hsl(230deg, 13%, 55%);
--overlay: hsl(231deg, 11%, 47%);
--surface-2: hsl(233deg, 12%, 39%);
--surface-1: hsl(234deg, 13%, 31%);
--surface: hsl(237deg, 16%, 23%);
--based: hsl(240deg, 21%, 15%);
--mantle: hsl(240deg, 21%, 12%);
--crust: hsl(240deg, 23%, 9%);
}
}
/*

View File

@ -8,7 +8,7 @@
<title>TrevStack</title>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="tap" class="bg-crust text-text min-h-dvh">
<body data-sveltekit-preload-data="tap" class="bg-crust text-text min-h-screen">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@ -1,5 +1,7 @@
export function newState<T>(s: T) {
const state = $state(s);
return state;
export function newState<T>(s: T) {
const state = $state(s);
return state;
}

View File

@ -1,176 +1,175 @@
import type { DescMessage, DescService, MessageInitShape, MessageShape } from '@bufbuild/protobuf';
import type { Violation } from '@bufbuild/protovalidate';
import type { Client } from '@connectrpc/connect';
import type { Action } from 'svelte/action';
import { create } from '@bufbuild/protobuf';
import { ValidationError } from '@bufbuild/protovalidate';
import { ConnectError } from '@connectrpc/connect';
import { Validator } from '../transport';
import { create, type DescMessage, type DescService, type MessageShape, type MessageInitShape } from "@bufbuild/protobuf";
import { ValidationError, type Violation } from "@bufbuild/protovalidate";
import { Validator } from "../transport";
import { ConnectError, type Client } from "@connectrpc/connect";
import type { Action } from "svelte/action";
type Options<Input extends DescMessage, Output extends DescMessage> = {
init?: MessageInitShape<Input>;
start?: boolean;
reset?: boolean;
onSubmit?: (formData: FormData, input: MessageShape<Input>) => Promise<MessageShape<Input>>;
onResult?: (result: MessageShape<Output>) => void;
onError?: (err: Violation[] | ConnectError) => void;
};
init?: MessageInitShape<Input>,
start?: boolean,
reset?: boolean,
onSubmit?: (formData: FormData, input: MessageShape<Input>) => Promise<MessageShape<Input>>,
onResult?: (result: MessageShape<Output>) => void,
onError?: (err: Violation[] | ConnectError) => void
}
type Violations<Field> = {
[field in keyof Field]?: Violation[];
[field in keyof Field]?: Violation[];
};
export function coolForm<Service extends DescService, Method extends Service['methods'][number]>(
client: Client<Service>,
method: Method,
options?: Options<Method['input'], Method['output']>
export function coolForm<
Service extends DescService,
Method extends Service['methods'][number]
>(
client: Client<Service>,
method: Method,
options?: Options<Method['input'], Method['output']>
) {
const input = $state(create(method.input as Method['input'], options?.init));
const output = $state(create(method.output as Method['output']));
const errors: Violations<Method['input']['field']> & { form?: ConnectError } = $state({});
let loading = $state(false);
const input = $state(create(method.input as Method['input'], options?.init));
const output = $state(create(method.output as Method['output']));
const errors: Violations<Method['input']['field']> & {
form?: ConnectError
} = $state({});
let loading = $state(false);
const validate = () => {
// Delete existing errors
for (const key in errors) {
delete errors[key];
}
const validate = () => {
// Delete existing errors
for (const key in errors) {
delete errors[key];
}
try {
Validator.validate(method['input'], input);
} catch (e) {
if (!(e instanceof ValidationError)) {
throw e;
}
try {
Validator.validate(method['input'], input);
} catch (e) {
if (!(e instanceof ValidationError)) {
throw e;
}
// Map violation errors to errors rune
for (const violation of e.violations) {
for (const field of violation.field) {
if (!('localName' in field)) {
continue;
}
// Map violation errors to errors rune
for (const violation of e.violations) {
for (const field of violation.field) {
if (!("localName" in field)) {
continue;
}
// Create localName property if it doesn't exist
if (!errors[field.localName]) {
Object.assign(errors, {
[field.localName]: [violation]
});
} else {
errors[field.localName]?.push(violation);
}
}
}
// Create localName property if it doesn't exist
if (!errors[field.localName]) {
Object.assign(errors, {
[field.localName]: [violation]
})
} else {
errors[field.localName]?.push(violation);
}
}
}
return e.violations;
}
return e.violations;
}
return [];
};
return [];
};
// When a request is successful
const success = (response: MessageShape<Method['output']>) => {
loading = false;
// When a request is successful
const success = (response: MessageShape<Method['output']>) => {
loading = false;
// Send the response up
options?.onResult?.(response);
// Send the response up
options?.onResult?.(response);
// Set the response
Object.assign(output, response);
// Set the response
Object.assign(output, response);
// If we want to reset the input
if (options && (options.reset == undefined || options.reset)) {
const cleared = create(method.input as Method['input'], options?.init);
Object.assign(input, cleared);
}
};
// If we want to reset the input
if (options && (options.reset == undefined || options.reset)) {
const cleared = create(method.input as Method['input'], options?.init)
Object.assign(input, cleared);
}
}
// When a request fails
const fail = (err: Violation[] | ConnectError | Error) => {
loading = false;
// When a request fails
const fail = (err: Violation[] | ConnectError | any) => {
loading = false;
// It's a Violation[]
if (Array.isArray(err)) {
// Send the error up
options?.onError?.(err);
return;
}
// It's a Violation[]
if (Array.isArray(err)) {
// Send the error up
options?.onError?.(err);
return;
}
// It's a ConnectError
if (err instanceof ConnectError) {
// Assign it to the form
errors.form = err;
// It's a ConnectError
if (err instanceof ConnectError) {
// Assign it to the form
errors.form = err;
// Send the error up
options?.onError?.(err);
return;
}
// Send the error up
options?.onError?.(err);
return;
}
throw err;
};
throw err;
}
const submit = () => {
loading = true;
const submit = () => {
loading = true;
// Validate
const validationErrors = validate();
if (validationErrors.length > 0) {
fail(validationErrors);
return;
}
// Validate
const validationErrors = validate();
if (validationErrors.length > 0) {
fail(validationErrors);
return;
}
// Send response
if (method.methodKind == 'unary') {
// @ts-expect-error I can't figure out how to make this typescript compliant
const response = client[method.localName]($state.snapshot(input)) as Promise<
MessageShape<Method['output']>
>;
// Send response
if (method.methodKind == "unary") {
// @ts-ignore I can't figure out how to make this typescript compliant
const response = client[method.localName]($state.snapshot(input)) as Promise<MessageShape<Method['output']>>
response
.then((resp) => {
success(resp);
})
.catch((err) => {
fail(err);
});
}
};
response
.then((resp) => {
success(resp);
}).catch(err => {
fail(err);
});
}
}
// A nice action to give to forms to run submit() on submit
const impair: Action<HTMLFormElement> = (form) => {
$effect(() => {
form.onsubmit = (event) => {
event.preventDefault();
// A nice action to give to forms to run submit() on submit
const impair: Action<HTMLFormElement> = (form) => {
$effect(() => {
form.onsubmit = (event) => {
event.preventDefault();
if (options?.onSubmit) {
const formData = new FormData(form);
options.onSubmit(formData, input).then((i) => {
Object.assign(input, i);
submit();
});
if (options?.onSubmit) {
const formData = new FormData(form);
options.onSubmit(formData, input).then((i) => {
Object.assign(input, i);
submit();
});
return;
}
return;
}
submit();
};
submit();
}
return () => {
form.onsubmit = () => {};
};
});
};
return () => {
form.onsubmit = () => { };
};
});
};
if (options?.start) {
submit();
}
if (options?.start) {
submit();
}
return {
input,
output,
errors,
loading: () => loading,
submit,
validate,
impair
};
return {
input,
output,
errors,
loading: () => loading,
submit,
validate,
impair
}
}

View File

@ -1,4 +1,7 @@
import { newState } from './conststate.svelte';
import { coolForm } from './coolforms.svelte';
import { coolForm } from "./coolforms.svelte";
import { newState } from "./conststate.svelte";
export { coolForm, newState };
export {
coolForm,
newState
};

View File

@ -1,12 +1,11 @@
import type { Interceptor } from '@connectrpc/connect';
import { createValidator } from '@bufbuild/protovalidate';
import { Code, ConnectError, createClient } from '@connectrpc/connect';
import { createConnectTransport } from '@connectrpc/connect-web';
import { goto } from '$app/navigation';
import { page } from '$app/state';
import { ItemService } from '$lib/connect/item/v1/item_pb';
import { createValidator } from "@bufbuild/protovalidate";
import { Code, ConnectError, createClient, type Interceptor } from '@connectrpc/connect';
import { AuthService } from '$lib/connect/user/v1/auth_pb';
import { UserService } from '$lib/connect/user/v1/user_pb';
import { ItemService } from '$lib/connect/item/v1/item_pb';
import { goto } from '$app/navigation';
import { page } from '$app/state';
const redirector: Interceptor = (next) => async (req) => {
try {
@ -16,10 +15,7 @@ const redirector: Interceptor = (next) => async (req) => {
if (error.code === Code.Unauthenticated) {
const redirectURL = new URL(page.url);
redirectURL.pathname = '/auth';
redirectURL.searchParams.append(
'redir',
encodeURIComponent(page.url.pathname + page.url.search)
);
redirectURL.searchParams.append('redir', encodeURIComponent(page.url.pathname + page.url.search));
await goto(redirectURL);
}

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { Avatar as AvatarPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
@ -13,7 +13,7 @@
bind:ref
data-slot="avatar-fallback"
class={cn(
'bg-surface flex size-full items-center justify-center rounded-full text-sm transition-all select-none',
'bg-surface outline-surface-2 flex size-full select-none items-center justify-center rounded-full text-sm transition-all',
className
)}
{...restProps}

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { Avatar as AvatarPrimitive } from 'bits-ui';
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
@ -12,6 +12,6 @@
<AvatarPrimitive.Image
bind:ref
data-slot="avatar-image"
class={cn('aspect-square size-full', className)}
class={cn("aspect-square size-full", className)}
{...restProps}
/>

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { Avatar as AvatarPrimitive } from 'bits-ui';
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
@ -12,10 +12,6 @@
<AvatarPrimitive.Root
bind:ref
data-slot="avatar"
class={cn(
'outline-surface-1 relative flex size-9 shrink-0 overflow-hidden rounded-full shadow-xs outline outline-offset-2',
className
)}
class={cn("relative outline outline-offset-2 outline-surface-1 flex size-9 shrink-0 overflow-hidden rounded-full", className)}
{...restProps}
/>

View File

@ -1,6 +1,6 @@
import Fallback from './avatar-fallback.svelte';
import Image from './avatar-image.svelte';
import Root from './avatar.svelte';
import Root from "./avatar.svelte";
import Image from "./avatar-image.svelte";
import Fallback from "./avatar-fallback.svelte";
export {
Root,
@ -9,5 +9,5 @@ export {
//
Root as Avatar,
Image as AvatarImage,
Fallback as AvatarFallback
Fallback as AvatarFallback,
};

View File

@ -1,14 +1,13 @@
<script lang="ts" module>
import type { WithElementRef } from 'bits-ui';
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
import type { VariantProps } from 'tailwind-variants';
import { LoaderCircle } from '@lucide/svelte';
import { type VariantProps, tv } from 'tailwind-variants';
import { cn } from '$lib/utils';
import { tv } from 'tailwind-variants';
import { LoaderCircle } from '@lucide/svelte';
export const buttonVariants = tv({
base: cn(
'inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all',
'shadow-xs inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all',
// Focus
'focus-visible:outline-accent focus-visible:outline-2 focus-visible:outline-offset-2',
@ -17,15 +16,15 @@
'disabled:pointer-events-none disabled:opacity-50',
// Images
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-5"
"[&_svg:not([class*='size-'])]:size-5 [&_svg]:pointer-events-none [&_svg]:shrink-0"
),
variants: {
variant: {
default: 'text-crust bg-accent hover:bg-accent/90 shadow-xs',
red: 'text-crust bg-red hover:bg-red/90 shadow-xs',
outline: 'text-text border-surface-1 hover:bg-surface border bg-transparent shadow-xs',
input: 'text-text border-surface-1 hover:border-overlay border bg-transparent shadow-xs',
ghost: 'text-text hover:bg-surface'
outline: 'text-text border-surface-1 hover:bg-surface shadow-xs border bg-transparent',
input: 'text-text border-surface-1 hover:border-overlay shadow-xs border bg-transparent',
ghost: 'text-text hover:bg-surface shadow-xs'
},
size: {
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
@ -48,6 +47,7 @@
variant?: ButtonVariant;
size?: ButtonSize;
loading?: boolean;
scan?: boolean;
};
</script>
@ -60,6 +60,7 @@
href = undefined,
type = 'button',
loading,
scan,
children,
...restProps
}: ButtonProps = $props();

View File

@ -1,5 +1,9 @@
import type { ButtonProps, ButtonSize, ButtonVariant } from './button.svelte';
import Root, { buttonVariants } from './button.svelte';
import Root, {
type ButtonProps,
type ButtonSize,
type ButtonVariant,
buttonVariants,
} from "./button.svelte";
export {
Root,
@ -9,5 +13,5 @@ export {
buttonVariants,
type ButtonProps,
type ButtonSize,
type ButtonVariant
type ButtonVariant,
};

View File

@ -1,7 +1,7 @@
<script lang="ts">
import { cn } from '$lib/utils';
import type { WithElementRef } from 'bits-ui';
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils';
type Props = WithElementRef<HTMLAttributes<HTMLDivElement>>;

View File

@ -1,7 +1,7 @@
import Root from './card.svelte';
import Root from "./card.svelte";
export {
Root,
//
Root as Card
Root as Card,
};

View File

@ -1,12 +1,16 @@
<script lang="ts">
import type { DateValue } from '@internationalized/date';
import type { DateRange } from 'bits-ui';
import { buttonVariants } from '$lib/ui/button';
import * as Popover from '$lib/ui/popover';
import { RangeCalendar } from '$lib/ui/range-calendar';
import { CalendarDate, DateFormatter, getLocalTimeZone } from '@internationalized/date';
import CalendarIcon from '@lucide/svelte/icons/calendar';
import type { DateRange } from 'bits-ui';
import {
CalendarDate,
DateFormatter,
type DateValue,
getLocalTimeZone
} from '@internationalized/date';
import { cn } from '$lib/utils.js';
import { buttonVariants } from '$lib/ui/button';
import { RangeCalendar } from '$lib/ui/range-calendar';
import * as Popover from '$lib/ui/popover';
let {
className,
@ -37,11 +41,7 @@
<div class={cn('grid gap-2', className)}>
<Popover.Root>
<Popover.Trigger
class={cn(
buttonVariants({ variant: 'input' }),
'bg-based text-md md:text-sm',
!value && 'text-subtext'
)}
class={cn(buttonVariants({ variant: 'input' }), 'bg-based', !value && 'text-subtext')}
>
<CalendarIcon class="mr-2 size-4" />
{#if value && value.start}
@ -58,7 +58,7 @@
Pick a date range
{/if}
</Popover.Trigger>
<Popover.Content class="bg-based w-auto p-0" align="start">
<Popover.Content class="w-auto p-0 bg-based" align="start">
<RangeCalendar
bind:value
onStartValueChange={(v) => {

View File

@ -1,3 +1,6 @@
import Root from './daterangepicker.svelte';
import Root from "./daterangepicker.svelte";
export { Root, Root as DateRangePicker };
export {
Root,
Root as DateRangePicker
};

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from 'bits-ui';
import { Dialog as DialogPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: DialogPrimitive.CloseProps = $props();
</script>

View File

@ -1,10 +1,9 @@
<script lang="ts">
import type { WithoutChildrenOrChild } from 'bits-ui';
import type { Snippet } from 'svelte';
import { Dialog as DialogPrimitive, type WithoutChildrenOrChild } from 'bits-ui';
import X from '@lucide/svelte/icons/x';
import { cn } from '$lib/utils.js';
import { Dialog as DialogPrimitive } from 'bits-ui';
import type { Snippet } from 'svelte';
import * as Dialog from './index.js';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
@ -35,7 +34,7 @@
{@render children?.()}
<DialogPrimitive.Close
class={cn(
'text-text hover:bg-crust absolute top-4 right-4 cursor-pointer rounded p-1 transition-all disabled:pointer-events-none',
'text-text absolute top-4 right-4 cursor-pointer p-1 rounded hover:bg-crust transition-all disabled:pointer-events-none',
// Focus
'focus-visible:outline-accent focus-visible:outline-2 focus-visible:outline-offset-2',

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { Dialog as DialogPrimitive } from 'bits-ui';
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
@ -12,6 +12,6 @@
<DialogPrimitive.Description
bind:ref
data-slot="dialog-description"
class={cn('text-muted-foreground text-sm', className)}
class={cn("text-muted-foreground text-sm", className)}
{...restProps}
/>

View File

@ -1,7 +1,7 @@
<script lang="ts">
import type { WithElementRef } from 'bits-ui';
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils.js';
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
@ -14,7 +14,7 @@
<div
bind:this={ref}
data-slot="dialog-footer"
class={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
class={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
{...restProps}
>
{@render children?.()}

View File

@ -1,7 +1,7 @@
<script lang="ts">
import type { WithElementRef } from 'bits-ui';
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils.js';
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
@ -14,7 +14,7 @@
<div
bind:this={ref}
data-slot="dialog-header"
class={cn('flex flex-col gap-2 text-center sm:text-left', className)}
class={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...restProps}
>
{@render children?.()}

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { Dialog as DialogPrimitive } from 'bits-ui';
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
@ -13,7 +13,7 @@
bind:ref
data-slot="dialog-overlay"
class={cn(
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...restProps}

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { Dialog as DialogPrimitive } from 'bits-ui';
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
@ -12,6 +12,6 @@
<DialogPrimitive.Title
bind:ref
data-slot="dialog-title"
class={cn('text-lg leading-none font-semibold', className)}
class={cn("text-lg font-semibold leading-none", className)}
{...restProps}
/>

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from 'bits-ui';
import { Dialog as DialogPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: DialogPrimitive.TriggerProps = $props();
</script>

View File

@ -1,13 +1,14 @@
import { Dialog as DialogPrimitive } from 'bits-ui';
import Close from './dialog-close.svelte';
import Content from './dialog-content.svelte';
import Description from './dialog-description.svelte';
import Footer from './dialog-footer.svelte';
import Header from './dialog-header.svelte';
import Overlay from './dialog-overlay.svelte';
import Title from './dialog-title.svelte';
import Trigger from './dialog-trigger.svelte';
import { Dialog as DialogPrimitive } from "bits-ui";
import Root from './dialog.svelte';
import Title from "./dialog-title.svelte";
import Footer from "./dialog-footer.svelte";
import Header from "./dialog-header.svelte";
import Overlay from "./dialog-overlay.svelte";
import Content from "./dialog-content.svelte";
import Description from "./dialog-description.svelte";
import Trigger from "./dialog-trigger.svelte";
import Close from "./dialog-close.svelte";
const Portal = DialogPrimitive.Portal;
@ -32,5 +33,5 @@ export {
Overlay as DialogOverlay,
Content as DialogContent,
Description as DialogDescription,
Close as DialogClose
Close as DialogClose,
};

View File

@ -1,10 +1,9 @@
<script lang="ts">
import type { WithoutChildrenOrChild } from 'bits-ui';
import type { Snippet } from 'svelte';
import Check from '@lucide/svelte/icons/check';
import Minus from '@lucide/svelte/icons/minus';
import { cn } from '$lib/utils.js';
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import { DropdownMenu as DropdownMenuPrimitive, type WithoutChildrenOrChild } from "bits-ui";
import Check from "@lucide/svelte/icons/check";
import Minus from "@lucide/svelte/icons/minus";
import { cn } from "$lib/utils.js";
import type { Snippet } from "svelte";
let {
ref = $bindable(null),
@ -24,13 +23,13 @@
bind:indeterminate
data-slot="dropdown-menu-checkbox-item"
class={cn(
'focus:bg-surface relative flex cursor-pointer items-center gap-2 rounded-sm py-2 pr-2 pl-8 text-sm outline-hidden select-none',
"focus:bg-surface outline-hidden relative flex cursor-pointer select-none items-center gap-2 rounded-sm py-2 pl-8 pr-2 text-sm",
// Disabled
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
// Images
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...restProps}
@ -40,7 +39,7 @@
{#if indeterminate}
<Minus class="size-4" />
{:else}
<Check class={cn('size-4', !checked && 'text-transparent')} />
<Check class={cn("size-4", !checked && "text-transparent")} />
{/if}
</span>
{@render childrenProp?.()}

View File

@ -12,6 +12,6 @@
<DropdownMenuPrimitive.Group
bind:ref
data-slot="dropdown-menu-group"
class={cn('border-surface border-b first:pt-0 last:border-none last:pb-0', className)}
class={cn('border-b border-surface first:pt-0 last:pb-0 last:border-none', className)}
{...restProps}
/>

View File

@ -1,7 +1,7 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils.js';
import { type WithElementRef } from 'bits-ui';
import { cn } from "$lib/utils.js";
import { type WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
@ -17,7 +17,7 @@
<div
bind:this={ref}
data-slot="dropdown-menu-label"
class={cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', className)}
class={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...restProps}
>
{@render children?.()}

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let {
ref = $bindable(null),

View File

@ -1,8 +1,7 @@
<script lang="ts">
import type { WithoutChild } from 'bits-ui';
import { DropdownMenu as DropdownMenuPrimitive, type WithoutChild } from 'bits-ui';
import Circle from '@lucide/svelte/icons/circle';
import { cn } from '$lib/utils.js';
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
@ -16,10 +15,10 @@
bind:ref
data-slot="dropdown-menu-radio-item"
class={cn(
'focus:bg-surface text-text relative flex cursor-pointer items-center gap-2 py-2 pr-2 pl-8 text-sm select-none',
"focus:bg-surface text-text relative flex cursor-pointer items-center gap-2 py-2 pr-2 pl-8 text-sm select-none",
// Disabled
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
// Images
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
@ -12,6 +12,6 @@
<DropdownMenuPrimitive.Separator
bind:ref
data-slot="dropdown-menu-separator"
class={cn('bg-surface-1 -mx-1 my-1 h-px', className)}
class={cn("bg-surface-1 -mx-1 my-1 h-px", className)}
{...restProps}
/>

View File

@ -1,7 +1,7 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils.js';
import { type WithElementRef } from 'bits-ui';
import type { HTMLAttributes } from "svelte/elements";
import { type WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
@ -14,7 +14,7 @@
<span
bind:this={ref}
data-slot="dropdown-menu-shortcut"
class={cn('text-text ml-auto text-xs tracking-widest', className)}
class={cn("text-text ml-auto text-xs tracking-widest", className)}
{...restProps}
>
{@render children?.()}

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
@ -13,7 +13,7 @@
bind:ref
data-slot="dropdown-menu-sub-content"
class={cn(
'bg-based text-text z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',
'bg-based text-text origin-(--radix-dropdown-menu-content-transform-origin) z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg',
// Animations
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',

View File

@ -1,7 +1,7 @@
<script lang="ts">
import ChevronRight from '@lucide/svelte/icons/chevron-right';
import { cn } from '$lib/utils.js';
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import ChevronRight from "@lucide/svelte/icons/chevron-right";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
@ -18,8 +18,8 @@
bind:ref
data-slot="dropdown-menu-sub-trigger"
class={cn(
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none',
inset && 'pl-8',
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground outline-hidden flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm",
inset && "pl-8",
className
)}
{...restProps}

View File

@ -1,17 +1,17 @@
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import CheckboxItem from './dropdown-menu-checkbox-item.svelte';
import Content from './dropdown-menu-content.svelte';
import Group from './dropdown-menu-group.svelte';
import Item from './dropdown-menu-item.svelte';
import Label from './dropdown-menu-label.svelte';
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import CheckboxItem from "./dropdown-menu-checkbox-item.svelte";
import Content from "./dropdown-menu-content.svelte";
import Group from "./dropdown-menu-group.svelte";
import Item from "./dropdown-menu-item.svelte";
import Link from './dropdown-menu-link.svelte';
import RadioGroup from './dropdown-menu-radio-group.svelte';
import RadioItem from './dropdown-menu-radio-item.svelte';
import Separator from './dropdown-menu-separator.svelte';
import Shortcut from './dropdown-menu-shortcut.svelte';
import SubContent from './dropdown-menu-sub-content.svelte';
import SubTrigger from './dropdown-menu-sub-trigger.svelte';
import Trigger from './dropdown-menu-trigger.svelte';
import Label from "./dropdown-menu-label.svelte";
import RadioGroup from "./dropdown-menu-radio-group.svelte";
import RadioItem from "./dropdown-menu-radio-item.svelte";
import Separator from "./dropdown-menu-separator.svelte";
import Shortcut from "./dropdown-menu-shortcut.svelte";
import Trigger from "./dropdown-menu-trigger.svelte";
import SubContent from "./dropdown-menu-sub-content.svelte";
import SubTrigger from "./dropdown-menu-sub-trigger.svelte";
const Sub = DropdownMenuPrimitive.Sub;
const Root = DropdownMenuPrimitive.Root;
@ -46,5 +46,5 @@ export {
Sub,
SubContent,
SubTrigger,
Trigger
Trigger,
};

View File

@ -1,31 +1,31 @@
import { getContext, hasContext, setContext } from 'svelte';
type Item = {
id: string;
name: string;
};
id: string;
name: string;
}
const key = 'form';
export function setFormContext(id: string, name: string) {
const item = getFormContext();
if (!item) {
const item: Item = $state({
id,
name
});
setContext(key, item);
const item = getFormContext();
if (!item) {
const item: Item = $state({
id,
name,
});
setContext(key, item);
return;
}
return;
}
item.id = id;
item.name = name;
item.id = id;
item.name = name;
}
export function getFormContext() {
if (!hasContext(key)) {
return null;
}
if (!hasContext(key)) {
return null;
}
return getContext(key) as Item;
return getContext(key) as Item;
}

View File

@ -1,10 +1,10 @@
<script lang="ts">
import type { Violation } from '@bufbuild/protovalidate';
import type { ConnectError } from '@connectrpc/connect';
import type { WithElementRef, WithoutChildren } from 'bits-ui';
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils';
import { getFormContext } from './context.svelte';
import type { WithElementRef, WithoutChildren } from 'bits-ui';
import type { HTMLAttributes } from 'svelte/elements';
import type { Violation } from '@bufbuild/protovalidate';
import type { ConnectError } from '@connectrpc/connect';
type Props = WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> & {
errors?: Violation[] | ConnectError;
@ -21,7 +21,7 @@
<div bind:this={ref} class={cn('text-red text-sm', className)} {...restProps}>
{#if errors && Array.isArray(errors)}
{#each errors as error (error)}
{#each errors as error}
<label for={item?.id}>{error.message}</label>
{/each}
{:else if errors}

View File

@ -1,8 +1,8 @@
<script lang="ts">
import type { WithElementRef } from 'bits-ui';
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils';
import { setFormContext } from './context.svelte';
import type { WithElementRef } from 'bits-ui';
import type { HTMLAttributes } from 'svelte/elements';
type Props = WithElementRef<HTMLAttributes<HTMLDivElement>> & {
name?: string;
@ -16,6 +16,6 @@
}
</script>
<div bind:this={ref} class={cn('flex flex-col gap-1', className)} {...restProps}>
<div bind:this={ref} class={cn('flex flex-col gap-1')} {...restProps}>
{@render children?.()}
</div>

View File

@ -1,5 +1,9 @@
import Errors from './errors.svelte';
import Field from './field.svelte';
import Errors from './errors.svelte';
import Label from './label.svelte';
export { Field, Errors, Label };
export {
Field,
Errors,
Label
};

View File

@ -1,8 +1,8 @@
<script lang="ts">
import type { WithElementRef } from 'bits-ui';
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils';
import { getFormContext } from './context.svelte';
import type { WithElementRef } from 'bits-ui';
import type { HTMLAttributes } from 'svelte/elements';
type Props = WithElementRef<HTMLAttributes<HTMLLabelElement>>;
let { ref = $bindable(null), class: className, children, ...restProps }: Props = $props();

View File

@ -1,7 +1,7 @@
import Root from './input.svelte';
import Root from "./input.svelte";
export {
Root,
//
Root as Input
Root as Input,
};

View File

@ -1,6 +1,6 @@
<script lang="ts">
import type { WithElementRef } from 'bits-ui';
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from 'svelte/elements';
import type { WithElementRef } from 'bits-ui';
import { cn } from '$lib/utils.js';
import { getFormContext } from '../form/context.svelte';
@ -8,7 +8,9 @@
type Props = WithElementRef<
Omit<HTMLInputAttributes, 'type'> &
({ type: 'file'; files?: FileList } | { type?: InputType; files?: undefined })
({ type: 'file'; files?: FileList } | { type?: InputType; files?: undefined }) & {
scan?: boolean;
}
>;
let {
@ -17,6 +19,7 @@
type,
files = $bindable(),
class: className,
scan,
id,
name,
...restProps
@ -37,7 +40,7 @@
{name}
bind:this={ref}
class={cn(
'border-surface-1 file:bg-surface hover:border-overlay placeholder:text-subtext text-text flex h-9 w-full min-w-0 cursor-pointer rounded-md border text-sm font-medium shadow-xs transition-all file:mr-2 file:px-3 file:py-2 md:text-sm',
'border-surface-1 file:bg-surface hover:border-overlay placeholder:text-subtext text-text shadow-xs flex h-9 w-full min-w-0 cursor-pointer rounded-md border text-sm font-medium transition-all file:mr-2 file:px-3 file:py-2 md:text-sm',
// Focus
'focus-visible:outline-accent focus-visible:outline-2 focus-visible:outline-offset-2',
@ -58,7 +61,7 @@
{name}
bind:this={ref}
class={cn(
'border-surface-1 hover:border-overlay placeholder:text-subtext text-text flex h-9 w-full min-w-0 rounded-md border px-3 py-1 shadow-xs transition-all md:text-sm',
'border-surface-1 hover:border-overlay placeholder:text-subtext text-text shadow-xs flex h-9 w-full min-w-0 rounded-md border px-3 py-1 transition-all md:text-sm',
// Focus
'focus-visible:outline-accent focus-visible:outline-2 focus-visible:outline-offset-2',

View File

@ -1,7 +1,7 @@
import Root from './label.svelte';
import Root from "./label.svelte";
export {
Root,
//
Root as Label
Root as Label,
};

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { Label as LabelPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
@ -13,7 +13,7 @@
bind:ref
data-slot="label"
class={cn(
'text-text flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
'flex items-center gap-2 text-sm text-text leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
className
)}
{...restProps}

View File

@ -1,3 +1,5 @@
import Pager from './pager.svelte';
import Pager from './pager.svelte'
export { Pager };
export {
Pager
}

View File

@ -1,10 +1,10 @@
import Content from './pagination-content.svelte';
import Ellipsis from './pagination-ellipsis.svelte';
import Item from './pagination-item.svelte';
import Link from './pagination-link.svelte';
import NextButton from './pagination-next-button.svelte';
import PrevButton from './pagination-prev-button.svelte';
import Root from './pagination.svelte';
import Root from "./pagination.svelte";
import Content from "./pagination-content.svelte";
import Item from "./pagination-item.svelte";
import Link from "./pagination-link.svelte";
import PrevButton from "./pagination-prev-button.svelte";
import NextButton from "./pagination-next-button.svelte";
import Ellipsis from "./pagination-ellipsis.svelte";
export {
Root,
@ -21,5 +21,5 @@ export {
Link as PaginationLink,
PrevButton as PaginationPrevButton,
NextButton as PaginationNextButton,
Ellipsis as PaginationEllipsis
Ellipsis as PaginationEllipsis,
};

View File

@ -1,7 +1,7 @@
<script lang="ts">
import type { WithElementRef } from 'bits-ui';
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils.js';
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
@ -14,7 +14,7 @@
<ul
bind:this={ref}
data-slot="pagination-content"
class={cn('flex flex-row items-center gap-1', className)}
class={cn("flex flex-row items-center gap-1", className)}
{...restProps}
>
{@render children?.()}

View File

@ -1,8 +1,8 @@
<script lang="ts">
import type { WithElementRef, WithoutChildren } from 'bits-ui';
import type { HTMLAttributes } from 'svelte/elements';
import Ellipsis from '@lucide/svelte/icons/ellipsis';
import { cn } from '$lib/utils.js';
import Ellipsis from "@lucide/svelte/icons/ellipsis";
import type { WithElementRef, WithoutChildren } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
@ -15,7 +15,7 @@
bind:this={ref}
aria-hidden="true"
data-slot="pagination-ellipsis"
class={cn('text-text flex size-9 items-center justify-center', className)}
class={cn("flex size-9 items-center justify-center text-text", className)}
{...restProps}
>
<Ellipsis class="size-4" />

View File

@ -1,6 +1,6 @@
<script lang="ts">
import type { WithElementRef } from 'bits-ui';
import type { HTMLLiAttributes } from 'svelte/elements';
import type { HTMLLiAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
let {
ref = $bindable(null),

View File

@ -1,13 +1,12 @@
<script lang="ts">
import type { Props } from '$lib/ui/button';
import { buttonVariants } from '$lib/ui/button';
import { cn } from '$lib/utils.js';
import { Pagination as PaginationPrimitive } from 'bits-ui';
import { Pagination as PaginationPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { type Props, buttonVariants } from "$lib/ui/button";
let {
ref = $bindable(null),
class: className,
size = 'icon',
size = "icon",
isActive = false,
page,
children,
@ -25,16 +24,16 @@
<PaginationPrimitive.Page
bind:ref
{page}
aria-current={isActive ? 'page' : undefined}
aria-current={isActive ? "page" : undefined}
data-slot="pagination-link"
data-active={isActive}
class={cn(
buttonVariants({
variant: 'ghost',
size
variant: "ghost",
size,
}),
'text-text',
isActive && 'bg-surface',
isActive && 'bg-surface-1',
className
)}
children={children || Fallback}

View File

@ -1,8 +1,8 @@
<script lang="ts">
import { buttonVariants } from '$lib/ui/button/index.js';
import ChevronRight from '@lucide/svelte/icons/chevron-right';
import { cn } from '$lib/utils.js';
import { Pagination as PaginationPrimitive } from 'bits-ui';
import { Pagination as PaginationPrimitive } from "bits-ui";
import ChevronRight from "@lucide/svelte/icons/chevron-right";
import { buttonVariants } from "$lib/ui/button/index.js";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
@ -23,9 +23,9 @@
aria-label="Go to next page"
class={cn(
buttonVariants({
size: 'default',
variant: 'ghost',
class: 'gap-1 px-2.5 sm:pr-2.5'
size: "default",
variant: "ghost",
class: "gap-1 px-2.5 sm:pr-2.5",
}),
className
)}

View File

@ -1,8 +1,8 @@
<script lang="ts">
import { buttonVariants } from '$lib/ui/button/index.js';
import ChevronLeft from '@lucide/svelte/icons/chevron-left';
import { cn } from '$lib/utils.js';
import { Pagination as PaginationPrimitive } from 'bits-ui';
import { Pagination as PaginationPrimitive } from "bits-ui";
import ChevronLeft from "@lucide/svelte/icons/chevron-left";
import { buttonVariants } from "$lib/ui/button/index.js";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
@ -22,9 +22,9 @@
aria-label="Go to previous page"
class={cn(
buttonVariants({
size: 'default',
variant: 'ghost',
class: 'gap-1 px-2.5 sm:pl-2.5'
size: "default",
variant: "ghost",
class: "gap-1 px-2.5 sm:pl-2.5",
}),
className
)}

View File

@ -1,8 +1,9 @@
<script lang="ts">
import { pushState } from '$app/navigation';
import { cn } from '$lib/utils.js';
import { Pagination as PaginationPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
import { pushState } from '$app/navigation';
let {
ref = $bindable(null),
class: className,
@ -17,7 +18,7 @@
<svelte:window
onpopstate={(state) => {
const sks = state.state['sveltekit:states'] as object | string;
const sks = state.state['sveltekit:states'] as {} | string;
if (typeof sks === 'string' && sks.includes('#pagination-')) {
page = Number(sks.split('#pagination-')[1]);
onPageChange?.(page);

View File

@ -1,7 +1,6 @@
import { Popover as PopoverPrimitive } from 'bits-ui';
import Content from './popover-content.svelte';
import Trigger from './popover-trigger.svelte';
import { Popover as PopoverPrimitive } from "bits-ui";
import Content from "./popover-content.svelte";
import Trigger from "./popover-trigger.svelte";
const Root = PopoverPrimitive.Root;
const Close = PopoverPrimitive.Close;
@ -14,5 +13,5 @@ export {
Root as Popover,
Content as PopoverContent,
Trigger as PopoverTrigger,
Close as PopoverClose
Close as PopoverClose,
};

View File

@ -1,12 +1,12 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { Popover as PopoverPrimitive } from 'bits-ui';
import { cn } from "$lib/utils.js";
import { Popover as PopoverPrimitive } from "bits-ui";
let {
ref = $bindable(null),
class: className,
sideOffset = 4,
align = 'center',
align = "center",
portalProps,
...restProps
}: PopoverPrimitive.ContentProps & {
@ -21,10 +21,10 @@
{sideOffset}
{align}
class={cn(
'bg-based text-text border-surface-1 z-50 w-72 rounded-md border p-4 shadow-md outline-hidden',
"bg-based text-text outline-hidden z-50 w-72 rounded-md border border-surface-1 p-4 shadow-md",
// Animation
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--bits-popover-content-transform-origin)',
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--bits-popover-content-transform-origin)",
className
)}
{...restProps}

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { Popover as PopoverPrimitive } from 'bits-ui';
import { cn } from "$lib/utils.js";
import { Popover as PopoverPrimitive } from "bits-ui";
let {
ref = $bindable(null),
@ -12,6 +12,6 @@
<PopoverPrimitive.Trigger
bind:ref
data-slot="popover-trigger"
class={cn('', className)}
class={cn("", className)}
{...restProps}
/>

View File

@ -1,15 +1,15 @@
import { RangeCalendar as RangeCalendarPrimitive } from 'bits-ui';
import Cell from './range-calendar-cell.svelte';
import Day from './range-calendar-day.svelte';
import GridRow from './range-calendar-grid-row.svelte';
import Grid from './range-calendar-grid.svelte';
import HeadCell from './range-calendar-head-cell.svelte';
import Header from './range-calendar-header.svelte';
import Heading from './range-calendar-heading.svelte';
import Months from './range-calendar-months.svelte';
import NextButton from './range-calendar-next-button.svelte';
import PrevButton from './range-calendar-prev-button.svelte';
import Root from './range-calendar.svelte';
import { RangeCalendar as RangeCalendarPrimitive } from "bits-ui";
import Root from "./range-calendar.svelte";
import Cell from "./range-calendar-cell.svelte";
import Day from "./range-calendar-day.svelte";
import Grid from "./range-calendar-grid.svelte";
import Header from "./range-calendar-header.svelte";
import Months from "./range-calendar-months.svelte";
import GridRow from "./range-calendar-grid-row.svelte";
import Heading from "./range-calendar-heading.svelte";
import HeadCell from "./range-calendar-head-cell.svelte";
import NextButton from "./range-calendar-next-button.svelte";
import PrevButton from "./range-calendar-prev-button.svelte";
const GridHead = RangeCalendarPrimitive.GridHead;
const GridBody = RangeCalendarPrimitive.GridBody;
@ -28,5 +28,5 @@ export {
NextButton,
PrevButton,
//
Root as RangeCalendar
Root as RangeCalendar,
};

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { RangeCalendar as RangeCalendarPrimitive } from 'bits-ui';
import { RangeCalendar as RangeCalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
@ -12,7 +12,7 @@
<RangeCalendarPrimitive.Cell
bind:ref
class={cn(
'[&:has([data-selected])]:bg-accent [&:has([data-selected][data-outside-month])]:bg-accent/50 relative size-9 p-0 text-center text-sm focus-within:relative focus-within:z-20 first:[&:has([data-selected])]:rounded-l-md last:[&:has([data-selected])]:rounded-r-md [&:has([data-selected][data-selection-end])]:rounded-r-md [&:has([data-selected][data-selection-start])]:rounded-l-md',
"[&:has([data-selected])]:bg-accent [&:has([data-selected][data-outside-month])]:bg-accent/50 relative size-9 p-0 text-center text-sm focus-within:relative focus-within:z-20 first:[&:has([data-selected])]:rounded-l-md last:[&:has([data-selected])]:rounded-r-md [&:has([data-selected][data-selection-end])]:rounded-r-md [&:has([data-selected][data-selection-start])]:rounded-l-md",
className
)}
{...restProps}

View File

@ -1,7 +1,7 @@
<script lang="ts">
import { RangeCalendar as RangeCalendarPrimitive } from 'bits-ui';
import { buttonVariants } from '$lib/ui/button/index.js';
import { cn } from '$lib/utils.js';
import { RangeCalendar as RangeCalendarPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
@ -17,7 +17,7 @@
class={cn(
buttonVariants({ variant: 'ghost' }),
'size-9 p-0 font-normal',
'[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-crust [&[data-today]:not([data-selected])]:rounded-md',
'[&[data-today]:not([data-selected])]:bg-blue [&[data-today]:not([data-selected])]:text-crust [&[data-today]:not([data-selected])]:rounded-md',
// Selected
'data-[selected]:bg-surface data-[selected]:hover:bg-surface-1 data-[selected]:rounded-none data-[selected]:opacity-100',
// Selection Start

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { RangeCalendar as RangeCalendarPrimitive } from 'bits-ui';
import { RangeCalendar as RangeCalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
@ -9,4 +9,4 @@
}: RangeCalendarPrimitive.GridRowProps = $props();
</script>
<RangeCalendarPrimitive.GridRow bind:ref class={cn('flex', className)} {...restProps} />
<RangeCalendarPrimitive.GridRow bind:ref class={cn("flex", className)} {...restProps} />

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { RangeCalendar as RangeCalendarPrimitive } from 'bits-ui';
import { RangeCalendar as RangeCalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
@ -11,6 +11,6 @@
<RangeCalendarPrimitive.Grid
bind:ref
class={cn('w-full border-collapse space-y-1', className)}
class={cn("w-full border-collapse space-y-1", className)}
{...restProps}
/>

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { RangeCalendar as RangeCalendarPrimitive } from 'bits-ui';
import { RangeCalendar as RangeCalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
@ -11,6 +11,6 @@
<RangeCalendarPrimitive.HeadCell
bind:ref
class={cn('text-muted-foreground w-9 rounded-md text-[0.8rem] font-normal', className)}
class={cn("text-muted-foreground w-9 rounded-md text-[0.8rem] font-normal", className)}
{...restProps}
/>

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { RangeCalendar as RangeCalendarPrimitive } from 'bits-ui';
import { RangeCalendar as RangeCalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
@ -11,6 +11,6 @@
<RangeCalendarPrimitive.Header
bind:ref
class={cn('relative flex w-full items-center justify-between pt-1', className)}
class={cn("relative flex w-full items-center justify-between pt-1", className)}
{...restProps}
/>

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { RangeCalendar as RangeCalendarPrimitive } from 'bits-ui';
import { RangeCalendar as RangeCalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
@ -11,6 +11,6 @@
<RangeCalendarPrimitive.Heading
bind:ref
class={cn('text-sm font-medium', className)}
class={cn("text-sm font-medium", className)}
{...restProps}
/>

View File

@ -1,7 +1,7 @@
<script lang="ts">
import type { WithElementRef } from 'bits-ui';
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils.js';
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
@ -13,7 +13,7 @@
<div
bind:this={ref}
class={cn('mt-4 flex flex-col space-y-4 sm:flex-row sm:space-y-0 sm:space-x-4', className)}
class={cn("mt-4 flex flex-col space-y-4 sm:flex-row sm:space-x-4 sm:space-y-0", className)}
{...restProps}
>
{@render children?.()}

View File

@ -1,8 +1,8 @@
<script lang="ts">
import { buttonVariants } from '$lib/ui/button/index.js';
import ChevronRight from '@lucide/svelte/icons/chevron-right';
import { cn } from '$lib/utils.js';
import { RangeCalendar as RangeCalendarPrimitive } from 'bits-ui';
import { RangeCalendar as RangeCalendarPrimitive } from "bits-ui";
import ChevronRight from "@lucide/svelte/icons/chevron-right";
import { buttonVariants } from "$lib/ui/button/index.js";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
@ -19,8 +19,8 @@
<RangeCalendarPrimitive.NextButton
bind:ref
class={cn(
buttonVariants({ variant: 'outline' }),
'size-7 bg-transparent p-0 opacity-50 hover:opacity-100',
buttonVariants({ variant: "outline" }),
"size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
className
)}
children={children || Fallback}

View File

@ -1,8 +1,8 @@
<script lang="ts">
import { buttonVariants } from '$lib/ui/button/index.js';
import ChevronLeft from '@lucide/svelte/icons/chevron-left';
import { cn } from '$lib/utils.js';
import { RangeCalendar as RangeCalendarPrimitive } from 'bits-ui';
import { RangeCalendar as RangeCalendarPrimitive } from "bits-ui";
import ChevronLeft from "@lucide/svelte/icons/chevron-left";
import { buttonVariants } from "$lib/ui/button/index.js";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
@ -19,8 +19,8 @@
<RangeCalendarPrimitive.PrevButton
bind:ref
class={cn(
buttonVariants({ variant: 'outline' }),
'size-7 bg-transparent p-0 opacity-50 hover:opacity-100',
buttonVariants({ variant: "outline" }),
"size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
className
)}
children={children || Fallback}

View File

@ -1,14 +1,13 @@
<script lang="ts">
import type { WithoutChildrenOrChild } from 'bits-ui';
import { cn } from '$lib/utils.js';
import { RangeCalendar as RangeCalendarPrimitive } from 'bits-ui';
import * as RangeCalendar from './index.js';
import { RangeCalendar as RangeCalendarPrimitive, type WithoutChildrenOrChild } from "bits-ui";
import * as RangeCalendar from "./index.js";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
value = $bindable(),
placeholder = $bindable(),
weekdayFormat = 'short',
weekdayFormat = "short",
class: className,
...restProps
}: WithoutChildrenOrChild<RangeCalendarPrimitive.RootProps> = $props();
@ -19,7 +18,7 @@
bind:value
bind:placeholder
{weekdayFormat}
class={cn('p-3', className)}
class={cn("p-3", className)}
{...restProps}
>
{#snippet children({ months, weekdays })}

View File

@ -1,10 +1,11 @@
import { Select as SelectPrimitive } from 'bits-ui';
import Content from './select-content.svelte';
import Group from './select-group.svelte';
import Item from './select-item.svelte';
import Label from './select-label.svelte';
import Separator from './select-separator.svelte';
import Trigger from './select-trigger.svelte';
import { Select as SelectPrimitive } from "bits-ui";
import Group from "./select-group.svelte";
import Label from "./select-label.svelte";
import Item from "./select-item.svelte";
import Content from "./select-content.svelte";
import Trigger from "./select-trigger.svelte";
import Separator from "./select-separator.svelte";
const Root = SelectPrimitive.Root;
@ -23,5 +24,5 @@ export {
Item as SelectItem,
Content as SelectContent,
Trigger as SelectTrigger,
Separator as SelectSeparator
Separator as SelectSeparator,
};

View File

@ -1,7 +1,6 @@
<script lang="ts">
import type { WithoutChild } from 'bits-ui';
import { Select as SelectPrimitive, type WithoutChild } from 'bits-ui';
import { cn } from '$lib/utils.js';
import { Select as SelectPrimitive } from 'bits-ui';
let {
ref = $bindable(null),

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { Select as SelectPrimitive } from 'bits-ui';
import { Select as SelectPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: SelectPrimitive.GroupProps = $props();
</script>

View File

@ -1,8 +1,7 @@
<script lang="ts">
import type { WithoutChild } from 'bits-ui';
import Check from '@lucide/svelte/icons/check';
import { Select as SelectPrimitive, type WithoutChild } from 'bits-ui';
import { cn } from '$lib/utils.js';
import { Select as SelectPrimitive } from 'bits-ui';
let {
ref = $bindable(null),

View File

@ -1,7 +1,7 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils.js';
import { type WithElementRef } from 'bits-ui';
import { cn } from "$lib/utils.js";
import { type WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
@ -14,7 +14,7 @@
<div
bind:this={ref}
data-slot="select-label"
class={cn('text-muted-foreground px-2 py-1.5 text-xs', className)}
class={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...restProps}
>
{@render children?.()}

View File

@ -1,7 +1,7 @@
<script lang="ts">
import type { Separator as SeparatorPrimitive } from 'bits-ui';
import { Separator } from '$lib/ui/separator/index.js';
import { cn } from '$lib/utils.js';
import type { Separator as SeparatorPrimitive } from "bits-ui";
import { Separator } from "$lib/ui/separator/index.js";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
@ -13,6 +13,6 @@
<Separator
bind:ref
data-slot="select-separator"
class={cn('bg-border pointer-events-none -mx-1 my-1 h-px', className)}
class={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...restProps}
/>

Some files were not shown because too many files have changed in this diff Show More