98 Commits

Author SHA1 Message Date
296d2713ad bump: v0.0.20 -> v0.0.21 2025-05-12 11:36:17 -04:00
1e2d9a7894 build(nix): updated nix hashes 2025-05-12 11:33:24 -04:00
2fde1a8eba build(client): updated npm dependencies 2025-05-12 11:31:54 -04:00
07cec78aa5 feat: next 2025-05-12 11:30:33 -04:00
cdeaa13d92 feat: better components 2025-05-12 11:27:33 -04:00
398ddde169 bump: v0.0.19 -> v0.0.20 2025-04-17 22:23:25 -04:00
74b9bb86a1 fix: don't check all 2025-04-17 22:20:52 -04:00
701f80540b fix: yaml formatting 2025-04-17 22:18:56 -04:00
3926fa09ff bump: v0.0.18 -> v0.0.19 2025-04-17 22:14:31 -04:00
e3d9b437c5 Merge pull request #2 from spotdemo4/create-pull-request/patch
Bump deps
2025-04-17 22:10:25 -04:00
deaaec2c57 build(nix): updated nix hashes 2025-04-18 02:09:57 +00:00
bf5f07222c build(client): updated npm dependencies 2025-04-18 02:08:28 +00:00
75f89c0edf fix: wrap writeShellApplication 2025-04-17 22:02:50 -04:00
ad9ac18c18 refactor: scripts to apps 2025-04-17 22:00:23 -04:00
24d3067e52 refactor: nix flake 2025-04-17 20:49:51 -04:00
1c7cf7966f feat: improved building 2025-04-17 20:30:21 -04:00
7a799868fb feat: nix checks 2025-04-17 07:33:41 -04:00
0093a0879e feat: update nix flake 2025-04-17 05:30:22 -04:00
071b719309 Merge pull request #1 from spotdemo4/create-pull-request/patch
Bump deps
2025-04-17 05:23:25 -04:00
f6c5acee8f build(nix): updated nix hashes 2025-04-17 09:21:51 +00:00
8fe2b57c1e build(go): updated go dependencies 2025-04-17 09:21:10 +00:00
d4ae79a1cd build(client): updated npm dependencies 2025-04-17 09:21:04 +00:00
d49b699e83 fix: run lint on pull requests 2025-04-17 05:15:06 -04:00
2eaa9b300c fix: set git config 2025-04-17 05:00:12 -04:00
d0cf852b95 bump: v0.0.17 -> v0.0.18 2025-04-16 05:44:08 -04:00
ddce48625e feat: docker binary cache 2025-04-16 05:42:28 -04:00
fbe5efdf0f bump: v0.0.16 -> v0.0.17 2025-04-16 05:33:20 -04:00
77f8362f88 fix: add package write perms 2025-04-16 05:31:33 -04:00
f18107f9c4 bump: v0.0.15 -> v0.0.16 2025-04-16 05:21:56 -04:00
dbcb719166 bump: v0.0.14 -> v0.0.15 2025-04-16 04:44:40 -04:00
cdbb7e2c4d fix: publish to ghcr too 2025-04-16 04:42:48 -04:00
3629be7e0e bump: v0.0.13 -> v0.0.14 2025-04-16 04:26:29 -04:00
32e767757b build(nix): updated nix hashes 2025-04-16 04:23:25 -04:00
c1b14e03ac build(client): updated npm dependencies 2025-04-16 04:22:07 -04:00
b466f5cfb0 bump: v0.0.12 -> v0.0.13 2025-04-16 03:41:47 -04:00
c2778f175f fix: update subpackage 2025-04-16 03:40:35 -04:00
35e6403067 fix: dumb npm cli bug 2025-04-16 03:38:46 -04:00
dc1fb3b8b4 fix: dumb npm cli bug 2025-04-16 03:38:36 -04:00
45072a0e37 bump: v0.0.11 -> v0.0.12 2025-04-16 03:32:37 -04:00
095c477745 feat: dockerhub action 2025-04-16 03:31:38 -04:00
a25ff41363 fix: workflow dispatch formatting 2025-04-16 02:55:42 -04:00
1b780af78d feat: update workflow dispatch 2025-04-16 02:54:15 -04:00
7d36ea5925 fix: weird npm cli bug 2025-04-16 02:51:36 -04:00
e38719c292 feat: update workflow 2025-04-16 02:40:37 -04:00
beef83e02e fix: break out linting into seperate steps 2025-04-16 01:46:49 -04:00
b98a53eed7 fix: use trevstack cachix 2025-04-16 01:20:31 -04:00
481758e33b feat: fix nix update command 2025-04-16 01:19:56 -04:00
a9506ab67b build(nix): updated nix hashes 2025-04-16 01:18:41 -04:00
967e2650ad feat: sqlc, nix formatting 2025-04-16 00:58:44 -04:00
32f85fd0be bump: v0.0.10 -> v0.0.11 2025-04-10 19:16:35 -04:00
f9772bce47 feat: migrations 2025-04-10 19:15:21 -04:00
1667b78a0a build(nix): updated nix hashes 2025-04-10 19:14:39 -04:00
b741e5f1a2 build(server): updated go dependencies 2025-04-10 19:13:34 -04:00
439adecf0e build(client): updated npm dependencies 2025-04-10 19:13:31 -04:00
e9c44cbc94 feat: bob 2025-04-10 00:59:28 -04:00
dfd6789aa9 build(nix): updated nix hashes 2025-04-10 00:13:56 -04:00
8d36962bef build(server): updated go dependencies 2025-04-10 00:08:42 -04:00
35a2f0a918 build(client): updated npm dependencies 2025-04-10 00:08:36 -04:00
dd0995b241 WIP: stuff 2025-04-05 14:27:36 -04:00
93bc18022a WIP: passkey auth 2025-03-23 14:33:25 -04:00
f05e745d05 feat: shallow routing 2025-03-19 03:25:58 -04:00
8b494430a5 fix: daterangepicker key 2025-03-19 02:20:39 -04:00
d2238bdf9b fix: daterangepicker key 2025-03-19 02:12:27 -04:00
dc3106b4a4 bump: v0.0.9 -> v0.0.10 2025-03-19 00:46:21 -04:00
bc0e8c55e3 revert: use npm ci 2025-03-19 00:43:10 -04:00
124ddc20df fix: use nix packages for linting 2025-03-19 00:35:56 -04:00
218da3aef8 build(nix): updated nix hashes 2025-03-19 00:35:35 -04:00
e18457bacd build(client): updated npm dependencies 2025-03-19 00:34:26 -04:00
afb85cec88 build(nix): updated nix hashes 2025-03-19 00:33:28 -04:00
4e96ee38ca build(client): updated npm dependencies 2025-03-19 00:32:16 -04:00
89129f3495 bump: v0.0.8 -> v0.0.9 2025-03-18 23:27:02 -04:00
f8fb729c03 fix: use cachix for release workflow as well 2025-03-18 23:22:47 -04:00
b906e614ba fix: use cachix because github cache sucks 2025-03-18 23:18:30 -04:00
645faf398e fix: rely entirely on nix packages 2025-03-18 22:53:05 -04:00
add5afb17c fix: use legaly peer deps? 2025-03-18 22:38:33 -04:00
ab4ccc3d46 build(nix): updated nix hashes 2025-03-18 22:35:51 -04:00
ed7489f53d build(client): updated npm dependencies 2025-03-18 22:34:36 -04:00
94366907d8 fix: install npm packages before linting 2025-03-18 22:32:02 -04:00
ef11ac81a3 feat: lint workflow 2025-03-18 22:26:01 -04:00
73e7c563e0 bump: v0.0.7 -> v0.0.8 2025-03-18 22:14:11 -04:00
cd3da0aa8f feat: go caching 2025-03-18 22:12:56 -04:00
6706330252 bump: v0.0.6 -> v0.0.7 2025-03-18 22:07:05 -04:00
2b03164307 feat: workflow caching 2025-03-18 22:05:36 -04:00
0ffc1cd7f9 bump: v0.0.5 -> v0.0.6 2025-03-18 21:53:16 -04:00
c418792653 fix: select all build files 2025-03-18 21:52:00 -04:00
8956317197 bump: v0.0.4 -> v0.0.5 2025-03-18 21:42:25 -04:00
2361602b62 fix: use different release action 2025-03-18 21:40:43 -04:00
eba0067e1e bump: 0.0.3 -> 0.0.4 2025-03-18 21:25:42 -04:00
8d78f79fe2 fix: use nix develop in build action 2025-03-18 21:24:19 -04:00
5328e5b4a1 bump: 0.0.2 -> 0.0.3 2025-03-18 21:23:13 -04:00
81855d079f feat: release workflow 2025-03-18 21:21:16 -04:00
e857a14fd7 fix: add git dep 2025-03-18 19:25:18 -04:00
5e5a2cbaaa refactor: seperate default from trevstack in nix packages 2025-03-18 19:13:15 -04:00
267d293927 feat: linting 2025-03-18 19:02:50 -04:00
d8de02f789 refactor: move handlers into versioned dirs 2025-03-18 18:25:37 -04:00
be0981f7b7 feat: auth redirect 2025-03-18 17:46:54 -04:00
86b74e0ebc fix: conditional client build 2025-03-18 17:41:29 -04:00
21cd91156c fix: select dropdown transition 2025-03-18 17:05:21 -04:00
220 changed files with 15152 additions and 3940 deletions

View File

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

1
.envrc
View File

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

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

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

88
.github/workflows/release.yaml vendored Normal file
View File

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

41
.github/workflows/update.yaml vendored Normal file
View File

@ -0,0 +1,41 @@
name: Update Workflow
on:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
jobs:
update:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Nix
uses: cachix/install-nix-action@v31
with:
nix_path: nixpkgs=channel:nixos-unstable
- name: Use Cachix
uses: cachix/cachix-action@v16
with:
name: trevstack
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
# 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"
- name: Update
run: nix run .#update
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7
with:
delete-branch: true
title: Bump deps

16
.gitignore vendored
View File

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

22
.scripts/bump.sh Executable file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env bash
git_root=$(git rev-parse --show-toplevel)
git_version=$(git describe --tags --abbrev=0)
version=${git_version#v}
next_version=$(echo "${version}" | awk -F. -v OFS=. '{$NF += 1 ; print}')
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}"

41
.scripts/update.sh Executable file
View File

@ -0,0 +1,41 @@
#!/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.nix; then
git add flake.nix
git commit -m "build(nix): updated nix 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(go): 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

79
.treli.yaml Normal file
View File

@ -0,0 +1,79 @@
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 . || npx prettier --write .
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

7
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,7 @@
{
"go.lintTool": "revive",
"go.lintFlags": [
"--config=server/revive.toml"
],
"sqlfluff.config": "server/db/.sqlfluff",
}

View File

@ -9,6 +9,7 @@ WORKDIR /tmp/build
RUN nix \ RUN nix \
--extra-experimental-features "nix-command flakes" \ --extra-experimental-features "nix-command flakes" \
--option filter-syscalls false \ --option filter-syscalls false \
--accept-flake-config \
build build
# Copy the Nix store closure into a directory. The Nix store closure is the # Copy the Nix store closure into a directory. The Nix store closure is the
@ -25,4 +26,4 @@ WORKDIR /app
# Copy /nix/store # Copy /nix/store
COPY --from=builder /tmp/nix-store-closure /nix/store COPY --from=builder /tmp/nix-store-closure /nix/store
COPY --from=builder /tmp/build/result /app COPY --from=builder /tmp/build/result /app
CMD ["/app/bin/server"] CMD ["/app/bin/trevstack"]

View File

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

6
buf.lock Normal file
View File

@ -0,0 +1,6 @@
# Generated by buf. DO NOT EDIT.
version: v2
deps:
- name: buf.build/bufbuild/protovalidate
commit: 8976f5be98c146529b1cc15cd2012b60
digest: b5:5d513af91a439d9e78cacac0c9455c7cb885a8737d30405d0b91974fe05276d19c07a876a51a107213a3d01b83ecc912996cdad4cddf7231f91379079cf7488d

View File

@ -2,3 +2,5 @@
version: v2 version: v2
modules: modules:
- path: proto - path: proto
deps:
- buf.build/bufbuild/protovalidate

2
client/.gitignore vendored Normal file
View File

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

View File

@ -2,3 +2,14 @@
package-lock.json package-lock.json
pnpm-lock.yaml pnpm-lock.yaml
yarn.lock yarn.lock
# Build output
build
.svelte-kit
node_modules
# Static
static
# Generated
src/lib/connect

View File

@ -3,10 +3,8 @@
"singleQuote": true, "singleQuote": true,
"trailingComma": "none", "trailingComma": "none",
"printWidth": 100, "printWidth": 100,
"plugins": [ "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"prettier-plugin-svelte", "tailwindFunctions": ["tv", "cn", "clsx"],
"prettier-plugin-tailwindcss"
],
"overrides": [ "overrides": [
{ {
"files": "*.svelte", "files": "*.svelte",

View File

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

2609
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{ {
"name": "trevstack", "name": "trevstack",
"private": true, "private": true,
"version": "0.0.2", "version": "0.0.21",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
@ -14,33 +14,38 @@
"lint": "prettier --check . && eslint ." "lint": "prettier --check . && eslint ."
}, },
"devDependencies": { "devDependencies": {
"@bufbuild/protovalidate": "^0.1.1",
"@connectrpc/connect": "^2.0.2", "@connectrpc/connect": "^2.0.2",
"@connectrpc/connect-web": "^2.0.2", "@connectrpc/connect-web": "^2.0.2",
"@eslint/compat": "^1.2.7", "@eslint/compat": "^1.2.9",
"@eslint/js": "^9.18.0", "@eslint/js": "^9.18.0",
"@lucide/svelte": "^0.479.0", "@lucide/svelte": "^0.479.0",
"@scalar/api-reference": "^1.28.5", "@scalar/api-reference": "^1.28.32",
"@simplewebauthn/browser": "^13.1.0",
"@sveltejs/adapter-static": "^3.0.8", "@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/kit": "^2.20.1", "@sveltejs/kit": "^2.20.8",
"@sveltejs/vite-plugin-svelte": "^5.0.3", "@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tailwindcss/vite": "^4.0.14", "@tailwindcss/vite": "^4.1.6",
"bits-ui": "^1.3.13", "bits-ui": "^1.4.8",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"eslint": "^9.22.0", "eslint": "^9.26.0",
"eslint-config-prettier": "^10.1.1", "eslint-config-prettier": "^10.1.5",
"eslint-plugin-svelte": "^3.3.2", "eslint-plugin-svelte": "^3.5.1",
"globals": "^16.0.0", "fast-deep-equal": "^3.1.3",
"globals": "^16.1.0",
"mode-watcher": "^1.0.7",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.3.3", "prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.11", "prettier-plugin-tailwindcss": "^0.6.11",
"svelte": "^5.23.2", "svelte": "^5.28.2",
"svelte-check": "^4.1.5", "svelte-check": "^4.1.7",
"svelte-sonner": "^0.3.28", "svelte-sonner": "^0.3.28",
"tailwind-merge": "^3.0.2", "tailwind-merge": "^3.3.0",
"tailwind-variants": "^1.0.0",
"tailwindcss": "^4.0.13", "tailwindcss": "^4.0.13",
"tw-animate-css": "^1.2.3", "tw-animate-css": "^1.2.9",
"typescript": "^5.8.2", "typescript": "^5.8.3",
"typescript-eslint": "^8.26.1", "typescript-eslint": "^8.32.0",
"vite": "^6.2.2" "vite": "^6.3.5"
} }
} }

View File

@ -1,24 +1,105 @@
@import "tailwindcss"; @import 'tailwindcss';
@import "tw-animate-css"; @import "tw-animate-css";
@theme { @custom-variant dark (&:where(.dark, .dark *));
--color-crust: #11111b;
--color-mantle: #181825;
--color-base: #1e1e2e;
--color-surface-0: #313244; @theme inline {
--color-surface-1: #45475a; --spacing-body: calc(100vh - 180px);
--color-surface-2: #585b70;
--color-overlay-0: #6c7086; --color-accent: var(--sky);
--color-overlay-1: #7f849c;
--color-overlay-2: #9399b2;
--color-subtext-0: #a6adc8; --color-rosewater: var(--rosewater);
--color-subtext-1: #bac2de; --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-text: #cdd6f4; @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%);
}
--color-sky: #89dceb; .dark {
--color-red: #f38ba8; --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> <title>TrevStack</title>
%sveltekit.head% %sveltekit.head%
</head> </head>
<body data-sveltekit-preload-data="tap" class="min-h-screen bg-base text-text"> <body data-sveltekit-preload-data="tap" class="bg-crust text-text min-h-screen">
<div style="display: contents">%sveltekit.body%</div> <div style="display: contents">%sveltekit.body%</div>
</body> </body>
</html> </html>

File diff suppressed because one or more lines are too long

View File

@ -1,9 +1,10 @@
// @generated by protoc-gen-es v2.2.3 with parameter "target=ts" // @generated by protoc-gen-es v2.2.5 with parameter "target=ts"
// @generated from file item/v1/item.proto (package item.v1, syntax proto3) // @generated from file item/v1/item.proto (package item.v1, syntax proto3)
/* eslint-disable */ /* eslint-disable */
import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv1"; import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv1";
import { fileDesc, messageDesc, serviceDesc } 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 type { Timestamp } from "@bufbuild/protobuf/wkt";
import { file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt"; import { file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt";
import type { Message } from "@bufbuild/protobuf"; import type { Message } from "@bufbuild/protobuf";
@ -12,16 +13,16 @@ import type { Message } from "@bufbuild/protobuf";
* Describes the file item/v1/item.proto. * Describes the file item/v1/item.proto.
*/ */
export const file_item_v1_item: GenFile = /*@__PURE__*/ export const file_item_v1_item: GenFile = /*@__PURE__*/
fileDesc("ChJpdGVtL3YxL2l0ZW0ucHJvdG8SB2l0ZW0udjEinAEKBEl0ZW0SDwoCaWQYASABKA1IAIgBARIMCgRuYW1lGAIgASgJEhMKC2Rlc2NyaXB0aW9uGAMgASgJEg0KBXByaWNlGAQgASgCEhAKCHF1YW50aXR5GAUgASgNEi4KBWFkZGVkGAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgBiAEBQgUKA19pZEIICgZfYWRkZWQiHAoOR2V0SXRlbVJlcXVlc3QSCgoCaWQYASABKA0iLgoPR2V0SXRlbVJlc3BvbnNlEhsKBGl0ZW0YASABKAsyDS5pdGVtLnYxLkl0ZW0i3wEKD0dldEl0ZW1zUmVxdWVzdBIuCgVzdGFydBgBIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBIAIgBARIsCgNlbmQYAiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wSAGIAQESEwoGZmlsdGVyGAMgASgJSAKIAQESEgoFbGltaXQYBCABKA1IA4gBARITCgZvZmZzZXQYBSABKA1IBIgBAUIICgZfc3RhcnRCBgoEX2VuZEIJCgdfZmlsdGVyQggKBl9saW1pdEIJCgdfb2Zmc2V0Ij8KEEdldEl0ZW1zUmVzcG9uc2USHAoFaXRlbXMYASADKAsyDS5pdGVtLnYxLkl0ZW0SDQoFY291bnQYAiABKAQiMAoRQ3JlYXRlSXRlbVJlcXVlc3QSGwoEaXRlbRgBIAEoCzINLml0ZW0udjEuSXRlbSIxChJDcmVhdGVJdGVtUmVzcG9uc2USGwoEaXRlbRgBIAEoCzINLml0ZW0udjEuSXRlbSIwChFVcGRhdGVJdGVtUmVxdWVzdBIbCgRpdGVtGAEgASgLMg0uaXRlbS52MS5JdGVtIjEKElVwZGF0ZUl0ZW1SZXNwb25zZRIbCgRpdGVtGAEgASgLMg0uaXRlbS52MS5JdGVtIh8KEURlbGV0ZUl0ZW1SZXF1ZXN0EgoKAmlkGAEgASgNIhQKEkRlbGV0ZUl0ZW1SZXNwb25zZTLrAgoLSXRlbVNlcnZpY2USPgoHR2V0SXRlbRIXLml0ZW0udjEuR2V0SXRlbVJlcXVlc3QaGC5pdGVtLnYxLkdldEl0ZW1SZXNwb25zZSIAEkEKCEdldEl0ZW1zEhguaXRlbS52MS5HZXRJdGVtc1JlcXVlc3QaGS5pdGVtLnYxLkdldEl0ZW1zUmVzcG9uc2UiABJHCgpDcmVhdGVJdGVtEhouaXRlbS52MS5DcmVhdGVJdGVtUmVxdWVzdBobLml0ZW0udjEuQ3JlYXRlSXRlbVJlc3BvbnNlIgASRwoKVXBkYXRlSXRlbRIaLml0ZW0udjEuVXBkYXRlSXRlbVJlcXVlc3QaGy5pdGVtLnYxLlVwZGF0ZUl0ZW1SZXNwb25zZSIAEkcKCkRlbGV0ZUl0ZW0SGi5pdGVtLnYxLkRlbGV0ZUl0ZW1SZXF1ZXN0GhsuaXRlbS52MS5EZWxldGVJdGVtUmVzcG9uc2UiAEKdAQoLY29tLml0ZW0udjFCCUl0ZW1Qcm90b1ABWkZnaXRodWIuY29tL3Nwb3RkZW1vNC90cmV2c3RhY2svc2VydmVyL2ludGVybmFsL3NlcnZpY2VzL2l0ZW0vdjE7aXRlbXYxogIDSVhYqgIHSXRlbS5WMcoCB0l0ZW1cVjHiAhNJdGVtXFYxXEdQQk1ldGFkYXRh6gIISXRlbTo6VjFiBnByb3RvMw", [file_google_protobuf_timestamp]); fileDesc("ChJpdGVtL3YxL2l0ZW0ucHJvdG8SB2l0ZW0udjEiigEKBEl0ZW0SCgoCaWQYASABKAMSFQoEbmFtZRgCIAEoCUIHukgEcgIQAxIpCgVhZGRlZBgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASEwoLZGVzY3JpcHRpb24YBCABKAkSDQoFcHJpY2UYBSABKAISEAoIcXVhbnRpdHkYBiABKAUiHAoOR2V0SXRlbVJlcXVlc3QSCgoCaWQYASABKAMiLgoPR2V0SXRlbVJlc3BvbnNlEhsKBGl0ZW0YASABKAsyDS5pdGVtLnYxLkl0ZW0i3wEKD0dldEl0ZW1zUmVxdWVzdBIuCgVzdGFydBgBIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBIAIgBARIsCgNlbmQYAiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wSAGIAQESEwoGZmlsdGVyGAMgASgJSAKIAQESEgoFbGltaXQYBCABKAVIA4gBARITCgZvZmZzZXQYBSABKAVIBIgBAUIICgZfc3RhcnRCBgoEX2VuZEIJCgdfZmlsdGVyQggKBl9saW1pdEIJCgdfb2Zmc2V0Ij8KEEdldEl0ZW1zUmVzcG9uc2USHAoFaXRlbXMYASADKAsyDS5pdGVtLnYxLkl0ZW0SDQoFY291bnQYAiABKAMiVwoRQ3JlYXRlSXRlbVJlcXVlc3QSDAoEbmFtZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCRINCgVwcmljZRgDIAEoAhIQCghxdWFudGl0eRgEIAEoBSJLChJDcmVhdGVJdGVtUmVzcG9uc2USCgoCaWQYASABKAMSKQoFYWRkZWQYAiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIqcBChFVcGRhdGVJdGVtUmVxdWVzdBIKCgJpZBgBIAEoAxIRCgRuYW1lGAIgASgJSACIAQESGAoLZGVzY3JpcHRpb24YAyABKAlIAYgBARISCgVwcmljZRgEIAEoAkgCiAEBEhUKCHF1YW50aXR5GAUgASgFSAOIAQFCBwoFX25hbWVCDgoMX2Rlc2NyaXB0aW9uQggKBl9wcmljZUILCglfcXVhbnRpdHkiFAoSVXBkYXRlSXRlbVJlc3BvbnNlIh8KEURlbGV0ZUl0ZW1SZXF1ZXN0EgoKAmlkGAEgASgDIhQKEkRlbGV0ZUl0ZW1SZXNwb25zZTLrAgoLSXRlbVNlcnZpY2USPgoHR2V0SXRlbRIXLml0ZW0udjEuR2V0SXRlbVJlcXVlc3QaGC5pdGVtLnYxLkdldEl0ZW1SZXNwb25zZSIAEkEKCEdldEl0ZW1zEhguaXRlbS52MS5HZXRJdGVtc1JlcXVlc3QaGS5pdGVtLnYxLkdldEl0ZW1zUmVzcG9uc2UiABJHCgpDcmVhdGVJdGVtEhouaXRlbS52MS5DcmVhdGVJdGVtUmVxdWVzdBobLml0ZW0udjEuQ3JlYXRlSXRlbVJlc3BvbnNlIgASRwoKVXBkYXRlSXRlbRIaLml0ZW0udjEuVXBkYXRlSXRlbVJlcXVlc3QaGy5pdGVtLnYxLlVwZGF0ZUl0ZW1SZXNwb25zZSIAEkcKCkRlbGV0ZUl0ZW0SGi5pdGVtLnYxLkRlbGV0ZUl0ZW1SZXF1ZXN0GhsuaXRlbS52MS5EZWxldGVJdGVtUmVzcG9uc2UiAEKcAQoLY29tLml0ZW0udjFCCUl0ZW1Qcm90b1ABWkVnaXRodWIuY29tL3Nwb3RkZW1vNC90cmV2c3RhY2svc2VydmVyL2ludGVybmFsL2Nvbm5lY3QvaXRlbS92MTtpdGVtdjGiAgNJWFiqAgdJdGVtLlYxygIHSXRlbVxWMeICE0l0ZW1cVjFcR1BCTWV0YWRhdGHqAghJdGVtOjpWMWIGcHJvdG8z", [file_buf_validate_validate, file_google_protobuf_timestamp]);
/** /**
* @generated from message item.v1.Item * @generated from message item.v1.Item
*/ */
export type Item = Message<"item.v1.Item"> & { export type Item = Message<"item.v1.Item"> & {
/** /**
* @generated from field: optional uint32 id = 1; * @generated from field: int64 id = 1;
*/ */
id?: number; id: bigint;
/** /**
* @generated from field: string name = 2; * @generated from field: string name = 2;
@ -29,24 +30,24 @@ export type Item = Message<"item.v1.Item"> & {
name: string; name: string;
/** /**
* @generated from field: string description = 3; * @generated from field: google.protobuf.Timestamp added = 3;
*/
added?: Timestamp;
/**
* @generated from field: string description = 4;
*/ */
description: string; description: string;
/** /**
* @generated from field: float price = 4; * @generated from field: float price = 5;
*/ */
price: number; price: number;
/** /**
* @generated from field: uint32 quantity = 5; * @generated from field: int32 quantity = 6;
*/ */
quantity: number; quantity: number;
/**
* @generated from field: optional google.protobuf.Timestamp added = 6;
*/
added?: Timestamp;
}; };
/** /**
@ -61,9 +62,9 @@ export const ItemSchema: GenMessage<Item> = /*@__PURE__*/
*/ */
export type GetItemRequest = Message<"item.v1.GetItemRequest"> & { export type GetItemRequest = Message<"item.v1.GetItemRequest"> & {
/** /**
* @generated from field: uint32 id = 1; * @generated from field: int64 id = 1;
*/ */
id: number; id: bigint;
}; };
/** /**
@ -110,12 +111,12 @@ export type GetItemsRequest = Message<"item.v1.GetItemsRequest"> & {
filter?: string; filter?: string;
/** /**
* @generated from field: optional uint32 limit = 4; * @generated from field: optional int32 limit = 4;
*/ */
limit?: number; limit?: number;
/** /**
* @generated from field: optional uint32 offset = 5; * @generated from field: optional int32 offset = 5;
*/ */
offset?: number; offset?: number;
}; };
@ -137,7 +138,7 @@ export type GetItemsResponse = Message<"item.v1.GetItemsResponse"> & {
items: Item[]; items: Item[];
/** /**
* @generated from field: uint64 count = 2; * @generated from field: int64 count = 2;
*/ */
count: bigint; count: bigint;
}; };
@ -154,9 +155,24 @@ export const GetItemsResponseSchema: GenMessage<GetItemsResponse> = /*@__PURE__*
*/ */
export type CreateItemRequest = Message<"item.v1.CreateItemRequest"> & { export type CreateItemRequest = Message<"item.v1.CreateItemRequest"> & {
/** /**
* @generated from field: item.v1.Item item = 1; * @generated from field: string name = 1;
*/ */
item?: Item; 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;
}; };
/** /**
@ -171,9 +187,14 @@ export const CreateItemRequestSchema: GenMessage<CreateItemRequest> = /*@__PURE_
*/ */
export type CreateItemResponse = Message<"item.v1.CreateItemResponse"> & { export type CreateItemResponse = Message<"item.v1.CreateItemResponse"> & {
/** /**
* @generated from field: item.v1.Item item = 1; * @generated from field: int64 id = 1;
*/ */
item?: Item; id: bigint;
/**
* @generated from field: google.protobuf.Timestamp added = 2;
*/
added?: Timestamp;
}; };
/** /**
@ -188,9 +209,29 @@ export const CreateItemResponseSchema: GenMessage<CreateItemResponse> = /*@__PUR
*/ */
export type UpdateItemRequest = Message<"item.v1.UpdateItemRequest"> & { export type UpdateItemRequest = Message<"item.v1.UpdateItemRequest"> & {
/** /**
* @generated from field: item.v1.Item item = 1; * @generated from field: int64 id = 1;
*/ */
item?: Item; 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;
}; };
/** /**
@ -204,10 +245,6 @@ export const UpdateItemRequestSchema: GenMessage<UpdateItemRequest> = /*@__PURE_
* @generated from message item.v1.UpdateItemResponse * @generated from message item.v1.UpdateItemResponse
*/ */
export type UpdateItemResponse = Message<"item.v1.UpdateItemResponse"> & { export type UpdateItemResponse = Message<"item.v1.UpdateItemResponse"> & {
/**
* @generated from field: item.v1.Item item = 1;
*/
item?: Item;
}; };
/** /**
@ -222,9 +259,9 @@ export const UpdateItemResponseSchema: GenMessage<UpdateItemResponse> = /*@__PUR
*/ */
export type DeleteItemRequest = Message<"item.v1.DeleteItemRequest"> & { export type DeleteItemRequest = Message<"item.v1.DeleteItemRequest"> & {
/** /**
* @generated from field: uint32 id = 1; * @generated from field: int64 id = 1;
*/ */
id: number; id: bigint;
}; };
/** /**

View File

@ -0,0 +1,240 @@
// @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,4 +1,4 @@
// @generated by protoc-gen-es v2.2.3 with parameter "target=ts" // @generated by protoc-gen-es v2.2.5 with parameter "target=ts"
// @generated from file user/v1/user.proto (package user.v1, syntax proto3) // @generated from file user/v1/user.proto (package user.v1, syntax proto3)
/* eslint-disable */ /* eslint-disable */
@ -10,16 +10,16 @@ import type { Message } from "@bufbuild/protobuf";
* Describes the file user/v1/user.proto. * Describes the file user/v1/user.proto.
*/ */
export const file_user_v1_user: GenFile = /*@__PURE__*/ export const file_user_v1_user: GenFile = /*@__PURE__*/
fileDesc("ChJ1c2VyL3YxL3VzZXIucHJvdG8SB3VzZXIudjEiVgoEVXNlchIKCgJpZBgBIAEoDRIQCgh1c2VybmFtZRgCIAEoCRIcCg9wcm9maWxlX3BpY3R1cmUYAyABKAlIAIgBAUISChBfcHJvZmlsZV9waWN0dXJlIhAKDkdldFVzZXJSZXF1ZXN0Ii4KD0dldFVzZXJSZXNwb25zZRIbCgR1c2VyGAEgASgLMg0udXNlci52MS5Vc2VyIl0KFVVwZGF0ZVBhc3N3b3JkUmVxdWVzdBIUCgxvbGRfcGFzc3dvcmQYASABKAkSFAoMbmV3X3Bhc3N3b3JkGAIgASgJEhgKEGNvbmZpcm1fcGFzc3dvcmQYAyABKAkiNQoWVXBkYXRlUGFzc3dvcmRSZXNwb25zZRIbCgR1c2VyGAEgASgLMg0udXNlci52MS5Vc2VyIj4KEEdldEFQSUtleVJlcXVlc3QSEAoIcGFzc3dvcmQYASABKAkSGAoQY29uZmlybV9wYXNzd29yZBgCIAEoCSIgChFHZXRBUElLZXlSZXNwb25zZRILCgNrZXkYASABKAkiPgobVXBkYXRlUHJvZmlsZVBpY3R1cmVSZXF1ZXN0EhEKCWZpbGVfbmFtZRgBIAEoCRIMCgRkYXRhGAIgASgMIjsKHFVwZGF0ZVByb2ZpbGVQaWN0dXJlUmVzcG9uc2USGwoEdXNlchgBIAEoCzINLnVzZXIudjEuVXNlcjLPAgoLVXNlclNlcnZpY2USPgoHR2V0VXNlchIXLnVzZXIudjEuR2V0VXNlclJlcXVlc3QaGC51c2VyLnYxLkdldFVzZXJSZXNwb25zZSIAElMKDlVwZGF0ZVBhc3N3b3JkEh4udXNlci52MS5VcGRhdGVQYXNzd29yZFJlcXVlc3QaHy51c2VyLnYxLlVwZGF0ZVBhc3N3b3JkUmVzcG9uc2UiABJECglHZXRBUElLZXkSGS51c2VyLnYxLkdldEFQSUtleVJlcXVlc3QaGi51c2VyLnYxLkdldEFQSUtleVJlc3BvbnNlIgASZQoUVXBkYXRlUHJvZmlsZVBpY3R1cmUSJC51c2VyLnYxLlVwZGF0ZVByb2ZpbGVQaWN0dXJlUmVxdWVzdBolLnVzZXIudjEuVXBkYXRlUHJvZmlsZVBpY3R1cmVSZXNwb25zZSIAQp0BCgtjb20udXNlci52MUIJVXNlclByb3RvUAFaRmdpdGh1Yi5jb20vc3BvdGRlbW80L3RyZXZzdGFjay9zZXJ2ZXIvaW50ZXJuYWwvc2VydmljZXMvdXNlci92MTt1c2VydjGiAgNVWFiqAgdVc2VyLlYxygIHVXNlclxWMeICE1VzZXJcVjFcR1BCTWV0YWRhdGHqAghVc2VyOjpWMWIGcHJvdG8z"); fileDesc("ChJ1c2VyL3YxL3VzZXIucHJvdG8SB3VzZXIudjEiXAoEVXNlchIKCgJpZBgBIAEoAxIQCgh1c2VybmFtZRgCIAEoCRIfChJwcm9maWxlX3BpY3R1cmVfaWQYAyABKANIAIgBAUIVChNfcHJvZmlsZV9waWN0dXJlX2lkIhAKDkdldFVzZXJSZXF1ZXN0Ii4KD0dldFVzZXJSZXNwb25zZRIbCgR1c2VyGAEgASgLMg0udXNlci52MS5Vc2VyIl0KFVVwZGF0ZVBhc3N3b3JkUmVxdWVzdBIUCgxvbGRfcGFzc3dvcmQYASABKAkSFAoMbmV3X3Bhc3N3b3JkGAIgASgJEhgKEGNvbmZpcm1fcGFzc3dvcmQYAyABKAkiNQoWVXBkYXRlUGFzc3dvcmRSZXNwb25zZRIbCgR1c2VyGAEgASgLMg0udXNlci52MS5Vc2VyIj4KEEdldEFQSUtleVJlcXVlc3QSEAoIcGFzc3dvcmQYASABKAkSGAoQY29uZmlybV9wYXNzd29yZBgCIAEoCSIgChFHZXRBUElLZXlSZXNwb25zZRILCgNrZXkYASABKAkiPgobVXBkYXRlUHJvZmlsZVBpY3R1cmVSZXF1ZXN0EhEKCWZpbGVfbmFtZRgBIAEoCRIMCgRkYXRhGAIgASgMIjsKHFVwZGF0ZVByb2ZpbGVQaWN0dXJlUmVzcG9uc2USGwoEdXNlchgBIAEoCzINLnVzZXIudjEuVXNlciIhCh9CZWdpblBhc3NrZXlSZWdpc3RyYXRpb25SZXF1ZXN0IjgKIEJlZ2luUGFzc2tleVJlZ2lzdHJhdGlvblJlc3BvbnNlEhQKDG9wdGlvbnNfanNvbhgBIAEoCSI3CiBGaW5pc2hQYXNza2V5UmVnaXN0cmF0aW9uUmVxdWVzdBITCgthdHRlc3RhdGlvbhgBIAEoCSIjCiFGaW5pc2hQYXNza2V5UmVnaXN0cmF0aW9uUmVzcG9uc2UyuAQKC1VzZXJTZXJ2aWNlEj4KB0dldFVzZXISFy51c2VyLnYxLkdldFVzZXJSZXF1ZXN0GhgudXNlci52MS5HZXRVc2VyUmVzcG9uc2UiABJTCg5VcGRhdGVQYXNzd29yZBIeLnVzZXIudjEuVXBkYXRlUGFzc3dvcmRSZXF1ZXN0Gh8udXNlci52MS5VcGRhdGVQYXNzd29yZFJlc3BvbnNlIgASRAoJR2V0QVBJS2V5EhkudXNlci52MS5HZXRBUElLZXlSZXF1ZXN0GhoudXNlci52MS5HZXRBUElLZXlSZXNwb25zZSIAEmUKFFVwZGF0ZVByb2ZpbGVQaWN0dXJlEiQudXNlci52MS5VcGRhdGVQcm9maWxlUGljdHVyZVJlcXVlc3QaJS51c2VyLnYxLlVwZGF0ZVByb2ZpbGVQaWN0dXJlUmVzcG9uc2UiABJxChhCZWdpblBhc3NrZXlSZWdpc3RyYXRpb24SKC51c2VyLnYxLkJlZ2luUGFzc2tleVJlZ2lzdHJhdGlvblJlcXVlc3QaKS51c2VyLnYxLkJlZ2luUGFzc2tleVJlZ2lzdHJhdGlvblJlc3BvbnNlIgASdAoZRmluaXNoUGFzc2tleVJlZ2lzdHJhdGlvbhIpLnVzZXIudjEuRmluaXNoUGFzc2tleVJlZ2lzdHJhdGlvblJlcXVlc3QaKi51c2VyLnYxLkZpbmlzaFBhc3NrZXlSZWdpc3RyYXRpb25SZXNwb25zZSIAQpwBCgtjb20udXNlci52MUIJVXNlclByb3RvUAFaRWdpdGh1Yi5jb20vc3BvdGRlbW80L3RyZXZzdGFjay9zZXJ2ZXIvaW50ZXJuYWwvY29ubmVjdC91c2VyL3YxO3VzZXJ2MaICA1VYWKoCB1VzZXIuVjHKAgdVc2VyXFYx4gITVXNlclxWMVxHUEJNZXRhZGF0YeoCCFVzZXI6OlYxYgZwcm90bzM");
/** /**
* @generated from message user.v1.User * @generated from message user.v1.User
*/ */
export type User = Message<"user.v1.User"> & { export type User = Message<"user.v1.User"> & {
/** /**
* @generated from field: uint32 id = 1; * @generated from field: int64 id = 1;
*/ */
id: number; id: bigint;
/** /**
* @generated from field: string username = 2; * @generated from field: string username = 2;
@ -27,9 +27,9 @@ export type User = Message<"user.v1.User"> & {
username: string; username: string;
/** /**
* @generated from field: optional string profile_picture = 3; * @generated from field: optional int64 profile_picture_id = 3;
*/ */
profilePicture?: string; profilePictureId?: bigint;
}; };
/** /**
@ -191,6 +191,66 @@ export type UpdateProfilePictureResponse = Message<"user.v1.UpdateProfilePicture
export const UpdateProfilePictureResponseSchema: GenMessage<UpdateProfilePictureResponse> = /*@__PURE__*/ export const UpdateProfilePictureResponseSchema: GenMessage<UpdateProfilePictureResponse> = /*@__PURE__*/
messageDesc(file_user_v1_user, 8); 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 * @generated from service user.v1.UserService
*/ */
@ -227,6 +287,22 @@ export const UserService: GenService<{
input: typeof UpdateProfilePictureRequestSchema; input: typeof UpdateProfilePictureRequestSchema;
output: typeof UpdateProfilePictureResponseSchema; 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__*/ }> = /*@__PURE__*/
serviceDesc(file_user_v1_user, 0); serviceDesc(file_user_v1_user, 0);

View File

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

View File

@ -0,0 +1,175 @@
import { create, type DescMessage, type DescService, type MessageShape, type MessageInitShape } from "@bufbuild/protobuf";
import { ValidationError, type Violation } from "@bufbuild/protovalidate";
import { Validator } from "../transport";
import { ConnectError, type Client } from "@connectrpc/connect";
import type { Action } from "svelte/action";
type Options<Input extends DescMessage, Output extends DescMessage> = {
init?: MessageInitShape<Input>,
start?: boolean,
reset?: boolean,
onSubmit?: (formData: FormData, input: MessageShape<Input>) => Promise<MessageShape<Input>>,
onResult?: (result: MessageShape<Output>) => void,
onError?: (err: Violation[] | ConnectError) => void
}
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 | any) => {
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-ignore I can't figure out how to make this typescript compliant
const response = client[method.localName]($state.snapshot(input)) as Promise<MessageShape<Method['output']>>
response
.then((resp) => {
success(resp);
}).catch(err => {
fail(err);
});
}
}
// 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

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

View File

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

View File

@ -1,150 +0,0 @@
// @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

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

View File

@ -1,27 +1,35 @@
import { createConnectTransport } from "@connectrpc/connect-web" import { createConnectTransport } from '@connectrpc/connect-web';
import { Code, ConnectError, createClient, type Interceptor } from "@connectrpc/connect" import { createValidator } from "@bufbuild/protovalidate";
import { AuthService } from "$lib/services/user/v1/auth_pb"; import { Code, ConnectError, createClient, type Interceptor } from '@connectrpc/connect';
import { UserService } from "$lib/services/user/v1/user_pb"; import { AuthService } from '$lib/connect/user/v1/auth_pb';
import { ItemService } from "$lib/services/item/v1/item_pb"; import { UserService } from '$lib/connect/user/v1/user_pb';
import { goto } from "$app/navigation"; import { ItemService } from '$lib/connect/item/v1/item_pb';
import { goto } from '$app/navigation';
import { page } from '$app/state';
const redirector: Interceptor = (next) => async (req) => { const redirector: Interceptor = (next) => async (req) => {
try { try {
return await next(req); return await next(req);
} catch (e) { } catch (e) {
const error = ConnectError.from(e); const error = ConnectError.from(e);
if (error.code === Code.Unauthenticated) { if (error.code === Code.Unauthenticated) {
await goto('/auth'); const redirectURL = new URL(page.url);
} redirectURL.pathname = '/auth';
throw e; redirectURL.searchParams.append('redir', encodeURIComponent(page.url.pathname + page.url.search));
}
await goto(redirectURL);
}
throw e;
}
}; };
const transport = createConnectTransport({ const transport = createConnectTransport({
baseUrl: `${window.location.origin}/grpc`, baseUrl: `${window.location.origin}/grpc`,
interceptors: [redirector], interceptors: [redirector]
}); });
export const AuthClient = createClient(AuthService, transport); export const AuthClient = createClient(AuthService, transport);
export const UserClient = createClient(UserService, transport); export const UserClient = createClient(UserService, transport);
export const ItemClient = createClient(ItemService, transport); export const ItemClient = createClient(ItemService, transport);
export const Validator = createValidator();

View File

@ -1,15 +0,0 @@
<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

@ -1,29 +0,0 @@
<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 flex justify-center items-center hover:brightness-120 focus:outline-sky w-fit cursor-pointer rounded p-2 px-4 text-sm font-medium transition-all focus:outline-2 focus:outline-offset-1',
className
)}
{onclick}
>
{@render children?.()}
</Button.Root>

View File

@ -1,170 +0,0 @@
<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="grow flex items-center justify-center">
{#each ['start', 'end'] as const as type}
<DateRangePicker.Input {type}>
{#snippet children({ segments })}
{#each segments as { part, 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 grow flex justify-center items-center focus:outline-sky ml-1 cursor-pointer 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="select-none font-medium" />
<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-x-4 sm:space-y-0">
{#each months as month}
<DateRangePicker.Grid class="w-full border-collapse select-none space-y-1">
<DateRangePicker.GridHead>
<DateRangePicker.GridRow class="mb-1 flex w-full justify-between">
{#each weekdays as day}
<DateRangePicker.HeadCell
class="text-overlay-0 font-normal! w-10 rounded text-xs"
>
{day.slice(0, 2)}
</DateRangePicker.HeadCell>
{/each}
</DateRangePicker.GridRow>
</DateRangePicker.GridHead>
<DateRangePicker.GridBody>
{#each month.weeks as weekDates}
<DateRangePicker.GridRow class="flex w-full">
{#each weekDates as date}
<DateRangePicker.Cell
{date}
month={month.value}
class="p-0! relative m-0 size-10 overflow-visible text-center text-sm focus-within:relative focus-within:z-20"
>
<DateRangePicker.Day
class={'hover:border-sky focus-visible:ring-foreground! data-selected:rounded-none data-selection-end:rounded-r data-selection-start:rounded-l 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 data-disabled:pointer-events-none data-outside-month:pointer-events-none data-highlighted:rounded-none data-unavailable:line-through group relative inline-flex size-10 items-center justify-center overflow-visible whitespace-nowrap rounded border border-transparent bg-transparent p-0 text-sm font-normal transition-all'}
>
<div
class="bg-sky group-data-selected:bg-background group-data-today:block absolute top-[5px] hidden size-1 rounded-full transition-all"
></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

@ -1,51 +0,0 @@
<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

@ -1,74 +0,0 @@
<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 left-[50%] top-[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

@ -1,63 +0,0 @@
<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="select-none font-medium">...</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 select-none items-center justify-center rounded bg-transparent font-medium transition-all 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

@ -1,82 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils';
import { Check, ChevronsDown, ChevronsUp, ChevronsUpDown, X } from '@lucide/svelte';
import { Select } from 'bits-ui';
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 gap-2 inline-flex grow cursor-pointer select-none items-center justify-between rounded-l py-2 pl-2 text-sm transition-colors focus:outline focus:outline-offset-1"
aria-label={placeholder}
>
{selectedLabel}
<ChevronsUpDown class="text-overlay-0" size="20" />
</Select.Trigger>
<Select.Portal>
<Select.Content
class="focus-override border-surface-0 bg-mantle shadow-popover 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 outline-hidden z-50 select-none rounded border p-1"
sideOffset={10}
>
<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-disabled:cursor-not-allowed data-highlighted:bg-surface-0 outline-hidden data-disabled:opacity-50 flex h-10 w-full cursor-pointer select-none items-center gap-4 rounded px-5 py-3 text-sm capitalize"
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>
</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

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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -0,0 +1,17 @@
<script lang="ts">
import { cn } from '$lib/utils';
import type { WithElementRef } from 'bits-ui';
import type { HTMLAttributes } from 'svelte/elements';
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

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

View File

@ -0,0 +1,76 @@
<script lang="ts">
import CalendarIcon from '@lucide/svelte/icons/calendar';
import type { DateRange } from 'bits-ui';
import {
CalendarDate,
DateFormatter,
type DateValue,
getLocalTimeZone
} from '@internationalized/date';
import { cn } from '$lib/utils.js';
import { buttonVariants } from '$lib/ui/button';
import { RangeCalendar } from '$lib/ui/range-calendar';
import * as Popover from '$lib/ui/popover';
let {
className,
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', !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="w-auto p-0 bg-based" 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

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

View File

@ -0,0 +1,7 @@
<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

@ -0,0 +1,50 @@
<script lang="ts">
import { Dialog as DialogPrimitive, type WithoutChildrenOrChild } from 'bits-ui';
import X from '@lucide/svelte/icons/x';
import type { Snippet } from 'svelte';
import * as Dialog from './index.js';
import { cn } from '$lib/utils.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 absolute top-4 right-4 cursor-pointer p-1 rounded hover:bg-crust transition-all disabled:pointer-events-none',
// Focus
'focus-visible:outline-accent focus-visible:outline-2 focus-visible:outline-offset-2',
// 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

@ -0,0 +1,17 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
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

@ -0,0 +1,21 @@
<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

@ -0,0 +1,21 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
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

@ -0,0 +1,20 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
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

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

View File

@ -0,0 +1,7 @@
<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

@ -0,0 +1,28 @@
<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

@ -0,0 +1,37 @@
import { Dialog as DialogPrimitive } from "bits-ui";
import Root from './dialog.svelte';
import Title from "./dialog-title.svelte";
import Footer from "./dialog-footer.svelte";
import Header from "./dialog-header.svelte";
import Overlay from "./dialog-overlay.svelte";
import Content from "./dialog-content.svelte";
import Description from "./dialog-description.svelte";
import Trigger from "./dialog-trigger.svelte";
import Close from "./dialog-close.svelte";
const Portal = DialogPrimitive.Portal;
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

@ -0,0 +1,47 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive, type WithoutChildrenOrChild } from "bits-ui";
import Check from "@lucide/svelte/icons/check";
import Minus from "@lucide/svelte/icons/minus";
import { cn } from "$lib/utils.js";
import type { Snippet } from "svelte";
let {
ref = $bindable(null),
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 outline-hidden relative flex cursor-pointer select-none items-center gap-2 rounded-sm py-2 pl-8 pr-2 text-sm",
// Disabled
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
// Images
"[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
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

@ -0,0 +1,31 @@
<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

@ -0,0 +1,17 @@
<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-b border-surface first:pt-0 last:pb-0 last:border-none', className)}
{...restProps}
/>

View File

@ -0,0 +1,27 @@
<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

@ -0,0 +1,24 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import { type WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
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

@ -0,0 +1,22 @@
<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

@ -0,0 +1,16 @@
<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

@ -0,0 +1,39 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive, type WithoutChild } from 'bits-ui';
import Circle from '@lucide/svelte/icons/circle';
import { cn } from '$lib/utils.js';
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

@ -0,0 +1,17 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
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

@ -0,0 +1,21 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { type WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
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

@ -0,0 +1,23 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
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 origin-(--radix-dropdown-menu-content-transform-origin) z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg',
// Animations
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className
)}
{...restProps}
/>

View File

@ -0,0 +1,29 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import ChevronRight from "@lucide/svelte/icons/chevron-right";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
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 outline-hidden flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm",
inset && "pl-8",
className
)}
{...restProps}
>
{@render children?.()}
<ChevronRight class="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>

View File

@ -0,0 +1,23 @@
<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

@ -0,0 +1,50 @@
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import CheckboxItem from "./dropdown-menu-checkbox-item.svelte";
import Content from "./dropdown-menu-content.svelte";
import Group from "./dropdown-menu-group.svelte";
import Item from "./dropdown-menu-item.svelte";
import Link from './dropdown-menu-link.svelte';
import Label from "./dropdown-menu-label.svelte";
import RadioGroup from "./dropdown-menu-radio-group.svelte";
import RadioItem from "./dropdown-menu-radio-item.svelte";
import Separator from "./dropdown-menu-separator.svelte";
import Shortcut from "./dropdown-menu-shortcut.svelte";
import Trigger from "./dropdown-menu-trigger.svelte";
import SubContent from "./dropdown-menu-sub-content.svelte";
import SubTrigger from "./dropdown-menu-sub-trigger.svelte";
const Sub = DropdownMenuPrimitive.Sub;
const Root = DropdownMenuPrimitive.Root;
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

@ -0,0 +1,31 @@
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

@ -0,0 +1,30 @@
<script lang="ts">
import { cn } from '$lib/utils';
import { getFormContext } from './context.svelte';
import type { WithElementRef, WithoutChildren } from 'bits-ui';
import type { HTMLAttributes } from 'svelte/elements';
import type { Violation } from '@bufbuild/protovalidate';
import type { ConnectError } from '@connectrpc/connect';
type Props = WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> & {
errors?: Violation[] | ConnectError;
};
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}
<label for={item?.id}>{error.message}</label>
{/each}
{:else if errors}
<span>{errors.message}</span>
{/if}
</div>

View File

@ -0,0 +1,21 @@
<script lang="ts">
import { cn } from '$lib/utils';
import { setFormContext } from './context.svelte';
import type { WithElementRef } from 'bits-ui';
import type { HTMLAttributes } from 'svelte/elements';
type Props = WithElementRef<HTMLAttributes<HTMLDivElement>> & {
name?: string;
};
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')} {...restProps}>
{@render children?.()}
</div>

View File

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

View File

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

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

View File

@ -0,0 +1,78 @@
<script lang="ts">
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from 'svelte/elements';
import type { WithElementRef } from 'bits-ui';
import { cn } from '$lib/utils.js';
import { getFormContext } from '../form/context.svelte';
type InputType = Exclude<HTMLInputTypeAttribute, 'file'>;
type Props = WithElementRef<
Omit<HTMLInputAttributes, 'type'> &
({ type: 'file'; files?: FileList } | { type?: InputType; files?: undefined }) & {
scan?: boolean;
}
>;
let {
ref = $bindable(null),
value = $bindable(),
type,
files = $bindable(),
class: className,
scan,
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 shadow-xs flex h-9 w-full min-w-0 cursor-pointer rounded-md border text-sm font-medium transition-all file:mr-2 file:px-3 file:py-2 md:text-sm',
// Focus
'focus-visible:outline-accent focus-visible:outline-2 focus-visible:outline-offset-2',
// 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 shadow-xs flex h-9 w-full min-w-0 rounded-md border px-3 py-1 transition-all md:text-sm',
// Focus
'focus-visible:outline-accent focus-visible:outline-2 focus-visible:outline-offset-2',
// Disabled
'disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{type}
bind:value
{...restProps}
/>
{/if}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,49 @@
<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

@ -0,0 +1,25 @@
import Root from "./pagination.svelte";
import Content from "./pagination-content.svelte";
import Item from "./pagination-item.svelte";
import Link from "./pagination-link.svelte";
import PrevButton from "./pagination-prev-button.svelte";
import NextButton from "./pagination-next-button.svelte";
import Ellipsis from "./pagination-ellipsis.svelte";
export {
Root,
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

@ -0,0 +1,21 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
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

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

View File

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

View File

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

View File

@ -0,0 +1,34 @@
<script lang="ts">
import { Pagination as PaginationPrimitive } from "bits-ui";
import ChevronRight from "@lucide/svelte/icons/chevron-right";
import { buttonVariants } from "$lib/ui/button/index.js";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: PaginationPrimitive.NextButtonProps = $props();
</script>
{#snippet Fallback()}
<span>Next</span>
<ChevronRight class="size-4" />
{/snippet}
<!-- TODO: Fix this error: Expression produces a union type that is too complex to represent. Note: Removing `Fallback` in children={children || Fallback} fixes, makes you wonder how/why `Fallback` is causing this. -->
<PaginationPrimitive.NextButton
bind:ref
aria-label="Go to next page"
class={cn(
buttonVariants({
size: "default",
variant: "ghost",
class: "gap-1 px-2.5 sm:pr-2.5",
}),
className
)}
children={children || Fallback}
{...restProps}
/>

View File

@ -0,0 +1,33 @@
<script lang="ts">
import { Pagination as PaginationPrimitive } from "bits-ui";
import ChevronLeft from "@lucide/svelte/icons/chevron-left";
import { buttonVariants } from "$lib/ui/button/index.js";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: PaginationPrimitive.PrevButtonProps = $props();
</script>
{#snippet Fallback()}
<ChevronLeft class="size-4" />
<span>Previous</span>
{/snippet}
<PaginationPrimitive.PrevButton
bind:ref
aria-label="Go to previous page"
class={cn(
buttonVariants({
size: "default",
variant: "ghost",
class: "gap-1 px-2.5 sm:pl-2.5",
}),
className
)}
children={children || Fallback}
{...restProps}
/>

View File

@ -0,0 +1,47 @@
<script lang="ts">
import { Pagination as PaginationPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
import { pushState } from '$app/navigation';
let {
ref = $bindable(null),
class: className,
count = 0,
perPage = 10,
page = $bindable(1),
siblingCount = 1,
onPageChange,
...restProps
}: PaginationPrimitive.RootProps = $props();
</script>
<svelte:window
onpopstate={(state) => {
const sks = state.state['sveltekit:states'] as {} | string;
if (typeof sks === 'string' && sks.includes('#pagination-')) {
page = Number(sks.split('#pagination-')[1]);
onPageChange?.(page);
} else {
page = 1;
onPageChange?.(page);
}
}}
/>
<PaginationPrimitive.Root
bind:ref
bind:page
onPageChange={(p) => {
pushState('', `#pagination-${p}`);
onPageChange?.(p);
}}
role="navigation"
aria-label="pagination"
data-slot="pagination"
class={cn('mx-auto flex w-full justify-center', className)}
{count}
{perPage}
{siblingCount}
{...restProps}
/>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,36 @@
<script lang="ts">
import { RangeCalendar as RangeCalendarPrimitive } from 'bits-ui';
import { buttonVariants } from '$lib/ui/button/index.js';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: RangeCalendarPrimitive.DayProps = $props();
export { className as class };
</script>
<RangeCalendarPrimitive.Day
bind:ref
class={cn(
buttonVariants({ variant: 'ghost' }),
'size-9 p-0 font-normal',
'[&[data-today]:not([data-selected])]:bg-blue [&[data-today]:not([data-selected])]:text-crust [&[data-today]:not([data-selected])]:rounded-md',
// Selected
'data-[selected]:bg-surface data-[selected]:hover:bg-surface-1 data-[selected]:rounded-none data-[selected]:opacity-100',
// Selection Start
'data-[selection-start]:bg-surface-1 data-[selection-start]:text-text data-[selection-start]:hover:bg-surface-2 data-[selection-start]:rounded-l-md',
// Selection End
'data-[selection-end]:bg-surface-1 data-[selection-end]:text-text data-[selection-end]:hover:bg-surface-2 data-[selection-end]:rounded-r-md',
// Outside months
'data-[outside-month]:text-muted-foreground [&[data-outside-month][data-selected]]:bg-accent/50 [&[data-outside-month][data-selected]]:text-muted-foreground data-[outside-month]:pointer-events-none data-[outside-month]:opacity-50 [&[data-outside-month][data-selected]]:opacity-30',
// Disabled
'data-[disabled]:text-muted-foreground data-[disabled]:opacity-50',
// Unavailable
'data-[unavailable]:text-destructive-foreground data-[unavailable]:line-through',
className
)}
{...restProps}
/>

View File

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

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