Merge remote-tracking branch 'origin/main' into feat/custom-fields

This commit is contained in:
Elias Schneider
2026-05-31 16:12:20 +02:00
116 changed files with 1023 additions and 565 deletions
+1
View File
@@ -10,6 +10,7 @@ require (
github.com/aws/smithy-go v1.25.1 github.com/aws/smithy-go v1.25.1
github.com/caarlos0/env/v11 v11.4.1 github.com/caarlos0/env/v11 v11.4.1
github.com/cenkalti/backoff/v5 v5.0.3 github.com/cenkalti/backoff/v5 v5.0.3
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec
github.com/disintegration/imaging v1.6.2 github.com/disintegration/imaging v1.6.2
github.com/dunglas/go-urlpattern v0.0.0-20241020164140-716dfa1c80b1 github.com/dunglas/go-urlpattern v0.0.0-20241020164140-716dfa1c80b1
+2
View File
@@ -65,6 +65,8 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+74 -40
View File
@@ -15,6 +15,7 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/coreos/go-systemd/activation"
"github.com/fsnotify/fsnotify" "github.com/fsnotify/fsnotify"
sloggin "github.com/gin-contrib/slog" sloggin "github.com/gin-contrib/slog"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -162,24 +163,26 @@ func initServer(r *gin.Engine) (*serverConfig, error) {
return nil, err return nil, err
} }
network, addr := listenerNetworkAndAddr() var socketFn func() (*socket, error)
listener, err := net.Listen(network, addr) //nolint:noctx switch {
if err != nil { case common.EnvConfig.SystemdSocket:
return nil, fmt.Errorf("failed to create %s listener: %w", network, err) socketFn = systemdSocket
case common.EnvConfig.UnixSocket != "":
socketFn = unixSocket
default:
socketFn = tcpSocket
} }
if err := setUnixSocketMode(network, addr); err != nil { socket, err := socketFn()
listener.Close() if err != nil {
return nil, err return nil, err
} }
return &serverConfig{ addr := socket.addr
addr: addr, listener := socket.listener
certProvider: certProvider, server := newHTTPServer(r, protocols)
listener: listener,
server: newHTTPServer(r, protocols), return &serverConfig{addr, certProvider, listener, server, tlsConfig}, nil
tlsConfig: tlsConfig,
}, nil
} }
func initServerProtocols() (*http.Protocols, *tls.Config, *tlsCertProvider, error) { func initServerProtocols() (*http.Protocols, *tls.Config, *tlsCertProvider, error) {
@@ -207,6 +210,64 @@ func initServerProtocols() (*http.Protocols, *tls.Config, *tlsCertProvider, erro
return protocols, tlsConfig, certProvider, nil return protocols, tlsConfig, certProvider, nil
} }
type socket struct {
addr string
listener net.Listener
}
func systemdSocket() (*socket, error) {
listeners, err := activation.Listeners()
if err != nil {
return nil, fmt.Errorf("failed to receive socket from systemd: %w", err)
}
if len(listeners) == 0 {
return nil, errors.New("did not receive any sockets from systemd")
}
if len(listeners) > 1 {
return nil, errors.New("received too many sockets from systemd")
}
return &socket{"(systemd)", listeners[0]}, nil
}
func unixSocket() (*socket, error) {
addr := common.EnvConfig.UnixSocket
os.Remove(addr) // remove dangling the socket file to avoid file-exist error
listener, err := net.Listen("unix", addr) //nolint:noctx
if err != nil {
return nil, fmt.Errorf("failed to create UNIX socket: %w", err)
}
if common.EnvConfig.UnixSocketMode != "" {
mode, err := strconv.ParseUint(common.EnvConfig.UnixSocketMode, 8, 32)
if err != nil {
listener.Close()
return nil, fmt.Errorf("failed to parse UNIX socket mode '%s': %w", common.EnvConfig.UnixSocketMode, err)
}
if err := os.Chmod(addr, os.FileMode(mode)); err != nil {
listener.Close()
return nil, fmt.Errorf("failed to set UNIX socket mode '%s': %w", common.EnvConfig.UnixSocketMode, err)
}
}
return &socket{addr, listener}, nil
}
func tcpSocket() (*socket, error) {
addr := net.JoinHostPort(common.EnvConfig.Host, common.EnvConfig.Port)
listener, err := net.Listen("tcp", addr) //nolint:noctx
if err != nil {
return nil, fmt.Errorf("failed to create TCP socket: %w", err)
}
return &socket{addr, listener}, nil
}
func newHTTPServer(r *gin.Engine, protocols *http.Protocols) *http.Server { func newHTTPServer(r *gin.Engine, protocols *http.Protocols) *http.Server {
return &http.Server{ return &http.Server{
MaxHeaderBytes: 1 << 20, MaxHeaderBytes: 1 << 20,
@@ -226,33 +287,6 @@ func newHTTPServer(r *gin.Engine, protocols *http.Protocols) *http.Server {
} }
} }
func listenerNetworkAndAddr() (string, string) {
if common.EnvConfig.UnixSocket == "" {
return "tcp", net.JoinHostPort(common.EnvConfig.Host, common.EnvConfig.Port)
}
addr := common.EnvConfig.UnixSocket
os.Remove(addr) // remove dangling the socket file to avoid file-exist error
return "unix", addr
}
func setUnixSocketMode(network, addr string) error {
if network != "unix" || common.EnvConfig.UnixSocketMode == "" {
return nil
}
mode, err := strconv.ParseUint(common.EnvConfig.UnixSocketMode, 8, 32)
if err != nil {
return fmt.Errorf("failed to parse UNIX socket mode '%s': %w", common.EnvConfig.UnixSocketMode, err)
}
if err := os.Chmod(addr, os.FileMode(mode)); err != nil {
return fmt.Errorf("failed to set UNIX socket mode '%s': %w", common.EnvConfig.UnixSocketMode, err)
}
return nil
}
func runServer(ctx context.Context, config *serverConfig) error { func runServer(ctx context.Context, config *serverConfig) error {
slog.Info("Server listening", slog.String("addr", config.addr), slog.Bool("tls", config.tlsConfig != nil)) slog.Info("Server listening", slog.String("addr", config.addr), slog.Bool("tls", config.tlsConfig != nil))
+55 -32
View File
@@ -2,9 +2,12 @@ package cmds
import ( import (
"context" "context"
"fmt"
"log/slog" "log/slog"
"net"
"net/http" "net/http"
"os" "os"
"strings"
"time" "time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -13,8 +16,14 @@ import (
) )
type healthcheckFlags struct { type healthcheckFlags struct {
Endpoint string Endpoint string
Verbose bool UnixSocket string
Verbose bool
}
type healthcheckResult struct {
StatusCode int
URL string
} }
func init() { func init() {
@@ -29,47 +38,26 @@ func init() {
ctx, cancel := context.WithTimeout(cmd.Context(), 5*time.Second) ctx, cancel := context.WithTimeout(cmd.Context(), 5*time.Second)
defer cancel() defer cancel()
url := flags.Endpoint + "/healthz" if flags.UnixSocket == "" && !cmd.Flags().Changed("endpoint") {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) flags.UnixSocket = common.EnvConfig.UnixSocket
}
result, err := healthcheck(ctx, flags)
if err != nil { if err != nil {
slog.ErrorContext(ctx, slog.ErrorContext(ctx,
"Failed to create request object", "Healthcheck failed",
"error", err, "error", err,
"url", url,
"ms", time.Since(start).Milliseconds(), "ms", time.Since(start).Milliseconds(),
) )
os.Exit(1) os.Exit(1)
} }
res, err := http.DefaultClient.Do(req)
if err != nil {
slog.ErrorContext(ctx,
"Failed to perform request",
"error", err,
"url", url,
"ms", time.Since(start).Milliseconds(),
)
os.Exit(1)
}
defer res.Body.Close()
if res.StatusCode < 200 || res.StatusCode >= 300 {
if err != nil {
slog.ErrorContext(ctx,
"Healthcheck failed",
"status", res.StatusCode,
"url", url,
"ms", time.Since(start).Milliseconds(),
)
os.Exit(1)
}
}
if flags.Verbose { if flags.Verbose {
slog.InfoContext(ctx, slog.InfoContext(ctx,
"Healthcheck succeeded", "Healthcheck succeeded",
"status", res.StatusCode, "status", result.StatusCode,
"url", url, "url", result.URL,
"unixSocket", flags.UnixSocket,
"ms", time.Since(start).Milliseconds(), "ms", time.Since(start).Milliseconds(),
) )
} }
@@ -77,7 +65,42 @@ func init() {
} }
healthcheckCmd.Flags().StringVarP(&flags.Endpoint, "endpoint", "e", "http://localhost:"+common.EnvConfig.Port, "Endpoint for Pocket ID") healthcheckCmd.Flags().StringVarP(&flags.Endpoint, "endpoint", "e", "http://localhost:"+common.EnvConfig.Port, "Endpoint for Pocket ID")
healthcheckCmd.Flags().StringVar(&flags.UnixSocket, "unix-socket", "", "UNIX socket path for Pocket ID")
healthcheckCmd.Flags().BoolVarP(&flags.Verbose, "verbose", "v", false, "Enable verbose mode") healthcheckCmd.Flags().BoolVarP(&flags.Verbose, "verbose", "v", false, "Enable verbose mode")
rootCmd.AddCommand(healthcheckCmd) rootCmd.AddCommand(healthcheckCmd)
} }
func healthcheck(ctx context.Context, flags healthcheckFlags) (*healthcheckResult, error) {
url := strings.TrimRight(flags.Endpoint, "/") + "/healthz"
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request object for %q: %w", url, err)
}
client := http.DefaultClient
if flags.UnixSocket != "" {
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.Proxy = nil
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
dialer := net.Dialer{}
return dialer.DialContext(ctx, "unix", flags.UnixSocket)
}
client = &http.Client{Transport: transport}
}
res, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to perform request to %q: %w", url, err)
}
defer res.Body.Close()
if res.StatusCode < 200 || res.StatusCode >= 300 {
return nil, fmt.Errorf("unexpected status %d from %q", res.StatusCode, url)
}
return &healthcheckResult{
StatusCode: res.StatusCode,
URL: url,
}, nil
}
+79
View File
@@ -0,0 +1,79 @@
package cmds
import (
"context"
"net"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestHealthcheckTCPSuccess(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/healthz", r.URL.Path)
w.WriteHeader(http.StatusNoContent)
}))
defer server.Close()
result, err := healthcheck(t.Context(), healthcheckFlags{
Endpoint: server.URL,
})
require.NoError(t, err)
require.Equal(t, http.StatusNoContent, result.StatusCode)
require.Equal(t, server.URL+"/healthz", result.URL)
}
func TestHealthcheckFailsOnUnexpectedStatus(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer server.Close()
_, err := healthcheck(t.Context(), healthcheckFlags{
Endpoint: server.URL,
})
require.Error(t, err)
require.ErrorContains(t, err, "unexpected status 500")
}
func TestHealthcheckUnixSocket(t *testing.T) {
socketPath := filepath.Join(t.TempDir(), "pocket-id.sock")
listener, err := (&net.ListenConfig{}).Listen(t.Context(), "unix", socketPath)
require.NoError(t, err)
server := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/healthz", r.URL.Path)
w.WriteHeader(http.StatusNoContent)
}),
ReadHeaderTimeout: time.Second,
}
errCh := make(chan error, 1)
go func() {
errCh <- server.Serve(listener)
}()
t.Cleanup(func() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
require.NoError(t, server.Shutdown(ctx))
require.ErrorIs(t, <-errCh, http.ErrServerClosed)
})
result, err := healthcheck(t.Context(), healthcheckFlags{
Endpoint: "http://localhost:1411",
UnixSocket: socketPath,
})
require.NoError(t, err)
require.Equal(t, http.StatusNoContent, result.StatusCode)
require.Equal(t, "http://localhost:1411/healthz", result.URL)
}
+4
View File
@@ -68,6 +68,7 @@ type EnvConfigSchema struct {
Host string `env:"HOST" options:"toLower"` Host string `env:"HOST" options:"toLower"`
UnixSocket string `env:"UNIX_SOCKET"` UnixSocket string `env:"UNIX_SOCKET"`
UnixSocketMode string `env:"UNIX_SOCKET_MODE"` UnixSocketMode string `env:"UNIX_SOCKET_MODE"`
SystemdSocket bool `env:"SYSTEMD_SOCKET"`
LocalIPv6Ranges string `env:"LOCAL_IPV6_RANGES"` LocalIPv6Ranges string `env:"LOCAL_IPV6_RANGES"`
TLSCertFile string `env:"TLS_CERT" options:"file"` TLSCertFile string `env:"TLS_CERT" options:"file"`
@@ -153,6 +154,9 @@ func ValidateEnvConfig(config *EnvConfigSchema) error {
if err := validateFileBackend(config); err != nil { if err := validateFileBackend(config); err != nil {
return err return err
} }
if config.SystemdSocket && config.UnixSocket != "" {
return errors.New("SYSTEMD_SOCKET and UNIX_SOCKET are mutually exclusive")
}
if err := validateLocalIPv6Ranges(config.LocalIPv6Ranges); err != nil { if err := validateLocalIPv6Ranges(config.LocalIPv6Ranges); err != nil {
return err return err
} }
+2 -2
View File
@@ -124,11 +124,11 @@ func ValidateCallbackURLPattern(raw string) bool {
} }
// ValidateResponseMode validates response_mode parameter // ValidateResponseMode validates response_mode parameter
// If responseMode is present, it must be "form_post" or "query" // If responseMode is present, it must be "form_post", "query", or "fragment"
// Empty responseMode is allowed (field not provided, use default) // Empty responseMode is allowed (field not provided, use default)
func ValidateResponseMode(responseMode string) bool { func ValidateResponseMode(responseMode string) bool {
switch responseMode { switch responseMode {
case "form_post", "query": case "form_post", "query", "fragment":
return true return true
case "": case "":
return true return true
+2 -1
View File
@@ -102,8 +102,9 @@ func TestValidateResponseMode(t *testing.T) {
}{ }{
{"valid form_post", "form_post", true}, {"valid form_post", "form_post", true},
{"valid query", "query", true}, {"valid query", "query", true},
{"valid fragment", "fragment", true},
{"valid empty", "", true}, {"valid empty", "", true},
{"invalid fragment", "fragment", false}, {"invalid unknown", "unknown", false},
} }
for _, tt := range tests { for _, tt := range tests {
@@ -54,7 +54,7 @@ func CreateDefaultProfilePicture(initials string) (*bytes.Buffer, error) {
img := imaging.New(profilePictureSize, profilePictureSize, color.RGBA{R: 255, G: 255, B: 255, A: 255}) img := imaging.New(profilePictureSize, profilePictureSize, color.RGBA{R: 255, G: 255, B: 255, A: 255})
// Load the font // Load the font
fontBytes, err := resources.FS.ReadFile("fonts/PlayfairDisplay-Bold.ttf") fontBytes, err := resources.FS.ReadFile("fonts/Gloock.ttf")
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to read font file: %w", err) return nil, fmt.Errorf("failed to read font file: %w", err)
} }
@@ -84,7 +84,7 @@ func CreateDefaultProfilePicture(initials string) (*bytes.Buffer, error) {
// Center the initials // Center the initials
x := (profilePictureSize - font.MeasureString(face, initials).Ceil()) / 2 x := (profilePictureSize - font.MeasureString(face, initials).Ceil()) / 2
y := (profilePictureSize-face.Metrics().Height.Ceil())/2 + face.Metrics().Ascent.Ceil() - 10 y := (profilePictureSize-face.Metrics().Height.Ceil())/2 + face.Metrics().Ascent.Ceil()
drawer.Dot = fixed.P(x, y) drawer.Dot = fixed.P(x, y)
// Draw the initials // Draw the initials
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -249,7 +249,7 @@
"edit": "Düzenle", "edit": "Düzenle",
"user_groups_updated_successfully": "Kullanıcı grupları başarıyla güncellendi", "user_groups_updated_successfully": "Kullanıcı grupları başarıyla güncellendi",
"user_updated_successfully": "Kullanıcı başarıyla güncellendi", "user_updated_successfully": "Kullanıcı başarıyla güncellendi",
"custom_claims_updated_successfully": "Özel alanlar başarıyla güncellendi", "custom_claims_updated_successfully": "Özel yetkiler başarıyla güncellendi",
"back": "Geri", "back": "Geri",
"user_details_firstname_lastname": "Kullanıcı Detayları {firstName} {lastName}", "user_details_firstname_lastname": "Kullanıcı Detayları {firstName} {lastName}",
"manage_which_groups_this_user_belongs_to": "Bu kullanıcının ait olduğu grupları yönetin.", "manage_which_groups_this_user_belongs_to": "Bu kullanıcının ait olduğu grupları yönetin.",
+63 -62
View File
@@ -1,64 +1,65 @@
{ {
"name": "pocket-id-frontend", "name": "pocket-id-frontend",
"version": "2.7.0", "version": "2.7.0",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"preinstall": "npx only-allow pnpm", "preinstall": "npx only-allow pnpm",
"dev": "vite dev --port 3000", "dev": "vite dev --port 3000",
"build": "vite build", "build": "vite build",
"preview": "vite preview --port 3000", "preview": "vite preview --port 3000",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . && eslint .", "lint": "prettier --check . && eslint .",
"format": "prettier --write ." "format": "prettier --write ."
}, },
"dependencies": { "dependencies": {
"@simplewebauthn/browser": "^13.3.0", "@fontsource/gloock": "^5.2.8",
"@tailwindcss/vite": "^4.3.0", "@simplewebauthn/browser": "^13.3.0",
"axios": "^1.16.1", "@tailwindcss/vite": "^4.3.0",
"clsx": "^2.1.1", "axios": "^1.16.1",
"date-fns": "^4.2.1", "clsx": "^2.1.1",
"jose": "^6.2.3", "date-fns": "^4.2.1",
"qrcode": "^1.5.4", "jose": "^6.2.3",
"runed": "^0.37.1", "qrcode": "^1.5.4",
"sveltekit-superforms": "^2.30.1", "runed": "^0.37.1",
"tailwind-merge": "^3.6.0", "sveltekit-superforms": "^2.30.1",
"zod": "^4.4.3" "tailwind-merge": "^3.6.0",
}, "zod": "^4.4.3"
"devDependencies": { },
"@inlang/paraglide-js": "^2.18.0", "devDependencies": {
"@inlang/plugin-m-function-matcher": "^2.2.6", "@inlang/paraglide-js": "^2.18.0",
"@inlang/plugin-message-format": "^4.4.0", "@inlang/plugin-m-function-matcher": "^2.2.6",
"@internationalized/date": "^3.12.1", "@inlang/plugin-message-format": "^4.4.0",
"@lucide/svelte": "^1.16.0", "@internationalized/date": "^3.12.1",
"@sveltejs/adapter-static": "^3.0.10", "@lucide/svelte": "^1.16.0",
"@sveltejs/kit": "^2.60.1", "@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/vite-plugin-svelte": "^7.1.2", "@sveltejs/kit": "^2.60.1",
"@types/eslint": "^9.6.1", "@sveltejs/vite-plugin-svelte": "^7.1.2",
"@types/node": "^25.9.0", "@types/eslint": "^9.6.1",
"@types/qrcode": "^1.5.6", "@types/node": "^25.9.0",
"bits-ui": "^2.18.1", "@types/qrcode": "^1.5.6",
"eslint": "^10.4.0", "bits-ui": "^2.18.1",
"eslint-config-prettier": "^10.1.8", "eslint": "^10.4.0",
"eslint-plugin-svelte": "^3.17.1", "eslint-config-prettier": "^10.1.8",
"formsnap": "^2.0.1", "eslint-plugin-svelte": "^3.17.1",
"globals": "^17.6.0", "formsnap": "^2.0.1",
"mode-watcher": "^1.1.0", "globals": "^17.6.0",
"prettier": "^3.8.3", "mode-watcher": "^1.1.0",
"prettier-plugin-svelte": "^3.5.2", "prettier": "^3.8.3",
"prettier-plugin-tailwindcss": "^0.8.0", "prettier-plugin-svelte": "^3.5.2",
"rollup": "^4.60.4", "prettier-plugin-tailwindcss": "^0.8.0",
"svelte": "^5.55.8", "rollup": "^4.60.4",
"svelte-check": "^4.4.8", "svelte": "^5.55.8",
"svelte-sonner": "^1.1.1", "svelte-check": "^4.4.8",
"tailwind-variants": "^3.2.2", "svelte-sonner": "^1.1.1",
"tailwindcss": "^4.3.0", "tailwind-variants": "^3.2.2",
"tslib": "^2.8.1", "tailwindcss": "^4.3.0",
"tw-animate-css": "^1.4.0", "tslib": "^2.8.1",
"typescript": "^6.0.3", "tw-animate-css": "^1.4.0",
"typescript-eslint": "^8.59.4", "typescript": "^6.0.3",
"vite": "^8.0.13", "typescript-eslint": "^8.59.4",
"vite-plugin-compression": "^0.5.1" "vite": "^8.0.13",
} "vite-plugin-compression": "^0.5.1"
}
} }
+6 -61
View File
@@ -1,5 +1,6 @@
@import 'tailwindcss'; @import 'tailwindcss';
@import 'tw-animate-css'; @import 'tw-animate-css';
@import '@fontsource/gloock';
@variant dark (&:where(.dark, .dark *)); @variant dark (&:where(.dark, .dark *));
@@ -22,7 +23,7 @@
} }
:root { :root {
--radius: 0.625rem; --radius: 1rem;
--background: oklch(1 0 0); --background: oklch(1 0 0);
--foreground: oklch(0.145 0 0); --foreground: oklch(0.145 0 0);
--card: oklch(1 0 0); --card: oklch(1 0 0);
@@ -133,13 +134,8 @@
--color-sidebar-border: var(--sidebar-border); --color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring); --color-sidebar-ring: var(--sidebar-ring);
/* Animations */
--animate-accordion-up: accordion-up 0.2s ease-out;
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-caret-blink: caret-blink 1.25s ease-out infinite;
/* Font */ /* Font */
--font-playfair: 'Playfair Display', serif; --font-glooock: 'glooock ', serif;
} }
@layer base { @layer base {
@@ -154,60 +150,6 @@
button { button {
@apply cursor-pointer; @apply cursor-pointer;
} }
@font-face {
font-family: 'Playfair Display';
font-weight: 400;
src: url('/fonts/PlayfairDisplay-Regular.woff') format('woff');
}
@font-face {
font-family: 'Playfair Display';
font-weight: 500;
src: url('/fonts/PlayfairDisplay-Medium.woff') format('woff');
}
@font-face {
font-family: 'Playfair Display';
font-weight: 600;
src: url('/fonts/PlayfairDisplay-SemiBold.woff') format('woff');
}
@font-face {
font-family: 'Playfair Display';
font-weight: 700;
src: url('/fonts/PlayfairDisplay-Bold.woff') format('woff');
}
}
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--bits-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--bits-accordion-content-height);
}
to {
height: 0;
}
}
@keyframes caret-blink {
0%,
70%,
100% {
opacity: 1;
}
20%,
50% {
opacity: 0;
}
} }
.animate-fade-in { .animate-fade-in {
@@ -220,6 +162,7 @@
opacity: 0; opacity: 0;
transform: translateY(10px); transform: translateY(10px);
} }
to { to {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
@@ -230,6 +173,7 @@
0% { 0% {
transform: scale(1.3); transform: scale(1.3);
} }
100% { 100% {
transform: scale(1); transform: scale(1);
} }
@@ -246,6 +190,7 @@
40% { 40% {
opacity: 0; opacity: 0;
} }
100% { 100% {
opacity: 1; opacity: 1;
} }
@@ -81,18 +81,20 @@
variant="outline" variant="outline"
role="combobox" role="combobox"
aria-expanded={open} aria-expanded={open}
class="h-auto min-h-10 w-full justify-between" class="h-auto min-h-10 w-full bg-input/30!"
> >
<div class="flex flex-wrap items-center gap-1"> <div class="flex items-center justify-between w-full">
{#if selectedItems.length > 0} <div class="flex flex-wrap items-center gap-1">
{#each selectedLabels as label} {#if selectedItems.length > 0}
<Badge variant="secondary">{label}</Badge> {#each selectedLabels as label}
{/each} <Badge variant="secondary">{label}</Badge>
{:else} {/each}
<span class="text-muted-foreground font-normal">{m.select_items()}</span> {:else}
{/if} <span class="text-muted-foreground font-normal">{m.select_items()}</span>
{/if}
</div>
<LucideChevronDown class="ml-2 size-4 shrink-0 opacity-50" />
</div> </div>
<LucideChevronDown class="ml-2 size-4 shrink-0 opacity-50" />
</Button> </Button>
{/snippet} {/snippet}
</Popover.Trigger> </Popover.Trigger>
@@ -70,10 +70,12 @@
role="combobox" role="combobox"
aria-expanded={open} aria-expanded={open}
{...props} {...props}
class={cn('justify-between', restProps.class)} class={restProps.class}
> >
{items.find((item) => item.value === value)?.label || selectText} <span class="flex justify-between w-full">
<LucideChevronDown class="ml-2 size-4 shrink-0 opacity-50" /> {items.find((item) => item.value === value)?.label || selectText}
<LucideChevronDown class="size-4 shrink-0 opacity-50" />
</span>
</Button> </Button>
{/snippet} {/snippet}
</Popover.Trigger> </Popover.Trigger>
@@ -1,8 +1,9 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/state'; import { page } from '$app/state';
import appConfigStore from '$lib/stores/application-configuration-store'; import { m } from '$lib/paraglide/messages';
import userStore from '$lib/stores/user-store'; import userStore from '$lib/stores/user-store';
import Logo from '../logo.svelte'; import Logo from '../logo.svelte';
import Separator from '../ui/separator/separator.svelte';
import HeaderAvatar from './header-avatar.svelte'; import HeaderAvatar from './header-avatar.svelte';
import ModeSwitcher from './mode-switcher.svelte'; import ModeSwitcher from './mode-switcher.svelte';
@@ -19,7 +20,11 @@
); );
</script> </script>
<div class=" w-full {isAuthPage ? 'absolute top-0 z-10 mt-3 lg:mt-8 pr-2 lg:pr-3' : 'border-b'}"> <div
class=" w-full {isAuthPage
? 'absolute top-0 z-10 mt-3 lg:mt-8 pr-2 lg:pr-3'
: 'pt-3 bg-muted/40 dark:bg-background '}"
>
<div <div
class="{!isAuthPage class="{!isAuthPage
? 'max-w-410' ? 'max-w-410'
@@ -27,10 +32,11 @@
> >
<div class="flex h-16 items-center"> <div class="flex h-16 items-center">
{#if !isAuthPage} {#if !isAuthPage}
<a href="/" class="flex items-center gap-3 transition-opacity hover:opacity-80"> <a href="/" class="flex items-center transition-opacity hover:opacity-80">
<Logo class="size-8" /> <Logo class="size-8" />
<h1 class="text-lg font-semibold tracking-tight" data-testid="application-name"> <Separator orientation="vertical" class="h-5! bg-neutral-600 ml-2 mr-3" />
{$appConfigStore.appName} <h1 class="text-2xl font-glooock" data-testid="application-name">
{m.settings()}
</h1> </h1>
</a> </a>
{/if} {/if}
@@ -23,7 +23,7 @@
</script> </script>
<Item.Root variant="transparent" class="hover:bg-muted transition-colors py-3 px-0 sm:px-4"> <Item.Root variant="transparent" class="hover:bg-muted transition-colors py-3 px-0 sm:px-4">
<Item.Media class="bg-primary/10 text-primary rounded-lg p-2"> <Item.Media class="bg-primary/10 text-primary rounded-full p-3">
{#if icon}{@const Icon = icon} {#if icon}{@const Icon = icon}
<Icon class="size-5" /> <Icon class="size-5" />
{/if} {/if}
@@ -9,7 +9,7 @@
value, value,
size = 200, size = 200,
color = '#000000', color = '#000000',
backgroundColor = '#FFFFFF', backgroundColor = 'transparent',
...restProps ...restProps
}: HTMLAttributes<HTMLCanvasElement> & { }: HTMLAttributes<HTMLCanvasElement> & {
value: string | null; value: string | null;
@@ -39,4 +39,4 @@
}); });
</script> </script>
<canvas {...restProps} bind:this={canvasEl} class={cn('rounded-lg', restProps.class)}></canvas> <canvas {...restProps} bind:this={canvasEl} class={cn('rounded', restProps.class)}></canvas>
@@ -187,7 +187,6 @@
value={signupLink} value={signupLink}
size={180} size={180}
color={mode.current === 'dark' ? '#FFFFFF' : '#000000'} color={mode.current === 'dark' ? '#FFFFFF' : '#000000'}
backgroundColor={mode.current === 'dark' ? '#000000' : '#FFFFFF'}
/> />
<CopyToClipboard value={signupLink!}> <CopyToClipboard value={signupLink!}>
<p data-testId="signup-token-link" class="px-2 text-center text-sm break-all"> <p data-testId="signup-token-link" class="px-2 text-center text-sm break-all">
@@ -16,7 +16,7 @@
class={buttonVariants({ class={buttonVariants({
variant: 'outline', variant: 'outline',
size: 'sm', size: 'sm',
class: 'ml-auto h-8' class: 'ml-auto'
})} })}
> >
<Settings2Icon /> <Settings2Icon />
@@ -38,11 +38,13 @@
{...props} {...props}
variant="outline" variant="outline"
size="sm" size="sm"
class="h-8 border-dashed" class="order-dashed"
data-testid={`facet-${title.toLowerCase()}-trigger`} data-testid={`facet-${title.toLowerCase()}-trigger`}
> >
<ListFilterIcon /> <span class="flex gap-2 items-center">
{title} <ListFilterIcon />
{title}
</span>
{#if selectedValues.size > 0} {#if selectedValues.size > 0}
<Separator orientation="vertical" class="mx-2 h-4" /> <Separator orientation="vertical" class="mx-2 h-4" />
<Badge variant="secondary" class="rounded-sm px-1 font-normal lg:hidden"> <Badge variant="secondary" class="rounded-sm px-1 font-normal lg:hidden">
@@ -243,7 +243,7 @@
class="h-12 w-full justify-start px-4 font-medium hover:bg-transparent" class="h-12 w-full justify-start px-4 font-medium hover:bg-transparent"
onclick={() => onSort(column.column)} onclick={() => onSort(column.column)}
> >
<span class="flex items-center"> <span class="flex items-center w-full">
{column.label} {column.label}
<ChevronDown <ChevronDown
class={cn( class={cn(
@@ -314,9 +314,7 @@
<DropdownMenu.Item <DropdownMenu.Item
onclick={() => action.onClick(item)} onclick={() => action.onClick(item)}
disabled={action.disabled} disabled={action.disabled}
class={action.variant === 'danger' class={action.variant === 'danger' ? 'text-red-500!' : ''}
? 'text-red-500 focus:!text-red-700'
: ''}
> >
{#if action.icon} {#if action.icon}
{@const Icon = action.icon} {@const Icon = action.icon}
@@ -1,18 +1,27 @@
<script lang="ts"> <script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui'; import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
import { buttonVariants } from '$lib/components/ui/button/index.js'; import {
buttonVariants,
type ButtonVariant,
type ButtonSize
} from '$lib/components/ui/button/index.js';
import { cn } from '$lib/utils/style.js'; import { cn } from '$lib/utils/style.js';
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
variant = 'default',
size = 'default',
...restProps ...restProps
}: AlertDialogPrimitive.ActionProps = $props(); }: AlertDialogPrimitive.ActionProps & {
variant?: ButtonVariant;
size?: ButtonSize;
} = $props();
</script> </script>
<AlertDialogPrimitive.Action <AlertDialogPrimitive.Action
bind:ref bind:ref
data-slot="alert-dialog-action" data-slot="alert-dialog-action"
class={cn(buttonVariants(), className)} class={cn(buttonVariants({ variant, size }), 'cn-alert-dialog-action', className)}
{...restProps} {...restProps}
/> />
@@ -1,18 +1,27 @@
<script lang="ts"> <script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui'; import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
import { buttonVariants } from '$lib/components/ui/button/index.js'; import {
buttonVariants,
type ButtonVariant,
type ButtonSize
} from '$lib/components/ui/button/index.js';
import { cn } from '$lib/utils/style.js'; import { cn } from '$lib/utils/style.js';
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
variant = 'outline',
size = 'default',
...restProps ...restProps
}: AlertDialogPrimitive.CancelProps = $props(); }: AlertDialogPrimitive.CancelProps & {
variant?: ButtonVariant;
size?: ButtonSize;
} = $props();
</script> </script>
<AlertDialogPrimitive.Cancel <AlertDialogPrimitive.Cancel
bind:ref bind:ref
data-slot="alert-dialog-cancel" data-slot="alert-dialog-cancel"
class={cn(buttonVariants({ variant: 'outline' }), className)} class={cn(buttonVariants({ variant, size }), 'cn-alert-dialog-cancel', className)}
{...restProps} {...restProps}
/> />
@@ -1,27 +1,32 @@
<script lang="ts"> <script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui'; import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
import AlertDialogPortal from './alert-dialog-portal.svelte';
import AlertDialogOverlay from './alert-dialog-overlay.svelte'; import AlertDialogOverlay from './alert-dialog-overlay.svelte';
import { cn, type WithoutChild, type WithoutChildrenOrChild } from '$lib/utils/style.js'; import { cn, type WithoutChild, type WithoutChildrenOrChild } from '$lib/utils/style.js';
import type { ComponentProps } from 'svelte';
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
size = 'default',
portalProps, portalProps,
...restProps ...restProps
}: WithoutChild<AlertDialogPrimitive.ContentProps> & { }: WithoutChild<AlertDialogPrimitive.ContentProps> & {
portalProps?: WithoutChildrenOrChild<AlertDialogPrimitive.PortalProps>; size?: 'default' | 'sm';
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof AlertDialogPortal>>;
} = $props(); } = $props();
</script> </script>
<AlertDialogPrimitive.Portal {...portalProps}> <AlertDialogPortal {...portalProps}>
<AlertDialogOverlay /> <AlertDialogOverlay />
<AlertDialogPrimitive.Content <AlertDialogPrimitive.Content
bind:ref bind:ref
data-slot="alert-dialog-content" data-slot="alert-dialog-content"
data-size={size}
class={cn( class={cn(
'bg-background 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 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 duration-200 sm:max-w-lg', 'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 bg-popover text-popover-foreground ring-foreground/10 gap-4 rounded-4xl p-5 sm:p-8 sm:pb-6 ring-1 duration-100 data-[size=default]:max-w-sm data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-md group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 outline-none',
className className
)} )}
{...restProps} {...restProps}
/> />
</AlertDialogPrimitive.Portal> </AlertDialogPortal>
@@ -12,6 +12,9 @@
<AlertDialogPrimitive.Description <AlertDialogPrimitive.Description
bind:ref bind:ref
data-slot="alert-dialog-description" data-slot="alert-dialog-description"
class={cn('text-muted-foreground text-sm', className)} class={cn(
'text-muted-foreground *:[a]:hover:text-foreground text-sm text-balance md:text-pretty *:[a]:underline *:[a]:underline-offset-3',
className
)}
{...restProps} {...restProps}
/> />
@@ -13,7 +13,10 @@
<div <div
bind:this={ref} bind:this={ref}
data-slot="alert-dialog-footer" data-slot="alert-dialog-footer"
class={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)} class={cn(
'-mx-4 -mb-4 rounded-b-xl p-4 flex flex-col-reverse gap-2 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end',
className
)}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
@@ -13,7 +13,10 @@
<div <div
bind:this={ref} bind:this={ref}
data-slot="alert-dialog-header" data-slot="alert-dialog-header"
class={cn('flex flex-col gap-2 text-center sm:text-left', className)} class={cn(
'grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]',
className
)}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
@@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn, type WithElementRef } from '$lib/utils/style.js';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="alert-dialog-media"
class={cn(
"bg-muted mb-2 inline-flex size-10 items-center justify-center rounded-md sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-6",
className
)}
{...restProps}
>
{@render children?.()}
</div>
@@ -13,7 +13,7 @@
bind:ref bind:ref
data-slot="alert-dialog-overlay" data-slot="alert-dialog-overlay"
class={cn( class={cn(
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50', 'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 z-50',
className className
)} )}
{...restProps} {...restProps}
@@ -0,0 +1,7 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
let { ...restProps }: AlertDialogPrimitive.PortalProps = $props();
</script>
<AlertDialogPrimitive.Portal {...restProps} />
@@ -12,6 +12,9 @@
<AlertDialogPrimitive.Title <AlertDialogPrimitive.Title
bind:ref bind:ref
data-slot="alert-dialog-title" data-slot="alert-dialog-title"
class={cn('text-lg font-semibold', className)} class={cn(
'text-base font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2',
className
)}
{...restProps} {...restProps}
/> />
@@ -0,0 +1,7 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
let { open = $bindable(false), ...restProps }: AlertDialogPrimitive.RootProps = $props();
</script>
<AlertDialogPrimitive.Root bind:open {...restProps} />
@@ -1,4 +1,5 @@
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui'; import Root from './alert-dialog.svelte';
import Portal from './alert-dialog-portal.svelte';
import Trigger from './alert-dialog-trigger.svelte'; import Trigger from './alert-dialog-trigger.svelte';
import Title from './alert-dialog-title.svelte'; import Title from './alert-dialog-title.svelte';
import Action from './alert-dialog-action.svelte'; import Action from './alert-dialog-action.svelte';
@@ -8,29 +9,32 @@ import Header from './alert-dialog-header.svelte';
import Overlay from './alert-dialog-overlay.svelte'; import Overlay from './alert-dialog-overlay.svelte';
import Content from './alert-dialog-content.svelte'; import Content from './alert-dialog-content.svelte';
import Description from './alert-dialog-description.svelte'; import Description from './alert-dialog-description.svelte';
import Media from './alert-dialog-media.svelte';
const Root = AlertDialogPrimitive.Root;
export { export {
Root, Root,
Title, Title,
Action, Action,
Cancel, Cancel,
Portal,
Footer, Footer,
Header, Header,
Trigger, Trigger,
Overlay, Overlay,
Content, Content,
Description, Description,
Media,
// //
Root as AlertDialog, Root as AlertDialog,
Title as AlertDialogTitle, Title as AlertDialogTitle,
Action as AlertDialogAction, Action as AlertDialogAction,
Cancel as AlertDialogCancel, Cancel as AlertDialogCancel,
Portal as AlertDialogPortal,
Footer as AlertDialogFooter, Footer as AlertDialogFooter,
Header as AlertDialogHeader, Header as AlertDialogHeader,
Trigger as AlertDialogTrigger, Trigger as AlertDialogTrigger,
Overlay as AlertDialogOverlay, Overlay as AlertDialogOverlay,
Content as AlertDialogContent, Content as AlertDialogContent,
Description as AlertDialogDescription Description as AlertDialogDescription,
Media as AlertDialogMedia
}; };
@@ -2,7 +2,7 @@
import { tv, type VariantProps } from 'tailwind-variants'; import { tv, type VariantProps } from 'tailwind-variants';
export const alertVariants = tv({ export const alertVariants = tv({
base: 'relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current', base: 'relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-3xl border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',
variants: { variants: {
variant: { variant: {
default: 'bg-card text-card-foreground', default: 'bg-card text-card-foreground',
@@ -2,15 +2,16 @@
import { type VariantProps, tv } from 'tailwind-variants'; import { type VariantProps, tv } from 'tailwind-variants';
export const badgeVariants = tv({ export const badgeVariants = tv({
base: 'focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-md border px-2 py-0.5 text-xs font-medium transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3', base: 'h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive group/badge inline-flex w-fit shrink-0 items-center justify-center overflow-hidden whitespace-nowrap transition-colors focus-visible:ring-[3px] [&>svg]:pointer-events-none',
variants: { variants: {
variant: { variant: {
default: 'bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent', default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
secondary: secondary: 'bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80',
'bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent',
destructive: destructive:
'bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white', 'bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20',
outline: 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground' outline: 'border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground',
ghost: 'hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50',
link: 'text-primary underline-offset-4 hover:underline'
} }
}, },
defaultVariants: { defaultVariants: {
@@ -4,22 +4,24 @@
import { tv, type VariantProps } from 'tailwind-variants'; import { tv, type VariantProps } from 'tailwind-variants';
export const buttonVariants = tv({ export const buttonVariants = tv({
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0", base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-full text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:shrink-0 transition active:scale-95",
variants: { variants: {
variant: { variant: {
default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90', default: 'bg-primary text-primary-foreground hover:bg-primary/80',
destructive:
'bg-destructive shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white',
outline: outline:
'bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border', 'border border-border bg-background hover:bg-muted hover:text-foreground dark:hover:bg-input/30 aria-expanded:bg-muted aria-expanded:text-foreground dark:bg-transparent',
secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80', secondary:
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', 'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
ghost:
'hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground',
destructive:
'bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30',
link: 'text-primary underline-offset-4 hover:underline' link: 'text-primary underline-offset-4 hover:underline'
}, },
size: { size: {
default: 'h-10 px-4 py-2', default: 'h-10 px-4 py-2',
sm: 'h-8 rounded-md px-3', sm: 'h-9 px-3 py-2',
lg: 'h-11 rounded-md px-8', lg: 'h-11 px-8',
icon: 'h-10 w-10' icon: 'h-10 w-10'
} }
}, },
@@ -96,9 +98,6 @@
tabindex={disabled ? -1 : undefined} tabindex={disabled ? -1 : undefined}
{...restProps} {...restProps}
> >
{#if isLoading}
<Spinner />
{/if}
{@render children?.()} {@render children?.()}
</a> </a>
{:else} {:else}
@@ -111,9 +110,15 @@
onclick={handleOnClick} onclick={handleOnClick}
{...restProps} {...restProps}
> >
{#if isLoading} <span class="flex items-center w-full justify-center">
<Spinner /> <Spinner
{/if} class={cn(
{@render children?.()} 'grid overflow-hidden transition-[width,opacity,margin-right] duration-400 ease-[linear(0,0.897_14.4%,1.311_31.2%,1.338_46%,1.054_80.4%,1)]',
isLoading ? 'w-4 opacity-100 mr-2' : 'w-0 opacity-0'
)}
aria-hidden={!isLoading}
/>
{@render children?.()}
</span>
</button> </button>
{/if} {/if}
@@ -14,7 +14,7 @@
bind:this={ref} bind:this={ref}
data-slot="card" data-slot="card"
class={cn( class={cn(
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm', 'bg-card text-card-foreground flex flex-col gap-6 rounded-4xl border py-6 shadow-sm',
className className
)} )}
{...restProps} {...restProps}
@@ -23,14 +23,14 @@
bind:ref bind:ref
data-slot="dialog-content" data-slot="dialog-content"
class={cn( class={cn(
'bg-background 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 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 duration-200 sm:max-w-lg', 'bg-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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-4xl border p-6 shadow-lg duration-200 sm:max-w-lg',
className className
)} )}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
<DialogPrimitive.Close <DialogPrimitive.Close
class="ring-offset-background focus:ring-ring absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4" class="ring-offset-popover focus:ring-ring absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
> >
<XIcon /> <XIcon />
<span class="sr-only">Close</span> <span class="sr-only">Close</span>
@@ -0,0 +1,16 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
value = $bindable([]),
...restProps
}: DropdownMenuPrimitive.CheckboxGroupProps = $props();
</script>
<DropdownMenuPrimitive.CheckboxGroup
bind:ref
bind:value
data-slot="dropdown-menu-checkbox-group"
{...restProps}
/>
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'; import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import CheckIcon from '@lucide/svelte/icons/check';
import MinusIcon from '@lucide/svelte/icons/minus'; import MinusIcon from '@lucide/svelte/icons/minus';
import CheckIcon from '@lucide/svelte/icons/check';
import { cn, type WithoutChildrenOrChild } from '$lib/utils/style.js'; import { cn, type WithoutChildrenOrChild } from '$lib/utils/style.js';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
@@ -23,17 +23,20 @@
bind:indeterminate bind:indeterminate
data-slot="dropdown-menu-checkbox-item" data-slot="dropdown-menu-checkbox-item"
class={cn( class={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-1.5 rounded-md py-1.5 pr-9 pl-2 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className className
)} )}
{...restProps} {...restProps}
> >
{#snippet children({ checked, indeterminate })} {#snippet children({ checked, indeterminate })}
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> <span
class="absolute right-2 flex items-center justify-center pointer-events-none"
data-slot="dropdown-menu-checkbox-item-indicator"
>
{#if indeterminate} {#if indeterminate}
<MinusIcon class="size-4" /> <MinusIcon />
{:else} {:else if checked}
<CheckIcon class={cn('size-4', !checked && 'text-transparent')} /> <CheckIcon />
{/if} {/if}
</span> </span>
{@render childrenProp?.()} {@render childrenProp?.()}
@@ -1,27 +1,31 @@
<script lang="ts"> <script lang="ts">
import { cn } from '$lib/utils/style.js'; import { cn, type WithoutChildrenOrChild } from '$lib/utils/style.js';
import DropdownMenuPortal from './dropdown-menu-portal.svelte';
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'; import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import type { ComponentProps } from 'svelte';
let { let {
ref = $bindable(null), ref = $bindable(null),
sideOffset = 4, sideOffset = 4,
align = 'start',
portalProps, portalProps,
class: className, class: className,
...restProps ...restProps
}: DropdownMenuPrimitive.ContentProps & { }: DropdownMenuPrimitive.ContentProps & {
portalProps?: DropdownMenuPrimitive.PortalProps; portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DropdownMenuPortal>>;
} = $props(); } = $props();
</script> </script>
<DropdownMenuPrimitive.Portal {...portalProps}> <DropdownMenuPortal {...portalProps}>
<DropdownMenuPrimitive.Content <DropdownMenuPrimitive.Content
bind:ref bind:ref
data-slot="dropdown-menu-content" data-slot="dropdown-menu-content"
{sideOffset} {sideOffset}
{align}
class={cn( class={cn(
'bg-popover text-popover-foreground 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 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 p-1 shadow-md', 'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 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 ring-foreground/10 bg-popover text-popover-foreground min-w-32 rounded-lg p-1 shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 overflow-x-hidden overflow-y-auto outline-none data-closed:overflow-hidden',
className className
)} )}
{...restProps} {...restProps}
/> />
</DropdownMenuPrimitive.Portal> </DropdownMenuPortal>
@@ -17,6 +17,6 @@
bind:ref bind:ref
data-slot="dropdown-menu-group-heading" data-slot="dropdown-menu-group-heading"
data-inset={inset} data-inset={inset}
class={cn('px-2 py-1.5 text-sm font-semibold data-[inset]:pl-8', className)} class={cn('px-2 py-1.5 text-sm font-semibold data-[inset]:ps-8', className)}
{...restProps} {...restProps}
/> />
@@ -20,7 +20,7 @@
data-inset={inset} data-inset={inset}
data-variant={variant} data-variant={variant}
class={cn( class={cn(
"data-highlighted:bg-accent data-highlighted:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:data-highlighted:bg-destructive/10 dark:data-[variant=destructive]:data-highlighted:bg-destructive/20 data-[variant=destructive]:data-highlighted:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground gap-1.5 rounded-md px-2 py-1.5 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 group/dropdown-menu-item relative flex cursor-pointer items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className className
)} )}
{...restProps} {...restProps}
@@ -17,7 +17,10 @@
bind:this={ref} bind:this={ref}
data-slot="dropdown-menu-label" data-slot="dropdown-menu-label"
data-inset={inset} data-inset={inset}
class={cn('px-2 py-1.5 text-sm font-semibold data-[inset]:pl-8', className)} class={cn(
'text-muted-foreground px-1.5 py-1 text-xs font-medium data-inset:pl-7 data-[inset]:pl-8',
className
)}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
@@ -0,0 +1,7 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
let { ...restProps }: DropdownMenuPrimitive.PortalProps = $props();
</script>
<DropdownMenuPrimitive.Portal {...restProps} />
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'; import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import CircleIcon from '@lucide/svelte/icons/circle'; import CheckIcon from '@lucide/svelte/icons/check';
import { cn, type WithoutChild } from '$lib/utils/style.js'; import { cn, type WithoutChild } from '$lib/utils/style.js';
let { let {
@@ -15,15 +15,18 @@
bind:ref bind:ref
data-slot="dropdown-menu-radio-item" data-slot="dropdown-menu-radio-item"
class={cn( class={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className className
)} )}
{...restProps} {...restProps}
> >
{#snippet children({ checked })} {#snippet children({ checked })}
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> <span
class="absolute right-2 flex items-center justify-center"
data-slot="dropdown-menu-radio-item-indicator"
>
{#if checked} {#if checked}
<CircleIcon class="size-2 fill-current" /> <CheckIcon />
{/if} {/if}
</span> </span>
{@render childrenProp?.({ checked })} {@render childrenProp?.({ checked })}
@@ -13,7 +13,10 @@
<span <span
bind:this={ref} bind:this={ref}
data-slot="dropdown-menu-shortcut" data-slot="dropdown-menu-shortcut"
class={cn('text-muted-foreground ml-auto text-xs tracking-widest', className)} class={cn(
'text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest',
className
)}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
@@ -13,7 +13,7 @@
bind:ref bind:ref
data-slot="dropdown-menu-sub-content" data-slot="dropdown-menu-sub-content"
class={cn( class={cn(
'bg-popover text-popover-foreground 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 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg', 'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 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 ring-foreground/10 bg-popover text-popover-foreground min-w-[96px] rounded-lg p-1 shadow-lg ring-1 duration-100 w-auto',
className className
)} )}
{...restProps} {...restProps}
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { cn } from '$lib/utils/style.js';
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'; import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
import { cn } from '$lib/utils/style.js';
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -19,11 +19,11 @@
data-slot="dropdown-menu-sub-trigger" data-slot="dropdown-menu-sub-trigger"
data-inset={inset} data-inset={inset}
class={cn( class={cn(
"data-highlighted:bg-accent data-highlighted:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-1.5 rounded-md px-1.5 py-1 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 flex cursor-default items-center outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className className
)} )}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
<ChevronRightIcon class="ml-auto size-4" /> <ChevronRightIcon class="ml-auto" />
</DropdownMenuPrimitive.SubTrigger> </DropdownMenuPrimitive.SubTrigger>
@@ -0,0 +1,7 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
let { open = $bindable(false), ...restProps }: DropdownMenuPrimitive.SubProps = $props();
</script>
<DropdownMenuPrimitive.Sub bind:open {...restProps} />
@@ -0,0 +1,7 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
let { open = $bindable(false), ...restProps }: DropdownMenuPrimitive.RootProps = $props();
</script>
<DropdownMenuPrimitive.Root bind:open {...restProps} />
@@ -1,4 +1,6 @@
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'; import Root from './dropdown-menu.svelte';
import Sub from './dropdown-menu-sub.svelte';
import CheckboxGroup from './dropdown-menu-checkbox-group.svelte';
import CheckboxItem from './dropdown-menu-checkbox-item.svelte'; import CheckboxItem from './dropdown-menu-checkbox-item.svelte';
import Content from './dropdown-menu-content.svelte'; import Content from './dropdown-menu-content.svelte';
import Group from './dropdown-menu-group.svelte'; import Group from './dropdown-menu-group.svelte';
@@ -12,15 +14,18 @@ import Trigger from './dropdown-menu-trigger.svelte';
import SubContent from './dropdown-menu-sub-content.svelte'; import SubContent from './dropdown-menu-sub-content.svelte';
import SubTrigger from './dropdown-menu-sub-trigger.svelte'; import SubTrigger from './dropdown-menu-sub-trigger.svelte';
import GroupHeading from './dropdown-menu-group-heading.svelte'; import GroupHeading from './dropdown-menu-group-heading.svelte';
const Sub = DropdownMenuPrimitive.Sub; import Portal from './dropdown-menu-portal.svelte';
const Root = DropdownMenuPrimitive.Root;
export { export {
CheckboxGroup,
CheckboxItem, CheckboxItem,
Content, Content,
Portal,
Root as DropdownMenu, Root as DropdownMenu,
CheckboxGroup as DropdownMenuCheckboxGroup,
CheckboxItem as DropdownMenuCheckboxItem, CheckboxItem as DropdownMenuCheckboxItem,
Content as DropdownMenuContent, Content as DropdownMenuContent,
Portal as DropdownMenuPortal,
Group as DropdownMenuGroup, Group as DropdownMenuGroup,
Item as DropdownMenuItem, Item as DropdownMenuItem,
Label as DropdownMenuLabel, Label as DropdownMenuLabel,
@@ -24,7 +24,7 @@
bind:this={ref} bind:this={ref}
data-slot="input" data-slot="input"
class={cn( class={cn(
'selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-2 text-base font-medium shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm', 'selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-full border bg-transparent px-3 py-2 text-base font-medium shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]', 'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive', 'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
className className
@@ -39,7 +39,7 @@
bind:this={ref} bind:this={ref}
data-slot="input" data-slot="input"
class={cn( class={cn(
'border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm', 'border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-full border px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]', 'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive', 'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
className className
@@ -2,7 +2,7 @@
import { tv, type VariantProps } from 'tailwind-variants'; import { tv, type VariantProps } from 'tailwind-variants';
export const itemVariants = tv({ export const itemVariants = tv({
base: 'group/item focus-visible:border-ring focus-visible:ring-ring/50 flex flex-wrap items-center rounded-xl border border-transparent text-sm outline-none transition-colors duration-100 focus-visible:ring-[3px]', base: 'group/item focus-visible:border-ring focus-visible:ring-ring/50 flex flex-wrap items-center rounded-4xl border border-transparent text-sm outline-none transition-colors duration-100 focus-visible:ring-[3px]',
variants: { variants: {
variant: { variant: {
default: 'bg-transparent [a&]:hover:bg-accent/50 [a&]:transition-colors', default: 'bg-transparent [a&]:hover:bg-accent/50 [a&]:transition-colors',
@@ -12,8 +12,8 @@
transparent: 'bg-transparent' transparent: 'bg-transparent'
}, },
size: { size: {
default: 'gap-4 p-4', default: 'gap-4 p-6',
sm: 'gap-2.5 px-4 py-3' sm: 'gap-2.5 px-4 py-4'
} }
}, },
defaultVariants: { defaultVariants: {
@@ -1,17 +1,28 @@
import { Popover as PopoverPrimitive } from 'bits-ui'; import Root from './popover.svelte';
import Close from './popover-close.svelte';
import Content from './popover-content.svelte'; import Content from './popover-content.svelte';
import Description from './popover-description.svelte';
import Header from './popover-header.svelte';
import Title from './popover-title.svelte';
import Trigger from './popover-trigger.svelte'; import Trigger from './popover-trigger.svelte';
const Root = PopoverPrimitive.Root; import Portal from './popover-portal.svelte';
const Close = PopoverPrimitive.Close;
export { export {
Root, Root,
Content, Content,
Description,
Header,
Title,
Trigger, Trigger,
Close, Close,
Portal,
// //
Root as Popover, Root as Popover,
Content as PopoverContent, Content as PopoverContent,
Description as PopoverDescription,
Header as PopoverHeader,
Title as PopoverTitle,
Trigger as PopoverTrigger, Trigger as PopoverTrigger,
Close as PopoverClose Close as PopoverClose,
Portal as PopoverPortal
}; };
@@ -0,0 +1,7 @@
<script lang="ts">
import { Popover as PopoverPrimitive } from 'bits-ui';
let { ref = $bindable(null), ...restProps }: PopoverPrimitive.CloseProps = $props();
</script>
<PopoverPrimitive.Close bind:ref data-slot="popover-close" {...restProps} />
@@ -1,6 +1,8 @@
<script lang="ts"> <script lang="ts">
import { cn } from '$lib/utils/style.js'; import { cn, type WithoutChildrenOrChild } from '$lib/utils/style.js';
import { Popover as PopoverPrimitive } from 'bits-ui'; import { Popover as PopoverPrimitive } from 'bits-ui';
import type { ComponentProps } from 'svelte';
import PopoverPortal from './popover-portal.svelte';
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -11,22 +13,22 @@
portalProps, portalProps,
...restProps ...restProps
}: PopoverPrimitive.ContentProps & { }: PopoverPrimitive.ContentProps & {
portalProps?: PopoverPrimitive.PortalProps; portalProps?: WithoutChildrenOrChild<ComponentProps<typeof PopoverPortal>>;
sameWidth?: boolean; sameWidth?: boolean;
} = $props(); } = $props();
</script> </script>
<PopoverPrimitive.Portal {...portalProps}> <PopoverPortal {...portalProps}>
<PopoverPrimitive.Content <PopoverPrimitive.Content
bind:ref bind:ref
data-slot="popover-content" data-slot="popover-content"
{sideOffset} {sideOffset}
{align} {align}
class={cn( class={cn(
'bg-popover text-popover-foreground 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 z-50 w-72 origin-(--bits-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden', 'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 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 ring-foreground/10 flex flex-col gap-2.5 rounded-sm p-2.5 text-sm shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 w-72 origin-(--transform-origin) outline-hidden',
sameWidth && 'w-[var(--bits-popover-anchor-width)]', sameWidth && 'w-[var(--bits-popover-anchor-width)]',
className className
)} )}
{...restProps} {...restProps}
/> />
</PopoverPrimitive.Portal> </PopoverPortal>
@@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn, type WithElementRef } from '$lib/utils/style.js';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="popover-description"
class={cn('text-muted-foreground', className)}
{...restProps}
>
{@render children?.()}
</div>
@@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn, type WithElementRef } from '$lib/utils/style.js';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="popover-header"
class={cn('flex flex-col gap-0.5 text-sm', className)}
{...restProps}
>
{@render children?.()}
</div>
@@ -0,0 +1,7 @@
<script lang="ts">
import { Popover as PopoverPrimitive } from 'bits-ui';
let { ...restProps }: PopoverPrimitive.PortalProps = $props();
</script>
<PopoverPrimitive.Portal {...restProps} />
@@ -0,0 +1,15 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn, type WithElementRef } from '$lib/utils/style.js';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div bind:this={ref} data-slot="popover-title" class={cn('font-medium', className)} {...restProps}>
{@render children?.()}
</div>
@@ -0,0 +1,7 @@
<script lang="ts">
import { Popover as PopoverPrimitive } from 'bits-ui';
let { open = $bindable(false), ...restProps }: PopoverPrimitive.RootProps = $props();
</script>
<PopoverPrimitive.Root bind:open {...restProps} />
@@ -1,5 +1,4 @@
import { Select as SelectPrimitive } from 'bits-ui'; import Root from './select.svelte';
import Group from './select-group.svelte'; import Group from './select-group.svelte';
import Label from './select-label.svelte'; import Label from './select-label.svelte';
import Item from './select-item.svelte'; import Item from './select-item.svelte';
@@ -8,8 +7,8 @@ import Trigger from './select-trigger.svelte';
import Separator from './select-separator.svelte'; import Separator from './select-separator.svelte';
import ScrollDownButton from './select-scroll-down-button.svelte'; import ScrollDownButton from './select-scroll-down-button.svelte';
import ScrollUpButton from './select-scroll-up-button.svelte'; import ScrollUpButton from './select-scroll-up-button.svelte';
import GroupHeading from './select-group-heading.svelte';
const Root = SelectPrimitive.Root; import Portal from './select-portal.svelte';
export { export {
Root, Root,
@@ -21,6 +20,8 @@ export {
Separator, Separator,
ScrollDownButton, ScrollDownButton,
ScrollUpButton, ScrollUpButton,
GroupHeading,
Portal,
// //
Root as Select, Root as Select,
Group as SelectGroup, Group as SelectGroup,
@@ -30,5 +31,7 @@ export {
Trigger as SelectTrigger, Trigger as SelectTrigger,
Separator as SelectSeparator, Separator as SelectSeparator,
ScrollDownButton as SelectScrollDownButton, ScrollDownButton as SelectScrollDownButton,
ScrollUpButton as SelectScrollUpButton ScrollUpButton as SelectScrollUpButton,
GroupHeading as SelectGroupHeading,
Portal as SelectPortal
}; };
@@ -1,8 +1,11 @@
<script lang="ts"> <script lang="ts">
import { Select as SelectPrimitive } from 'bits-ui'; import type { WithoutChildrenOrChild } from '$lib/utils/style.js';
import SelectScrollUpButton from './select-scroll-up-button.svelte';
import SelectScrollDownButton from './select-scroll-down-button.svelte';
import { cn, type WithoutChild } from '$lib/utils/style.js'; import { cn, type WithoutChild } from '$lib/utils/style.js';
import { Select as SelectPrimitive } from 'bits-ui';
import type { ComponentProps } from 'svelte';
import SelectPortal from './select-portal.svelte';
import SelectScrollDownButton from './select-scroll-down-button.svelte';
import SelectScrollUpButton from './select-scroll-up-button.svelte';
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -10,19 +13,21 @@
sideOffset = 4, sideOffset = 4,
portalProps, portalProps,
children, children,
preventScroll = true,
...restProps ...restProps
}: WithoutChild<SelectPrimitive.ContentProps> & { }: WithoutChild<SelectPrimitive.ContentProps> & {
portalProps?: SelectPrimitive.PortalProps; portalProps?: WithoutChildrenOrChild<ComponentProps<typeof SelectPortal>>;
} = $props(); } = $props();
</script> </script>
<SelectPrimitive.Portal {...portalProps}> <SelectPortal {...portalProps}>
<SelectPrimitive.Content <SelectPrimitive.Content
bind:ref bind:ref
{sideOffset} {sideOffset}
{preventScroll}
data-slot="select-content" data-slot="select-content"
class={cn( class={cn(
'bg-popover text-popover-foreground 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 relative z-50 max-h-(--bits-select-content-available-height) min-w-[8rem] origin-(--bits-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1', 'bg-popover y-5 p-1 text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 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 ring-foreground/10 min-w-36 rounded-lg shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 relative isolate z-50 overflow-x-hidden overflow-y-auto',
className className
)} )}
{...restProps} {...restProps}
@@ -30,11 +35,11 @@
<SelectScrollUpButton /> <SelectScrollUpButton />
<SelectPrimitive.Viewport <SelectPrimitive.Viewport
class={cn( class={cn(
'h-(--bits-select-anchor-height) w-full min-w-(--bits-select-anchor-width) scroll-my-1 p-1' 'h-(--bits-select-anchor-height) w-full min-w-(--bits-select-anchor-width) scroll-my-1'
)} )}
> >
{@render children?.()} {@render children?.()}
</SelectPrimitive.Viewport> </SelectPrimitive.Viewport>
<SelectScrollDownButton /> <SelectScrollDownButton />
</SelectPrimitive.Content> </SelectPrimitive.Content>
</SelectPrimitive.Portal> </SelectPortal>
@@ -0,0 +1,21 @@
<script lang="ts">
import { Select as SelectPrimitive } from 'bits-ui';
import { cn } from '$lib/utils/style.js';
import type { ComponentProps } from 'svelte';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: ComponentProps<typeof SelectPrimitive.GroupHeading> = $props();
</script>
<SelectPrimitive.GroupHeading
bind:ref
data-slot="select-group-heading"
class={cn('text-muted-foreground px-2 py-1.5 text-xs', className)}
{...restProps}
>
{@render children?.()}
</SelectPrimitive.GroupHeading>
@@ -1,7 +1,17 @@
<script lang="ts"> <script lang="ts">
import { Select as SelectPrimitive } from 'bits-ui'; import { Select as SelectPrimitive } from 'bits-ui';
import { cn } from '$lib/utils/style.js';
let { ref = $bindable(null), ...restProps }: SelectPrimitive.GroupProps = $props(); let {
ref = $bindable(null),
class: className,
...restProps
}: SelectPrimitive.GroupProps = $props();
</script> </script>
<SelectPrimitive.Group data-slot="select-group" {...restProps} /> <SelectPrimitive.Group
bind:ref
data-slot="select-group"
class={cn('scroll-my-1 p-1', className)}
{...restProps}
/>
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import CheckIcon from '@lucide/svelte/icons/check';
import { Select as SelectPrimitive } from 'bits-ui'; import { Select as SelectPrimitive } from 'bits-ui';
import { cn, type WithoutChild } from '$lib/utils/style.js'; import { cn, type WithoutChild } from '$lib/utils/style.js';
import CheckIcon from '@lucide/svelte/icons/check';
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -18,15 +18,15 @@
{value} {value}
data-slot="select-item" data-slot="select-item"
class={cn( class={cn(
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2", "focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-1.5 rounded-md py-1.5 pr-9 pl-2 text-sm [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 focus:bg-accent data-highlighted:bg-accent data-highlighted:text-accent-foreground focus:text-accent-foreground relative flex w-full cursor-pointer items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className className
)} )}
{...restProps} {...restProps}
> >
{#snippet children({ selected, highlighted })} {#snippet children({ selected, highlighted })}
<span class="absolute right-2 flex size-3.5 items-center justify-center"> <span class="absolute end-2 flex size-3.5 items-center justify-center">
{#if selected} {#if selected}
<CheckIcon class="size-4" /> <CheckIcon class="cn-select-item-indicator-icon" />
{/if} {/if}
</span> </span>
{#if childrenProp} {#if childrenProp}
@@ -13,7 +13,7 @@
<div <div
bind:this={ref} bind:this={ref}
data-slot="select-label" data-slot="select-label"
class={cn('text-muted-foreground px-2 py-1.5 text-xs', className)} class={cn('text-muted-foreground px-1.5 py-1 text-xs', className)}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
@@ -0,0 +1,7 @@
<script lang="ts">
import { Select as SelectPrimitive } from 'bits-ui';
let { ...restProps }: SelectPrimitive.PortalProps = $props();
</script>
<SelectPrimitive.Portal {...restProps} />
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
import { Select as SelectPrimitive } from 'bits-ui'; import { Select as SelectPrimitive } from 'bits-ui';
import { cn, type WithoutChildrenOrChild } from '$lib/utils/style.js'; import { cn, type WithoutChildrenOrChild } from '$lib/utils/style.js';
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -12,9 +12,12 @@
<SelectPrimitive.ScrollDownButton <SelectPrimitive.ScrollDownButton
bind:ref bind:ref
data-slot="select-scroll-up-button" data-slot="select-scroll-down-button"
class={cn('flex cursor-default items-center justify-center py-1', className)} class={cn(
"bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4 bottom-0 w-full",
className
)}
{...restProps} {...restProps}
> >
<ChevronDownIcon class="size-4" /> <ChevronDownIcon />
</SelectPrimitive.ScrollDownButton> </SelectPrimitive.ScrollDownButton>
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import ChevronUpIcon from '@lucide/svelte/icons/chevron-up';
import { Select as SelectPrimitive } from 'bits-ui'; import { Select as SelectPrimitive } from 'bits-ui';
import { cn, type WithoutChildrenOrChild } from '$lib/utils/style.js'; import { cn, type WithoutChildrenOrChild } from '$lib/utils/style.js';
import ChevronUpIcon from '@lucide/svelte/icons/chevron-up';
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -12,9 +12,12 @@
<SelectPrimitive.ScrollUpButton <SelectPrimitive.ScrollUpButton
bind:ref bind:ref
data-slot="select-scroll-down-button" data-slot="select-scroll-up-button"
class={cn('flex cursor-default items-center justify-center py-1', className)} class={cn(
"bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4 top-0 w-full",
className
)}
{...restProps} {...restProps}
> >
<ChevronUpIcon class="size-4" /> <ChevronUpIcon />
</SelectPrimitive.ScrollUpButton> </SelectPrimitive.ScrollUpButton>
@@ -13,6 +13,6 @@
<Separator <Separator
bind:ref bind:ref
data-slot="select-separator" data-slot="select-separator"
class={cn('bg-border pointer-events-none -mx-1 my-1 h-px', className)} class={cn('bg-border -mx-1 my-1 h-px pointer-events-none', className)}
{...restProps} {...restProps}
/> />
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { Select as SelectPrimitive } from 'bits-ui'; import { Select as SelectPrimitive } from 'bits-ui';
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
import { cn, type WithoutChild } from '$lib/utils/style.js'; import { cn, type WithoutChild } from '$lib/utils/style.js';
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -19,11 +19,11 @@
data-slot="select-trigger" data-slot="select-trigger"
data-size={size} data-size={size}
class={cn( class={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "border-input data-placeholder:text-muted-foreground dark:bg-input/30 dark:hover:bg-input/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 gap-1.5 rounded-full border bg-transparent py-2 px-4 text-sm transition-colors select-none focus-visible:ring-3 aria-invalid:ring-3 data-[size=default]:h-9 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:flex *:data-[slot=select-value]:gap-1.5 [&_svg:not([class*='size-'])]:size-4 flex w-fit items-center justify-between whitespace-nowrap outline-none disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
className className
)} )}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
<ChevronDownIcon class="size-4 opacity-50" /> <ChevronDownIcon class="text-muted-foreground size-4" />
</SelectPrimitive.Trigger> </SelectPrimitive.Trigger>
@@ -0,0 +1,11 @@
<script lang="ts">
import { Select as SelectPrimitive } from 'bits-ui';
let {
open = $bindable(false),
value = $bindable(),
...restProps
}: SelectPrimitive.RootProps = $props();
</script>
<SelectPrimitive.Root bind:open bind:value={value as never} {...restProps} />
@@ -5,15 +5,18 @@
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
'data-slot': dataSlot = 'separator',
...restProps ...restProps
}: SeparatorPrimitive.RootProps = $props(); }: SeparatorPrimitive.RootProps = $props();
</script> </script>
<SeparatorPrimitive.Root <SeparatorPrimitive.Root
bind:ref bind:ref
data-slot="separator-root" data-slot={dataSlot}
class={cn( class={cn(
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px', 'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px',
// this is different in shadcn/ui but self-stretch breaks things for us
'data-[orientation=vertical]:h-full',
className className
)} )}
{...restProps} {...restProps}
@@ -23,7 +23,7 @@
<SwitchPrimitive.Thumb <SwitchPrimitive.Thumb
data-slot="switch-thumb" data-slot="switch-thumb"
class={cn( class={cn(
'bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0' 'bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0'
)} )}
/> />
</SwitchPrimitive.Root> </SwitchPrimitive.Root>
+3 -1
View File
@@ -1,6 +1,6 @@
import Root from './tabs.svelte'; import Root from './tabs.svelte';
import Content from './tabs-content.svelte'; import Content from './tabs-content.svelte';
import List from './tabs-list.svelte'; import List, { tabsListVariants, type TabsListVariant } from './tabs-list.svelte';
import Trigger from './tabs-trigger.svelte'; import Trigger from './tabs-trigger.svelte';
export { export {
@@ -8,6 +8,8 @@ export {
Content, Content,
List, List,
Trigger, Trigger,
tabsListVariants,
type TabsListVariant,
// //
Root as Tabs, Root as Tabs,
Content as TabsContent, Content as TabsContent,
@@ -12,6 +12,6 @@
<TabsPrimitive.Content <TabsPrimitive.Content
bind:ref bind:ref
data-slot="tabs-content" data-slot="tabs-content"
class={cn('flex-1 outline-none', className)} class={cn('text-sm flex-1 outline-none', className)}
{...restProps} {...restProps}
/> />
@@ -1,16 +1,40 @@
<script lang="ts"> <script lang="ts" module>
import { cn } from '$lib/utils/style'; import { tv, type VariantProps } from 'tailwind-variants';
import { Tabs as TabsPrimitive } from 'bits-ui';
let { ref = $bindable(null), class: className, ...restProps }: TabsPrimitive.ListProps = $props(); export const tabsListVariants = tv({
base: 'rounded-lg p-[3px] group-data-horizontal/tabs:h-8 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col',
variants: {
variant: {
default: 'cn-tabs-list-variant-default bg-muted',
line: 'cn-tabs-list-variant-line gap-1 bg-transparent'
}
},
defaultVariants: {
variant: 'default'
}
});
export type TabsListVariant = VariantProps<typeof tabsListVariants>['variant'];
</script>
<script lang="ts">
import { Tabs as TabsPrimitive } from 'bits-ui';
import { cn } from '$lib/utils/style.js';
let {
ref = $bindable(null),
variant = 'default',
class: className,
...restProps
}: TabsPrimitive.ListProps & {
variant?: TabsListVariant;
} = $props();
</script> </script>
<TabsPrimitive.List <TabsPrimitive.List
bind:ref bind:ref
data-slot="tabs-list" data-slot="tabs-list"
class={cn( data-variant={variant}
'bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]', class={cn(tabsListVariants({ variant }), className)}
className
)}
{...restProps} {...restProps}
/> />
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { cn } from '$lib/utils/style.js';
import { Tabs as TabsPrimitive } from 'bits-ui'; import { Tabs as TabsPrimitive } from 'bits-ui';
import { cn } from '$lib/utils/style.js';
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -17,4 +17,4 @@
className className
)} )}
{...restProps} {...restProps}
></TabsPrimitive.Trigger> />
@@ -32,6 +32,6 @@
bind:value bind:value
onValueChange={onTabChange} onValueChange={onTabChange}
data-slot="tabs" data-slot="tabs"
class={cn('flex flex-col gap-2', className)} class={cn('gap-2 group/tabs flex data-[orientation=horizontal]:flex-col', className)}
{...restProps} {...restProps}
/> />
@@ -1,10 +1,8 @@
import { Tooltip as TooltipPrimitive } from 'bits-ui'; import Root from './tooltip.svelte';
import Trigger from './tooltip-trigger.svelte'; import Trigger from './tooltip-trigger.svelte';
import Content from './tooltip-content.svelte'; import Content from './tooltip-content.svelte';
import Provider from './tooltip-provider.svelte';
const Root = TooltipPrimitive.Root; import Portal from './tooltip-portal.svelte';
const Provider = TooltipPrimitive.Provider;
const Portal = TooltipPrimitive.Portal;
export { export {
Root, Root,
@@ -1,6 +1,9 @@
<script lang="ts"> <script lang="ts">
import { Tooltip as TooltipPrimitive } from 'bits-ui'; import { Tooltip as TooltipPrimitive } from 'bits-ui';
import { cn } from '$lib/utils/style.js'; import { cn } from '$lib/utils/style.js';
import TooltipPortal from './tooltip-portal.svelte';
import type { ComponentProps } from 'svelte';
import type { WithoutChildrenOrChild } from '$lib/utils/style.js';
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -9,20 +12,22 @@
side = 'top', side = 'top',
children, children,
arrowClasses, arrowClasses,
portalProps,
...restProps ...restProps
}: TooltipPrimitive.ContentProps & { }: TooltipPrimitive.ContentProps & {
arrowClasses?: string; arrowClasses?: string;
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof TooltipPortal>>;
} = $props(); } = $props();
</script> </script>
<TooltipPrimitive.Portal> <TooltipPortal {...portalProps}>
<TooltipPrimitive.Content <TooltipPrimitive.Content
bind:ref bind:ref
data-slot="tooltip-content" data-slot="tooltip-content"
{sideOffset} {sideOffset}
{side} {side}
class={cn( class={cn(
'bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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 z-50 w-fit origin-(--bits-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance', 'data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-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 inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs has-data-[slot=kbd]:pr-1.5 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm bg-foreground text-background z-50 w-fit max-w-xs origin-(--bits-tooltip-content-transform-origin)',
className className
)} )}
{...restProps} {...restProps}
@@ -32,11 +37,11 @@
{#snippet child({ props })} {#snippet child({ props })}
<div <div
class={cn( class={cn(
'bg-primary z-50 size-2.5 rotate-45 rounded-[2px]', 'size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground z-50',
side === 'top' && 'translate-x-1/2 translate-y-[calc(-50%_+_2px)]', 'data-[side=top]:translate-x-1/2 data-[side=top]:translate-y-[calc(-50%+2px)]',
side === 'bottom' && '-translate-x-1/2 -translate-y-[calc(-50%_+_1px)]', 'data-[side=bottom]:-translate-x-1/2 data-[side=bottom]:-translate-y-[calc(-50%+1px)]',
side === 'right' && 'translate-x-[calc(50%_+_2px)] translate-y-1/2', 'data-[side=right]:translate-x-[calc(50%+2px)] data-[side=right]:translate-y-1/2',
side === 'left' && '-translate-y-[calc(50%_-_3px)]', 'data-[side=left]:-translate-y-[calc(50%-3px)]',
arrowClasses arrowClasses
)} )}
{...props} {...props}
@@ -44,4 +49,4 @@
{/snippet} {/snippet}
</TooltipPrimitive.Arrow> </TooltipPrimitive.Arrow>
</TooltipPrimitive.Content> </TooltipPrimitive.Content>
</TooltipPrimitive.Portal> </TooltipPortal>
@@ -0,0 +1,7 @@
<script lang="ts">
import { Tooltip as TooltipPrimitive } from 'bits-ui';
let { ...restProps }: TooltipPrimitive.PortalProps = $props();
</script>
<TooltipPrimitive.Portal {...restProps} />
@@ -0,0 +1,7 @@
<script lang="ts">
import { Tooltip as TooltipPrimitive } from 'bits-ui';
let { delayDuration = 0, ...restProps }: TooltipPrimitive.ProviderProps = $props();
</script>
<TooltipPrimitive.Provider {delayDuration} {...restProps} />
@@ -0,0 +1,7 @@
<script lang="ts">
import { Tooltip as TooltipPrimitive } from 'bits-ui';
let { open = $bindable(false), ...restProps }: TooltipPrimitive.RootProps = $props();
</script>
<TooltipPrimitive.Root bind:open {...restProps} />
@@ -7,7 +7,7 @@
<div class="bg-muted mx-auto rounded-2xl p-3"> <div class="bg-muted mx-auto rounded-2xl p-3">
<Logo class="size-10" /> <Logo class="size-10" />
</div> </div>
<p class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">{m.browser_unsupported()}</p> <p class="font-glooock mt-5 text-3xl font-bold sm:text-4xl">{m.browser_unsupported()}</p>
<p class="text-muted-foreground mt-3"> <p class="text-muted-foreground mt-3">
{m.this_browser_does_not_support_passkeys()} {m.this_browser_does_not_support_passkeys()}
</p> </p>
+46 -33
View File
@@ -180,11 +180,10 @@
throw new Error('Invalid redirect URL protocol'); throw new Error('Invalid redirect URL protocol');
} }
redirectURL.searchParams.append('error', error); window.location.href = createRedirectURL(callbackURL, {
if (authorizeState) { error,
redirectURL.searchParams.append('state', authorizeState); state: authorizeState
} });
window.location.href = redirectURL.toString();
} }
function onSuccess(code: string, callbackURL: string, issuer: string) { function onSuccess(code: string, callbackURL: string, issuer: string) {
@@ -193,10 +192,6 @@
throw new Error('Invalid redirect URL protocol'); throw new Error('Invalid redirect URL protocol');
} }
redirectURL.searchParams.append('code', code);
redirectURL.searchParams.append('state', authorizeState);
redirectURL.searchParams.append('iss', issuer);
success = true; success = true;
setTimeout(() => { setTimeout(() => {
if (responseMode === 'form_post') { if (responseMode === 'form_post') {
@@ -231,16 +226,34 @@
document.body.appendChild(form); document.body.appendChild(form);
form.submit(); form.submit();
} else { } else {
// Default query parameter redirect (response_mode=query or not specified) window.location.href = createRedirectURL(callbackURL, {
const redirectURL = new URL(callbackURL); code,
redirectURL.searchParams.append('code', code); state: authorizeState,
redirectURL.searchParams.append('state', authorizeState); iss: issuer
redirectURL.searchParams.append('iss', issuer); });
window.location.href = redirectURL.toString();
} }
}, 1000); }, 1000);
} }
function createRedirectURL(url: string, params: Record<string, string>) {
const redirectURL = new URL(url);
const responseParams =
responseMode === 'fragment'
? new URLSearchParams(redirectURL.hash.slice(1))
: redirectURL.searchParams;
for (const [key, value] of Object.entries(params)) {
if (value) {
responseParams.set(key, value);
}
}
if (responseMode === 'fragment') {
redirectURL.hash = responseParams.toString();
}
return redirectURL.toString();
}
</script> </script>
<svelte:head> <svelte:head>
@@ -252,7 +265,7 @@
{:else} {:else}
<SignInWrapper showAlternativeSignInMethodButton={$userStore == null}> <SignInWrapper showAlternativeSignInMethodButton={$userStore == null}>
<ClientProviderImages {client} {success} error={!!errorMessage} /> <ClientProviderImages {client} {success} error={!!errorMessage} />
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl"> <h1 class="font-glooock mt-5 text-3xl font-bold sm:text-4xl">
{m.sign_in_to({ name: client.name })} {m.sign_in_to({ name: client.name })}
</h1> </h1>
{#if errorMessage} {#if errorMessage}
@@ -260,7 +273,22 @@
{errorMessage}. {errorMessage}.
</p> </p>
{/if} {/if}
{#if accountSelectionRequired && $userStore && !errorMessage} {#if authorizationRequired}
<div class="w-full max-w-md" transition:slide={{ duration: 300 }}>
<Card.Root class="mt-6 mb-10">
<Card.Header>
<p class="text-muted-foreground text-start">
<FormattedMessage
m={m.client_wants_to_access_the_following_information({ client: client.name })}
/>
</p>
</Card.Header>
<Card.Content>
<ScopeList {scope} />
</Card.Content>
</Card.Root>
</div>
{:else if accountSelectionRequired && $userStore && !errorMessage}
<div transition:slide={{ duration: 300 }} class="flex flex-col items-center"> <div transition:slide={{ duration: 300 }} class="flex flex-col items-center">
<p class="text-muted-foreground mt-2 mb-8"> <p class="text-muted-foreground mt-2 mb-8">
<FormattedMessage m={m.account_selection_signin_confirmation({ name: client.name })} /> <FormattedMessage m={m.account_selection_signin_confirmation({ name: client.name })} />
@@ -301,21 +329,6 @@
})} })}
/> />
</p> </p>
{:else if authorizationRequired}
<div class="w-full max-w-md" transition:slide={{ duration: 300 }}>
<Card.Root class="mt-6 mb-10">
<Card.Header>
<p class="text-muted-foreground text-start">
<FormattedMessage
m={m.client_wants_to_access_the_following_information({ client: client.name })}
/>
</p>
</Card.Header>
<Card.Content>
<ScopeList {scope} />
</Card.Content>
</Card.Root>
</div>
{/if} {/if}
<!-- Flex flow is reversed so the sign in button, which has auto-focus, is the first one in the DOM, for a11y --> <!-- Flex flow is reversed so the sign in button, which has auto-focus, is the first one in the DOM, for a11y -->
<div class="flex w-full max-w-md flex-row-reverse gap-2"> <div class="flex w-full max-w-md flex-row-reverse gap-2">
+1 -1
View File
@@ -79,7 +79,7 @@
<LoginLogoErrorSuccessIndicator {success} error={!!errorMessage} /> <LoginLogoErrorSuccessIndicator {success} error={!!errorMessage} />
{/if} {/if}
</div> </div>
<h1 class="font-playfair mt-5 text-4xl font-bold">{m.authorize_device()}</h1> <h1 class="font-glooock mt-5 text-4xl font-bold">{m.authorize_device()}</h1>
{#if errorMessage} {#if errorMessage}
<p class="text-muted-foreground mt-2"> <p class="text-muted-foreground mt-2">
{errorMessage}. {m.please_try_again()} {errorMessage}. {m.please_try_again()}
+1 -1
View File
@@ -44,7 +44,7 @@
<div class="flex justify-center"> <div class="flex justify-center">
<LoginLogoErrorSuccessIndicator error={!!error} /> <LoginLogoErrorSuccessIndicator error={!!error} />
</div> </div>
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl"> <h1 class="font-glooock mt-5 text-3xl font-bold sm:text-4xl">
{m.sign_in_to_appname({ appName: $appConfigStore.appName })} {m.sign_in_to_appname({ appName: $appConfigStore.appName })}
</h1> </h1>
{#if error} {#if error}
@@ -35,7 +35,7 @@
<div class="bg-muted mx-auto rounded-2xl p-3"> <div class="bg-muted mx-auto rounded-2xl p-3">
<Logo class="size-10" /> <Logo class="size-10" />
</div> </div>
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">{m.alternative_sign_in()}</h1> <h1 class="font-glooock mt-5 text-3xl font-bold sm:text-4xl">{m.alternative_sign_in()}</h1>
<p class="text-muted-foreground mt-3"> <p class="text-muted-foreground mt-3">
{m.if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods()} {m.if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods()}
</p> </p>
@@ -59,7 +59,7 @@
<div class="flex justify-center"> <div class="flex justify-center">
<LoginLogoErrorSuccessIndicator error={!!error} /> <LoginLogoErrorSuccessIndicator error={!!error} />
</div> </div>
<h1 class="font-playfair mt-5 text-4xl font-bold">{m.login_code()}</h1> <h1 class="font-glooock mt-5 text-4xl font-bold">{m.login_code()}</h1>
{#if error} {#if error}
<p class="text-muted-foreground mt-2"> <p class="text-muted-foreground mt-2">
{error}. {m.please_try_again()} {error}. {m.please_try_again()}
@@ -5,9 +5,9 @@
import Input from '$lib/components/ui/input/input.svelte'; import Input from '$lib/components/ui/input/input.svelte';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import UserService from '$lib/services/user-service'; import UserService from '$lib/services/user-service';
import { preventDefault } from '$lib/utils/event-util';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import LoginLogoErrorSuccessIndicator from '../../components/login-logo-error-success-indicator.svelte'; import LoginLogoErrorSuccessIndicator from '../../components/login-logo-error-success-indicator.svelte';
import { preventDefault } from '$lib/utils/event-util';
const { data } = $props(); const { data } = $props();
@@ -37,7 +37,7 @@
<div class="flex justify-center"> <div class="flex justify-center">
<LoginLogoErrorSuccessIndicator {success} error={!!error} /> <LoginLogoErrorSuccessIndicator {success} error={!!error} />
</div> </div>
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">{m.email_login()}</h1> <h1 class="font-glooock mt-5 text-3xl font-bold sm:text-4xl">{m.email_login()}</h1>
{#if error} {#if error}
<p class="text-muted-foreground mt-2" in:fade> <p class="text-muted-foreground mt-2" in:fade>
{error}. {m.please_try_again()} {error}. {m.please_try_again()}
+1 -1
View File
@@ -34,7 +34,7 @@
<Logo class="size-10" /> <Logo class="size-10" />
</div> </div>
</div> </div>
<h1 class="font-playfair mt-5 text-4xl font-bold">{m.sign_out()}</h1> <h1 class="font-glooock mt-5 text-4xl">{m.sign_out()}</h1>
<p class="text-muted-foreground mt-2"> <p class="text-muted-foreground mt-2">
<FormattedMessage <FormattedMessage
+2 -10
View File
@@ -4,7 +4,6 @@
import Sidebar from '$lib/components/sidebar.svelte'; import Sidebar from '$lib/components/sidebar.svelte';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import userStore from '$lib/stores/user-store'; import userStore from '$lib/stores/user-store';
import { LucideSettings } from '@lucide/svelte';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import { fade, fly } from 'svelte/transition'; import { fade, fly } from 'svelte/transition';
import type { LayoutData } from './$types'; import type { LayoutData } from './$types';
@@ -46,7 +45,7 @@
<section> <section>
<div <div
class="bg-muted/40 dark:bg-background flex min-h-[calc(100vh-64px)] w-full flex-col justify-between" class="bg-muted/40 dark:bg-background flex min-h-[calc(100vh-86px)] w-full flex-col justify-between"
> >
<main <main
in:fade={{ duration: 200 }} in:fade={{ duration: 200 }}
@@ -54,13 +53,6 @@
> >
<div class="min-w-[200px] xl:min-w-[250px]"> <div class="min-w-[200px] xl:min-w-[250px]">
<div in:fly={{ x: -15, duration: 200 }} class="sticky top-6"> <div in:fly={{ x: -15, duration: 200 }} class="sticky top-6">
<div class="mx-auto grid w-full gap-2">
<h1 class="mb-4 flex items-center gap-2 text-2xl font-semibold">
<LucideSettings class="size-5" />
{m.settings()}
</h1>
</div>
<Sidebar <Sidebar
{items} {items}
storageKey="sidebar-open:settings" storageKey="sidebar-open:settings"
@@ -70,7 +62,7 @@
</div> </div>
</div> </div>
<div class="flex w-full flex-col gap-4 overflow-hidden"> <div class="flex w-full flex-col gap-4 overflow-hidden pb-2">
<FadeWrapper> <FadeWrapper>
<EmailVerificationStateBox /> <EmailVerificationStateBox />
{@render children()} {@render children()}
@@ -141,7 +141,7 @@
</Card.Content> </Card.Content>
</Card.Root> </Card.Root>
<Item.Group class="bg-card rounded-xl border p-4 shadow-sm"> <Item.Group class="bg-card border shadow-sm rounded-4xl p-5">
<Item.Root class="border-none bg-transparent p-0"> <Item.Root class="border-none bg-transparent p-0">
<Item.Media class="text-primary/80"> <Item.Media class="text-primary/80">
<KeyRound class="size-5" /> <KeyRound class="size-5" />
@@ -64,7 +64,6 @@
value={loginCodeLink} value={loginCodeLink}
size={150} size={150}
color={mode.current === 'dark' ? '#FFFFFF' : '#000000'} color={mode.current === 'dark' ? '#FFFFFF' : '#000000'}
backgroundColor={mode.current === 'dark' ? '#000000' : '#FFFFFF'}
/> />
<CopyToClipboard value={loginCodeLink!}> <CopyToClipboard value={loginCodeLink!}>
<p class="text-sm" data-testId="login-code-link">{loginCodeLink!}</p> <p class="text-sm" data-testId="login-code-link">{loginCodeLink!}</p>

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