Compare commits

..

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

222 changed files with 4344 additions and 14964 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

13
.dockerignore Normal file
View File

@ -0,0 +1,13 @@
.direnv
.env
build
result
# Client
/client/node_modules
/client/.svelte-kit
# Server
/server/client
/server/tmp
/server/internal/handlers/client/client

1
.envrc
View File

@ -1,2 +1 @@
use flake
watch_file .scripts/*

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

View File

@ -1,73 +0,0 @@
name: Release
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 }}"
- run: >
nix build
.#trevstack-linux-amd64
.#trevstack-linux-arm64
.#trevstack-linux-arm
.#trevstack-windows-amd64
.#trevstack-darwin-amd64
.#trevstack-darwin-arm64
- uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
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: 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: Push images
uses: ./.actions/push
with:
server_url: ghcr.io
repository: ${{ github.repository }}
tag: ${{ github.ref_name }}

42
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,42 @@
name: Release Workflow
on:
push:
tags:
- '*'
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Nix
uses: nixbuild/nix-quick-install-action@v30
- name: Restore and save Nix store + go modules
uses: nix-community/cache-nix-action@v6
with:
primary-key: nix-${{ runner.os }}-${{ hashFiles('**/*.nix', '**/flake.lock') }}
restore-prefixes-first-match: nix-${{ runner.os }}-
gc-max-store-size-linux: 1G
purge: true
purge-prefixes: nix-${{ runner.os }}-
purge-created: 0
purge-primary-key: never
paths: |
~/.cache/go-build
~/go/pkg/mod
- name: Build
run: nix develop --command ts-build
- name: Create release
uses: softprops/action-gh-release@v2
with:
files: |-
build/**

View File

@ -1,40 +0,0 @@
name: Update
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
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"
- run: nix run .#update
- name: Create pull request
id: cpr
uses: peter-evans/create-pull-request@v7
with:
branch: update
title: update
body: automatic update
- name: Enable automerge
run: gh pr merge --merge --auto "${{ steps.cpr.outputs.pull-request-number }}"
env:
GH_TOKEN: ${{ secrets.PAT }}

16
.gitignore vendored
View File

@ -1,7 +1,13 @@
.direnv
.env
/docker-compose.*
/build/
build
result
# Nix
/result*
/.direnv/
# Client
/client/node_modules
/client/.svelte-kit
# Server
/server/client
/server/tmp
/server/internal/handlers/client/client

View File

@ -1,40 +0,0 @@
#!/usr/bin/env bash
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
echo "bumping client"
cd "${git_root}/client"
npm version "${next_version}"
git add package-lock.json
git add package.json
echo "bumping nix"
cd "${git_root}"
nix-update --flake --version "${next_version}" --subpackage client default
git add flake.nix
git commit -m "bump: v${version} -> v${next_version}"
git push origin main
git tag -a "v${next_version}" -m "bump: v${version} -> v${next_version}"
git push origin "v${next_version}"

View File

@ -1,49 +0,0 @@
#!/usr/bin/env bash
git_root=$(git rev-parse --show-toplevel)
updated=false
echo "updating nix flake"
cd "${git_root}"
nix flake update
if ! git diff --exit-code flake.lock; then
git add flake.lock
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
if ! git diff --exit-code package.json package-lock.json; then
git add package-lock.json
git add package.json
git commit -m "build(client): updated npm dependencies"
updated=true
fi
echo "updating server"
cd "${git_root}/server"
go get -u
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"
updated=true
fi
if [ "${updated}" = true ]; then
echo "updating nix hashes"
cd "${git_root}"
nix-update --flake --version=skip --subpackage client default
git add flake.nix
git commit -m "build(nix): updated nix hashes"
fi

View File

@ -1,79 +0,0 @@
apps:
buf:
color: "#cba6f7"
exts:
- proto
onstart: buf lint
onchange: buf lint && buf generate
eslint:
color: "#fab387"
dir: client
exts:
- js
- ts
- svelte
onstart: npx eslint .
onchange: npx eslint .
golang:
color: "#89dceb"
dir: server
exts:
- go
onstart: go build -o ./tmp/app -tags dev cmd/trevstack/main.go && ./tmp/app
onchange: go build -o ./tmp/app -tags dev cmd/trevstack/main.go && ./tmp/app
nix:
color: "#74c7ec"
exts:
- nix
onstart: nix fmt .
onchange: nix fmt .
prettier:
color: "#fab387"
dir: client
exts:
- js
- ts
- svelte
onstart: npx prettier --check .
onchange: npx prettier --check .
revive:
color: "#89dceb"
dir: server
exts:
- go
onstart: revive -config revive.toml -set_exit_status ./...
onchange: revive -config revive.toml -set_exit_status ./...
sqlc:
color: "#a6e3a1"
dir: server
exts:
- sql
onstart: sqlc vet
onchange: sqlc vet && sqlc generate
sqlfluff:
color: "#a6e3a1"
dir: server/db
exts:
- sql
onstart: sqlfluff lint
onchange: sqlfluff lint
svelte:
color: "#fab387"
dir: client
exts:
- svelte
onstart: npx svelte-check
onchange: npx svelte-check
vite:
color: "#fab387"
dir: client
onstart: npx vite dev

View File

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

46
.vscode/settings.json vendored
View File

@ -1,46 +0,0 @@
{
"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"
}
}

28
Dockerfile Normal file
View File

@ -0,0 +1,28 @@
# 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 \
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/server"]

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

@ -3,8 +3,8 @@ servers:
- url: /grpc
info:
title: Trevstack API
version: 0.0.47
description: API for Trevstack
version: 1.0.0
description: API for trevstack
contact:
name: Trev
email: spam@trev.xyz
@ -15,4 +15,4 @@ components:
scheme: bearer
bearerFormat: JWT
security:
- bearerAuth: []
- bearerAuth: []

View File

@ -1,34 +1,27 @@
version: v2
clean: true
inputs:
- directory: proto
managed:
enabled: true
override:
- file_option: go_package_prefix
value: github.com/spotdemo4/trevstack/server/internal/connect
disable:
- file_option: go_package
module: buf.build/bufbuild/protovalidate
value: github.com/spotdemo4/trevstack/server/internal/services
plugins:
- local: protoc-gen-go
out: server/internal/connect
out: server/internal/services
opt: paths=source_relative
- local: protoc-gen-connect-go
out: server/internal/connect
out: server/internal/services
opt: paths=source_relative
- local: protoc-gen-es
out: client/src/lib/connect
out: client/src/lib/services
opt: target=ts
include_imports: true
- local: protoc-gen-connect-openapi
out: client/static/openapi
strategy: all
opt:
- base=openapi.yaml
- base=base.openapi.yaml
- path=openapi.yaml

View File

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

2
client/.gitignore vendored
View File

@ -1,2 +0,0 @@
/node_modules/
/.svelte-kit/

View File

@ -4,7 +4,6 @@ pnpm-lock.yaml
yarn.lock
# Build output
build
.svelte-kit
node_modules
@ -12,4 +11,4 @@ node_modules
static
# Generated
src/lib/connect
src/lib/services

View File

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

2108
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.8",
"type": "module",
"scripts": {
"dev": "vite dev",
@ -14,38 +14,33 @@
"lint": "prettier --check . && eslint ."
},
"devDependencies": {
"@bufbuild/protovalidate": "^0.1.1",
"@connectrpc/connect": "^2.0.2",
"@connectrpc/connect-web": "^2.0.2",
"@eslint/compat": "^1.2.9",
"@eslint/compat": "^1.2.7",
"@eslint/js": "^9.18.0",
"@ianvs/prettier-plugin-sort-imports": "^4.4.1",
"@lucide/svelte": "^0.479.0",
"@scalar/api-reference": "^1.28.34",
"@simplewebauthn/browser": "^13.1.0",
"@scalar/api-reference": "^1.28.5",
"@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/kit": "^2.21.1",
"@sveltejs/kit": "^2.20.1",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tailwindcss/vite": "^4.1.7",
"bits-ui": "^1.5.3",
"@tailwindcss/vite": "^4.0.14",
"bits-ui": "^1.3.13",
"clsx": "^2.1.1",
"eslint": "^9.27.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-svelte": "^3.8.1",
"globals": "^16.1.0",
"mode-watcher": "^1.0.7",
"eslint": "^9.22.0",
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-svelte": "^3.3.2",
"globals": "^16.0.0",
"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.23.2",
"svelte-check": "^4.1.5",
"svelte-sonner": "^0.3.28",
"tailwind-merge": "^3.3.0",
"tailwind-variants": "^1.0.0",
"tailwind-merge": "^3.0.2",
"tailwindcss": "^4.0.13",
"tw-animate-css": "^1.3.0",
"typescript": "^5.8.3",
"typescript-eslint": "^8.32.1",
"vite": "^6.3.5"
"tw-animate-css": "^1.2.3",
"typescript": "^5.8.2",
"typescript-eslint": "^8.26.1",
"vite": "^6.2.2"
}
}

View File

@ -1,105 +1,24 @@
@import 'tailwindcss';
@import 'tw-animate-css';
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--color-crust: #11111b;
--color-mantle: #181825;
--color-base: #1e1e2e;
@theme inline {
--spacing-body: calc(100vh - 180px);
--color-surface-0: #313244;
--color-surface-1: #45475a;
--color-surface-2: #585b70;
--color-accent: var(--sky);
--color-overlay-0: #6c7086;
--color-overlay-1: #7f849c;
--color-overlay-2: #9399b2;
--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-subtext-0: #a6adc8;
--color-subtext-1: #bac2de;
--color-text: #cdd6f4;
--color-sky: #89dceb;
--color-red: #f38ba8;
}
@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%);
}
.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%);
}
}
/*
-- Cheat sheet --
Focus Outline: blue
Border: surface-1
Hover: bump color by 2 (eg crust -> based), if accent color drop opacity (eg blue -> blue/90)
*/

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-base text-text min-h-screen">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@ -1,333 +0,0 @@
// @generated by protoc-gen-es v2.2.5 with parameter "target=ts"
// @generated from file item/v1/item.proto (package item.v1, syntax proto3)
/* eslint-disable */
import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv1";
import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv1";
import { file_buf_validate_validate } from "../../buf/validate/validate_pb";
import type { Timestamp } from "@bufbuild/protobuf/wkt";
import { file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt";
import type { Message } from "@bufbuild/protobuf";
/**
* Describes the file item/v1/item.proto.
*/
export const file_item_v1_item: GenFile = /*@__PURE__*/
fileDesc("ChJpdGVtL3YxL2l0ZW0ucHJvdG8SB2l0ZW0udjEiigEKBEl0ZW0SCgoCaWQYASABKAMSFQoEbmFtZRgCIAEoCUIHukgEcgIQAxIpCgVhZGRlZBgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASEwoLZGVzY3JpcHRpb24YBCABKAkSDQoFcHJpY2UYBSABKAISEAoIcXVhbnRpdHkYBiABKAUiHAoOR2V0SXRlbVJlcXVlc3QSCgoCaWQYASABKAMiLgoPR2V0SXRlbVJlc3BvbnNlEhsKBGl0ZW0YASABKAsyDS5pdGVtLnYxLkl0ZW0i3wEKD0dldEl0ZW1zUmVxdWVzdBIuCgVzdGFydBgBIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBIAIgBARIsCgNlbmQYAiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wSAGIAQESEwoGZmlsdGVyGAMgASgJSAKIAQESEgoFbGltaXQYBCABKAVIA4gBARITCgZvZmZzZXQYBSABKAVIBIgBAUIICgZfc3RhcnRCBgoEX2VuZEIJCgdfZmlsdGVyQggKBl9saW1pdEIJCgdfb2Zmc2V0Ij8KEEdldEl0ZW1zUmVzcG9uc2USHAoFaXRlbXMYASADKAsyDS5pdGVtLnYxLkl0ZW0SDQoFY291bnQYAiABKAMiVwoRQ3JlYXRlSXRlbVJlcXVlc3QSDAoEbmFtZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCRINCgVwcmljZRgDIAEoAhIQCghxdWFudGl0eRgEIAEoBSJLChJDcmVhdGVJdGVtUmVzcG9uc2USCgoCaWQYASABKAMSKQoFYWRkZWQYAiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIqcBChFVcGRhdGVJdGVtUmVxdWVzdBIKCgJpZBgBIAEoAxIRCgRuYW1lGAIgASgJSACIAQESGAoLZGVzY3JpcHRpb24YAyABKAlIAYgBARISCgVwcmljZRgEIAEoAkgCiAEBEhUKCHF1YW50aXR5GAUgASgFSAOIAQFCBwoFX25hbWVCDgoMX2Rlc2NyaXB0aW9uQggKBl9wcmljZUILCglfcXVhbnRpdHkiFAoSVXBkYXRlSXRlbVJlc3BvbnNlIh8KEURlbGV0ZUl0ZW1SZXF1ZXN0EgoKAmlkGAEgASgDIhQKEkRlbGV0ZUl0ZW1SZXNwb25zZTLrAgoLSXRlbVNlcnZpY2USPgoHR2V0SXRlbRIXLml0ZW0udjEuR2V0SXRlbVJlcXVlc3QaGC5pdGVtLnYxLkdldEl0ZW1SZXNwb25zZSIAEkEKCEdldEl0ZW1zEhguaXRlbS52MS5HZXRJdGVtc1JlcXVlc3QaGS5pdGVtLnYxLkdldEl0ZW1zUmVzcG9uc2UiABJHCgpDcmVhdGVJdGVtEhouaXRlbS52MS5DcmVhdGVJdGVtUmVxdWVzdBobLml0ZW0udjEuQ3JlYXRlSXRlbVJlc3BvbnNlIgASRwoKVXBkYXRlSXRlbRIaLml0ZW0udjEuVXBkYXRlSXRlbVJlcXVlc3QaGy5pdGVtLnYxLlVwZGF0ZUl0ZW1SZXNwb25zZSIAEkcKCkRlbGV0ZUl0ZW0SGi5pdGVtLnYxLkRlbGV0ZUl0ZW1SZXF1ZXN0GhsuaXRlbS52MS5EZWxldGVJdGVtUmVzcG9uc2UiAEKcAQoLY29tLml0ZW0udjFCCUl0ZW1Qcm90b1ABWkVnaXRodWIuY29tL3Nwb3RkZW1vNC90cmV2c3RhY2svc2VydmVyL2ludGVybmFsL2Nvbm5lY3QvaXRlbS92MTtpdGVtdjGiAgNJWFiqAgdJdGVtLlYxygIHSXRlbVxWMeICE0l0ZW1cVjFcR1BCTWV0YWRhdGHqAghJdGVtOjpWMWIGcHJvdG8z", [file_buf_validate_validate, file_google_protobuf_timestamp]);
/**
* @generated from message item.v1.Item
*/
export type Item = Message<"item.v1.Item"> & {
/**
* @generated from field: int64 id = 1;
*/
id: bigint;
/**
* @generated from field: string name = 2;
*/
name: string;
/**
* @generated from field: google.protobuf.Timestamp added = 3;
*/
added?: Timestamp;
/**
* @generated from field: string description = 4;
*/
description: string;
/**
* @generated from field: float price = 5;
*/
price: number;
/**
* @generated from field: int32 quantity = 6;
*/
quantity: number;
};
/**
* Describes the message item.v1.Item.
* Use `create(ItemSchema)` to create a new message.
*/
export const ItemSchema: GenMessage<Item> = /*@__PURE__*/
messageDesc(file_item_v1_item, 0);
/**
* @generated from message item.v1.GetItemRequest
*/
export type GetItemRequest = Message<"item.v1.GetItemRequest"> & {
/**
* @generated from field: int64 id = 1;
*/
id: bigint;
};
/**
* Describes the message item.v1.GetItemRequest.
* Use `create(GetItemRequestSchema)` to create a new message.
*/
export const GetItemRequestSchema: GenMessage<GetItemRequest> = /*@__PURE__*/
messageDesc(file_item_v1_item, 1);
/**
* @generated from message item.v1.GetItemResponse
*/
export type GetItemResponse = Message<"item.v1.GetItemResponse"> & {
/**
* @generated from field: item.v1.Item item = 1;
*/
item?: Item;
};
/**
* Describes the message item.v1.GetItemResponse.
* Use `create(GetItemResponseSchema)` to create a new message.
*/
export const GetItemResponseSchema: GenMessage<GetItemResponse> = /*@__PURE__*/
messageDesc(file_item_v1_item, 2);
/**
* @generated from message item.v1.GetItemsRequest
*/
export type GetItemsRequest = Message<"item.v1.GetItemsRequest"> & {
/**
* @generated from field: optional google.protobuf.Timestamp start = 1;
*/
start?: Timestamp;
/**
* @generated from field: optional google.protobuf.Timestamp end = 2;
*/
end?: Timestamp;
/**
* @generated from field: optional string filter = 3;
*/
filter?: string;
/**
* @generated from field: optional int32 limit = 4;
*/
limit?: number;
/**
* @generated from field: optional int32 offset = 5;
*/
offset?: number;
};
/**
* Describes the message item.v1.GetItemsRequest.
* Use `create(GetItemsRequestSchema)` to create a new message.
*/
export const GetItemsRequestSchema: GenMessage<GetItemsRequest> = /*@__PURE__*/
messageDesc(file_item_v1_item, 3);
/**
* @generated from message item.v1.GetItemsResponse
*/
export type GetItemsResponse = Message<"item.v1.GetItemsResponse"> & {
/**
* @generated from field: repeated item.v1.Item items = 1;
*/
items: Item[];
/**
* @generated from field: int64 count = 2;
*/
count: bigint;
};
/**
* Describes the message item.v1.GetItemsResponse.
* Use `create(GetItemsResponseSchema)` to create a new message.
*/
export const GetItemsResponseSchema: GenMessage<GetItemsResponse> = /*@__PURE__*/
messageDesc(file_item_v1_item, 4);
/**
* @generated from message item.v1.CreateItemRequest
*/
export type CreateItemRequest = Message<"item.v1.CreateItemRequest"> & {
/**
* @generated from field: string name = 1;
*/
name: string;
/**
* @generated from field: string description = 2;
*/
description: string;
/**
* @generated from field: float price = 3;
*/
price: number;
/**
* @generated from field: int32 quantity = 4;
*/
quantity: number;
};
/**
* Describes the message item.v1.CreateItemRequest.
* Use `create(CreateItemRequestSchema)` to create a new message.
*/
export const CreateItemRequestSchema: GenMessage<CreateItemRequest> = /*@__PURE__*/
messageDesc(file_item_v1_item, 5);
/**
* @generated from message item.v1.CreateItemResponse
*/
export type CreateItemResponse = Message<"item.v1.CreateItemResponse"> & {
/**
* @generated from field: int64 id = 1;
*/
id: bigint;
/**
* @generated from field: google.protobuf.Timestamp added = 2;
*/
added?: Timestamp;
};
/**
* Describes the message item.v1.CreateItemResponse.
* Use `create(CreateItemResponseSchema)` to create a new message.
*/
export const CreateItemResponseSchema: GenMessage<CreateItemResponse> = /*@__PURE__*/
messageDesc(file_item_v1_item, 6);
/**
* @generated from message item.v1.UpdateItemRequest
*/
export type UpdateItemRequest = Message<"item.v1.UpdateItemRequest"> & {
/**
* @generated from field: int64 id = 1;
*/
id: bigint;
/**
* @generated from field: optional string name = 2;
*/
name?: string;
/**
* @generated from field: optional string description = 3;
*/
description?: string;
/**
* @generated from field: optional float price = 4;
*/
price?: number;
/**
* @generated from field: optional int32 quantity = 5;
*/
quantity?: number;
};
/**
* Describes the message item.v1.UpdateItemRequest.
* Use `create(UpdateItemRequestSchema)` to create a new message.
*/
export const UpdateItemRequestSchema: GenMessage<UpdateItemRequest> = /*@__PURE__*/
messageDesc(file_item_v1_item, 7);
/**
* @generated from message item.v1.UpdateItemResponse
*/
export type UpdateItemResponse = Message<"item.v1.UpdateItemResponse"> & {
};
/**
* Describes the message item.v1.UpdateItemResponse.
* Use `create(UpdateItemResponseSchema)` to create a new message.
*/
export const UpdateItemResponseSchema: GenMessage<UpdateItemResponse> = /*@__PURE__*/
messageDesc(file_item_v1_item, 8);
/**
* @generated from message item.v1.DeleteItemRequest
*/
export type DeleteItemRequest = Message<"item.v1.DeleteItemRequest"> & {
/**
* @generated from field: int64 id = 1;
*/
id: bigint;
};
/**
* Describes the message item.v1.DeleteItemRequest.
* Use `create(DeleteItemRequestSchema)` to create a new message.
*/
export const DeleteItemRequestSchema: GenMessage<DeleteItemRequest> = /*@__PURE__*/
messageDesc(file_item_v1_item, 9);
/**
* @generated from message item.v1.DeleteItemResponse
*/
export type DeleteItemResponse = Message<"item.v1.DeleteItemResponse"> & {
};
/**
* Describes the message item.v1.DeleteItemResponse.
* Use `create(DeleteItemResponseSchema)` to create a new message.
*/
export const DeleteItemResponseSchema: GenMessage<DeleteItemResponse> = /*@__PURE__*/
messageDesc(file_item_v1_item, 10);
/**
* @generated from service item.v1.ItemService
*/
export const ItemService: GenService<{
/**
* @generated from rpc item.v1.ItemService.GetItem
*/
getItem: {
methodKind: "unary";
input: typeof GetItemRequestSchema;
output: typeof GetItemResponseSchema;
},
/**
* @generated from rpc item.v1.ItemService.GetItems
*/
getItems: {
methodKind: "unary";
input: typeof GetItemsRequestSchema;
output: typeof GetItemsResponseSchema;
},
/**
* @generated from rpc item.v1.ItemService.CreateItem
*/
createItem: {
methodKind: "unary";
input: typeof CreateItemRequestSchema;
output: typeof CreateItemResponseSchema;
},
/**
* @generated from rpc item.v1.ItemService.UpdateItem
*/
updateItem: {
methodKind: "unary";
input: typeof UpdateItemRequestSchema;
output: typeof UpdateItemResponseSchema;
},
/**
* @generated from rpc item.v1.ItemService.DeleteItem
*/
deleteItem: {
methodKind: "unary";
input: typeof DeleteItemRequestSchema;
output: typeof DeleteItemResponseSchema;
},
}> = /*@__PURE__*/
serviceDesc(file_item_v1_item, 0);

View File

@ -1,240 +0,0 @@
// @generated by protoc-gen-es v2.2.5 with parameter "target=ts"
// @generated from file user/v1/auth.proto (package user.v1, syntax proto3)
/* eslint-disable */
import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv1";
import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv1";
import { file_buf_validate_validate } from "../../buf/validate/validate_pb";
import type { Message } from "@bufbuild/protobuf";
/**
* Describes the file user/v1/auth.proto.
*/
export const file_user_v1_auth: GenFile = /*@__PURE__*/
fileDesc("ChJ1c2VyL3YxL2F1dGgucHJvdG8SB3VzZXIudjEiOwoMTG9naW5SZXF1ZXN0EhkKCHVzZXJuYW1lGAEgASgJQge6SARyAhADEhAKCHBhc3N3b3JkGAIgASgJIh4KDUxvZ2luUmVzcG9uc2USDQoFdG9rZW4YASABKAkiVgoNU2lnblVwUmVxdWVzdBIZCgh1c2VybmFtZRgBIAEoCUIHukgEcgIQAxIQCghwYXNzd29yZBgCIAEoCRIYChBjb25maXJtX3Bhc3N3b3JkGAMgASgJIhAKDlNpZ25VcFJlc3BvbnNlIg8KDUxvZ291dFJlcXVlc3QiEAoOTG9nb3V0UmVzcG9uc2UiLAoYQmVnaW5QYXNza2V5TG9naW5SZXF1ZXN0EhAKCHVzZXJuYW1lGAEgASgJIjEKGUJlZ2luUGFzc2tleUxvZ2luUmVzcG9uc2USFAoMb3B0aW9uc19qc29uGAEgASgJIkIKGUZpbmlzaFBhc3NrZXlMb2dpblJlcXVlc3QSEAoIdXNlcm5hbWUYASABKAkSEwoLYXR0ZXN0YXRpb24YAiABKAkiKwoaRmluaXNoUGFzc2tleUxvZ2luUmVzcG9uc2USDQoFdG9rZW4YASABKAkygAMKC0F1dGhTZXJ2aWNlEjgKBUxvZ2luEhUudXNlci52MS5Mb2dpblJlcXVlc3QaFi51c2VyLnYxLkxvZ2luUmVzcG9uc2UiABI7CgZTaWduVXASFi51c2VyLnYxLlNpZ25VcFJlcXVlc3QaFy51c2VyLnYxLlNpZ25VcFJlc3BvbnNlIgASOwoGTG9nb3V0EhYudXNlci52MS5Mb2dvdXRSZXF1ZXN0GhcudXNlci52MS5Mb2dvdXRSZXNwb25zZSIAElwKEUJlZ2luUGFzc2tleUxvZ2luEiEudXNlci52MS5CZWdpblBhc3NrZXlMb2dpblJlcXVlc3QaIi51c2VyLnYxLkJlZ2luUGFzc2tleUxvZ2luUmVzcG9uc2UiABJfChJGaW5pc2hQYXNza2V5TG9naW4SIi51c2VyLnYxLkZpbmlzaFBhc3NrZXlMb2dpblJlcXVlc3QaIy51c2VyLnYxLkZpbmlzaFBhc3NrZXlMb2dpblJlc3BvbnNlIgBCnAEKC2NvbS51c2VyLnYxQglBdXRoUHJvdG9QAVpFZ2l0aHViLmNvbS9zcG90ZGVtbzQvdHJldnN0YWNrL3NlcnZlci9pbnRlcm5hbC9jb25uZWN0L3VzZXIvdjE7dXNlcnYxogIDVVhYqgIHVXNlci5WMcoCB1VzZXJcVjHiAhNVc2VyXFYxXEdQQk1ldGFkYXRh6gIIVXNlcjo6VjFiBnByb3RvMw", [file_buf_validate_validate]);
/**
* @generated from message user.v1.LoginRequest
*/
export type LoginRequest = Message<"user.v1.LoginRequest"> & {
/**
* @generated from field: string username = 1;
*/
username: string;
/**
* @generated from field: string password = 2;
*/
password: string;
};
/**
* Describes the message user.v1.LoginRequest.
* Use `create(LoginRequestSchema)` to create a new message.
*/
export const LoginRequestSchema: GenMessage<LoginRequest> = /*@__PURE__*/
messageDesc(file_user_v1_auth, 0);
/**
* @generated from message user.v1.LoginResponse
*/
export type LoginResponse = Message<"user.v1.LoginResponse"> & {
/**
* @generated from field: string token = 1;
*/
token: string;
};
/**
* Describes the message user.v1.LoginResponse.
* Use `create(LoginResponseSchema)` to create a new message.
*/
export const LoginResponseSchema: GenMessage<LoginResponse> = /*@__PURE__*/
messageDesc(file_user_v1_auth, 1);
/**
* @generated from message user.v1.SignUpRequest
*/
export type SignUpRequest = Message<"user.v1.SignUpRequest"> & {
/**
* @generated from field: string username = 1;
*/
username: string;
/**
* @generated from field: string password = 2;
*/
password: string;
/**
* @generated from field: string confirm_password = 3;
*/
confirmPassword: string;
};
/**
* Describes the message user.v1.SignUpRequest.
* Use `create(SignUpRequestSchema)` to create a new message.
*/
export const SignUpRequestSchema: GenMessage<SignUpRequest> = /*@__PURE__*/
messageDesc(file_user_v1_auth, 2);
/**
* @generated from message user.v1.SignUpResponse
*/
export type SignUpResponse = Message<"user.v1.SignUpResponse"> & {
};
/**
* Describes the message user.v1.SignUpResponse.
* Use `create(SignUpResponseSchema)` to create a new message.
*/
export const SignUpResponseSchema: GenMessage<SignUpResponse> = /*@__PURE__*/
messageDesc(file_user_v1_auth, 3);
/**
* @generated from message user.v1.LogoutRequest
*/
export type LogoutRequest = Message<"user.v1.LogoutRequest"> & {
};
/**
* Describes the message user.v1.LogoutRequest.
* Use `create(LogoutRequestSchema)` to create a new message.
*/
export const LogoutRequestSchema: GenMessage<LogoutRequest> = /*@__PURE__*/
messageDesc(file_user_v1_auth, 4);
/**
* @generated from message user.v1.LogoutResponse
*/
export type LogoutResponse = Message<"user.v1.LogoutResponse"> & {
};
/**
* Describes the message user.v1.LogoutResponse.
* Use `create(LogoutResponseSchema)` to create a new message.
*/
export const LogoutResponseSchema: GenMessage<LogoutResponse> = /*@__PURE__*/
messageDesc(file_user_v1_auth, 5);
/**
* @generated from message user.v1.BeginPasskeyLoginRequest
*/
export type BeginPasskeyLoginRequest = Message<"user.v1.BeginPasskeyLoginRequest"> & {
/**
* @generated from field: string username = 1;
*/
username: string;
};
/**
* Describes the message user.v1.BeginPasskeyLoginRequest.
* Use `create(BeginPasskeyLoginRequestSchema)` to create a new message.
*/
export const BeginPasskeyLoginRequestSchema: GenMessage<BeginPasskeyLoginRequest> = /*@__PURE__*/
messageDesc(file_user_v1_auth, 6);
/**
* @generated from message user.v1.BeginPasskeyLoginResponse
*/
export type BeginPasskeyLoginResponse = Message<"user.v1.BeginPasskeyLoginResponse"> & {
/**
* @generated from field: string options_json = 1;
*/
optionsJson: string;
};
/**
* Describes the message user.v1.BeginPasskeyLoginResponse.
* Use `create(BeginPasskeyLoginResponseSchema)` to create a new message.
*/
export const BeginPasskeyLoginResponseSchema: GenMessage<BeginPasskeyLoginResponse> = /*@__PURE__*/
messageDesc(file_user_v1_auth, 7);
/**
* @generated from message user.v1.FinishPasskeyLoginRequest
*/
export type FinishPasskeyLoginRequest = Message<"user.v1.FinishPasskeyLoginRequest"> & {
/**
* @generated from field: string username = 1;
*/
username: string;
/**
* @generated from field: string attestation = 2;
*/
attestation: string;
};
/**
* Describes the message user.v1.FinishPasskeyLoginRequest.
* Use `create(FinishPasskeyLoginRequestSchema)` to create a new message.
*/
export const FinishPasskeyLoginRequestSchema: GenMessage<FinishPasskeyLoginRequest> = /*@__PURE__*/
messageDesc(file_user_v1_auth, 8);
/**
* @generated from message user.v1.FinishPasskeyLoginResponse
*/
export type FinishPasskeyLoginResponse = Message<"user.v1.FinishPasskeyLoginResponse"> & {
/**
* @generated from field: string token = 1;
*/
token: string;
};
/**
* Describes the message user.v1.FinishPasskeyLoginResponse.
* Use `create(FinishPasskeyLoginResponseSchema)` to create a new message.
*/
export const FinishPasskeyLoginResponseSchema: GenMessage<FinishPasskeyLoginResponse> = /*@__PURE__*/
messageDesc(file_user_v1_auth, 9);
/**
* @generated from service user.v1.AuthService
*/
export const AuthService: GenService<{
/**
* @generated from rpc user.v1.AuthService.Login
*/
login: {
methodKind: "unary";
input: typeof LoginRequestSchema;
output: typeof LoginResponseSchema;
},
/**
* @generated from rpc user.v1.AuthService.SignUp
*/
signUp: {
methodKind: "unary";
input: typeof SignUpRequestSchema;
output: typeof SignUpResponseSchema;
},
/**
* @generated from rpc user.v1.AuthService.Logout
*/
logout: {
methodKind: "unary";
input: typeof LogoutRequestSchema;
output: typeof LogoutResponseSchema;
},
/**
* @generated from rpc user.v1.AuthService.BeginPasskeyLogin
*/
beginPasskeyLogin: {
methodKind: "unary";
input: typeof BeginPasskeyLoginRequestSchema;
output: typeof BeginPasskeyLoginResponseSchema;
},
/**
* @generated from rpc user.v1.AuthService.FinishPasskeyLogin
*/
finishPasskeyLogin: {
methodKind: "unary";
input: typeof FinishPasskeyLoginRequestSchema;
output: typeof FinishPasskeyLoginResponseSchema;
},
}> = /*@__PURE__*/
serviceDesc(file_user_v1_auth, 0);

View File

@ -1,308 +0,0 @@
// @generated by protoc-gen-es v2.2.5 with parameter "target=ts"
// @generated from file user/v1/user.proto (package user.v1, syntax proto3)
/* eslint-disable */
import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv1";
import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv1";
import type { Message } from "@bufbuild/protobuf";
/**
* Describes the file user/v1/user.proto.
*/
export const file_user_v1_user: GenFile = /*@__PURE__*/
fileDesc("ChJ1c2VyL3YxL3VzZXIucHJvdG8SB3VzZXIudjEiXAoEVXNlchIKCgJpZBgBIAEoAxIQCgh1c2VybmFtZRgCIAEoCRIfChJwcm9maWxlX3BpY3R1cmVfaWQYAyABKANIAIgBAUIVChNfcHJvZmlsZV9waWN0dXJlX2lkIhAKDkdldFVzZXJSZXF1ZXN0Ii4KD0dldFVzZXJSZXNwb25zZRIbCgR1c2VyGAEgASgLMg0udXNlci52MS5Vc2VyIl0KFVVwZGF0ZVBhc3N3b3JkUmVxdWVzdBIUCgxvbGRfcGFzc3dvcmQYASABKAkSFAoMbmV3X3Bhc3N3b3JkGAIgASgJEhgKEGNvbmZpcm1fcGFzc3dvcmQYAyABKAkiNQoWVXBkYXRlUGFzc3dvcmRSZXNwb25zZRIbCgR1c2VyGAEgASgLMg0udXNlci52MS5Vc2VyIj4KEEdldEFQSUtleVJlcXVlc3QSEAoIcGFzc3dvcmQYASABKAkSGAoQY29uZmlybV9wYXNzd29yZBgCIAEoCSIgChFHZXRBUElLZXlSZXNwb25zZRILCgNrZXkYASABKAkiPgobVXBkYXRlUHJvZmlsZVBpY3R1cmVSZXF1ZXN0EhEKCWZpbGVfbmFtZRgBIAEoCRIMCgRkYXRhGAIgASgMIjsKHFVwZGF0ZVByb2ZpbGVQaWN0dXJlUmVzcG9uc2USGwoEdXNlchgBIAEoCzINLnVzZXIudjEuVXNlciIhCh9CZWdpblBhc3NrZXlSZWdpc3RyYXRpb25SZXF1ZXN0IjgKIEJlZ2luUGFzc2tleVJlZ2lzdHJhdGlvblJlc3BvbnNlEhQKDG9wdGlvbnNfanNvbhgBIAEoCSI3CiBGaW5pc2hQYXNza2V5UmVnaXN0cmF0aW9uUmVxdWVzdBITCgthdHRlc3RhdGlvbhgBIAEoCSIjCiFGaW5pc2hQYXNza2V5UmVnaXN0cmF0aW9uUmVzcG9uc2UyuAQKC1VzZXJTZXJ2aWNlEj4KB0dldFVzZXISFy51c2VyLnYxLkdldFVzZXJSZXF1ZXN0GhgudXNlci52MS5HZXRVc2VyUmVzcG9uc2UiABJTCg5VcGRhdGVQYXNzd29yZBIeLnVzZXIudjEuVXBkYXRlUGFzc3dvcmRSZXF1ZXN0Gh8udXNlci52MS5VcGRhdGVQYXNzd29yZFJlc3BvbnNlIgASRAoJR2V0QVBJS2V5EhkudXNlci52MS5HZXRBUElLZXlSZXF1ZXN0GhoudXNlci52MS5HZXRBUElLZXlSZXNwb25zZSIAEmUKFFVwZGF0ZVByb2ZpbGVQaWN0dXJlEiQudXNlci52MS5VcGRhdGVQcm9maWxlUGljdHVyZVJlcXVlc3QaJS51c2VyLnYxLlVwZGF0ZVByb2ZpbGVQaWN0dXJlUmVzcG9uc2UiABJxChhCZWdpblBhc3NrZXlSZWdpc3RyYXRpb24SKC51c2VyLnYxLkJlZ2luUGFzc2tleVJlZ2lzdHJhdGlvblJlcXVlc3QaKS51c2VyLnYxLkJlZ2luUGFzc2tleVJlZ2lzdHJhdGlvblJlc3BvbnNlIgASdAoZRmluaXNoUGFzc2tleVJlZ2lzdHJhdGlvbhIpLnVzZXIudjEuRmluaXNoUGFzc2tleVJlZ2lzdHJhdGlvblJlcXVlc3QaKi51c2VyLnYxLkZpbmlzaFBhc3NrZXlSZWdpc3RyYXRpb25SZXNwb25zZSIAQpwBCgtjb20udXNlci52MUIJVXNlclByb3RvUAFaRWdpdGh1Yi5jb20vc3BvdGRlbW80L3RyZXZzdGFjay9zZXJ2ZXIvaW50ZXJuYWwvY29ubmVjdC91c2VyL3YxO3VzZXJ2MaICA1VYWKoCB1VzZXIuVjHKAgdVc2VyXFYx4gITVXNlclxWMVxHUEJNZXRhZGF0YeoCCFVzZXI6OlYxYgZwcm90bzM");
/**
* @generated from message user.v1.User
*/
export type User = Message<"user.v1.User"> & {
/**
* @generated from field: int64 id = 1;
*/
id: bigint;
/**
* @generated from field: string username = 2;
*/
username: string;
/**
* @generated from field: optional int64 profile_picture_id = 3;
*/
profilePictureId?: bigint;
};
/**
* Describes the message user.v1.User.
* Use `create(UserSchema)` to create a new message.
*/
export const UserSchema: GenMessage<User> = /*@__PURE__*/
messageDesc(file_user_v1_user, 0);
/**
* @generated from message user.v1.GetUserRequest
*/
export type GetUserRequest = Message<"user.v1.GetUserRequest"> & {
};
/**
* Describes the message user.v1.GetUserRequest.
* Use `create(GetUserRequestSchema)` to create a new message.
*/
export const GetUserRequestSchema: GenMessage<GetUserRequest> = /*@__PURE__*/
messageDesc(file_user_v1_user, 1);
/**
* @generated from message user.v1.GetUserResponse
*/
export type GetUserResponse = Message<"user.v1.GetUserResponse"> & {
/**
* @generated from field: user.v1.User user = 1;
*/
user?: User;
};
/**
* Describes the message user.v1.GetUserResponse.
* Use `create(GetUserResponseSchema)` to create a new message.
*/
export const GetUserResponseSchema: GenMessage<GetUserResponse> = /*@__PURE__*/
messageDesc(file_user_v1_user, 2);
/**
* @generated from message user.v1.UpdatePasswordRequest
*/
export type UpdatePasswordRequest = Message<"user.v1.UpdatePasswordRequest"> & {
/**
* @generated from field: string old_password = 1;
*/
oldPassword: string;
/**
* @generated from field: string new_password = 2;
*/
newPassword: string;
/**
* @generated from field: string confirm_password = 3;
*/
confirmPassword: string;
};
/**
* Describes the message user.v1.UpdatePasswordRequest.
* Use `create(UpdatePasswordRequestSchema)` to create a new message.
*/
export const UpdatePasswordRequestSchema: GenMessage<UpdatePasswordRequest> = /*@__PURE__*/
messageDesc(file_user_v1_user, 3);
/**
* @generated from message user.v1.UpdatePasswordResponse
*/
export type UpdatePasswordResponse = Message<"user.v1.UpdatePasswordResponse"> & {
/**
* @generated from field: user.v1.User user = 1;
*/
user?: User;
};
/**
* Describes the message user.v1.UpdatePasswordResponse.
* Use `create(UpdatePasswordResponseSchema)` to create a new message.
*/
export const UpdatePasswordResponseSchema: GenMessage<UpdatePasswordResponse> = /*@__PURE__*/
messageDesc(file_user_v1_user, 4);
/**
* @generated from message user.v1.GetAPIKeyRequest
*/
export type GetAPIKeyRequest = Message<"user.v1.GetAPIKeyRequest"> & {
/**
* @generated from field: string password = 1;
*/
password: string;
/**
* @generated from field: string confirm_password = 2;
*/
confirmPassword: string;
};
/**
* Describes the message user.v1.GetAPIKeyRequest.
* Use `create(GetAPIKeyRequestSchema)` to create a new message.
*/
export const GetAPIKeyRequestSchema: GenMessage<GetAPIKeyRequest> = /*@__PURE__*/
messageDesc(file_user_v1_user, 5);
/**
* @generated from message user.v1.GetAPIKeyResponse
*/
export type GetAPIKeyResponse = Message<"user.v1.GetAPIKeyResponse"> & {
/**
* @generated from field: string key = 1;
*/
key: string;
};
/**
* Describes the message user.v1.GetAPIKeyResponse.
* Use `create(GetAPIKeyResponseSchema)` to create a new message.
*/
export const GetAPIKeyResponseSchema: GenMessage<GetAPIKeyResponse> = /*@__PURE__*/
messageDesc(file_user_v1_user, 6);
/**
* @generated from message user.v1.UpdateProfilePictureRequest
*/
export type UpdateProfilePictureRequest = Message<"user.v1.UpdateProfilePictureRequest"> & {
/**
* @generated from field: string file_name = 1;
*/
fileName: string;
/**
* @generated from field: bytes data = 2;
*/
data: Uint8Array;
};
/**
* Describes the message user.v1.UpdateProfilePictureRequest.
* Use `create(UpdateProfilePictureRequestSchema)` to create a new message.
*/
export const UpdateProfilePictureRequestSchema: GenMessage<UpdateProfilePictureRequest> = /*@__PURE__*/
messageDesc(file_user_v1_user, 7);
/**
* @generated from message user.v1.UpdateProfilePictureResponse
*/
export type UpdateProfilePictureResponse = Message<"user.v1.UpdateProfilePictureResponse"> & {
/**
* @generated from field: user.v1.User user = 1;
*/
user?: User;
};
/**
* Describes the message user.v1.UpdateProfilePictureResponse.
* Use `create(UpdateProfilePictureResponseSchema)` to create a new message.
*/
export const UpdateProfilePictureResponseSchema: GenMessage<UpdateProfilePictureResponse> = /*@__PURE__*/
messageDesc(file_user_v1_user, 8);
/**
* @generated from message user.v1.BeginPasskeyRegistrationRequest
*/
export type BeginPasskeyRegistrationRequest = Message<"user.v1.BeginPasskeyRegistrationRequest"> & {
};
/**
* Describes the message user.v1.BeginPasskeyRegistrationRequest.
* Use `create(BeginPasskeyRegistrationRequestSchema)` to create a new message.
*/
export const BeginPasskeyRegistrationRequestSchema: GenMessage<BeginPasskeyRegistrationRequest> = /*@__PURE__*/
messageDesc(file_user_v1_user, 9);
/**
* @generated from message user.v1.BeginPasskeyRegistrationResponse
*/
export type BeginPasskeyRegistrationResponse = Message<"user.v1.BeginPasskeyRegistrationResponse"> & {
/**
* @generated from field: string options_json = 1;
*/
optionsJson: string;
};
/**
* Describes the message user.v1.BeginPasskeyRegistrationResponse.
* Use `create(BeginPasskeyRegistrationResponseSchema)` to create a new message.
*/
export const BeginPasskeyRegistrationResponseSchema: GenMessage<BeginPasskeyRegistrationResponse> = /*@__PURE__*/
messageDesc(file_user_v1_user, 10);
/**
* @generated from message user.v1.FinishPasskeyRegistrationRequest
*/
export type FinishPasskeyRegistrationRequest = Message<"user.v1.FinishPasskeyRegistrationRequest"> & {
/**
* @generated from field: string attestation = 1;
*/
attestation: string;
};
/**
* Describes the message user.v1.FinishPasskeyRegistrationRequest.
* Use `create(FinishPasskeyRegistrationRequestSchema)` to create a new message.
*/
export const FinishPasskeyRegistrationRequestSchema: GenMessage<FinishPasskeyRegistrationRequest> = /*@__PURE__*/
messageDesc(file_user_v1_user, 11);
/**
* @generated from message user.v1.FinishPasskeyRegistrationResponse
*/
export type FinishPasskeyRegistrationResponse = Message<"user.v1.FinishPasskeyRegistrationResponse"> & {
};
/**
* Describes the message user.v1.FinishPasskeyRegistrationResponse.
* Use `create(FinishPasskeyRegistrationResponseSchema)` to create a new message.
*/
export const FinishPasskeyRegistrationResponseSchema: GenMessage<FinishPasskeyRegistrationResponse> = /*@__PURE__*/
messageDesc(file_user_v1_user, 12);
/**
* @generated from service user.v1.UserService
*/
export const UserService: GenService<{
/**
* @generated from rpc user.v1.UserService.GetUser
*/
getUser: {
methodKind: "unary";
input: typeof GetUserRequestSchema;
output: typeof GetUserResponseSchema;
},
/**
* @generated from rpc user.v1.UserService.UpdatePassword
*/
updatePassword: {
methodKind: "unary";
input: typeof UpdatePasswordRequestSchema;
output: typeof UpdatePasswordResponseSchema;
},
/**
* @generated from rpc user.v1.UserService.GetAPIKey
*/
getAPIKey: {
methodKind: "unary";
input: typeof GetAPIKeyRequestSchema;
output: typeof GetAPIKeyResponseSchema;
},
/**
* @generated from rpc user.v1.UserService.UpdateProfilePicture
*/
updateProfilePicture: {
methodKind: "unary";
input: typeof UpdateProfilePictureRequestSchema;
output: typeof UpdateProfilePictureResponseSchema;
},
/**
* @generated from rpc user.v1.UserService.BeginPasskeyRegistration
*/
beginPasskeyRegistration: {
methodKind: "unary";
input: typeof BeginPasskeyRegistrationRequestSchema;
output: typeof BeginPasskeyRegistrationResponseSchema;
},
/**
* @generated from rpc user.v1.UserService.FinishPasskeyRegistration
*/
finishPasskeyRegistration: {
methodKind: "unary";
input: typeof FinishPasskeyRegistrationRequestSchema;
output: typeof FinishPasskeyRegistrationResponseSchema;
},
}> = /*@__PURE__*/
serviceDesc(file_user_v1_user, 0);

View File

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

View File

@ -1,176 +0,0 @@
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';
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;
};
type Violations<Field> = {
[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']>
) {
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];
}
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;
}
// 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 [];
};
// When a request is successful
const success = (response: MessageShape<Method['output']>) => {
loading = false;
// Send the response up
options?.onResult?.(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);
}
};
// When a request fails
const fail = (err: Violation[] | ConnectError | Error) => {
loading = false;
// 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;
// Send the error up
options?.onError?.(err);
return;
}
throw err;
};
const submit = () => {
loading = true;
// 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']>
>;
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();
if (options?.onSubmit) {
const formData = new FormData(form);
options.onSubmit(formData, input).then((i) => {
Object.assign(input, i);
submit();
});
return;
}
submit();
};
return () => {
form.onsubmit = () => {};
};
});
};
if (options?.start) {
submit();
}
return {
input,
output,
errors,
loading: () => loading,
submit,
validate,
impair
};
}

View File

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

1
client/src/lib/index.ts Normal file
View File

@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

View File

@ -0,0 +1,306 @@
// @generated by protoc-gen-es v2.2.3 with parameter "target=ts"
// @generated from file item/v1/item.proto (package item.v1, syntax proto3)
/* eslint-disable */
import type { GenFile, GenMessage, GenService } from '@bufbuild/protobuf/codegenv1';
import { fileDesc, messageDesc, serviceDesc } from '@bufbuild/protobuf/codegenv1';
import type { Timestamp } from '@bufbuild/protobuf/wkt';
import { file_google_protobuf_timestamp } from '@bufbuild/protobuf/wkt';
import type { Message } from '@bufbuild/protobuf';
/**
* Describes the file item/v1/item.proto.
*/
export const file_item_v1_item: GenFile =
/*@__PURE__*/
fileDesc(
'ChJpdGVtL3YxL2l0ZW0ucHJvdG8SB2l0ZW0udjEinAEKBEl0ZW0SDwoCaWQYASABKA1IAIgBARIMCgRuYW1lGAIgASgJEhMKC2Rlc2NyaXB0aW9uGAMgASgJEg0KBXByaWNlGAQgASgCEhAKCHF1YW50aXR5GAUgASgNEi4KBWFkZGVkGAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgBiAEBQgUKA19pZEIICgZfYWRkZWQiHAoOR2V0SXRlbVJlcXVlc3QSCgoCaWQYASABKA0iLgoPR2V0SXRlbVJlc3BvbnNlEhsKBGl0ZW0YASABKAsyDS5pdGVtLnYxLkl0ZW0i3wEKD0dldEl0ZW1zUmVxdWVzdBIuCgVzdGFydBgBIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBIAIgBARIsCgNlbmQYAiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wSAGIAQESEwoGZmlsdGVyGAMgASgJSAKIAQESEgoFbGltaXQYBCABKA1IA4gBARITCgZvZmZzZXQYBSABKA1IBIgBAUIICgZfc3RhcnRCBgoEX2VuZEIJCgdfZmlsdGVyQggKBl9saW1pdEIJCgdfb2Zmc2V0Ij8KEEdldEl0ZW1zUmVzcG9uc2USHAoFaXRlbXMYASADKAsyDS5pdGVtLnYxLkl0ZW0SDQoFY291bnQYAiABKAQiMAoRQ3JlYXRlSXRlbVJlcXVlc3QSGwoEaXRlbRgBIAEoCzINLml0ZW0udjEuSXRlbSIxChJDcmVhdGVJdGVtUmVzcG9uc2USGwoEaXRlbRgBIAEoCzINLml0ZW0udjEuSXRlbSIwChFVcGRhdGVJdGVtUmVxdWVzdBIbCgRpdGVtGAEgASgLMg0uaXRlbS52MS5JdGVtIjEKElVwZGF0ZUl0ZW1SZXNwb25zZRIbCgRpdGVtGAEgASgLMg0uaXRlbS52MS5JdGVtIh8KEURlbGV0ZUl0ZW1SZXF1ZXN0EgoKAmlkGAEgASgNIhQKEkRlbGV0ZUl0ZW1SZXNwb25zZTLrAgoLSXRlbVNlcnZpY2USPgoHR2V0SXRlbRIXLml0ZW0udjEuR2V0SXRlbVJlcXVlc3QaGC5pdGVtLnYxLkdldEl0ZW1SZXNwb25zZSIAEkEKCEdldEl0ZW1zEhguaXRlbS52MS5HZXRJdGVtc1JlcXVlc3QaGS5pdGVtLnYxLkdldEl0ZW1zUmVzcG9uc2UiABJHCgpDcmVhdGVJdGVtEhouaXRlbS52MS5DcmVhdGVJdGVtUmVxdWVzdBobLml0ZW0udjEuQ3JlYXRlSXRlbVJlc3BvbnNlIgASRwoKVXBkYXRlSXRlbRIaLml0ZW0udjEuVXBkYXRlSXRlbVJlcXVlc3QaGy5pdGVtLnYxLlVwZGF0ZUl0ZW1SZXNwb25zZSIAEkcKCkRlbGV0ZUl0ZW0SGi5pdGVtLnYxLkRlbGV0ZUl0ZW1SZXF1ZXN0GhsuaXRlbS52MS5EZWxldGVJdGVtUmVzcG9uc2UiAEKdAQoLY29tLml0ZW0udjFCCUl0ZW1Qcm90b1ABWkZnaXRodWIuY29tL3Nwb3RkZW1vNC90cmV2c3RhY2svc2VydmVyL2ludGVybmFsL3NlcnZpY2VzL2l0ZW0vdjE7aXRlbXYxogIDSVhYqgIHSXRlbS5WMcoCB0l0ZW1cVjHiAhNJdGVtXFYxXEdQQk1ldGFkYXRh6gIISXRlbTo6VjFiBnByb3RvMw',
[file_google_protobuf_timestamp]
);
/**
* @generated from message item.v1.Item
*/
export type Item = Message<'item.v1.Item'> & {
/**
* @generated from field: optional uint32 id = 1;
*/
id?: number;
/**
* @generated from field: string name = 2;
*/
name: string;
/**
* @generated from field: string description = 3;
*/
description: string;
/**
* @generated from field: float price = 4;
*/
price: number;
/**
* @generated from field: uint32 quantity = 5;
*/
quantity: number;
/**
* @generated from field: optional google.protobuf.Timestamp added = 6;
*/
added?: Timestamp;
};
/**
* Describes the message item.v1.Item.
* Use `create(ItemSchema)` to create a new message.
*/
export const ItemSchema: GenMessage<Item> = /*@__PURE__*/ messageDesc(file_item_v1_item, 0);
/**
* @generated from message item.v1.GetItemRequest
*/
export type GetItemRequest = Message<'item.v1.GetItemRequest'> & {
/**
* @generated from field: uint32 id = 1;
*/
id: number;
};
/**
* Describes the message item.v1.GetItemRequest.
* Use `create(GetItemRequestSchema)` to create a new message.
*/
export const GetItemRequestSchema: GenMessage<GetItemRequest> =
/*@__PURE__*/
messageDesc(file_item_v1_item, 1);
/**
* @generated from message item.v1.GetItemResponse
*/
export type GetItemResponse = Message<'item.v1.GetItemResponse'> & {
/**
* @generated from field: item.v1.Item item = 1;
*/
item?: Item;
};
/**
* Describes the message item.v1.GetItemResponse.
* Use `create(GetItemResponseSchema)` to create a new message.
*/
export const GetItemResponseSchema: GenMessage<GetItemResponse> =
/*@__PURE__*/
messageDesc(file_item_v1_item, 2);
/**
* @generated from message item.v1.GetItemsRequest
*/
export type GetItemsRequest = Message<'item.v1.GetItemsRequest'> & {
/**
* @generated from field: optional google.protobuf.Timestamp start = 1;
*/
start?: Timestamp;
/**
* @generated from field: optional google.protobuf.Timestamp end = 2;
*/
end?: Timestamp;
/**
* @generated from field: optional string filter = 3;
*/
filter?: string;
/**
* @generated from field: optional uint32 limit = 4;
*/
limit?: number;
/**
* @generated from field: optional uint32 offset = 5;
*/
offset?: number;
};
/**
* Describes the message item.v1.GetItemsRequest.
* Use `create(GetItemsRequestSchema)` to create a new message.
*/
export const GetItemsRequestSchema: GenMessage<GetItemsRequest> =
/*@__PURE__*/
messageDesc(file_item_v1_item, 3);
/**
* @generated from message item.v1.GetItemsResponse
*/
export type GetItemsResponse = Message<'item.v1.GetItemsResponse'> & {
/**
* @generated from field: repeated item.v1.Item items = 1;
*/
items: Item[];
/**
* @generated from field: uint64 count = 2;
*/
count: bigint;
};
/**
* Describes the message item.v1.GetItemsResponse.
* Use `create(GetItemsResponseSchema)` to create a new message.
*/
export const GetItemsResponseSchema: GenMessage<GetItemsResponse> =
/*@__PURE__*/
messageDesc(file_item_v1_item, 4);
/**
* @generated from message item.v1.CreateItemRequest
*/
export type CreateItemRequest = Message<'item.v1.CreateItemRequest'> & {
/**
* @generated from field: item.v1.Item item = 1;
*/
item?: Item;
};
/**
* Describes the message item.v1.CreateItemRequest.
* Use `create(CreateItemRequestSchema)` to create a new message.
*/
export const CreateItemRequestSchema: GenMessage<CreateItemRequest> =
/*@__PURE__*/
messageDesc(file_item_v1_item, 5);
/**
* @generated from message item.v1.CreateItemResponse
*/
export type CreateItemResponse = Message<'item.v1.CreateItemResponse'> & {
/**
* @generated from field: item.v1.Item item = 1;
*/
item?: Item;
};
/**
* Describes the message item.v1.CreateItemResponse.
* Use `create(CreateItemResponseSchema)` to create a new message.
*/
export const CreateItemResponseSchema: GenMessage<CreateItemResponse> =
/*@__PURE__*/
messageDesc(file_item_v1_item, 6);
/**
* @generated from message item.v1.UpdateItemRequest
*/
export type UpdateItemRequest = Message<'item.v1.UpdateItemRequest'> & {
/**
* @generated from field: item.v1.Item item = 1;
*/
item?: Item;
};
/**
* Describes the message item.v1.UpdateItemRequest.
* Use `create(UpdateItemRequestSchema)` to create a new message.
*/
export const UpdateItemRequestSchema: GenMessage<UpdateItemRequest> =
/*@__PURE__*/
messageDesc(file_item_v1_item, 7);
/**
* @generated from message item.v1.UpdateItemResponse
*/
export type UpdateItemResponse = Message<'item.v1.UpdateItemResponse'> & {
/**
* @generated from field: item.v1.Item item = 1;
*/
item?: Item;
};
/**
* Describes the message item.v1.UpdateItemResponse.
* Use `create(UpdateItemResponseSchema)` to create a new message.
*/
export const UpdateItemResponseSchema: GenMessage<UpdateItemResponse> =
/*@__PURE__*/
messageDesc(file_item_v1_item, 8);
/**
* @generated from message item.v1.DeleteItemRequest
*/
export type DeleteItemRequest = Message<'item.v1.DeleteItemRequest'> & {
/**
* @generated from field: uint32 id = 1;
*/
id: number;
};
/**
* Describes the message item.v1.DeleteItemRequest.
* Use `create(DeleteItemRequestSchema)` to create a new message.
*/
export const DeleteItemRequestSchema: GenMessage<DeleteItemRequest> =
/*@__PURE__*/
messageDesc(file_item_v1_item, 9);
/**
* @generated from message item.v1.DeleteItemResponse
*/
export type DeleteItemResponse = Message<'item.v1.DeleteItemResponse'> & {};
/**
* Describes the message item.v1.DeleteItemResponse.
* Use `create(DeleteItemResponseSchema)` to create a new message.
*/
export const DeleteItemResponseSchema: GenMessage<DeleteItemResponse> =
/*@__PURE__*/
messageDesc(file_item_v1_item, 10);
/**
* @generated from service item.v1.ItemService
*/
export const ItemService: GenService<{
/**
* @generated from rpc item.v1.ItemService.GetItem
*/
getItem: {
methodKind: 'unary';
input: typeof GetItemRequestSchema;
output: typeof GetItemResponseSchema;
};
/**
* @generated from rpc item.v1.ItemService.GetItems
*/
getItems: {
methodKind: 'unary';
input: typeof GetItemsRequestSchema;
output: typeof GetItemsResponseSchema;
};
/**
* @generated from rpc item.v1.ItemService.CreateItem
*/
createItem: {
methodKind: 'unary';
input: typeof CreateItemRequestSchema;
output: typeof CreateItemResponseSchema;
};
/**
* @generated from rpc item.v1.ItemService.UpdateItem
*/
updateItem: {
methodKind: 'unary';
input: typeof UpdateItemRequestSchema;
output: typeof UpdateItemResponseSchema;
};
/**
* @generated from rpc item.v1.ItemService.DeleteItem
*/
deleteItem: {
methodKind: 'unary';
input: typeof DeleteItemRequestSchema;
output: typeof DeleteItemResponseSchema;
};
}> = /*@__PURE__*/ serviceDesc(file_item_v1_item, 0);

View File

@ -0,0 +1,154 @@
// @generated by protoc-gen-es v2.2.3 with parameter "target=ts"
// @generated from file user/v1/auth.proto (package user.v1, syntax proto3)
/* eslint-disable */
import type { GenFile, GenMessage, GenService } from '@bufbuild/protobuf/codegenv1';
import { fileDesc, messageDesc, serviceDesc } from '@bufbuild/protobuf/codegenv1';
import type { Message } from '@bufbuild/protobuf';
/**
* Describes the file user/v1/auth.proto.
*/
export const file_user_v1_auth: GenFile =
/*@__PURE__*/
fileDesc(
'ChJ1c2VyL3YxL2F1dGgucHJvdG8SB3VzZXIudjEiMgoMTG9naW5SZXF1ZXN0EhAKCHVzZXJuYW1lGAEgASgJEhAKCHBhc3N3b3JkGAIgASgJIh4KDUxvZ2luUmVzcG9uc2USDQoFdG9rZW4YASABKAkiTQoNU2lnblVwUmVxdWVzdBIQCgh1c2VybmFtZRgBIAEoCRIQCghwYXNzd29yZBgCIAEoCRIYChBjb25maXJtX3Bhc3N3b3JkGAMgASgJIhAKDlNpZ25VcFJlc3BvbnNlIg8KDUxvZ291dFJlcXVlc3QiEAoOTG9nb3V0UmVzcG9uc2UywQEKC0F1dGhTZXJ2aWNlEjgKBUxvZ2luEhUudXNlci52MS5Mb2dpblJlcXVlc3QaFi51c2VyLnYxLkxvZ2luUmVzcG9uc2UiABI7CgZTaWduVXASFi51c2VyLnYxLlNpZ25VcFJlcXVlc3QaFy51c2VyLnYxLlNpZ25VcFJlc3BvbnNlIgASOwoGTG9nb3V0EhYudXNlci52MS5Mb2dvdXRSZXF1ZXN0GhcudXNlci52MS5Mb2dvdXRSZXNwb25zZSIAQp0BCgtjb20udXNlci52MUIJQXV0aFByb3RvUAFaRmdpdGh1Yi5jb20vc3BvdGRlbW80L3RyZXZzdGFjay9zZXJ2ZXIvaW50ZXJuYWwvc2VydmljZXMvdXNlci92MTt1c2VydjGiAgNVWFiqAgdVc2VyLlYxygIHVXNlclxWMeICE1VzZXJcVjFcR1BCTWV0YWRhdGHqAghVc2VyOjpWMWIGcHJvdG8z'
);
/**
* @generated from message user.v1.LoginRequest
*/
export type LoginRequest = Message<'user.v1.LoginRequest'> & {
/**
* @generated from field: string username = 1;
*/
username: string;
/**
* @generated from field: string password = 2;
*/
password: string;
};
/**
* Describes the message user.v1.LoginRequest.
* Use `create(LoginRequestSchema)` to create a new message.
*/
export const LoginRequestSchema: GenMessage<LoginRequest> =
/*@__PURE__*/
messageDesc(file_user_v1_auth, 0);
/**
* @generated from message user.v1.LoginResponse
*/
export type LoginResponse = Message<'user.v1.LoginResponse'> & {
/**
* @generated from field: string token = 1;
*/
token: string;
};
/**
* Describes the message user.v1.LoginResponse.
* Use `create(LoginResponseSchema)` to create a new message.
*/
export const LoginResponseSchema: GenMessage<LoginResponse> =
/*@__PURE__*/
messageDesc(file_user_v1_auth, 1);
/**
* @generated from message user.v1.SignUpRequest
*/
export type SignUpRequest = Message<'user.v1.SignUpRequest'> & {
/**
* @generated from field: string username = 1;
*/
username: string;
/**
* @generated from field: string password = 2;
*/
password: string;
/**
* @generated from field: string confirm_password = 3;
*/
confirmPassword: string;
};
/**
* Describes the message user.v1.SignUpRequest.
* Use `create(SignUpRequestSchema)` to create a new message.
*/
export const SignUpRequestSchema: GenMessage<SignUpRequest> =
/*@__PURE__*/
messageDesc(file_user_v1_auth, 2);
/**
* @generated from message user.v1.SignUpResponse
*/
export type SignUpResponse = Message<'user.v1.SignUpResponse'> & {};
/**
* Describes the message user.v1.SignUpResponse.
* Use `create(SignUpResponseSchema)` to create a new message.
*/
export const SignUpResponseSchema: GenMessage<SignUpResponse> =
/*@__PURE__*/
messageDesc(file_user_v1_auth, 3);
/**
* @generated from message user.v1.LogoutRequest
*/
export type LogoutRequest = Message<'user.v1.LogoutRequest'> & {};
/**
* Describes the message user.v1.LogoutRequest.
* Use `create(LogoutRequestSchema)` to create a new message.
*/
export const LogoutRequestSchema: GenMessage<LogoutRequest> =
/*@__PURE__*/
messageDesc(file_user_v1_auth, 4);
/**
* @generated from message user.v1.LogoutResponse
*/
export type LogoutResponse = Message<'user.v1.LogoutResponse'> & {};
/**
* Describes the message user.v1.LogoutResponse.
* Use `create(LogoutResponseSchema)` to create a new message.
*/
export const LogoutResponseSchema: GenMessage<LogoutResponse> =
/*@__PURE__*/
messageDesc(file_user_v1_auth, 5);
/**
* @generated from service user.v1.AuthService
*/
export const AuthService: GenService<{
/**
* @generated from rpc user.v1.AuthService.Login
*/
login: {
methodKind: 'unary';
input: typeof LoginRequestSchema;
output: typeof LoginResponseSchema;
};
/**
* @generated from rpc user.v1.AuthService.SignUp
*/
signUp: {
methodKind: 'unary';
input: typeof SignUpRequestSchema;
output: typeof SignUpResponseSchema;
};
/**
* @generated from rpc user.v1.AuthService.Logout
*/
logout: {
methodKind: 'unary';
input: typeof LogoutRequestSchema;
output: typeof LogoutResponseSchema;
};
}> = /*@__PURE__*/ serviceDesc(file_user_v1_auth, 0);

View File

@ -0,0 +1,239 @@
// @generated by protoc-gen-es v2.2.3 with parameter "target=ts"
// @generated from file user/v1/user.proto (package user.v1, syntax proto3)
/* eslint-disable */
import type { GenFile, GenMessage, GenService } from '@bufbuild/protobuf/codegenv1';
import { fileDesc, messageDesc, serviceDesc } from '@bufbuild/protobuf/codegenv1';
import type { Message } from '@bufbuild/protobuf';
/**
* Describes the file user/v1/user.proto.
*/
export const file_user_v1_user: GenFile =
/*@__PURE__*/
fileDesc(
'ChJ1c2VyL3YxL3VzZXIucHJvdG8SB3VzZXIudjEiVgoEVXNlchIKCgJpZBgBIAEoDRIQCgh1c2VybmFtZRgCIAEoCRIcCg9wcm9maWxlX3BpY3R1cmUYAyABKAlIAIgBAUISChBfcHJvZmlsZV9waWN0dXJlIhAKDkdldFVzZXJSZXF1ZXN0Ii4KD0dldFVzZXJSZXNwb25zZRIbCgR1c2VyGAEgASgLMg0udXNlci52MS5Vc2VyIl0KFVVwZGF0ZVBhc3N3b3JkUmVxdWVzdBIUCgxvbGRfcGFzc3dvcmQYASABKAkSFAoMbmV3X3Bhc3N3b3JkGAIgASgJEhgKEGNvbmZpcm1fcGFzc3dvcmQYAyABKAkiNQoWVXBkYXRlUGFzc3dvcmRSZXNwb25zZRIbCgR1c2VyGAEgASgLMg0udXNlci52MS5Vc2VyIj4KEEdldEFQSUtleVJlcXVlc3QSEAoIcGFzc3dvcmQYASABKAkSGAoQY29uZmlybV9wYXNzd29yZBgCIAEoCSIgChFHZXRBUElLZXlSZXNwb25zZRILCgNrZXkYASABKAkiPgobVXBkYXRlUHJvZmlsZVBpY3R1cmVSZXF1ZXN0EhEKCWZpbGVfbmFtZRgBIAEoCRIMCgRkYXRhGAIgASgMIjsKHFVwZGF0ZVByb2ZpbGVQaWN0dXJlUmVzcG9uc2USGwoEdXNlchgBIAEoCzINLnVzZXIudjEuVXNlcjLPAgoLVXNlclNlcnZpY2USPgoHR2V0VXNlchIXLnVzZXIudjEuR2V0VXNlclJlcXVlc3QaGC51c2VyLnYxLkdldFVzZXJSZXNwb25zZSIAElMKDlVwZGF0ZVBhc3N3b3JkEh4udXNlci52MS5VcGRhdGVQYXNzd29yZFJlcXVlc3QaHy51c2VyLnYxLlVwZGF0ZVBhc3N3b3JkUmVzcG9uc2UiABJECglHZXRBUElLZXkSGS51c2VyLnYxLkdldEFQSUtleVJlcXVlc3QaGi51c2VyLnYxLkdldEFQSUtleVJlc3BvbnNlIgASZQoUVXBkYXRlUHJvZmlsZVBpY3R1cmUSJC51c2VyLnYxLlVwZGF0ZVByb2ZpbGVQaWN0dXJlUmVxdWVzdBolLnVzZXIudjEuVXBkYXRlUHJvZmlsZVBpY3R1cmVSZXNwb25zZSIAQp0BCgtjb20udXNlci52MUIJVXNlclByb3RvUAFaRmdpdGh1Yi5jb20vc3BvdGRlbW80L3RyZXZzdGFjay9zZXJ2ZXIvaW50ZXJuYWwvc2VydmljZXMvdXNlci92MTt1c2VydjGiAgNVWFiqAgdVc2VyLlYxygIHVXNlclxWMeICE1VzZXJcVjFcR1BCTWV0YWRhdGHqAghVc2VyOjpWMWIGcHJvdG8z'
);
/**
* @generated from message user.v1.User
*/
export type User = Message<'user.v1.User'> & {
/**
* @generated from field: uint32 id = 1;
*/
id: number;
/**
* @generated from field: string username = 2;
*/
username: string;
/**
* @generated from field: optional string profile_picture = 3;
*/
profilePicture?: string;
};
/**
* Describes the message user.v1.User.
* Use `create(UserSchema)` to create a new message.
*/
export const UserSchema: GenMessage<User> = /*@__PURE__*/ messageDesc(file_user_v1_user, 0);
/**
* @generated from message user.v1.GetUserRequest
*/
export type GetUserRequest = Message<'user.v1.GetUserRequest'> & {};
/**
* Describes the message user.v1.GetUserRequest.
* Use `create(GetUserRequestSchema)` to create a new message.
*/
export const GetUserRequestSchema: GenMessage<GetUserRequest> =
/*@__PURE__*/
messageDesc(file_user_v1_user, 1);
/**
* @generated from message user.v1.GetUserResponse
*/
export type GetUserResponse = Message<'user.v1.GetUserResponse'> & {
/**
* @generated from field: user.v1.User user = 1;
*/
user?: User;
};
/**
* Describes the message user.v1.GetUserResponse.
* Use `create(GetUserResponseSchema)` to create a new message.
*/
export const GetUserResponseSchema: GenMessage<GetUserResponse> =
/*@__PURE__*/
messageDesc(file_user_v1_user, 2);
/**
* @generated from message user.v1.UpdatePasswordRequest
*/
export type UpdatePasswordRequest = Message<'user.v1.UpdatePasswordRequest'> & {
/**
* @generated from field: string old_password = 1;
*/
oldPassword: string;
/**
* @generated from field: string new_password = 2;
*/
newPassword: string;
/**
* @generated from field: string confirm_password = 3;
*/
confirmPassword: string;
};
/**
* Describes the message user.v1.UpdatePasswordRequest.
* Use `create(UpdatePasswordRequestSchema)` to create a new message.
*/
export const UpdatePasswordRequestSchema: GenMessage<UpdatePasswordRequest> =
/*@__PURE__*/
messageDesc(file_user_v1_user, 3);
/**
* @generated from message user.v1.UpdatePasswordResponse
*/
export type UpdatePasswordResponse = Message<'user.v1.UpdatePasswordResponse'> & {
/**
* @generated from field: user.v1.User user = 1;
*/
user?: User;
};
/**
* Describes the message user.v1.UpdatePasswordResponse.
* Use `create(UpdatePasswordResponseSchema)` to create a new message.
*/
export const UpdatePasswordResponseSchema: GenMessage<UpdatePasswordResponse> =
/*@__PURE__*/
messageDesc(file_user_v1_user, 4);
/**
* @generated from message user.v1.GetAPIKeyRequest
*/
export type GetAPIKeyRequest = Message<'user.v1.GetAPIKeyRequest'> & {
/**
* @generated from field: string password = 1;
*/
password: string;
/**
* @generated from field: string confirm_password = 2;
*/
confirmPassword: string;
};
/**
* Describes the message user.v1.GetAPIKeyRequest.
* Use `create(GetAPIKeyRequestSchema)` to create a new message.
*/
export const GetAPIKeyRequestSchema: GenMessage<GetAPIKeyRequest> =
/*@__PURE__*/
messageDesc(file_user_v1_user, 5);
/**
* @generated from message user.v1.GetAPIKeyResponse
*/
export type GetAPIKeyResponse = Message<'user.v1.GetAPIKeyResponse'> & {
/**
* @generated from field: string key = 1;
*/
key: string;
};
/**
* Describes the message user.v1.GetAPIKeyResponse.
* Use `create(GetAPIKeyResponseSchema)` to create a new message.
*/
export const GetAPIKeyResponseSchema: GenMessage<GetAPIKeyResponse> =
/*@__PURE__*/
messageDesc(file_user_v1_user, 6);
/**
* @generated from message user.v1.UpdateProfilePictureRequest
*/
export type UpdateProfilePictureRequest = Message<'user.v1.UpdateProfilePictureRequest'> & {
/**
* @generated from field: string file_name = 1;
*/
fileName: string;
/**
* @generated from field: bytes data = 2;
*/
data: Uint8Array;
};
/**
* Describes the message user.v1.UpdateProfilePictureRequest.
* Use `create(UpdateProfilePictureRequestSchema)` to create a new message.
*/
export const UpdateProfilePictureRequestSchema: GenMessage<UpdateProfilePictureRequest> =
/*@__PURE__*/
messageDesc(file_user_v1_user, 7);
/**
* @generated from message user.v1.UpdateProfilePictureResponse
*/
export type UpdateProfilePictureResponse = Message<'user.v1.UpdateProfilePictureResponse'> & {
/**
* @generated from field: user.v1.User user = 1;
*/
user?: User;
};
/**
* Describes the message user.v1.UpdateProfilePictureResponse.
* Use `create(UpdateProfilePictureResponseSchema)` to create a new message.
*/
export const UpdateProfilePictureResponseSchema: GenMessage<UpdateProfilePictureResponse> =
/*@__PURE__*/
messageDesc(file_user_v1_user, 8);
/**
* @generated from service user.v1.UserService
*/
export const UserService: GenService<{
/**
* @generated from rpc user.v1.UserService.GetUser
*/
getUser: {
methodKind: 'unary';
input: typeof GetUserRequestSchema;
output: typeof GetUserResponseSchema;
};
/**
* @generated from rpc user.v1.UserService.UpdatePassword
*/
updatePassword: {
methodKind: 'unary';
input: typeof UpdatePasswordRequestSchema;
output: typeof UpdatePasswordResponseSchema;
};
/**
* @generated from rpc user.v1.UserService.GetAPIKey
*/
getAPIKey: {
methodKind: 'unary';
input: typeof GetAPIKeyRequestSchema;
output: typeof GetAPIKeyResponseSchema;
};
/**
* @generated from rpc user.v1.UserService.UpdateProfilePicture
*/
updateProfilePicture: {
methodKind: 'unary';
input: typeof UpdateProfilePictureRequestSchema;
output: typeof UpdateProfilePictureResponseSchema;
};
}> = /*@__PURE__*/ serviceDesc(file_user_v1_user, 0);

View File

@ -1,4 +1,4 @@
import type { User } from './connect/user/v1/user_pb';
import type { User } from './services/user/v1/user_pb';
export const userState: { user: User | undefined } = $state({
user: undefined

View File

@ -1,12 +1,9 @@
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 { Code, ConnectError, createClient, type Interceptor } from '@connectrpc/connect';
import { AuthService } from '$lib/services/user/v1/auth_pb';
import { UserService } from '$lib/services/user/v1/user_pb';
import { ItemService } from '$lib/services/item/v1/item_pb';
import { goto } from '$app/navigation';
import { page } from '$app/state';
import { ItemService } from '$lib/connect/item/v1/item_pb';
import { AuthService } from '$lib/connect/user/v1/auth_pb';
import { UserService } from '$lib/connect/user/v1/user_pb';
const redirector: Interceptor = (next) => async (req) => {
try {
@ -14,14 +11,7 @@ const redirector: Interceptor = (next) => async (req) => {
} catch (e) {
const error = ConnectError.from(e);
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)
);
await goto(redirectURL);
await goto('/auth');
}
throw e;
}
@ -35,5 +25,3 @@ const transport = createConnectTransport({
export const AuthClient = createClient(AuthService, transport);
export const UserClient = createClient(UserService, transport);
export const ItemClient = createClient(ItemService, transport);
export const Validator = createValidator();

View File

@ -0,0 +1,15 @@
<script lang="ts">
import { userState } from '$lib/sharedState.svelte';
import { Avatar } from 'bits-ui';
</script>
<Avatar.Root class="flex h-full w-full items-center justify-center">
<Avatar.Image
src={userState.user?.profilePicture}
alt={`${userState.user?.username}'s avatar`}
class="rounded-full"
/>
<Avatar.Fallback class="font-medium uppercase"
>{userState.user?.username.substring(0, 2)}</Avatar.Fallback
>
</Avatar.Root>

View File

@ -0,0 +1,29 @@
<script lang="ts">
import { Button } from 'bits-ui';
import { cn } from '$lib/utils';
import type { MouseEventHandler } from 'svelte/elements';
import type { Snippet } from 'svelte';
let {
className,
type,
onclick,
children
}: {
className?: string;
type?: 'submit' | 'reset' | 'button' | null;
onclick?: () => MouseEventHandler<HTMLButtonElement> | null | undefined;
children?: Snippet<[]>;
} = $props();
</script>
<Button.Root
{type}
class={cn(
'bg-sky text-crust focus:outline-sky flex w-fit cursor-pointer items-center justify-center rounded p-2 px-4 text-sm font-medium transition-all hover:brightness-120 focus:outline-2 focus:outline-offset-1',
className
)}
{onclick}
>
{@render children?.()}
</Button.Root>

View File

@ -0,0 +1,170 @@
<script lang="ts">
import { ArrowLeft, ArrowRight, Minus, Calendar, X } from '@lucide/svelte';
import { DateRangePicker, type DateRange } from 'bits-ui';
import { fade } from 'svelte/transition';
import { getLocalTimeZone } from '@internationalized/date';
import { cn } from '$lib/utils';
let {
className,
start = $bindable(),
end = $bindable(),
onchange
}: {
className?: string;
start?: Date;
end?: Date;
onchange?: (start?: Date, end?: Date) => void;
} = $props();
let daterange: DateRange = $state({
start: undefined,
end: undefined
});
let rerender = $state(false);
</script>
<!-- Need to rerender because setting to undefined doesn't work -->
{#key rerender}
<DateRangePicker.Root
bind:value={daterange}
onValueChange={(v) => {
if (v.start && v.end) {
start = v.start.toDate(getLocalTimeZone());
end = v.end.toDate(getLocalTimeZone());
if (onchange) {
onchange(start, end);
}
}
}}
class={cn(className)}
>
<div
class="bg-mantle border-surface-0 hover:border-surface-2 flex items-center rounded border pl-2 text-sm drop-shadow-md transition-all"
>
<div class="flex grow items-center justify-center">
{#each ['start', 'end'] as const as type (type)}
<DateRangePicker.Input {type}>
{#snippet children({ segments })}
{#each segments as { part, value } (value)}
<div class="inline-block select-none">
{#if part === 'literal'}
<DateRangePicker.Segment {part} class="text-overlay-0 p-1">
{value}
</DateRangePicker.Segment>
{:else}
<DateRangePicker.Segment
{part}
class="aria-[valuetext=Empty]:text-overlay-0 hover:bg-surface-0 focus:bg-surface-0 focus:outline-sky rounded p-0.5 transition-all focus:outline focus:outline-offset-1"
>
{value}
</DateRangePicker.Segment>
{/if}
</div>
{/each}
{/snippet}
</DateRangePicker.Input>
{#if type === 'start'}
<div aria-hidden="true" class="px-1">
<Minus size="10" />
</div>
{/if}
{/each}
</div>
<DateRangePicker.Trigger
class="text-overlay-2 hover:bg-surface-0 focus:outline-sky ml-1 flex grow cursor-pointer items-center justify-center p-2 transition-all focus:outline focus:outline-offset-1"
>
<Calendar size="20" />
</DateRangePicker.Trigger>
<button
class="text-overlay-2 hover:bg-surface-0 focus:outline-sky cursor-pointer rounded-r p-2 transition-all focus:outline focus:outline-offset-1"
onclick={() => {
if (daterange) {
daterange.end = undefined;
daterange.start = undefined;
}
start = undefined;
end = undefined;
if (onchange) {
onchange(start, end);
}
rerender = !rerender;
}}
>
<X size="20" />
</button>
</div>
<DateRangePicker.Content forceMount>
{#snippet child({ props, open })}
{#if open}
<div
{...props}
class="absolute z-50"
transition:fade={{
duration: 100
}}
>
<DateRangePicker.Calendar
class="border-surface-0 bg-mantle mt-1 rounded border p-3 drop-shadow-md"
>
{#snippet children({ months, weekdays })}
<DateRangePicker.Header class="flex items-center justify-between">
<DateRangePicker.PrevButton
class="hover:bg-surface-0 inline-flex size-10 cursor-pointer items-center justify-center rounded transition-all active:scale-[0.98]"
>
<ArrowLeft />
</DateRangePicker.PrevButton>
<DateRangePicker.Heading class="font-medium select-none" />
<DateRangePicker.NextButton
class="hover:bg-surface-0 inline-flex size-10 cursor-pointer items-center justify-center rounded transition-all active:scale-[0.98]"
>
<ArrowRight />
</DateRangePicker.NextButton>
</DateRangePicker.Header>
<div class="flex flex-col space-y-4 pt-4 sm:flex-row sm:space-y-0 sm:space-x-4">
{#each months as month (month)}
<DateRangePicker.Grid class="w-full border-collapse space-y-1 select-none">
<DateRangePicker.GridHead>
<DateRangePicker.GridRow class="mb-1 flex w-full justify-between">
{#each weekdays as day (day)}
<DateRangePicker.HeadCell
class="text-overlay-0 w-10 rounded text-xs font-normal!"
>
{day.slice(0, 2)}
</DateRangePicker.HeadCell>
{/each}
</DateRangePicker.GridRow>
</DateRangePicker.GridHead>
<DateRangePicker.GridBody>
{#each month.weeks as weekDates (weekDates)}
<DateRangePicker.GridRow class="flex w-full">
{#each weekDates as date (date)}
<DateRangePicker.Cell
{date}
month={month.value}
class="relative m-0 size-10 overflow-visible p-0! text-center text-sm focus-within:relative focus-within:z-20"
>
<DateRangePicker.Day
class="hover:border-sky focus-visible:ring-foreground! data-highlighted:bg-surface-0 data-selected:bg-surface-1 data-selection-end:bg-surface-2 data-selection-start:bg-surface-2 data-disabled:text-text/30 data-unavailable:text-overlay-0 group relative inline-flex size-10 items-center justify-center overflow-visible rounded border border-transparent bg-transparent p-0 text-sm font-normal whitespace-nowrap transition-all data-disabled:pointer-events-none data-highlighted:rounded-none data-outside-month:pointer-events-none data-selected:rounded-none data-selection-end:rounded-r data-selection-start:rounded-l data-unavailable:line-through"
>
<div
class="bg-sky group-data-selected:bg-background absolute top-[5px] hidden size-1 rounded-full transition-all group-data-today:block"
></div>
{date.day}
</DateRangePicker.Day>
</DateRangePicker.Cell>
{/each}
</DateRangePicker.GridRow>
{/each}
</DateRangePicker.GridBody>
</DateRangePicker.Grid>
{/each}
</div>
{/snippet}
</DateRangePicker.Calendar>
</div>
{/if}
{/snippet}
</DateRangePicker.Content>
</DateRangePicker.Root>
{/key}

View File

@ -0,0 +1,51 @@
<script lang="ts">
import { cn } from '$lib/utils';
import { X } from '@lucide/svelte';
let {
name,
value = $bindable(''),
type = 'text',
placeholder,
className,
onchange
}: {
name?: string;
value?: string | number;
type?: string;
placeholder?: string;
className?: string;
onchange?: (e: Event) => void;
} = $props();
</script>
<div
class={cn(
'border-surface-0 hover:border-surface-2 flex items-center justify-between gap-1 rounded border p-0 drop-shadow-md transition-all',
className
)}
>
<input
id={name}
{name}
{type}
{placeholder}
class="focus:outline-sky grow rounded-l p-2 text-sm transition-all focus:outline focus:outline-offset-1"
bind:value
{onchange}
/>
<button
class="text-overlay-2 hover:bg-surface-0 focus:outline-sky cursor-pointer rounded-r p-2 transition-all focus:outline focus:outline-offset-1"
type="button"
onclick={(e) => {
if (value) {
value = '';
if (onchange) {
onchange(e);
}
}
}}
>
<X size="20" />
</button>
</div>

View File

@ -0,0 +1,74 @@
<script lang="ts">
import { X } from '@lucide/svelte';
import { Dialog } from 'bits-ui';
import { fade } from 'svelte/transition';
import type { Snippet } from 'svelte';
let {
trigger,
title,
content,
open = $bindable(false)
}: {
trigger: Snippet<[Record<string, unknown>]>;
title: Snippet;
content: Snippet;
open?: boolean;
} = $props();
</script>
<Dialog.Root bind:open>
<Dialog.Trigger>
{#snippet child({ props })}
{@render trigger(props)}
{/snippet}
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay forceMount>
{#snippet child({ props, open })}
{#if open}
<div
{...props}
transition:fade={{
duration: 100
}}
>
<div class="fixed inset-0 z-50 mt-[50px] bg-black/50 transition-all"></div>
</div>
{/if}
{/snippet}
</Dialog.Overlay>
<Dialog.Content forceMount>
{#snippet child({ props, open: propopen })}
{#if propopen}
<div
{...props}
transition:fade={{
duration: 100
}}
>
<div
class="bg-mantle border-surface-0 fixed inset-0 top-[50%] left-[50%] z-50 size-fit w-96 -translate-x-1/2 -translate-y-1/2 transform overflow-y-auto rounded-xl border pb-1 drop-shadow-md"
>
<div class="border-surface-0 flex justify-between border-b p-2">
<h1 class="grow truncate p-1 text-center text-xl font-bold">
{@render title()}
</h1>
<button
tabindex="-1"
class="text-overlay-2 hover:bg-surface-0 focus:outline-sky cursor-pointer rounded p-1 transition-all focus:outline focus:outline-offset-1"
onclick={() => {
open = false;
}}
>
<X />
</button>
</div>
{@render content()}
</div>
</div>
{/if}
{/snippet}
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>

View File

@ -0,0 +1,63 @@
<script lang="ts">
import { cn } from '$lib/utils';
import { ChevronLeft, ChevronRight } from '@lucide/svelte';
import { Pagination } from 'bits-ui';
let {
count = $bindable(),
limit = $bindable(),
offset = $bindable(0),
className,
onchange
}: {
count: number;
limit: number;
offset?: number;
className?: string;
onchange?: (e: number) => void;
} = $props();
</script>
{#key count && limit}
<Pagination.Root
{count}
perPage={limit}
onPageChange={(e) => {
offset = (e - 1) * limit;
window.scrollTo(0, 0);
onchange?.(e);
}}
>
{#snippet children({ pages, range })}
<div class={cn('mb-2 flex items-center justify-center gap-2', className)}>
<Pagination.PrevButton
class="hover:bg-surface-0 disabled:text-overlay-0 inline-flex cursor-pointer items-center justify-center rounded p-2 transition-all disabled:cursor-not-allowed hover:disabled:bg-transparent"
>
<ChevronLeft />
</Pagination.PrevButton>
<div class="flex items-center gap-2">
{#each pages as page (page.key)}
{#if page.type === 'ellipsis'}
<div class="font-medium select-none">...</div>
{:else}
<Pagination.Page
{page}
class="hover:bg-surface-0 data-selected:bg-surface-0 data-selected:text-background inline-flex size-10 cursor-pointer items-center justify-center rounded bg-transparent font-medium transition-all select-none disabled:cursor-not-allowed disabled:opacity-50 hover:disabled:bg-transparent"
>
{page.value}
</Pagination.Page>
{/if}
{/each}
</div>
<Pagination.NextButton
class="hover:bg-surface-0 disabled:text-overlay-0 inline-flex cursor-pointer items-center justify-center rounded p-2 transition-all disabled:cursor-not-allowed hover:disabled:bg-transparent"
>
<ChevronRight />
</Pagination.NextButton>
</div>
<p class="text-overlay-2 text-center text-sm">
Showing {range.start} - {range.end}
</p>
{/snippet}
</Pagination.Root>
{/key}

View File

@ -0,0 +1,94 @@
<script lang="ts">
import { cn } from '$lib/utils';
import { Check, ChevronsDown, ChevronsUp, ChevronsUpDown, X } from '@lucide/svelte';
import { Select } from 'bits-ui';
import { fade } from 'svelte/transition';
let {
value = $bindable('10'),
placeholder = 'Select an item',
items = [],
defaultValue = '',
className,
onchange
}: {
value?: string;
placeholder?: string;
items: { value: string; label: string; disabled?: boolean }[];
defaultValue?: string;
className?: string;
onchange?: (e: string) => void;
} = $props();
const selectedLabel = $derived(value ? items.find((i) => i.value === value)?.label : placeholder);
</script>
<div
class={cn(
'border-surface-0 bg-mantle hover:border-surface-2 flex items-center justify-between rounded border p-0 drop-shadow-md transition-all',
className
)}
>
<Select.Root type="single" {items} bind:value onValueChange={onchange}>
<Select.Trigger
class="focus:outline-sky data-placeholder:text-overlay-0 inline-flex grow cursor-pointer items-center justify-between gap-2 rounded-l py-2 pl-2 text-sm transition-colors select-none focus:outline focus:outline-offset-1"
aria-label={placeholder}
>
{selectedLabel}
<ChevronsUpDown class="text-overlay-0" size="20" />
</Select.Trigger>
<Select.Portal>
<Select.Content forceMount>
{#snippet child({ wrapperProps, props, open })}
{#if open}
<div {...wrapperProps}>
<div
{...props}
class="border-surface-0 bg-mantle shadow-popover z-50 mt-1 rounded border p-1 outline-hidden select-none"
transition:fade={{
duration: 100
}}
>
<Select.ScrollUpButton class="flex w-full items-center justify-center">
<ChevronsUp size="20" />
</Select.ScrollUpButton>
<Select.Viewport class="p-1">
{#each items as item, i (i + item.value)}
<Select.Item
class="data-highlighted:bg-surface-0 flex h-10 w-full cursor-pointer items-center gap-4 rounded px-5 py-3 text-sm capitalize outline-hidden select-none data-disabled:cursor-not-allowed data-disabled:opacity-50"
value={item.value}
label={item.label}
disabled={item.disabled}
>
{#snippet children({ selected })}
{item.label}
{#if selected}
<div class="ml-auto">
<Check size="20" />
</div>
{/if}
{/snippet}
</Select.Item>
{/each}
</Select.Viewport>
<Select.ScrollDownButton class="flex w-full items-center justify-center">
<ChevronsDown size="20" />
</Select.ScrollDownButton>
</div>
</div>
{/if}
{/snippet}
</Select.Content>
</Select.Portal>
</Select.Root>
<button
class="text-overlay-2 hover:bg-surface-0 focus:outline-sky cursor-pointer rounded-r p-2 transition-all focus:outline focus:outline-offset-1"
type="button"
onclick={() => {
value = defaultValue;
onchange?.(value);
}}
>
<X size="20" />
</button>
</div>

View File

@ -1,20 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { Avatar as AvatarPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
class: className,
...restProps
}: AvatarPrimitive.FallbackProps = $props();
</script>
<AvatarPrimitive.Fallback
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',
className
)}
{...restProps}
/>

View File

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

View File

@ -1,21 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { Avatar as AvatarPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
class: className,
...restProps
}: AvatarPrimitive.RootProps = $props();
</script>
<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
)}
{...restProps}
/>

View File

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

View File

@ -1,94 +0,0 @@
<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 { cn } from '$lib/utils';
import { tv } from 'tailwind-variants';
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',
// Focus
'focus-visible:outline-accent focus-visible:outline-2 focus-visible:outline-offset-2',
// Disabled
'disabled:pointer-events-none disabled:opacity-50',
// Images
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-5"
),
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'
},
size: {
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5',
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: 'h-9 min-w-9 px-3'
}
},
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;
loading?: boolean;
};
</script>
<script lang="ts">
let {
class: className,
variant = 'default',
size = 'default',
ref = $bindable(null),
href = undefined,
type = 'button',
loading,
children,
...restProps
}: ButtonProps = $props();
</script>
{#if href}
<a
bind:this={ref}
data-slot="button"
class={cn(buttonVariants({ variant, size }), className)}
{href}
{...restProps}
>
{#if loading}
<LoaderCircle class="animate-spin" />
{/if}
{@render children?.()}
</a>
{:else}
<button
bind:this={ref}
data-slot="button"
class={cn(buttonVariants({ variant, size }), className)}
{type}
{...restProps}
>
{#if loading}
<LoaderCircle class="animate-spin" />
{/if}
{@render children?.()}
</button>
{/if}

View File

@ -1,13 +0,0 @@
import type { ButtonProps, ButtonSize, ButtonVariant } from './button.svelte';
import Root, { buttonVariants } from './button.svelte';
export {
Root,
type ButtonProps as Props,
//
Root as Button,
buttonVariants,
type ButtonProps,
type ButtonSize,
type ButtonVariant
};

View File

@ -1,17 +0,0 @@
<script lang="ts">
import type { WithElementRef } from 'bits-ui';
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils';
type Props = WithElementRef<HTMLAttributes<HTMLDivElement>>;
let { ref = $bindable(null), class: className, children, ...restProps }: Props = $props();
</script>
<div
bind:this={ref}
class={cn('bg-based border-surface-1 rounded border p-5 shadow-md', className)}
{...restProps}
>
{@render children?.()}
</div>

View File

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

View File

@ -1,76 +0,0 @@
<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 { cn } from '$lib/utils.js';
let {
className,
start,
end,
onchange
}: {
className?: string;
start?: Date;
end?: Date;
onchange?: (start: Date, end: Date) => void;
} = $props();
let value: DateRange = $state({
start: start
? new CalendarDate(start.getFullYear(), start.getMonth(), start.getDate())
: undefined,
end: end ? new CalendarDate(end.getFullYear(), end.getMonth(), end.getDate()) : undefined
});
const df = new DateFormatter('en-US', {
dateStyle: 'medium'
});
let startValue: DateValue | undefined = $state(undefined);
</script>
<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'
)}
>
<CalendarIcon class="mr-2 size-4" />
{#if value && value.start}
{#if value.end}
{df.format(value.start.toDate(getLocalTimeZone()))} - {df.format(
value.end.toDate(getLocalTimeZone())
)}
{:else}
{df.format(value.start.toDate(getLocalTimeZone()))}
{/if}
{:else if startValue}
{df.format(startValue.toDate(getLocalTimeZone()))}
{:else}
Pick a date range
{/if}
</Popover.Trigger>
<Popover.Content class="bg-based w-auto p-0" align="start">
<RangeCalendar
bind:value
onStartValueChange={(v) => {
startValue = v;
}}
onValueChange={(v) => {
if (v.start && v.end) {
onchange?.(v.start.toDate(getLocalTimeZone()), v.end.toDate(getLocalTimeZone()));
}
}}
numberOfMonths={2}
/>
</Popover.Content>
</Popover.Root>
</div>

View File

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

View File

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

View File

@ -1,51 +0,0 @@
<script lang="ts">
import type { WithoutChildrenOrChild } from 'bits-ui';
import type { Snippet } from 'svelte';
import X from '@lucide/svelte/icons/x';
import { cn } from '$lib/utils.js';
import { Dialog as DialogPrimitive } from 'bits-ui';
import * as Dialog from './index.js';
let {
ref = $bindable(null),
class: className,
portalProps,
children,
...restProps
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
portalProps?: DialogPrimitive.PortalProps;
children: Snippet;
} = $props();
</script>
<Dialog.Portal {...portalProps}>
<Dialog.Overlay />
<DialogPrimitive.Content
bind:ref
data-slot="dialog-content"
class={cn(
'bg-mantle text-text border-surface-1 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg sm:max-w-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 duration-200',
className
)}
{...restProps}
>
{@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',
// Focus
'focus-visible:outline-accent focus-visible:outline-2 focus-visible:outline-offset-2',
// Images
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
)}
>
<X />
<span class="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</Dialog.Portal>

View File

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

View File

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

View File

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

View File

@ -1,20 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { Dialog as DialogPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
class: className,
...restProps
}: DialogPrimitive.OverlayProps = $props();
</script>
<DialogPrimitive.Overlay
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',
className
)}
{...restProps}
/>

View File

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

View File

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

View File

@ -1,28 +0,0 @@
<script lang="ts">
import { pushState } from '$app/navigation';
import { Dialog as DialogPrimitive } from 'bits-ui';
let { open = $bindable(), onOpenChange, ...restProps }: DialogPrimitive.RootProps = $props();
</script>
<svelte:window
onpopstate={() => {
if (open) {
open = false;
}
}}
/>
<DialogPrimitive.Root
onOpenChange={(v) => {
if (v) {
pushState('', '#dialog');
} else {
history.back();
}
onOpenChange?.(v);
}}
bind:open
{...restProps}
/>

View File

@ -1,36 +0,0 @@
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 Root from './dialog.svelte';
const Portal = DialogPrimitive.Portal;
export {
Root,
Title,
Portal,
Footer,
Header,
Trigger,
Overlay,
Content,
Description,
Close,
//
Root as Dialog,
Title as DialogTitle,
Portal as DialogPortal,
Footer as DialogFooter,
Header as DialogHeader,
Trigger as DialogTrigger,
Overlay as DialogOverlay,
Content as DialogContent,
Description as DialogDescription,
Close as DialogClose
};

View File

@ -1,48 +0,0 @@
<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';
let {
ref = $bindable(null),
checked = $bindable(false),
indeterminate = $bindable(false),
class: className,
children: childrenProp,
...restProps
}: WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> & {
children?: Snippet;
} = $props();
</script>
<DropdownMenuPrimitive.CheckboxItem
bind:ref
bind:checked
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',
// Disabled
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
// Images
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...restProps}
>
{#snippet children({ checked, indeterminate })}
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
{#if indeterminate}
<Minus class="size-4" />
{:else}
<Check class={cn('size-4', !checked && 'text-transparent')} />
{/if}
</span>
{@render childrenProp?.()}
{/snippet}
</DropdownMenuPrimitive.CheckboxItem>

View File

@ -1,31 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
sideOffset = 4,
portalProps,
class: className,
...restProps
}: DropdownMenuPrimitive.ContentProps & {
portalProps?: DropdownMenuPrimitive.PortalProps;
} = $props();
</script>
<DropdownMenuPrimitive.Portal {...portalProps}>
<DropdownMenuPrimitive.Content
bind:ref
data-slot="dropdown-menu-content"
{sideOffset}
class={cn(
'bg-based text-text border-surface z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md',
// 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',
className
)}
{...restProps}
/>
</DropdownMenuPrimitive.Portal>

View File

@ -1,17 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils';
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
class: className,
...restProps
}: DropdownMenuPrimitive.GroupProps = $props();
</script>
<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)}
{...restProps}
/>

View File

@ -1,27 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
class: className,
...restProps
}: DropdownMenuPrimitive.ItemProps = $props();
</script>
<DropdownMenuPrimitive.Item
bind:ref
data-slot="dropdown-menu-item"
class={cn(
'focus:bg-surface text-text relative flex cursor-pointer items-center gap-2 px-4 py-2 text-sm outline-hidden transition-all select-none',
// Disabled
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
// Images
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...restProps}
/>

View File

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

View File

@ -1,22 +0,0 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import Item from './dropdown-menu-item.svelte';
let {
ref = $bindable(null),
class: className,
href,
children,
...restProps
}: DropdownMenuPrimitive.ItemProps & {
href?: string;
} = $props();
</script>
<Item bind:ref class={className} {...restProps}>
{#snippet child({ props })}
<a {...props} {href}>
{@render children?.()}
</a>
{/snippet}
</Item>

View File

@ -1,16 +0,0 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
value = $bindable(),
...restProps
}: DropdownMenuPrimitive.RadioGroupProps = $props();
</script>
<DropdownMenuPrimitive.RadioGroup
bind:ref
bind:value
data-slot="dropdown-menu-radio-group"
{...restProps}
/>

View File

@ -1,40 +0,0 @@
<script lang="ts">
import 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),
class: className,
children: childrenProp,
...restProps
}: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props();
</script>
<DropdownMenuPrimitive.RadioItem
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',
// Disabled
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
// Images
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...restProps}
>
{#snippet children({ checked })}
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
{#if checked}
<Circle class="size-2 fill-current" />
{:else}
<Circle class="size-2" />
{/if}
</span>
{@render childrenProp?.({ checked })}
{/snippet}
</DropdownMenuPrimitive.RadioItem>

View File

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

View File

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

View File

@ -1,23 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
class: className,
...restProps
}: DropdownMenuPrimitive.SubContentProps = $props();
</script>
<DropdownMenuPrimitive.SubContent
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',
// 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',
className
)}
{...restProps}
/>

View File

@ -1,29 +0,0 @@
<script lang="ts">
import ChevronRight from '@lucide/svelte/icons/chevron-right';
import { cn } from '$lib/utils.js';
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
class: className,
inset,
children,
...restProps
}: DropdownMenuPrimitive.SubTriggerProps & {
inset?: boolean;
} = $props();
</script>
<DropdownMenuPrimitive.SubTrigger
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',
className
)}
{...restProps}
>
{@render children?.()}
<ChevronRight class="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>

View File

@ -1,23 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils';
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
class: className,
...restProps
}: DropdownMenuPrimitive.TriggerProps = $props();
</script>
<DropdownMenuPrimitive.Trigger
bind:ref
data-slot="dropdown-menu-trigger"
class={cn(
'flex cursor-pointer items-center gap-1 transition-all',
// Focus
'focus-visible:outline-accent focus-visible:outline-2 focus-visible:outline-offset-2',
className
)}
{...restProps}
/>

View File

@ -1,50 +0,0 @@
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 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';
const Sub = DropdownMenuPrimitive.Sub;
const Root = DropdownMenuPrimitive.Root;
export {
CheckboxItem,
Content,
Root as DropdownMenu,
CheckboxItem as DropdownMenuCheckboxItem,
Content as DropdownMenuContent,
Group as DropdownMenuGroup,
Item as DropdownMenuItem,
Link as DropdownMenuLink,
Label as DropdownMenuLabel,
RadioGroup as DropdownMenuRadioGroup,
RadioItem as DropdownMenuRadioItem,
Separator as DropdownMenuSeparator,
Shortcut as DropdownMenuShortcut,
Sub as DropdownMenuSub,
SubContent as DropdownMenuSubContent,
SubTrigger as DropdownMenuSubTrigger,
Trigger as DropdownMenuTrigger,
Group,
Item,
Link,
Label,
RadioGroup,
RadioItem,
Root,
Separator,
Shortcut,
Sub,
SubContent,
SubTrigger,
Trigger
};

View File

@ -1,31 +0,0 @@
import { getContext, hasContext, setContext } from 'svelte';
type Item = {
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);
return;
}
item.id = id;
item.name = name;
}
export function getFormContext() {
if (!hasContext(key)) {
return null;
}
return getContext(key) as Item;
}

View File

@ -1,30 +0,0 @@
<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';
type Props = WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> & {
errors?: Violation[] | ConnectError;
};
let {
ref = $bindable(null),
class: className,
errors = $bindable(),
...restProps
}: Props = $props();
const item = getFormContext();
</script>
<div bind:this={ref} class={cn('text-red text-sm', className)} {...restProps}>
{#if errors && Array.isArray(errors)}
{#each errors as error (error)}
<label for={item?.id}>{error.message}</label>
{/each}
{:else if errors}
<span>{errors.message}</span>
{/if}
</div>

View File

@ -1,21 +0,0 @@
<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';
type Props = WithElementRef<HTMLAttributes<HTMLDivElement>> & {
name?: string;
};
let { ref = $bindable(null), class: className, name, children, ...restProps }: Props = $props();
const uid = $props.id();
if (name) {
setFormContext(uid, name);
}
</script>
<div bind:this={ref} class={cn('flex flex-col gap-1', className)} {...restProps}>
{@render children?.()}
</div>

View File

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

View File

@ -1,29 +0,0 @@
<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';
type Props = WithElementRef<HTMLAttributes<HTMLLabelElement>>;
let { ref = $bindable(null), class: className, children, ...restProps }: Props = $props();
const item = getFormContext();
function formatName(name: string) {
// Replace _ with spaces
name = name.replace('_', ' ');
// Capitalize first letter
name = name.charAt(0).toUpperCase() + name.slice(1);
return name;
}
</script>
<label bind:this={ref} class={cn('text-sm', className)} for={item?.id} {...restProps}>
{#if children}
{@render children()}
{:else if item}
{formatName(item.name)}
{/if}
</label>

View File

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

View File

@ -1,75 +0,0 @@
<script lang="ts">
import type { WithElementRef } from 'bits-ui';
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from 'svelte/elements';
import { cn } from '$lib/utils.js';
import { getFormContext } from '../form/context.svelte';
type InputType = Exclude<HTMLInputTypeAttribute, 'file'>;
type Props = WithElementRef<
Omit<HTMLInputAttributes, 'type'> &
({ type: 'file'; files?: FileList } | { type?: InputType; files?: undefined })
>;
let {
ref = $bindable(null),
value = $bindable(),
type,
files = $bindable(),
class: className,
id,
name,
...restProps
}: Props = $props();
const item = getFormContext();
if (item && !id) {
id = item.id;
}
if (item && !name) {
name = item.name;
}
</script>
{#if type === 'file'}
<input
{id}
{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',
// Focus
'focus-visible:outline-accent focus-visible:outline-2 focus-visible:outline-offset-2',
// Disabled
'disabled:cursor-not-allowed disabled:opacity-50',
className
)}
type="file"
bind:files
bind:value
{...restProps}
/>
{:else}
<input
{id}
{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',
// Focus
'focus-visible:outline-accent focus-visible:outline-2 focus-visible:outline-offset-2',
// Disabled
'disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{type}
bind:value
{...restProps}
/>
{/if}

View File

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

View File

@ -1,20 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { Label as LabelPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
class: className,
...restProps
}: LabelPrimitive.RootProps = $props();
</script>
<LabelPrimitive.Root
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',
className
)}
{...restProps}
/>

View File

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

View File

@ -1,49 +0,0 @@
<script lang="ts">
import * as Pagination from '$lib/ui/pagination';
type Props = {
count: number;
limit?: number;
offset?: number;
onsubmit?: (offset: number) => void;
};
let { count = $bindable(), limit = 10, offset = $bindable(0), onsubmit }: Props = $props();
</script>
<Pagination.Root
{count}
page={offset / limit + 1}
perPage={limit}
onPageChange={async (num) => {
offset = num * limit - limit;
scrollTo({
top: 0,
behavior: 'smooth'
});
onsubmit?.(offset);
}}
>
{#snippet children({ pages, currentPage })}
<Pagination.Content>
<Pagination.Item>
<Pagination.PrevButton />
</Pagination.Item>
{#each pages as page (page.key)}
{#if page.type === 'ellipsis'}
<Pagination.Item class="hidden md:block">
<Pagination.Ellipsis />
</Pagination.Item>
{:else}
<Pagination.Item class="hidden md:block">
<Pagination.Link {page} isActive={currentPage === page.value}>
{page.value}
</Pagination.Link>
</Pagination.Item>
{/if}
{/each}
<Pagination.Item>
<Pagination.NextButton />
</Pagination.Item>
</Pagination.Content>
{/snippet}
</Pagination.Root>

View File

@ -1,25 +0,0 @@
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';
export {
Root,
Content,
Item,
Link,
PrevButton,
NextButton,
Ellipsis,
//
Root as Pagination,
Content as PaginationContent,
Item as PaginationItem,
Link as PaginationLink,
PrevButton as PaginationPrevButton,
NextButton as PaginationNextButton,
Ellipsis as PaginationEllipsis
};

View File

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

View File

@ -1,23 +0,0 @@
<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';
let {
ref = $bindable(null),
class: className,
...restProps
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLSpanElement>>> = $props();
</script>
<span
bind:this={ref}
aria-hidden="true"
data-slot="pagination-ellipsis"
class={cn('text-text flex size-9 items-center justify-center', className)}
{...restProps}
>
<Ellipsis class="size-4" />
<span class="sr-only">More pages</span>
</span>

View File

@ -1,14 +0,0 @@
<script lang="ts">
import type { WithElementRef } from 'bits-ui';
import type { HTMLLiAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
children,
...restProps
}: WithElementRef<HTMLLiAttributes> = $props();
</script>
<li bind:this={ref} data-slot="pagination-item" {...restProps}>
{@render children?.()}
</li>

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