Compare commits
209 Commits
Author | SHA1 | Date | |
---|---|---|---|
e6b378c170 | |||
![]() |
fe05a64eb0 | ||
![]() |
7b3d66886d | ||
![]() |
bc74994ac4 | ||
![]() |
e20a67f7a4 | ||
![]() |
01e2f3eca3 | ||
![]() |
95a2a00cec | ||
![]() |
b6058aa434 | ||
![]() |
0adbbc3f06 | ||
![]() |
46058ae5d6 | ||
![]() |
94b367c2fb | ||
![]() |
548efa254c | ||
68166c8d3a | |||
3a5fa69bf6 | |||
23be247cdb | |||
9a204d3808 | |||
00e36b6c77 | |||
3b34d50120 | |||
13b652d425 | |||
3bdef16173 | |||
968378e8bb | |||
6767df7f91 | |||
f9245c4145 | |||
e20156a2de | |||
4f9dee1e27 | |||
fe8a1376fa | |||
7619be6d11 | |||
1062595d7f | |||
d829c1efb2 | |||
a1f22433a0 | |||
43fc67ded6 | |||
8e7781a346 | |||
68dd90048f | |||
7bf54bbd8c | |||
9fa5818860 | |||
77859b3d94 | |||
9e26479f67 | |||
000797f930 | |||
1e8e06738b | |||
28dbf76789 | |||
93aa1ebd3b | |||
bf13344cbe | |||
62358e100c | |||
7ee1cd94dc | |||
893aa4db51 | |||
6b9da9dc15 | |||
44e08b62fd | |||
3feb35ea7b | |||
849fec6f01 | |||
d27ee1202b | |||
32ac21afd2 | |||
39959f041d | |||
124d702ec4 | |||
2587483733 | |||
575ec574dd | |||
815cf96374 | |||
2b6c24bc86 | |||
632774d051 | |||
1d6b419a15 | |||
2da7526265 | |||
92877b669e | |||
10168843e1 | |||
0889f9c7b1 | |||
084010e38c | |||
8158c195f5 | |||
174d15de5b | |||
56523795d5 | |||
32bdb3d709 | |||
b30d14af9a | |||
1220a37b60 | |||
a3e008c317 | |||
58498c87af | |||
fd9abb948a | |||
2b07f74cc1 | |||
ee4d2984dd | |||
dd80776bb1 | |||
d0e7ae9284 | |||
63433be0bb | |||
1a856e575e | |||
a3e4154fb6 | |||
4839b74bf7 | |||
dcf5a16c3f | |||
932d82c1fc | |||
db509ffa8a | |||
c4392601b1 | |||
ca421b313d | |||
71df7b4711 | |||
106a43aaf1 | |||
5077682fa5 | |||
634bff4411 | |||
bce4d598fb | |||
3fd1e1f4a3 | |||
de1baa4517 | |||
be309409ad | |||
b6ef0cab53 | |||
e8d9a4adff | |||
fc90905dcf | |||
6494d74ab2 | |||
ca313960c4 | |||
0cb262524c | |||
05aff14703 | |||
73cf074d6d | |||
3e545d4fb1 | |||
b07364f146 | |||
c9dd6d9061 | |||
06dc437033 | |||
d91c90a5c2 | |||
e6ab5700de | |||
20726c55d5 | |||
95ce559ff3 | |||
bfc1580218 | |||
296d2713ad | |||
1e2d9a7894 | |||
2fde1a8eba | |||
07cec78aa5 | |||
cdeaa13d92 | |||
398ddde169 | |||
74b9bb86a1 | |||
701f80540b | |||
3926fa09ff | |||
e3d9b437c5 | |||
![]() |
deaaec2c57 | ||
![]() |
bf5f07222c | ||
75f89c0edf | |||
ad9ac18c18 | |||
24d3067e52 | |||
1c7cf7966f | |||
7a799868fb | |||
0093a0879e | |||
071b719309 | |||
![]() |
f6c5acee8f | ||
![]() |
8fe2b57c1e | ||
![]() |
d4ae79a1cd | ||
d49b699e83 | |||
2eaa9b300c | |||
d0cf852b95 | |||
ddce48625e | |||
fbe5efdf0f | |||
77f8362f88 | |||
f18107f9c4 | |||
dbcb719166 | |||
cdbb7e2c4d | |||
3629be7e0e | |||
32e767757b | |||
c1b14e03ac | |||
b466f5cfb0 | |||
c2778f175f | |||
35e6403067 | |||
dc1fb3b8b4 | |||
45072a0e37 | |||
095c477745 | |||
a25ff41363 | |||
1b780af78d | |||
7d36ea5925 | |||
e38719c292 | |||
beef83e02e | |||
b98a53eed7 | |||
481758e33b | |||
a9506ab67b | |||
967e2650ad | |||
32f85fd0be | |||
f9772bce47 | |||
1667b78a0a | |||
b741e5f1a2 | |||
439adecf0e | |||
e9c44cbc94 | |||
dfd6789aa9 | |||
8d36962bef | |||
35a2f0a918 | |||
dd0995b241 | |||
93bc18022a | |||
f05e745d05 | |||
8b494430a5 | |||
d2238bdf9b | |||
dc3106b4a4 | |||
bc0e8c55e3 | |||
124ddc20df | |||
218da3aef8 | |||
e18457bacd | |||
afb85cec88 | |||
4e96ee38ca | |||
89129f3495 | |||
f8fb729c03 | |||
b906e614ba | |||
645faf398e | |||
add5afb17c | |||
ab4ccc3d46 | |||
ed7489f53d | |||
94366907d8 | |||
ef11ac81a3 | |||
73e7c563e0 | |||
cd3da0aa8f | |||
6706330252 | |||
2b03164307 | |||
0ffc1cd7f9 | |||
c418792653 | |||
8956317197 | |||
2361602b62 | |||
eba0067e1e | |||
8d78f79fe2 | |||
5328e5b4a1 | |||
81855d079f | |||
e857a14fd7 | |||
5e5a2cbaaa | |||
267d293927 | |||
d8de02f789 | |||
be0981f7b7 | |||
86b74e0ebc | |||
21cd91156c |
21
.actions/init/action.yaml
Normal file
21
.actions/init/action.yaml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
name: "Initialize"
|
||||||
|
description: "Install nix & use cachix"
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
token:
|
||||||
|
description: "cachix auth token"
|
||||||
|
required: true
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- name: Install nix
|
||||||
|
uses: cachix/install-nix-action@v31
|
||||||
|
with:
|
||||||
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
|
|
||||||
|
- name: Use cachix
|
||||||
|
uses: cachix/cachix-action@v16
|
||||||
|
with:
|
||||||
|
name: trevstack
|
||||||
|
authToken: "${{ inputs.token }}"
|
63
.actions/push/action.yaml
Normal file
63
.actions/push/action.yaml
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
name: "Docker Push"
|
||||||
|
description: "Push to docker registry"
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
server_url:
|
||||||
|
required: true
|
||||||
|
repository:
|
||||||
|
required: true
|
||||||
|
tag:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- name: Set env
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
REGISTRY=$(basename ${{ inputs.server_url }})
|
||||||
|
|
||||||
|
NR=${{ inputs.repository }}
|
||||||
|
NAMESPACE="${NR%%/*}"
|
||||||
|
REPOSITORY="${NR##*/}"
|
||||||
|
|
||||||
|
TAG=${{ inputs.tag }}
|
||||||
|
VERSION=${TAG#v}
|
||||||
|
|
||||||
|
echo "REGISTRY=${REGISTRY}" >> $GITHUB_ENV
|
||||||
|
echo "NAMESPACE=${NAMESPACE}" >> $GITHUB_ENV
|
||||||
|
echo "REPOSITORY=${REPOSITORY}" >> $GITHUB_ENV
|
||||||
|
echo "VERSION=${VERSION}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Push images
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
docker image tag $REPOSITORY:$VERSION-amd64 $REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION-amd64
|
||||||
|
docker push $REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION-amd64
|
||||||
|
|
||||||
|
docker image tag $REPOSITORY:$VERSION-arm64 $REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION-arm64
|
||||||
|
docker push $REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION-arm64
|
||||||
|
|
||||||
|
docker image tag $REPOSITORY:$VERSION-arm $REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION-arm
|
||||||
|
docker push $REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION-arm
|
||||||
|
|
||||||
|
- name: Push manifest
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
docker manifest create $REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION \
|
||||||
|
$REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION-amd64 \
|
||||||
|
$REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION-arm64 \
|
||||||
|
$REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION-arm
|
||||||
|
docker manifest annotate $REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION $REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION-amd64 --arch amd64
|
||||||
|
docker manifest annotate $REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION $REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION-arm64 --arch arm64
|
||||||
|
docker manifest annotate $REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION $REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION-arm --arch arm
|
||||||
|
docker manifest push $REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION
|
||||||
|
|
||||||
|
docker manifest create $REGISTRY/$NAMESPACE/$REPOSITORY:latest \
|
||||||
|
$REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION-amd64 \
|
||||||
|
$REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION-arm64 \
|
||||||
|
$REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION-arm
|
||||||
|
docker manifest annotate $REGISTRY/$NAMESPACE/$REPOSITORY:latest $REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION-amd64 --arch amd64
|
||||||
|
docker manifest annotate $REGISTRY/$NAMESPACE/$REPOSITORY:latest $REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION-arm64 --arch arm64
|
||||||
|
docker manifest annotate $REGISTRY/$NAMESPACE/$REPOSITORY:latest $REGISTRY/$NAMESPACE/$REPOSITORY:$VERSION-arm --arch arm
|
||||||
|
docker manifest push $REGISTRY/$NAMESPACE/$REPOSITORY:latest
|
@ -1,13 +0,0 @@
|
|||||||
.direnv
|
|
||||||
.env
|
|
||||||
build
|
|
||||||
result
|
|
||||||
|
|
||||||
# Client
|
|
||||||
/client/node_modules
|
|
||||||
/client/.svelte-kit
|
|
||||||
|
|
||||||
# Server
|
|
||||||
/server/client
|
|
||||||
/server/tmp
|
|
||||||
/server/internal/handlers/client/client
|
|
23
.gitea/workflows/check.yaml
Normal file
23
.gitea/workflows/check.yaml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
name: Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
types: [opened, reopened, edited, auto_merge_enabled]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
name: check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: |
|
||||||
|
contains(github.event.head_commit.message, 'bump:') == false &&
|
||||||
|
contains(github.event.head_commit.message, 'Merge pull request') == false
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: ./.actions/init
|
||||||
|
with:
|
||||||
|
token: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||||
|
|
||||||
|
- run: nix flake check
|
68
.gitea/workflows/release.yaml
Normal file
68
.gitea/workflows/release.yaml
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: ./.actions/init
|
||||||
|
with:
|
||||||
|
token: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||||
|
|
||||||
|
- run: nix flake check
|
||||||
|
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: check
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: ./.actions/init
|
||||||
|
with:
|
||||||
|
token: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||||
|
|
||||||
|
- run: >
|
||||||
|
nix build
|
||||||
|
.#trevstack-linux-amd64
|
||||||
|
.#trevstack-linux-arm64
|
||||||
|
.#trevstack-linux-arm
|
||||||
|
.#trevstack-windows-amd64
|
||||||
|
.#trevstack-darwin-amd64
|
||||||
|
.#trevstack-darwin-arm64
|
||||||
|
|
||||||
|
- uses: akkuman/gitea-release-action@v1
|
||||||
|
with:
|
||||||
|
files: |-
|
||||||
|
result*/bin/*
|
||||||
|
|
||||||
|
package:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: release
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: ./.actions/init
|
||||||
|
with:
|
||||||
|
token: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||||
|
|
||||||
|
- uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ github.server_url }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.PAT }}
|
||||||
|
|
||||||
|
- name: Build & load images
|
||||||
|
run: |
|
||||||
|
nix build .#trevstack-linux-amd64-image && ./result | docker load
|
||||||
|
nix build .#trevstack-linux-arm64-image && ./result | docker load
|
||||||
|
nix build .#trevstack-linux-arm-image && ./result | docker load
|
||||||
|
|
||||||
|
- name: Push images
|
||||||
|
uses: ./.actions/push
|
||||||
|
with:
|
||||||
|
server_url: ${{ github.server_url }}
|
||||||
|
repository: ${{ github.repository }}
|
||||||
|
tag: ${{ github.ref_name }}
|
50
.gitea/workflows/update.yaml
Normal file
50
.gitea/workflows/update.yaml
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
name: Update
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 0 * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: ./.actions/init
|
||||||
|
with:
|
||||||
|
token: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||||
|
|
||||||
|
# https://github.com/actions/checkout/issues/13
|
||||||
|
- name: Set git config
|
||||||
|
run: |
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
git checkout -B update
|
||||||
|
|
||||||
|
- run: nix run .#update
|
||||||
|
|
||||||
|
- name: Create pull request
|
||||||
|
env:
|
||||||
|
PAT: ${{ secrets.PAT }}
|
||||||
|
run: |
|
||||||
|
URL="${{ gitea.server_url }}"
|
||||||
|
REPO_OWNER_SLASH_NAME="${{ gitea.repository }}"
|
||||||
|
|
||||||
|
if ! git ls-remote --exit-code origin update; then
|
||||||
|
git push origin update --force
|
||||||
|
|
||||||
|
PR_RESPONSE=$(curl -s -X POST -H "Authorization: token $PAT" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"title":"update","body":"automatic update","head":"update","base":"main"}' \
|
||||||
|
"$URL/api/v1/repos/$REPO_OWNER_SLASH_NAME/pulls")
|
||||||
|
|
||||||
|
PR_NUMBER=$(echo "$PR_RESPONSE" | jq -r '.number')
|
||||||
|
|
||||||
|
curl -s -X POST -H "Authorization: token $PAT" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"Do":"merge","merge_when_checks_succeed":true,"delete_branch_after_merge":true}' \
|
||||||
|
"$URL/api/v1/repos/$REPO_OWNER_SLASH_NAME/pulls/$PR_NUMBER/merge"
|
||||||
|
|
||||||
|
else
|
||||||
|
git push origin update --force
|
||||||
|
fi
|
23
.github/workflows/check.yaml
vendored
Normal file
23
.github/workflows/check.yaml
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
name: Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
types: [opened, reopened, edited, auto_merge_enabled]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
name: check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: |
|
||||||
|
contains(github.event.head_commit.message, 'bump:') == false &&
|
||||||
|
contains(github.event.head_commit.message, 'Merge pull request') == false
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: ./.actions/init
|
||||||
|
with:
|
||||||
|
token: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||||
|
|
||||||
|
- run: nix flake check
|
73
.github/workflows/release.yaml
vendored
Normal file
73
.github/workflows/release.yaml
vendored
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: ./.actions/init
|
||||||
|
with:
|
||||||
|
token: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||||
|
|
||||||
|
- run: nix flake check
|
||||||
|
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: check
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: ./.actions/init
|
||||||
|
with:
|
||||||
|
token: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||||
|
|
||||||
|
- run: >
|
||||||
|
nix build
|
||||||
|
.#trevstack-linux-amd64
|
||||||
|
.#trevstack-linux-arm64
|
||||||
|
.#trevstack-linux-arm
|
||||||
|
.#trevstack-windows-amd64
|
||||||
|
.#trevstack-darwin-amd64
|
||||||
|
.#trevstack-darwin-arm64
|
||||||
|
|
||||||
|
- uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
generate_release_notes: true
|
||||||
|
files: |-
|
||||||
|
result*/bin/*
|
||||||
|
|
||||||
|
package:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: release
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: ./.actions/init
|
||||||
|
with:
|
||||||
|
token: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||||
|
|
||||||
|
- uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build & load images
|
||||||
|
run: |
|
||||||
|
nix build .#trevstack-linux-amd64-image && ./result | docker load
|
||||||
|
nix build .#trevstack-linux-arm64-image && ./result | docker load
|
||||||
|
nix build .#trevstack-linux-arm-image && ./result | docker load
|
||||||
|
|
||||||
|
- name: Push images
|
||||||
|
uses: ./.actions/push
|
||||||
|
with:
|
||||||
|
server_url: ghcr.io
|
||||||
|
repository: ${{ github.repository }}
|
||||||
|
tag: ${{ github.ref_name }}
|
40
.github/workflows/update.yaml
vendored
Normal file
40
.github/workflows/update.yaml
vendored
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
name: Update
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 0 * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: ./.actions/init
|
||||||
|
with:
|
||||||
|
token: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||||
|
|
||||||
|
# https://github.com/actions/checkout/issues/13
|
||||||
|
- name: Set git config
|
||||||
|
run: |
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
- run: nix run .#update
|
||||||
|
|
||||||
|
- name: Create pull request
|
||||||
|
id: cpr
|
||||||
|
uses: peter-evans/create-pull-request@v7
|
||||||
|
with:
|
||||||
|
branch: update
|
||||||
|
title: update
|
||||||
|
body: automatic update
|
||||||
|
|
||||||
|
- name: Enable automerge
|
||||||
|
run: gh pr merge --merge --auto "${{ steps.cpr.outputs.pull-request-number }}"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.PAT }}
|
16
.gitignore
vendored
16
.gitignore
vendored
@ -1,13 +1,7 @@
|
|||||||
.direnv
|
|
||||||
.env
|
.env
|
||||||
build
|
/docker-compose.*
|
||||||
result
|
/build/
|
||||||
|
|
||||||
# Client
|
# Nix
|
||||||
/client/node_modules
|
/result*
|
||||||
/client/.svelte-kit
|
/.direnv/
|
||||||
|
|
||||||
# Server
|
|
||||||
/server/client
|
|
||||||
/server/tmp
|
|
||||||
/server/internal/handlers/client/client
|
|
40
.scripts/bump.sh
Executable file
40
.scripts/bump.sh
Executable file
@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
git_root=$(git rev-parse --show-toplevel)
|
||||||
|
git_version=$(git describe --tags --abbrev=0)
|
||||||
|
version=${git_version#v}
|
||||||
|
|
||||||
|
major=$(echo "${version}" | cut -d . -f1)
|
||||||
|
minor=$(echo "${version}" | cut -d . -f2)
|
||||||
|
patch=$(echo "${version}" | cut -d . -f3)
|
||||||
|
case "${1-patch}" in
|
||||||
|
major) major=$((major + 1)) ;;
|
||||||
|
minor) minor=$((minor + 1)) ;;
|
||||||
|
*) patch=$((patch + 1)) ;;
|
||||||
|
esac
|
||||||
|
next_version="${major}.${minor}.${patch}"
|
||||||
|
|
||||||
|
echo "${version} -> ${next_version}"
|
||||||
|
|
||||||
|
echo "bumping openapi"
|
||||||
|
cd "${git_root}"
|
||||||
|
sed -i -e "s/${version}/${next_version}/g" openapi.yaml
|
||||||
|
sed -i -e "s/${version}/${next_version}/g" client/static/openapi/openapi.yaml
|
||||||
|
git add openapi.yaml
|
||||||
|
git add client/static/openapi/openapi.yaml
|
||||||
|
|
||||||
|
echo "bumping client"
|
||||||
|
cd "${git_root}/client"
|
||||||
|
npm version "${next_version}"
|
||||||
|
git add package-lock.json
|
||||||
|
git add package.json
|
||||||
|
|
||||||
|
echo "bumping nix"
|
||||||
|
cd "${git_root}"
|
||||||
|
nix-update --flake --version "${next_version}" --subpackage client default
|
||||||
|
git add flake.nix
|
||||||
|
|
||||||
|
git commit -m "bump: v${version} -> v${next_version}"
|
||||||
|
git push origin main
|
||||||
|
git tag -a "v${next_version}" -m "bump: v${version} -> v${next_version}"
|
||||||
|
git push origin "v${next_version}"
|
49
.scripts/update.sh
Executable file
49
.scripts/update.sh
Executable file
@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
git_root=$(git rev-parse --show-toplevel)
|
||||||
|
updated=false
|
||||||
|
|
||||||
|
echo "updating nix flake"
|
||||||
|
cd "${git_root}"
|
||||||
|
nix flake update
|
||||||
|
if ! git diff --exit-code flake.lock; then
|
||||||
|
git add flake.lock
|
||||||
|
git commit -m "build(nix): updated nix dependencies"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "updating protobuf deps"
|
||||||
|
cd "${git_root}/proto"
|
||||||
|
buf dep update
|
||||||
|
if ! git diff --exit-code buf.lock; then
|
||||||
|
git add buf.lock
|
||||||
|
git commit -m "build(buf): updated buf dependencies"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "updating client"
|
||||||
|
cd "${git_root}/client"
|
||||||
|
npm update --save && npm i
|
||||||
|
if ! git diff --exit-code package.json package-lock.json; then
|
||||||
|
git add package-lock.json
|
||||||
|
git add package.json
|
||||||
|
git commit -m "build(client): updated npm dependencies"
|
||||||
|
updated=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "updating server"
|
||||||
|
cd "${git_root}/server"
|
||||||
|
go get -u
|
||||||
|
go mod tidy
|
||||||
|
if ! git diff --exit-code go.mod go.sum; then
|
||||||
|
git add go.mod
|
||||||
|
git add go.sum
|
||||||
|
git commit -m "build(server): updated go dependencies"
|
||||||
|
updated=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${updated}" = true ]; then
|
||||||
|
echo "updating nix hashes"
|
||||||
|
cd "${git_root}"
|
||||||
|
nix-update --flake --version=skip --subpackage client default
|
||||||
|
git add flake.nix
|
||||||
|
git commit -m "build(nix): updated nix hashes"
|
||||||
|
fi
|
79
.treli.yaml
Normal file
79
.treli.yaml
Normal 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 .
|
||||||
|
|
||||||
|
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
|
10
.vscode/extensions.json
vendored
Normal file
10
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"golang.go",
|
||||||
|
"dorzey.vscode-sqlfluff",
|
||||||
|
"bufbuild.vscode-buf",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"svelte.svelte-vscode",
|
||||||
|
"esbenp.prettier-vscode"
|
||||||
|
]
|
||||||
|
}
|
46
.vscode/settings.json
vendored
Normal file
46
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
|
||||||
|
// Go
|
||||||
|
"go.lintTool": "revive",
|
||||||
|
"go.formatTool": "goimports",
|
||||||
|
"go.buildTags": "dev",
|
||||||
|
"go.lintFlags": ["--config=server/revive.toml"],
|
||||||
|
"gopls": { "ui.semanticTokens": true },
|
||||||
|
"[go]": {
|
||||||
|
"editor.defaultFormatter": "golang.go"
|
||||||
|
},
|
||||||
|
|
||||||
|
// SQLFluff
|
||||||
|
"sqlfluff.config": "server/db/.sqlfluff",
|
||||||
|
"[sql]": {
|
||||||
|
"editor.defaultFormatter": "dorzey.vscode-sqlfluff"
|
||||||
|
},
|
||||||
|
|
||||||
|
// Proto
|
||||||
|
"[proto]": {
|
||||||
|
"editor.defaultFormatter": "bufbuild.vscode-buf"
|
||||||
|
},
|
||||||
|
|
||||||
|
// ESLint
|
||||||
|
"eslint.workingDirectories": ["./client"],
|
||||||
|
"eslint.validate": ["svelte", "javascript", "typescript"],
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit"
|
||||||
|
},
|
||||||
|
|
||||||
|
// Svelte
|
||||||
|
"svelte.enable-ts-plugin": true,
|
||||||
|
"[svelte]": {
|
||||||
|
"editor.defaultFormatter": "svelte.svelte-vscode"
|
||||||
|
},
|
||||||
|
|
||||||
|
// Prettier
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
}
|
||||||
|
}
|
28
Dockerfile
28
Dockerfile
@ -1,28 +0,0 @@
|
|||||||
# Nix builder
|
|
||||||
FROM nixos/nix:latest AS builder
|
|
||||||
|
|
||||||
# Copy our source and setup our working dir.
|
|
||||||
COPY . /tmp/build
|
|
||||||
WORKDIR /tmp/build
|
|
||||||
|
|
||||||
# Build our Nix environment
|
|
||||||
RUN nix \
|
|
||||||
--extra-experimental-features "nix-command flakes" \
|
|
||||||
--option filter-syscalls false \
|
|
||||||
build
|
|
||||||
|
|
||||||
# Copy the Nix store closure into a directory. The Nix store closure is the
|
|
||||||
# entire set of Nix store values that we need for our build.
|
|
||||||
RUN mkdir /tmp/nix-store-closure
|
|
||||||
RUN cp -R $(nix-store -qR result/) /tmp/nix-store-closure
|
|
||||||
|
|
||||||
# Final image is based on scratch. We copy a bunch of Nix dependencies
|
|
||||||
# but they're fully self-contained so we don't need Nix anymore.
|
|
||||||
FROM scratch
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy /nix/store
|
|
||||||
COPY --from=builder /tmp/nix-store-closure /nix/store
|
|
||||||
COPY --from=builder /tmp/build/result /app
|
|
||||||
CMD ["/app/bin/server"]
|
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) Trev
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
125
README.md
Normal file
125
README.md
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
## TrevStack
|
||||||
|
|
||||||
|
This is a CRUD app to use as a template for starting projects
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **Communicate anywhere**. Define a [protocol buffer](https://protobuf.dev/), and [Connect](https://connectrpc.com/) generates type-safe code to facilitate communication between the server and any client (web, mobile, embedded, etc). The protocol buffers can contain annotations to validate fields on the client and server. For clients that cannot use Connect, an OpenAPI spec is also generated
|
||||||
|
- **Build anywhere**. The dev environment, testing and building is all declared in a single [Nix](https://nixos.org/) flake. Every developer and server can use the same environment
|
||||||
|
- **Deploy anywhere**. CI/CD is already set up using github actions. New versions are automatically released for every major platform, along with a docker image. The binaries created require zero run-time dependencies and are relatively small (this app is 26 MiB)
|
||||||
|
- Can be entirely self-hosted
|
||||||
|
- Authentication is rolled in, including API key, fingerprint & passkey
|
||||||
|
- Automatic database migration on startup
|
||||||
|
- Light & dark modes with the [catppuccin](https://catppuccin.com/palette/) color palette
|
||||||
|
- Really good at running as a [progressive web app](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps)
|
||||||
|
|
||||||
|
While I personally prefer using a Svelte frontend and Go backend, feel free to swap them out with whatever you like, you'll be surprised how easy it is.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
1. [Install Nix](https://nixos.org/download/)
|
||||||
|
2. Run `nix develop`
|
||||||
|
3. Create a `server/.env` file that looks something like
|
||||||
|
|
||||||
|
```env
|
||||||
|
KEY=changeme
|
||||||
|
PORT=8080
|
||||||
|
URL=http://localhost:5173
|
||||||
|
DATABASE_URL=sqlite:/home/trev/.config/trevstack/sqlite.db
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Run `treli` to start the server & client
|
||||||
|
|
||||||
|
It's that simple. If you're feeling fancy, install [direnv](https://direnv.net/) and the dev environment will load automatically.
|
||||||
|
|
||||||
|
### Useful Commands
|
||||||
|
|
||||||
|
- `nix run #update`: updates all of the dependencies
|
||||||
|
|
||||||
|
- `nix run #bump [major | minor]`: bumps the current version up one. Defaults to "patch" (0.0.1 -> 0.0.2)
|
||||||
|
|
||||||
|
- `nix build [#trevstack-(GOOS)-(GOARCH)]`: builds the application. Defaults to building for your current platform, but can be built to many by specifying the GOOS and GOARCH values
|
||||||
|
|
||||||
|
- `nix flake check`: runs all validations
|
||||||
|
|
||||||
|
- `buf lint proto` & `buf generate`: lints and generates code from protocol buffers
|
||||||
|
|
||||||
|
- `sqlc vet` & `sqlc generate`: verifies and generates code from SQL files
|
||||||
|
|
||||||
|
- `dbmate new` & `dbmate up`: creates a new migration file and runs pending migrations
|
||||||
|
|
||||||
|
### Github Actions
|
||||||
|
|
||||||
|
To use github actions for CI/CD, you'll need to create a fine-grained personal access token for the repository with the permissions:
|
||||||
|
|
||||||
|
- Contents (read and write)
|
||||||
|
- Pull requests (read and write)
|
||||||
|
|
||||||
|
And change some settings for the repository:
|
||||||
|
|
||||||
|
- General -> Allow auto-merge: true
|
||||||
|
- Rules -> Rulesets -> New ruleset
|
||||||
|
- Branch targeting criteria: Default
|
||||||
|
- Branch rules
|
||||||
|
- Require status checks to pass -> Add checks -> "check"
|
||||||
|
- Actions -> General -> Workflow permissions
|
||||||
|
- Read and write permissions: true
|
||||||
|
- Allow GitHub Actions to create and approve pull requests: true
|
||||||
|
- Secrets and variables -> Actions -> Repository secrets
|
||||||
|
- PAT: (personal access token)
|
||||||
|
|
||||||
|
### Gitea Actions
|
||||||
|
|
||||||
|
To use gitea actions for CI/CD, you'll need to create an [API token](https://docs.gitea.com/development/api-usage) with the scopes:
|
||||||
|
|
||||||
|
- write:repository
|
||||||
|
- write:package
|
||||||
|
|
||||||
|
And change some settings for the repository:
|
||||||
|
|
||||||
|
- Repository -> Delete pull request branch after merge by default: true
|
||||||
|
- Branches -> Add New Rule
|
||||||
|
- Protected Branch Name Pattern: main
|
||||||
|
- Enable Status Check: true
|
||||||
|
- Status check patterns: Check / check\*
|
||||||
|
- Actions -> Secrets
|
||||||
|
- PAT: (API token)
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### Client
|
||||||
|
|
||||||
|
- **svelte 5** [[docs](https://svelte.dev/docs/svelte)] UI framework
|
||||||
|
- **tailwind 4** [[docs](https://tailwindcss.com/)] CSS framework
|
||||||
|
- **bits ui** [[docs](https://bits-ui.com/docs/)] headless components
|
||||||
|
- [components](https://github.com/spotdemo4/trevstack/tree/main/client/src/lib/ui) from **shadcn-svelte** [[docs](https://www.shadcn-svelte.com/docs)] altered to work with tailwind 4, fit the [catppuccin](https://catppuccin.com/palette/) color palette, and use [shallow routing](https://svelte.dev/docs/kit/shallow-routing)
|
||||||
|
- **connect rpc** [[docs](https://connectrpc.com/docs/web/)] to communicating with the server
|
||||||
|
- **protovalidate-es** [[docs](https://github.com/bufbuild/protovalidate-es)], along with a function [coolforms](https://github.com/spotdemo4/trevstack/blob/main/client/src/lib/coolforms/) to emulate the library [sveltekit-superforms](https://superforms.rocks/)
|
||||||
|
- **simplewebauthn** [[docs](https://simplewebauthn.dev/docs/packages/browser)] for passkey authentication
|
||||||
|
- **scalar** [[docs](https://github.com/scalar/scalar)] for displaying openapi specs
|
||||||
|
- **tw-animate-css** [[docs](https://github.com/Wombosvideo/tw-animate-css)] to animate with just tailwind classes
|
||||||
|
- vite [[docs](https://vite.dev/)] for dev server and bundling
|
||||||
|
- eslint [[docs](https://eslint.org/)] for linting
|
||||||
|
- prettier [[docs](https://prettier.io/)] for formatting
|
||||||
|
- prettier-plugin-svelte [[docs](https://github.com/sveltejs/prettier-plugin-svelte)]
|
||||||
|
- prettier-plugin-tailwindcss [[docs](https://github.com/tailwindlabs/prettier-plugin-tailwindcss)]
|
||||||
|
- prettier-plugin-sort-imports [[docs](https://github.com/IanVS/prettier-plugin-sort-imports)]
|
||||||
|
|
||||||
|
### Server
|
||||||
|
|
||||||
|
- **go** [[docs](https://go.dev/doc/)]
|
||||||
|
- **connect rpc** [[docs](https://connectrpc.com/docs/go/)] to serve gRPC & HTTP requests
|
||||||
|
- **protovalidate-go** [[docs](https://github.com/bufbuild/protovalidate-go)] for validating those requests
|
||||||
|
- **sqlc** [[docs](https://docs.sqlc.dev/en/latest/)] because writing the SQL yourself is better than an ORM
|
||||||
|
- **go-webauthn** [[docs](https://github.com/go-webauthn/webauthn)] because webauthn is hard
|
||||||
|
- **dbmate** [[docs](https://github.com/amacneil/dbmate)] for database migrations
|
||||||
|
- revive [[docs](https://github.com/mgechev/revive)] for linting
|
||||||
|
|
||||||
|
### Protocol Buffers / gRPC
|
||||||
|
|
||||||
|
- **buf** [[docs](https://buf.build/docs/)] CLI for linting & code generation
|
||||||
|
- **protovalidate** [[docs](https://buf.build/docs/protovalidate/)] provides annotations to validate proto messages & fields
|
||||||
|
- **protoc-gen-connect-openapi** [[docs](https://github.com/sudorandom/protoc-gen-connect-openapi)] generates openapi specs
|
||||||
|
- protoc-gen-go [[docs](https://pkg.go.dev/google.golang.org/protobuf)]
|
||||||
|
- protoc-gen-connect-go [[docs](https://connectrpc.com/docs/go)]
|
||||||
|
- protoc-gen-es [[docs](https://connectrpc.com/docs/web/)]
|
17
buf.gen.yaml
17
buf.gen.yaml
@ -1,27 +1,34 @@
|
|||||||
version: v2
|
version: v2
|
||||||
clean: true
|
clean: true
|
||||||
|
inputs:
|
||||||
|
- directory: proto
|
||||||
|
|
||||||
managed:
|
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.yaml
|
||||||
- path=openapi.yaml
|
- path=openapi.yaml
|
||||||
|
2
client/.gitignore
vendored
Normal file
2
client/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/node_modules/
|
||||||
|
/.svelte-kit/
|
@ -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
|
@ -5,7 +5,18 @@
|
|||||||
"printWidth": 100,
|
"printWidth": 100,
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"prettier-plugin-svelte",
|
"prettier-plugin-svelte",
|
||||||
"prettier-plugin-tailwindcss"
|
"prettier-plugin-tailwindcss",
|
||||||
|
"@ianvs/prettier-plugin-sort-imports"
|
||||||
|
],
|
||||||
|
"tailwindFunctions": ["tv", "cn", "clsx"],
|
||||||
|
"importOrder": [
|
||||||
|
"<TYPES>^(node:)",
|
||||||
|
"<TYPES>",
|
||||||
|
"<TYPES>^[.]",
|
||||||
|
"<BUILTIN_MODULES>",
|
||||||
|
"^(\\$lib/ui)(/.*)$",
|
||||||
|
"<THIRD_PARTY_MODULES>",
|
||||||
|
"^[.]"
|
||||||
],
|
],
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import prettier from 'eslint-config-prettier';
|
import { fileURLToPath } from 'node:url';
|
||||||
import js from '@eslint/js';
|
|
||||||
import { includeIgnoreFile } from '@eslint/compat';
|
import { includeIgnoreFile } from '@eslint/compat';
|
||||||
|
import js from '@eslint/js';
|
||||||
|
import prettier from 'eslint-config-prettier';
|
||||||
import svelte from 'eslint-plugin-svelte';
|
import svelte from 'eslint-plugin-svelte';
|
||||||
import globals from 'globals';
|
import globals from 'globals';
|
||||||
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),
|
||||||
|
2110
client/package-lock.json
generated
2110
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "trevstack",
|
"name": "trevstack",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.2",
|
"version": "0.0.47",
|
||||||
"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",
|
||||||
|
"@ianvs/prettier-plugin-sort-imports": "^4.4.1",
|
||||||
"@lucide/svelte": "^0.479.0",
|
"@lucide/svelte": "^0.479.0",
|
||||||
"@scalar/api-reference": "^1.28.5",
|
"@scalar/api-reference": "^1.28.34",
|
||||||
|
"@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.21.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||||
"@tailwindcss/vite": "^4.0.14",
|
"@tailwindcss/vite": "^4.1.7",
|
||||||
"bits-ui": "^1.3.13",
|
"bits-ui": "^1.5.3",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"eslint": "^9.22.0",
|
"eslint": "^9.27.0",
|
||||||
"eslint-config-prettier": "^10.1.1",
|
"eslint-config-prettier": "^10.1.5",
|
||||||
"eslint-plugin-svelte": "^3.3.2",
|
"eslint-plugin-svelte": "^3.8.1",
|
||||||
"globals": "^16.0.0",
|
"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.4.0",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
"svelte": "^5.23.2",
|
"svelte": "^5.31.1",
|
||||||
"svelte-check": "^4.1.5",
|
"svelte-check": "^4.2.1",
|
||||||
"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.3.0",
|
||||||
"typescript": "^5.8.2",
|
"typescript": "^5.8.3",
|
||||||
"typescript-eslint": "^8.26.1",
|
"typescript-eslint": "^8.32.1",
|
||||||
"vite": "^6.2.2"
|
"vite": "^6.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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-text: #cdd6f4;
|
--color-mauve: var(--mauve);
|
||||||
|
--color-red: var(--red);
|
||||||
--color-sky: #89dceb;
|
--color-maroon: var(--maroon);
|
||||||
--color-red: #f38ba8;
|
--color-peach: var(--peach);
|
||||||
|
--color-yellow: var(--yellow);
|
||||||
|
--color-green: var(--green);
|
||||||
|
--color-teal: var(--teal);
|
||||||
|
--color-sky: var(--sky);
|
||||||
|
--color-sapphire: var(--sapphire);
|
||||||
|
--color-blue: var(--blue);
|
||||||
|
--color-lavender: var(--lavender);
|
||||||
|
--color-text: var(--text);
|
||||||
|
--color-subtext-1: var(--subtext-1);
|
||||||
|
--color-subtext: var(--subtext);
|
||||||
|
--color-overlay-2: var(--overlay-2);
|
||||||
|
--color-overlay-1: var(--overlay-1);
|
||||||
|
--color-overlay: var(--overlay);
|
||||||
|
--color-surface-2: var(--surface-2);
|
||||||
|
--color-surface-1: var(--surface-1);
|
||||||
|
--color-surface: var(--surface);
|
||||||
|
--color-based: var(--based);
|
||||||
|
--color-mantle: var(--mantle);
|
||||||
|
--color-crust: var(--crust);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--rosewater: hsl(11deg, 59%, 67%);
|
||||||
|
--flamingo: hsl(0deg, 60%, 67%);
|
||||||
|
--pink: hsl(316deg, 73%, 69%);
|
||||||
|
--mauve: hsl(266deg, 85%, 58%);
|
||||||
|
--red: hsl(347deg, 87%, 44%);
|
||||||
|
--maroon: hsl(355deg, 76%, 59%);
|
||||||
|
--peach: hsl(22deg, 99%, 52%);
|
||||||
|
--yellow: hsl(35deg, 77%, 49%);
|
||||||
|
--green: hsl(109deg, 58%, 40%);
|
||||||
|
--teal: hsl(183deg, 74%, 35%);
|
||||||
|
--sky: hsl(197deg, 97%, 46%);
|
||||||
|
--sapphire: hsl(189deg, 70%, 42%);
|
||||||
|
--blue: hsl(220deg, 91%, 54%);
|
||||||
|
--lavender: hsl(231deg, 97%, 72%);
|
||||||
|
--text: hsl(234deg, 16%, 35%);
|
||||||
|
--subtext-1: hsl(233deg, 13%, 41%);
|
||||||
|
--subtext: hsl(233deg, 10%, 47%);
|
||||||
|
--overlay-2: hsl(232deg, 10%, 53%);
|
||||||
|
--overlay-1: hsl(231deg, 10%, 59%);
|
||||||
|
--overlay: hsl(228deg, 11%, 65%);
|
||||||
|
--surface-2: hsl(227deg, 12%, 71%);
|
||||||
|
--surface-1: hsl(225deg, 14%, 77%);
|
||||||
|
--surface: hsl(223deg, 16%, 83%);
|
||||||
|
--based: hsl(220deg, 23%, 95%);
|
||||||
|
--mantle: hsl(220deg, 22%, 92%);
|
||||||
|
--crust: hsl(220deg, 21%, 89%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--rosewater: hsl(10deg, 56%, 91%);
|
||||||
|
--flamingo: hsl(0deg, 59%, 88%);
|
||||||
|
--pink: hsl(316deg, 72%, 86%);
|
||||||
|
--mauve: hsl(267deg, 84%, 81%);
|
||||||
|
--red: hsl(343deg, 81%, 75%);
|
||||||
|
--maroon: hsl(350deg, 65%, 77%);
|
||||||
|
--peach: hsl(23deg, 92%, 75%);
|
||||||
|
--yellow: hsl(41deg, 86%, 83%);
|
||||||
|
--green: hsl(115deg, 54%, 76%);
|
||||||
|
--teal: hsl(170deg, 57%, 73%);
|
||||||
|
--sky: hsl(189deg, 71%, 73%);
|
||||||
|
--sapphire: hsl(199deg, 76%, 69%);
|
||||||
|
--blue: hsl(217deg, 92%, 76%);
|
||||||
|
--lavender: hsl(232deg, 97%, 85%);
|
||||||
|
--text: hsl(226deg, 64%, 88%);
|
||||||
|
--subtext-1: hsl(227deg, 35%, 80%);
|
||||||
|
--subtext: hsl(228deg, 24%, 72%);
|
||||||
|
--overlay-2: hsl(228deg, 17%, 64%);
|
||||||
|
--overlay-1: hsl(230deg, 13%, 55%);
|
||||||
|
--overlay: hsl(231deg, 11%, 47%);
|
||||||
|
--surface-2: hsl(233deg, 12%, 39%);
|
||||||
|
--surface-1: hsl(234deg, 13%, 31%);
|
||||||
|
--surface: hsl(237deg, 16%, 23%);
|
||||||
|
--based: hsl(240deg, 21%, 15%);
|
||||||
|
--mantle: hsl(240deg, 21%, 12%);
|
||||||
|
--crust: hsl(240deg, 23%, 9%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
-- Cheat sheet --
|
||||||
|
|
||||||
|
Focus Outline: blue
|
||||||
|
Border: surface-1
|
||||||
|
Hover: bump color by 2 (eg crust -> based), if accent color drop opacity (eg blue -> blue/90)
|
||||||
|
*/
|
||||||
|
@ -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-dvh">
|
||||||
<div style="display: contents">%sveltekit.body%</div>
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
4678
client/src/lib/connect/buf/validate/validate_pb.ts
Normal file
4678
client/src/lib/connect/buf/validate/validate_pb.ts
Normal file
File diff suppressed because one or more lines are too long
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
240
client/src/lib/connect/user/v1/auth_pb.ts
Normal file
240
client/src/lib/connect/user/v1/auth_pb.ts
Normal 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);
|
||||||
|
|
@ -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);
|
||||||
|
|
5
client/src/lib/coolforms/conststate.svelte.ts
Normal file
5
client/src/lib/coolforms/conststate.svelte.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export function newState<T>(s: T) {
|
||||||
|
const state = $state(s);
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
176
client/src/lib/coolforms/coolforms.svelte.ts
Normal file
176
client/src/lib/coolforms/coolforms.svelte.ts
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
import type { DescMessage, DescService, MessageInitShape, MessageShape } from '@bufbuild/protobuf';
|
||||||
|
import type { Violation } from '@bufbuild/protovalidate';
|
||||||
|
import type { Client } from '@connectrpc/connect';
|
||||||
|
import type { Action } from 'svelte/action';
|
||||||
|
import { create } from '@bufbuild/protobuf';
|
||||||
|
import { ValidationError } from '@bufbuild/protovalidate';
|
||||||
|
import { ConnectError } from '@connectrpc/connect';
|
||||||
|
import { Validator } from '../transport';
|
||||||
|
|
||||||
|
type Options<Input extends DescMessage, Output extends DescMessage> = {
|
||||||
|
init?: MessageInitShape<Input>;
|
||||||
|
start?: boolean;
|
||||||
|
reset?: boolean;
|
||||||
|
onSubmit?: (formData: FormData, input: MessageShape<Input>) => Promise<MessageShape<Input>>;
|
||||||
|
onResult?: (result: MessageShape<Output>) => void;
|
||||||
|
onError?: (err: Violation[] | ConnectError) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Violations<Field> = {
|
||||||
|
[field in keyof Field]?: Violation[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function coolForm<Service extends DescService, Method extends Service['methods'][number]>(
|
||||||
|
client: Client<Service>,
|
||||||
|
method: Method,
|
||||||
|
options?: Options<Method['input'], Method['output']>
|
||||||
|
) {
|
||||||
|
const input = $state(create(method.input as Method['input'], options?.init));
|
||||||
|
const output = $state(create(method.output as Method['output']));
|
||||||
|
const errors: Violations<Method['input']['field']> & { form?: ConnectError } = $state({});
|
||||||
|
let loading = $state(false);
|
||||||
|
|
||||||
|
const validate = () => {
|
||||||
|
// Delete existing errors
|
||||||
|
for (const key in errors) {
|
||||||
|
delete errors[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Validator.validate(method['input'], input);
|
||||||
|
} catch (e) {
|
||||||
|
if (!(e instanceof ValidationError)) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map violation errors to errors rune
|
||||||
|
for (const violation of e.violations) {
|
||||||
|
for (const field of violation.field) {
|
||||||
|
if (!('localName' in field)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create localName property if it doesn't exist
|
||||||
|
if (!errors[field.localName]) {
|
||||||
|
Object.assign(errors, {
|
||||||
|
[field.localName]: [violation]
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
errors[field.localName]?.push(violation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.violations;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
// When a request is successful
|
||||||
|
const success = (response: MessageShape<Method['output']>) => {
|
||||||
|
loading = false;
|
||||||
|
|
||||||
|
// Send the response up
|
||||||
|
options?.onResult?.(response);
|
||||||
|
|
||||||
|
// Set the response
|
||||||
|
Object.assign(output, response);
|
||||||
|
|
||||||
|
// If we want to reset the input
|
||||||
|
if (options && (options.reset == undefined || options.reset)) {
|
||||||
|
const cleared = create(method.input as Method['input'], options?.init);
|
||||||
|
Object.assign(input, cleared);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// When a request fails
|
||||||
|
const fail = (err: Violation[] | ConnectError | Error) => {
|
||||||
|
loading = false;
|
||||||
|
|
||||||
|
// It's a Violation[]
|
||||||
|
if (Array.isArray(err)) {
|
||||||
|
// Send the error up
|
||||||
|
options?.onError?.(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// It's a ConnectError
|
||||||
|
if (err instanceof ConnectError) {
|
||||||
|
// Assign it to the form
|
||||||
|
errors.form = err;
|
||||||
|
|
||||||
|
// Send the error up
|
||||||
|
options?.onError?.(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
};
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
loading = true;
|
||||||
|
|
||||||
|
// Validate
|
||||||
|
const validationErrors = validate();
|
||||||
|
if (validationErrors.length > 0) {
|
||||||
|
fail(validationErrors);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send response
|
||||||
|
if (method.methodKind == 'unary') {
|
||||||
|
// @ts-expect-error I can't figure out how to make this typescript compliant
|
||||||
|
const response = client[method.localName]($state.snapshot(input)) as Promise<
|
||||||
|
MessageShape<Method['output']>
|
||||||
|
>;
|
||||||
|
|
||||||
|
response
|
||||||
|
.then((resp) => {
|
||||||
|
success(resp);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
fail(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// A nice action to give to forms to run submit() on submit
|
||||||
|
const impair: Action<HTMLFormElement> = (form) => {
|
||||||
|
$effect(() => {
|
||||||
|
form.onsubmit = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (options?.onSubmit) {
|
||||||
|
const formData = new FormData(form);
|
||||||
|
options.onSubmit(formData, input).then((i) => {
|
||||||
|
Object.assign(input, i);
|
||||||
|
submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
submit();
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
form.onsubmit = () => {};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options?.start) {
|
||||||
|
submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
input,
|
||||||
|
output,
|
||||||
|
errors,
|
||||||
|
loading: () => loading,
|
||||||
|
submit,
|
||||||
|
validate,
|
||||||
|
impair
|
||||||
|
};
|
||||||
|
}
|
4
client/src/lib/coolforms/index.ts
Normal file
4
client/src/lib/coolforms/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { newState } from './conststate.svelte';
|
||||||
|
import { coolForm } from './coolforms.svelte';
|
||||||
|
|
||||||
|
export { coolForm, newState };
|
@ -1 +0,0 @@
|
|||||||
// place files you want to import through the `$lib` alias in this folder.
|
|
@ -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);
|
|
||||||
|
|
@ -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
|
||||||
});
|
});
|
@ -1,27 +1,39 @@
|
|||||||
import { createConnectTransport } from "@connectrpc/connect-web"
|
import type { Interceptor } from '@connectrpc/connect';
|
||||||
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 } from '@connectrpc/connect';
|
||||||
import { UserService } from "$lib/services/user/v1/user_pb";
|
import { createConnectTransport } from '@connectrpc/connect-web';
|
||||||
import { ItemService } from "$lib/services/item/v1/item_pb";
|
import { goto } from '$app/navigation';
|
||||||
import { goto } from "$app/navigation";
|
import { page } from '$app/state';
|
||||||
|
import { ItemService } from '$lib/connect/item/v1/item_pb';
|
||||||
|
import { AuthService } from '$lib/connect/user/v1/auth_pb';
|
||||||
|
import { UserService } from '$lib/connect/user/v1/user_pb';
|
||||||
|
|
||||||
const redirector: Interceptor = (next) => async (req) => {
|
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();
|
||||||
|
@ -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>
|
|
@ -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>
|
|
@ -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}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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}
|
|
@ -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>
|
|
20
client/src/lib/ui/avatar/avatar-fallback.svelte
Normal file
20
client/src/lib/ui/avatar/avatar-fallback.svelte
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
import { Avatar as AvatarPrimitive } from 'bits-ui';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: AvatarPrimitive.FallbackProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AvatarPrimitive.Fallback
|
||||||
|
bind:ref
|
||||||
|
data-slot="avatar-fallback"
|
||||||
|
class={cn(
|
||||||
|
'bg-surface flex size-full items-center justify-center rounded-full text-sm transition-all select-none',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
17
client/src/lib/ui/avatar/avatar-image.svelte
Normal file
17
client/src/lib/ui/avatar/avatar-image.svelte
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
import { Avatar as AvatarPrimitive } from 'bits-ui';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: AvatarPrimitive.ImageProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AvatarPrimitive.Image
|
||||||
|
bind:ref
|
||||||
|
data-slot="avatar-image"
|
||||||
|
class={cn('aspect-square size-full', className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
21
client/src/lib/ui/avatar/avatar.svelte
Normal file
21
client/src/lib/ui/avatar/avatar.svelte
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
import { Avatar as AvatarPrimitive } from 'bits-ui';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: AvatarPrimitive.RootProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AvatarPrimitive.Root
|
||||||
|
bind:ref
|
||||||
|
data-slot="avatar"
|
||||||
|
class={cn(
|
||||||
|
'outline-surface-1 relative flex size-9 shrink-0 overflow-hidden rounded-full shadow-xs outline outline-offset-2',
|
||||||
|
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
13
client/src/lib/ui/avatar/index.ts
Normal file
13
client/src/lib/ui/avatar/index.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import Fallback from './avatar-fallback.svelte';
|
||||||
|
import Image from './avatar-image.svelte';
|
||||||
|
import Root from './avatar.svelte';
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Image,
|
||||||
|
Fallback,
|
||||||
|
//
|
||||||
|
Root as Avatar,
|
||||||
|
Image as AvatarImage,
|
||||||
|
Fallback as AvatarFallback
|
||||||
|
};
|
94
client/src/lib/ui/button/button.svelte
Normal file
94
client/src/lib/ui/button/button.svelte
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
import type { WithElementRef } from 'bits-ui';
|
||||||
|
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
|
||||||
|
import type { VariantProps } from 'tailwind-variants';
|
||||||
|
import { LoaderCircle } from '@lucide/svelte';
|
||||||
|
import { cn } from '$lib/utils';
|
||||||
|
import { tv } from 'tailwind-variants';
|
||||||
|
|
||||||
|
export const buttonVariants = tv({
|
||||||
|
base: cn(
|
||||||
|
'inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all',
|
||||||
|
|
||||||
|
// Focus
|
||||||
|
'focus-visible:outline-accent focus-visible:outline-2 focus-visible:outline-offset-2',
|
||||||
|
|
||||||
|
// Disabled
|
||||||
|
'disabled:pointer-events-none disabled:opacity-50',
|
||||||
|
|
||||||
|
// Images
|
||||||
|
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-5"
|
||||||
|
),
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: 'text-crust bg-accent hover:bg-accent/90 shadow-xs',
|
||||||
|
red: 'text-crust bg-red hover:bg-red/90 shadow-xs',
|
||||||
|
outline: 'text-text border-surface-1 hover:bg-surface border bg-transparent shadow-xs',
|
||||||
|
input: 'text-text border-surface-1 hover:border-overlay border bg-transparent shadow-xs',
|
||||||
|
ghost: 'text-text hover:bg-surface'
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
||||||
|
sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5',
|
||||||
|
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
||||||
|
icon: 'h-9 min-w-9 px-3'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'default',
|
||||||
|
size: 'default'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ButtonVariant = VariantProps<typeof buttonVariants>['variant'];
|
||||||
|
export type ButtonSize = VariantProps<typeof buttonVariants>['size'];
|
||||||
|
|
||||||
|
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||||
|
WithElementRef<HTMLAnchorAttributes> & {
|
||||||
|
variant?: ButtonVariant;
|
||||||
|
size?: ButtonSize;
|
||||||
|
loading?: boolean;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
let {
|
||||||
|
class: className,
|
||||||
|
variant = 'default',
|
||||||
|
size = 'default',
|
||||||
|
ref = $bindable(null),
|
||||||
|
href = undefined,
|
||||||
|
type = 'button',
|
||||||
|
loading,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: ButtonProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if href}
|
||||||
|
<a
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="button"
|
||||||
|
class={cn(buttonVariants({ variant, size }), className)}
|
||||||
|
{href}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{#if loading}
|
||||||
|
<LoaderCircle class="animate-spin" />
|
||||||
|
{/if}
|
||||||
|
{@render children?.()}
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="button"
|
||||||
|
class={cn(buttonVariants({ variant, size }), className)}
|
||||||
|
{type}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{#if loading}
|
||||||
|
<LoaderCircle class="animate-spin" />
|
||||||
|
{/if}
|
||||||
|
{@render children?.()}
|
||||||
|
</button>
|
||||||
|
{/if}
|
13
client/src/lib/ui/button/index.ts
Normal file
13
client/src/lib/ui/button/index.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import type { ButtonProps, ButtonSize, ButtonVariant } from './button.svelte';
|
||||||
|
import Root, { buttonVariants } from './button.svelte';
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
type ButtonProps as Props,
|
||||||
|
//
|
||||||
|
Root as Button,
|
||||||
|
buttonVariants,
|
||||||
|
type ButtonProps,
|
||||||
|
type ButtonSize,
|
||||||
|
type ButtonVariant
|
||||||
|
};
|
17
client/src/lib/ui/card/card.svelte
Normal file
17
client/src/lib/ui/card/card.svelte
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { WithElementRef } from 'bits-ui';
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
import { cn } from '$lib/utils';
|
||||||
|
|
||||||
|
type Props = WithElementRef<HTMLAttributes<HTMLDivElement>>;
|
||||||
|
|
||||||
|
let { ref = $bindable(null), class: className, children, ...restProps }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
class={cn('bg-based border-surface-1 rounded border p-5 shadow-md', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
7
client/src/lib/ui/card/index.ts
Normal file
7
client/src/lib/ui/card/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import Root from './card.svelte';
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Card
|
||||||
|
};
|
76
client/src/lib/ui/daterangepicker/daterangepicker.svelte
Normal file
76
client/src/lib/ui/daterangepicker/daterangepicker.svelte
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { DateValue } from '@internationalized/date';
|
||||||
|
import type { DateRange } from 'bits-ui';
|
||||||
|
import { buttonVariants } from '$lib/ui/button';
|
||||||
|
import * as Popover from '$lib/ui/popover';
|
||||||
|
import { RangeCalendar } from '$lib/ui/range-calendar';
|
||||||
|
import { CalendarDate, DateFormatter, getLocalTimeZone } from '@internationalized/date';
|
||||||
|
import CalendarIcon from '@lucide/svelte/icons/calendar';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
className,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
onchange
|
||||||
|
}: {
|
||||||
|
className?: string;
|
||||||
|
start?: Date;
|
||||||
|
end?: Date;
|
||||||
|
onchange?: (start: Date, end: Date) => void;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let value: DateRange = $state({
|
||||||
|
start: start
|
||||||
|
? new CalendarDate(start.getFullYear(), start.getMonth(), start.getDate())
|
||||||
|
: undefined,
|
||||||
|
end: end ? new CalendarDate(end.getFullYear(), end.getMonth(), end.getDate()) : undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
const df = new DateFormatter('en-US', {
|
||||||
|
dateStyle: 'medium'
|
||||||
|
});
|
||||||
|
|
||||||
|
let startValue: DateValue | undefined = $state(undefined);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class={cn('grid gap-2', className)}>
|
||||||
|
<Popover.Root>
|
||||||
|
<Popover.Trigger
|
||||||
|
class={cn(
|
||||||
|
buttonVariants({ variant: 'input' }),
|
||||||
|
'bg-based text-md md:text-sm',
|
||||||
|
!value && 'text-subtext'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CalendarIcon class="mr-2 size-4" />
|
||||||
|
{#if value && value.start}
|
||||||
|
{#if value.end}
|
||||||
|
{df.format(value.start.toDate(getLocalTimeZone()))} - {df.format(
|
||||||
|
value.end.toDate(getLocalTimeZone())
|
||||||
|
)}
|
||||||
|
{:else}
|
||||||
|
{df.format(value.start.toDate(getLocalTimeZone()))}
|
||||||
|
{/if}
|
||||||
|
{:else if startValue}
|
||||||
|
{df.format(startValue.toDate(getLocalTimeZone()))}
|
||||||
|
{:else}
|
||||||
|
Pick a date range
|
||||||
|
{/if}
|
||||||
|
</Popover.Trigger>
|
||||||
|
<Popover.Content class="bg-based w-auto p-0" align="start">
|
||||||
|
<RangeCalendar
|
||||||
|
bind:value
|
||||||
|
onStartValueChange={(v) => {
|
||||||
|
startValue = v;
|
||||||
|
}}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
if (v.start && v.end) {
|
||||||
|
onchange?.(v.start.toDate(getLocalTimeZone()), v.end.toDate(getLocalTimeZone()));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
numberOfMonths={2}
|
||||||
|
/>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Root>
|
||||||
|
</div>
|
3
client/src/lib/ui/daterangepicker/index.ts
Normal file
3
client/src/lib/ui/daterangepicker/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import Root from './daterangepicker.svelte';
|
||||||
|
|
||||||
|
export { Root, Root as DateRangePicker };
|
7
client/src/lib/ui/dialog/dialog-close.svelte
Normal file
7
client/src/lib/ui/dialog/dialog-close.svelte
Normal 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} />
|
51
client/src/lib/ui/dialog/dialog-content.svelte
Normal file
51
client/src/lib/ui/dialog/dialog-content.svelte
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { WithoutChildrenOrChild } from 'bits-ui';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
import X from '@lucide/svelte/icons/x';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||||
|
import * as Dialog from './index.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
portalProps,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
|
||||||
|
portalProps?: DialogPrimitive.PortalProps;
|
||||||
|
children: Snippet;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Dialog.Portal {...portalProps}>
|
||||||
|
<Dialog.Overlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
bind:ref
|
||||||
|
data-slot="dialog-content"
|
||||||
|
class={cn(
|
||||||
|
'bg-mantle text-text border-surface-1 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg sm:max-w-lg',
|
||||||
|
|
||||||
|
// Animations
|
||||||
|
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 duration-200',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
<DialogPrimitive.Close
|
||||||
|
class={cn(
|
||||||
|
'text-text hover:bg-crust absolute top-4 right-4 cursor-pointer rounded p-1 transition-all disabled:pointer-events-none',
|
||||||
|
|
||||||
|
// Focus
|
||||||
|
'focus-visible:outline-accent focus-visible:outline-2 focus-visible:outline-offset-2',
|
||||||
|
|
||||||
|
// Images
|
||||||
|
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<X />
|
||||||
|
<span class="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</Dialog.Portal>
|
17
client/src/lib/ui/dialog/dialog-description.svelte
Normal file
17
client/src/lib/ui/dialog/dialog-description.svelte
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: DialogPrimitive.DescriptionProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
bind:ref
|
||||||
|
data-slot="dialog-description"
|
||||||
|
class={cn('text-muted-foreground text-sm', className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
21
client/src/lib/ui/dialog/dialog-footer.svelte
Normal file
21
client/src/lib/ui/dialog/dialog-footer.svelte
Normal 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>
|
21
client/src/lib/ui/dialog/dialog-header.svelte
Normal file
21
client/src/lib/ui/dialog/dialog-header.svelte
Normal 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-header"
|
||||||
|
class={cn('flex flex-col gap-2 text-center sm:text-left', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
20
client/src/lib/ui/dialog/dialog-overlay.svelte
Normal file
20
client/src/lib/ui/dialog/dialog-overlay.svelte
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: DialogPrimitive.OverlayProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
bind:ref
|
||||||
|
data-slot="dialog-overlay"
|
||||||
|
class={cn(
|
||||||
|
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
17
client/src/lib/ui/dialog/dialog-title.svelte
Normal file
17
client/src/lib/ui/dialog/dialog-title.svelte
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: DialogPrimitive.TitleProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
bind:ref
|
||||||
|
data-slot="dialog-title"
|
||||||
|
class={cn('text-lg leading-none font-semibold', className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
7
client/src/lib/ui/dialog/dialog-trigger.svelte
Normal file
7
client/src/lib/ui/dialog/dialog-trigger.svelte
Normal 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} />
|
28
client/src/lib/ui/dialog/dialog.svelte
Normal file
28
client/src/lib/ui/dialog/dialog.svelte
Normal 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}
|
||||||
|
/>
|
36
client/src/lib/ui/dialog/index.ts
Normal file
36
client/src/lib/ui/dialog/index.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||||
|
import Close from './dialog-close.svelte';
|
||||||
|
import Content from './dialog-content.svelte';
|
||||||
|
import Description from './dialog-description.svelte';
|
||||||
|
import Footer from './dialog-footer.svelte';
|
||||||
|
import Header from './dialog-header.svelte';
|
||||||
|
import Overlay from './dialog-overlay.svelte';
|
||||||
|
import Title from './dialog-title.svelte';
|
||||||
|
import Trigger from './dialog-trigger.svelte';
|
||||||
|
import Root from './dialog.svelte';
|
||||||
|
|
||||||
|
const Portal = DialogPrimitive.Portal;
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Title,
|
||||||
|
Portal,
|
||||||
|
Footer,
|
||||||
|
Header,
|
||||||
|
Trigger,
|
||||||
|
Overlay,
|
||||||
|
Content,
|
||||||
|
Description,
|
||||||
|
Close,
|
||||||
|
//
|
||||||
|
Root as Dialog,
|
||||||
|
Title as DialogTitle,
|
||||||
|
Portal as DialogPortal,
|
||||||
|
Footer as DialogFooter,
|
||||||
|
Header as DialogHeader,
|
||||||
|
Trigger as DialogTrigger,
|
||||||
|
Overlay as DialogOverlay,
|
||||||
|
Content as DialogContent,
|
||||||
|
Description as DialogDescription,
|
||||||
|
Close as DialogClose
|
||||||
|
};
|
@ -0,0 +1,48 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { WithoutChildrenOrChild } from 'bits-ui';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
import Check from '@lucide/svelte/icons/check';
|
||||||
|
import Minus from '@lucide/svelte/icons/minus';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
checked = $bindable(false),
|
||||||
|
indeterminate = $bindable(false),
|
||||||
|
class: className,
|
||||||
|
children: childrenProp,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> & {
|
||||||
|
children?: Snippet;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
|
bind:ref
|
||||||
|
bind:checked
|
||||||
|
bind:indeterminate
|
||||||
|
data-slot="dropdown-menu-checkbox-item"
|
||||||
|
class={cn(
|
||||||
|
'focus:bg-surface relative flex cursor-pointer items-center gap-2 rounded-sm py-2 pr-2 pl-8 text-sm outline-hidden select-none',
|
||||||
|
|
||||||
|
// Disabled
|
||||||
|
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||||
|
|
||||||
|
// Images
|
||||||
|
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{#snippet children({ checked, indeterminate })}
|
||||||
|
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
{#if indeterminate}
|
||||||
|
<Minus class="size-4" />
|
||||||
|
{:else}
|
||||||
|
<Check class={cn('size-4', !checked && 'text-transparent')} />
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
{@render childrenProp?.()}
|
||||||
|
{/snippet}
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
31
client/src/lib/ui/dropdown-menu/dropdown-menu-content.svelte
Normal file
31
client/src/lib/ui/dropdown-menu/dropdown-menu-content.svelte
Normal 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>
|
17
client/src/lib/ui/dropdown-menu/dropdown-menu-group.svelte
Normal file
17
client/src/lib/ui/dropdown-menu/dropdown-menu-group.svelte
Normal 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-surface border-b first:pt-0 last:border-none last:pb-0', className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
27
client/src/lib/ui/dropdown-menu/dropdown-menu-item.svelte
Normal file
27
client/src/lib/ui/dropdown-menu/dropdown-menu-item.svelte
Normal 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}
|
||||||
|
/>
|
24
client/src/lib/ui/dropdown-menu/dropdown-menu-label.svelte
Normal file
24
client/src/lib/ui/dropdown-menu/dropdown-menu-label.svelte
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
import { type WithElementRef } from 'bits-ui';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
inset,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||||
|
inset?: boolean;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="dropdown-menu-label"
|
||||||
|
class={cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
22
client/src/lib/ui/dropdown-menu/dropdown-menu-link.svelte
Normal file
22
client/src/lib/ui/dropdown-menu/dropdown-menu-link.svelte
Normal 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>
|
@ -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}
|
||||||
|
/>
|
@ -0,0 +1,40 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { WithoutChild } from 'bits-ui';
|
||||||
|
import Circle from '@lucide/svelte/icons/circle';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children: childrenProp,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.RadioItem
|
||||||
|
bind:ref
|
||||||
|
data-slot="dropdown-menu-radio-item"
|
||||||
|
class={cn(
|
||||||
|
'focus:bg-surface text-text relative flex cursor-pointer items-center gap-2 py-2 pr-2 pl-8 text-sm select-none',
|
||||||
|
|
||||||
|
// Disabled
|
||||||
|
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||||
|
|
||||||
|
// Images
|
||||||
|
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{#snippet children({ checked })}
|
||||||
|
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
{#if checked}
|
||||||
|
<Circle class="size-2 fill-current" />
|
||||||
|
{:else}
|
||||||
|
<Circle class="size-2" />
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
{@render childrenProp?.({ checked })}
|
||||||
|
{/snippet}
|
||||||
|
</DropdownMenuPrimitive.RadioItem>
|
@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: DropdownMenuPrimitive.SeparatorProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.Separator
|
||||||
|
bind:ref
|
||||||
|
data-slot="dropdown-menu-separator"
|
||||||
|
class={cn('bg-surface-1 -mx-1 my-1 h-px', className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
import { type WithElementRef } from 'bits-ui';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="dropdown-menu-shortcut"
|
||||||
|
class={cn('text-text ml-auto text-xs tracking-widest', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</span>
|
@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: DropdownMenuPrimitive.SubContentProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
bind:ref
|
||||||
|
data-slot="dropdown-menu-sub-content"
|
||||||
|
class={cn(
|
||||||
|
'bg-based text-text z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',
|
||||||
|
|
||||||
|
// Animations
|
||||||
|
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import ChevronRight from '@lucide/svelte/icons/chevron-right';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
inset,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: DropdownMenuPrimitive.SubTriggerProps & {
|
||||||
|
inset?: boolean;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
bind:ref
|
||||||
|
data-slot="dropdown-menu-sub-trigger"
|
||||||
|
class={cn(
|
||||||
|
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none',
|
||||||
|
inset && 'pl-8',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
<ChevronRight class="ml-auto size-4" />
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
23
client/src/lib/ui/dropdown-menu/dropdown-menu-trigger.svelte
Normal file
23
client/src/lib/ui/dropdown-menu/dropdown-menu-trigger.svelte
Normal 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}
|
||||||
|
/>
|
50
client/src/lib/ui/dropdown-menu/index.ts
Normal file
50
client/src/lib/ui/dropdown-menu/index.ts
Normal 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 Label from './dropdown-menu-label.svelte';
|
||||||
|
import Link from './dropdown-menu-link.svelte';
|
||||||
|
import RadioGroup from './dropdown-menu-radio-group.svelte';
|
||||||
|
import RadioItem from './dropdown-menu-radio-item.svelte';
|
||||||
|
import Separator from './dropdown-menu-separator.svelte';
|
||||||
|
import Shortcut from './dropdown-menu-shortcut.svelte';
|
||||||
|
import SubContent from './dropdown-menu-sub-content.svelte';
|
||||||
|
import SubTrigger from './dropdown-menu-sub-trigger.svelte';
|
||||||
|
import Trigger from './dropdown-menu-trigger.svelte';
|
||||||
|
|
||||||
|
const Sub = DropdownMenuPrimitive.Sub;
|
||||||
|
const Root = DropdownMenuPrimitive.Root;
|
||||||
|
|
||||||
|
export {
|
||||||
|
CheckboxItem,
|
||||||
|
Content,
|
||||||
|
Root as DropdownMenu,
|
||||||
|
CheckboxItem as DropdownMenuCheckboxItem,
|
||||||
|
Content as DropdownMenuContent,
|
||||||
|
Group as DropdownMenuGroup,
|
||||||
|
Item as DropdownMenuItem,
|
||||||
|
Link as DropdownMenuLink,
|
||||||
|
Label as DropdownMenuLabel,
|
||||||
|
RadioGroup as DropdownMenuRadioGroup,
|
||||||
|
RadioItem as DropdownMenuRadioItem,
|
||||||
|
Separator as DropdownMenuSeparator,
|
||||||
|
Shortcut as DropdownMenuShortcut,
|
||||||
|
Sub as DropdownMenuSub,
|
||||||
|
SubContent as DropdownMenuSubContent,
|
||||||
|
SubTrigger as DropdownMenuSubTrigger,
|
||||||
|
Trigger as DropdownMenuTrigger,
|
||||||
|
Group,
|
||||||
|
Item,
|
||||||
|
Link,
|
||||||
|
Label,
|
||||||
|
RadioGroup,
|
||||||
|
RadioItem,
|
||||||
|
Root,
|
||||||
|
Separator,
|
||||||
|
Shortcut,
|
||||||
|
Sub,
|
||||||
|
SubContent,
|
||||||
|
SubTrigger,
|
||||||
|
Trigger
|
||||||
|
};
|
31
client/src/lib/ui/form/context.svelte.ts
Normal file
31
client/src/lib/ui/form/context.svelte.ts
Normal 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;
|
||||||
|
}
|
30
client/src/lib/ui/form/errors.svelte
Normal file
30
client/src/lib/ui/form/errors.svelte
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Violation } from '@bufbuild/protovalidate';
|
||||||
|
import type { ConnectError } from '@connectrpc/connect';
|
||||||
|
import type { WithElementRef, WithoutChildren } from 'bits-ui';
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
import { cn } from '$lib/utils';
|
||||||
|
import { getFormContext } from './context.svelte';
|
||||||
|
|
||||||
|
type Props = WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> & {
|
||||||
|
errors?: Violation[] | ConnectError;
|
||||||
|
};
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
errors = $bindable(),
|
||||||
|
...restProps
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
const item = getFormContext();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={ref} class={cn('text-red text-sm', className)} {...restProps}>
|
||||||
|
{#if errors && Array.isArray(errors)}
|
||||||
|
{#each errors as error (error)}
|
||||||
|
<label for={item?.id}>{error.message}</label>
|
||||||
|
{/each}
|
||||||
|
{:else if errors}
|
||||||
|
<span>{errors.message}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
21
client/src/lib/ui/form/field.svelte
Normal file
21
client/src/lib/ui/form/field.svelte
Normal 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';
|
||||||
|
import { setFormContext } from './context.svelte';
|
||||||
|
|
||||||
|
type Props = WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
|
let { ref = $bindable(null), class: className, name, children, ...restProps }: Props = $props();
|
||||||
|
|
||||||
|
const uid = $props.id();
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
setFormContext(uid, name);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={ref} class={cn('flex flex-col gap-1', className)} {...restProps}>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
5
client/src/lib/ui/form/index.ts
Normal file
5
client/src/lib/ui/form/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import Errors from './errors.svelte';
|
||||||
|
import Field from './field.svelte';
|
||||||
|
import Label from './label.svelte';
|
||||||
|
|
||||||
|
export { Field, Errors, Label };
|
29
client/src/lib/ui/form/label.svelte
Normal file
29
client/src/lib/ui/form/label.svelte
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { WithElementRef } from 'bits-ui';
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
import { cn } from '$lib/utils';
|
||||||
|
import { getFormContext } from './context.svelte';
|
||||||
|
|
||||||
|
type Props = WithElementRef<HTMLAttributes<HTMLLabelElement>>;
|
||||||
|
let { ref = $bindable(null), class: className, children, ...restProps }: Props = $props();
|
||||||
|
|
||||||
|
const item = getFormContext();
|
||||||
|
|
||||||
|
function formatName(name: string) {
|
||||||
|
// Replace _ with spaces
|
||||||
|
name = name.replace('_', ' ');
|
||||||
|
|
||||||
|
// Capitalize first letter
|
||||||
|
name = name.charAt(0).toUpperCase() + name.slice(1);
|
||||||
|
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<label bind:this={ref} class={cn('text-sm', className)} for={item?.id} {...restProps}>
|
||||||
|
{#if children}
|
||||||
|
{@render children()}
|
||||||
|
{:else if item}
|
||||||
|
{formatName(item.name)}
|
||||||
|
{/if}
|
||||||
|
</label>
|
7
client/src/lib/ui/input/index.ts
Normal file
7
client/src/lib/ui/input/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import Root from './input.svelte';
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Input
|
||||||
|
};
|
75
client/src/lib/ui/input/input.svelte
Normal file
75
client/src/lib/ui/input/input.svelte
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { WithElementRef } from 'bits-ui';
|
||||||
|
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from 'svelte/elements';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
import { getFormContext } from '../form/context.svelte';
|
||||||
|
|
||||||
|
type InputType = Exclude<HTMLInputTypeAttribute, 'file'>;
|
||||||
|
|
||||||
|
type Props = WithElementRef<
|
||||||
|
Omit<HTMLInputAttributes, 'type'> &
|
||||||
|
({ type: 'file'; files?: FileList } | { type?: InputType; files?: undefined })
|
||||||
|
>;
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
value = $bindable(),
|
||||||
|
type,
|
||||||
|
files = $bindable(),
|
||||||
|
class: className,
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
...restProps
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
const item = getFormContext();
|
||||||
|
if (item && !id) {
|
||||||
|
id = item.id;
|
||||||
|
}
|
||||||
|
if (item && !name) {
|
||||||
|
name = item.name;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if type === 'file'}
|
||||||
|
<input
|
||||||
|
{id}
|
||||||
|
{name}
|
||||||
|
bind:this={ref}
|
||||||
|
class={cn(
|
||||||
|
'border-surface-1 file:bg-surface hover:border-overlay placeholder:text-subtext text-text flex h-9 w-full min-w-0 cursor-pointer rounded-md border text-sm font-medium shadow-xs transition-all file:mr-2 file:px-3 file:py-2 md:text-sm',
|
||||||
|
|
||||||
|
// Focus
|
||||||
|
'focus-visible:outline-accent focus-visible:outline-2 focus-visible:outline-offset-2',
|
||||||
|
|
||||||
|
// Disabled
|
||||||
|
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
|
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
type="file"
|
||||||
|
bind:files
|
||||||
|
bind:value
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<input
|
||||||
|
{id}
|
||||||
|
{name}
|
||||||
|
bind:this={ref}
|
||||||
|
class={cn(
|
||||||
|
'border-surface-1 hover:border-overlay placeholder:text-subtext text-text flex h-9 w-full min-w-0 rounded-md border px-3 py-1 shadow-xs transition-all md:text-sm',
|
||||||
|
|
||||||
|
// Focus
|
||||||
|
'focus-visible:outline-accent focus-visible:outline-2 focus-visible:outline-offset-2',
|
||||||
|
|
||||||
|
// Disabled
|
||||||
|
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
|
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{type}
|
||||||
|
bind:value
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
{/if}
|
7
client/src/lib/ui/label/index.ts
Normal file
7
client/src/lib/ui/label/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import Root from './label.svelte';
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Label
|
||||||
|
};
|
20
client/src/lib/ui/label/label.svelte
Normal file
20
client/src/lib/ui/label/label.svelte
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
import { Label as LabelPrimitive } from 'bits-ui';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: LabelPrimitive.RootProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
bind:ref
|
||||||
|
data-slot="label"
|
||||||
|
class={cn(
|
||||||
|
'text-text flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
3
client/src/lib/ui/pager/index.ts
Normal file
3
client/src/lib/ui/pager/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import Pager from './pager.svelte';
|
||||||
|
|
||||||
|
export { Pager };
|
49
client/src/lib/ui/pager/pager.svelte
Normal file
49
client/src/lib/ui/pager/pager.svelte
Normal 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>
|
25
client/src/lib/ui/pagination/index.ts
Normal file
25
client/src/lib/ui/pagination/index.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import Content from './pagination-content.svelte';
|
||||||
|
import Ellipsis from './pagination-ellipsis.svelte';
|
||||||
|
import Item from './pagination-item.svelte';
|
||||||
|
import Link from './pagination-link.svelte';
|
||||||
|
import NextButton from './pagination-next-button.svelte';
|
||||||
|
import PrevButton from './pagination-prev-button.svelte';
|
||||||
|
import Root from './pagination.svelte';
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Content,
|
||||||
|
Item,
|
||||||
|
Link,
|
||||||
|
PrevButton,
|
||||||
|
NextButton,
|
||||||
|
Ellipsis,
|
||||||
|
//
|
||||||
|
Root as Pagination,
|
||||||
|
Content as PaginationContent,
|
||||||
|
Item as PaginationItem,
|
||||||
|
Link as PaginationLink,
|
||||||
|
PrevButton as PaginationPrevButton,
|
||||||
|
NextButton as PaginationNextButton,
|
||||||
|
Ellipsis as PaginationEllipsis
|
||||||
|
};
|
21
client/src/lib/ui/pagination/pagination-content.svelte
Normal file
21
client/src/lib/ui/pagination/pagination-content.svelte
Normal 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<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>
|
23
client/src/lib/ui/pagination/pagination-ellipsis.svelte
Normal file
23
client/src/lib/ui/pagination/pagination-ellipsis.svelte
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { WithElementRef, WithoutChildren } from 'bits-ui';
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
import Ellipsis from '@lucide/svelte/icons/ellipsis';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLSpanElement>>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span
|
||||||
|
bind:this={ref}
|
||||||
|
aria-hidden="true"
|
||||||
|
data-slot="pagination-ellipsis"
|
||||||
|
class={cn('text-text flex size-9 items-center justify-center', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
<Ellipsis class="size-4" />
|
||||||
|
<span class="sr-only">More pages</span>
|
||||||
|
</span>
|
14
client/src/lib/ui/pagination/pagination-item.svelte
Normal file
14
client/src/lib/ui/pagination/pagination-item.svelte
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { WithElementRef } from 'bits-ui';
|
||||||
|
import type { HTMLLiAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLLiAttributes> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<li bind:this={ref} data-slot="pagination-item" {...restProps}>
|
||||||
|
{@render children?.()}
|
||||||
|
</li>
|
42
client/src/lib/ui/pagination/pagination-link.svelte
Normal file
42
client/src/lib/ui/pagination/pagination-link.svelte
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Props } from '$lib/ui/button';
|
||||||
|
import { buttonVariants } from '$lib/ui/button';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
import { Pagination as PaginationPrimitive } from 'bits-ui';
|
||||||
|
|
||||||
|
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',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
children={children || Fallback}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
34
client/src/lib/ui/pagination/pagination-next-button.svelte
Normal file
34
client/src/lib/ui/pagination/pagination-next-button.svelte
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { buttonVariants } from '$lib/ui/button/index.js';
|
||||||
|
import ChevronRight from '@lucide/svelte/icons/chevron-right';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
import { Pagination as PaginationPrimitive } from 'bits-ui';
|
||||||
|
|
||||||
|
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}
|
||||||
|
/>
|
33
client/src/lib/ui/pagination/pagination-prev-button.svelte
Normal file
33
client/src/lib/ui/pagination/pagination-prev-button.svelte
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { buttonVariants } from '$lib/ui/button/index.js';
|
||||||
|
import ChevronLeft from '@lucide/svelte/icons/chevron-left';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
import { Pagination as PaginationPrimitive } from 'bits-ui';
|
||||||
|
|
||||||
|
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}
|
||||||
|
/>
|
46
client/src/lib/ui/pagination/pagination.svelte
Normal file
46
client/src/lib/ui/pagination/pagination.svelte
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { pushState } from '$app/navigation';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
import { Pagination as PaginationPrimitive } from 'bits-ui';
|
||||||
|
|
||||||
|
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 object | 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}
|
||||||
|
/>
|
18
client/src/lib/ui/popover/index.ts
Normal file
18
client/src/lib/ui/popover/index.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
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
|
||||||
|
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user