forked from 0x2E/fusion
init
This commit is contained in:
parent
245c8933cb
commit
e9b065e9fb
170 changed files with 10768 additions and 0 deletions
6
.env
Normal file
6
.env
Normal file
|
@ -0,0 +1,6 @@
|
|||
# web server
|
||||
host="127.0.0.1"
|
||||
port=8080
|
||||
|
||||
# web ui password
|
||||
password="123"
|
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
.DS_*
|
||||
*.db
|
||||
*.paw
|
||||
tmp/
|
||||
build/
|
0
Dockerfile
Normal file
0
Dockerfile
Normal file
22
README.md
Normal file
22
README.md
Normal file
|
@ -0,0 +1,22 @@
|
|||
# Fusion
|
||||
|
||||
Fusion is an RSS aggregator and reader with:
|
||||
|
||||
- Lightweight, high performance, easy to deploy
|
||||
- Support RSS, Atom, JSON feeds
|
||||
- Import/Export OPML
|
||||
- Feed groups
|
||||
|
||||
## Installation
|
||||
|
||||
## ToDo
|
||||
|
||||
- Docker
|
||||
- Bookmark
|
||||
- PWA
|
||||
|
||||
## Credits
|
||||
|
||||
- Frontend is built with: [Sveltekit](https://github.com/sveltejs/kit), [shadcn-svelte](https://github.com/huntabyte/shadcn-svelte)
|
||||
- Backend is built with: [Echo framework](https://github.com/labstack/echo), [GORM](https://github.com/go-gorm/gorm)
|
||||
- Parsing feed with [gofeed](https://github.com/mmcdole/gofeed)
|
155
api/api.go
Normal file
155
api/api.go
Normal file
|
@ -0,0 +1,155 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/0x2e/fusion/conf"
|
||||
"github.com/0x2e/fusion/pkg/errorx"
|
||||
"github.com/0x2e/fusion/repo"
|
||||
"github.com/0x2e/fusion/server"
|
||||
|
||||
"github.com/go-playground/locales/en"
|
||||
ut "github.com/go-playground/universal-translator"
|
||||
"github.com/go-playground/validator/v10"
|
||||
en_translations "github.com/go-playground/validator/v10/translations/en"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/labstack/echo-contrib/session"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
)
|
||||
|
||||
func Run() {
|
||||
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
||||
|
||||
r := echo.New()
|
||||
r.HideBanner = true
|
||||
r.HTTPErrorHandler = errorHandler
|
||||
r.Validator = newCustomValidator()
|
||||
r.Use(middleware.Recover())
|
||||
r.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
|
||||
LogStatus: true,
|
||||
LogURI: true,
|
||||
LogError: true,
|
||||
HandleError: true, // forwards error to the global error handler, so it can decide appropriate status code
|
||||
LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
|
||||
if !strings.HasPrefix(v.URI, "/api") {
|
||||
return nil
|
||||
}
|
||||
if v.Error == nil {
|
||||
logger.LogAttrs(context.Background(), slog.LevelInfo, "REQUEST",
|
||||
slog.String("uri", v.URI),
|
||||
slog.Int("status", v.Status),
|
||||
)
|
||||
} else {
|
||||
logger.LogAttrs(context.Background(), slog.LevelError, "REQUEST_ERROR",
|
||||
slog.String("uri", v.URI),
|
||||
slog.Int("status", v.Status),
|
||||
slog.String("err", v.Error.Error()),
|
||||
)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}))
|
||||
r.Use(session.Middleware(sessions.NewCookieStore([]byte("secret"))))
|
||||
r.Use(middleware.StaticWithConfig(middleware.StaticConfig{
|
||||
Root: "./frontend",
|
||||
Index: "index.html",
|
||||
HTML5: true,
|
||||
Browse: false,
|
||||
}))
|
||||
r.Use(session.Middleware(sessions.NewCookieStore([]byte("fusion"))))
|
||||
r.Pre(middleware.RemoveTrailingSlash())
|
||||
|
||||
loginAPI := Session{}
|
||||
r.POST("/api/sessions", loginAPI.Create)
|
||||
|
||||
authed := r.Group("/api", func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
ok, err := loginAPI.Check(c)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized)
|
||||
}
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized)
|
||||
}
|
||||
return next(c)
|
||||
}
|
||||
})
|
||||
|
||||
feeds := authed.Group("/feeds")
|
||||
feedAPIHandler := newFeedAPI(server.NewFeed(repo.NewFeed(repo.DB), repo.NewItem(repo.DB)))
|
||||
feeds.GET("", feedAPIHandler.All)
|
||||
feeds.GET("/:id", feedAPIHandler.Get)
|
||||
feeds.POST("", feedAPIHandler.Create)
|
||||
feeds.POST("/validation", feedAPIHandler.CheckValidity)
|
||||
feeds.PATCH("/:id", feedAPIHandler.Update)
|
||||
feeds.DELETE("/:id", feedAPIHandler.Delete)
|
||||
feeds.POST("/refresh", feedAPIHandler.Refresh)
|
||||
|
||||
groups := authed.Group("/groups")
|
||||
groupAPIHandler := newGroupAPI(server.NewGroup(repo.NewGroup(repo.DB), repo.NewFeed(repo.DB)))
|
||||
groups.GET("", groupAPIHandler.All)
|
||||
groups.POST("", groupAPIHandler.Create)
|
||||
groups.PATCH("/:id", groupAPIHandler.Update)
|
||||
groups.DELETE("/:id", groupAPIHandler.Delete)
|
||||
|
||||
items := authed.Group("/items")
|
||||
itemAPIHandler := newItemAPI(server.NewItem(repo.NewItem(repo.DB)))
|
||||
items.GET("", itemAPIHandler.List)
|
||||
items.GET("/:id", itemAPIHandler.Get)
|
||||
items.PATCH("/:id", itemAPIHandler.Update)
|
||||
items.DELETE("/:id", itemAPIHandler.Delete)
|
||||
|
||||
r.Logger.Fatal(r.Start(fmt.Sprintf("%s:%d", conf.Conf.Host, conf.Conf.Port)))
|
||||
}
|
||||
|
||||
func errorHandler(err error, c echo.Context) {
|
||||
if err == errorx.ErrNotFound {
|
||||
err = echo.NewHTTPError(http.StatusNotFound, "Resource does not exists")
|
||||
}
|
||||
|
||||
c.Echo().DefaultHTTPErrorHandler(err, c)
|
||||
}
|
||||
|
||||
type CustomValidator struct {
|
||||
handler *validator.Validate
|
||||
trans ut.Translator
|
||||
}
|
||||
|
||||
func newCustomValidator() *CustomValidator {
|
||||
en := en.New()
|
||||
uni := ut.New(en, en)
|
||||
trans, _ := uni.GetTranslator("en")
|
||||
validate := validator.New()
|
||||
en_translations.RegisterDefaultTranslations(validate, trans)
|
||||
return &CustomValidator{
|
||||
handler: validate,
|
||||
trans: trans,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *CustomValidator) Validate(i interface{}) error {
|
||||
err := v.handler.Struct(i)
|
||||
if err != nil {
|
||||
errs := err.(validator.ValidationErrors)
|
||||
msg := strings.Builder{}
|
||||
for _, content := range errs.Translate(v.trans) {
|
||||
msg.WriteString(content)
|
||||
msg.WriteString(".")
|
||||
}
|
||||
err = echo.NewHTTPError(http.StatusBadRequest, msg.String())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func bindAndValidate(i interface{}, c echo.Context) error {
|
||||
if err := c.Bind(i); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Validate(i)
|
||||
}
|
109
api/feed.go
Normal file
109
api/feed.go
Normal file
|
@ -0,0 +1,109 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/0x2e/fusion/server"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
type feedAPI struct {
|
||||
srv *server.Feed
|
||||
}
|
||||
|
||||
func newFeedAPI(srv *server.Feed) *feedAPI {
|
||||
return &feedAPI{
|
||||
srv: srv,
|
||||
}
|
||||
}
|
||||
|
||||
func (f feedAPI) All(c echo.Context) error {
|
||||
resp, err := f.srv.All()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (f feedAPI) Get(c echo.Context) error {
|
||||
var req server.ReqFeedGet
|
||||
if err := bindAndValidate(&req, c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := f.srv.Get(&req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (f feedAPI) Create(c echo.Context) error {
|
||||
var req server.ReqFeedCreate
|
||||
if err := bindAndValidate(&req, c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := f.srv.Create(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusCreated)
|
||||
}
|
||||
|
||||
func (f feedAPI) CheckValidity(c echo.Context) error {
|
||||
var req server.ReqFeedCheckValidity
|
||||
if err := bindAndValidate(&req, c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := f.srv.CheckValidity(&req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusCreated, resp)
|
||||
}
|
||||
|
||||
func (f feedAPI) Update(c echo.Context) error {
|
||||
var req server.ReqFeedUpdate
|
||||
if err := bindAndValidate(&req, c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := f.srv.Update(&req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (f feedAPI) Delete(c echo.Context) error {
|
||||
var req server.ReqFeedDelete
|
||||
if err := bindAndValidate(&req, c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := f.srv.Delete(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (f feedAPI) Refresh(c echo.Context) error {
|
||||
var req server.ReqFeedRefresh
|
||||
if err := bindAndValidate(&req, c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := f.srv.Refresh(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusNoContent)
|
||||
}
|
68
api/group.go
Normal file
68
api/group.go
Normal file
|
@ -0,0 +1,68 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/0x2e/fusion/server"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
type groupAPI struct {
|
||||
srv *server.Group
|
||||
}
|
||||
|
||||
func newGroupAPI(srv *server.Group) *groupAPI {
|
||||
return &groupAPI{
|
||||
srv: srv,
|
||||
}
|
||||
}
|
||||
|
||||
func (f groupAPI) All(c echo.Context) error {
|
||||
resp, err := f.srv.All()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (f groupAPI) Create(c echo.Context) error {
|
||||
var req server.ReqGroupCreate
|
||||
if err := bindAndValidate(&req, c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := f.srv.Create(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusCreated)
|
||||
}
|
||||
|
||||
func (f groupAPI) Update(c echo.Context) error {
|
||||
var req server.ReqGroupUpdate
|
||||
if err := bindAndValidate(&req, c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := f.srv.Update(&req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (f groupAPI) Delete(c echo.Context) error {
|
||||
var req server.ReqGroupDelete
|
||||
if err := bindAndValidate(&req, c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := f.srv.Delete(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusNoContent)
|
||||
}
|
73
api/item.go
Normal file
73
api/item.go
Normal file
|
@ -0,0 +1,73 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/0x2e/fusion/server"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
type itemAPI struct {
|
||||
srv *server.Item
|
||||
}
|
||||
|
||||
func newItemAPI(srv *server.Item) *itemAPI {
|
||||
return &itemAPI{
|
||||
srv: srv,
|
||||
}
|
||||
}
|
||||
|
||||
func (i itemAPI) List(c echo.Context) error {
|
||||
var req server.ReqItemList
|
||||
if err := bindAndValidate(&req, c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := i.srv.List(&req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (i itemAPI) Get(c echo.Context) error {
|
||||
var req server.ReqItemGet
|
||||
if err := bindAndValidate(&req, c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := i.srv.Get(&req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (i itemAPI) Update(c echo.Context) error {
|
||||
var req server.ReqItemUpdate
|
||||
if err := bindAndValidate(&req, c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := i.srv.Update(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (i itemAPI) Delete(c echo.Context) error {
|
||||
var req server.ReqItemDelete
|
||||
if err := bindAndValidate(&req, c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := i.srv.Delete(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusNoContent)
|
||||
}
|
46
api/session.go
Normal file
46
api/session.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/0x2e/fusion/conf"
|
||||
|
||||
"github.com/labstack/echo-contrib/session"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
type Session struct{}
|
||||
|
||||
func (s Session) Create(c echo.Context) error {
|
||||
var req struct {
|
||||
Password string `json:"password" validate:"required"`
|
||||
}
|
||||
|
||||
if err := bindAndValidate(&req, c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if req.Password != conf.Conf.Password {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Wrong password")
|
||||
}
|
||||
|
||||
sess, _ := session.Get("login", c)
|
||||
sess.Values["password"] = conf.Conf.Password
|
||||
if err := sess.Save(c.Request(), c.Response()); err != nil {
|
||||
return c.NoContent(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusCreated)
|
||||
}
|
||||
|
||||
func (s Session) Check(c echo.Context) (bool, error) {
|
||||
sess, err := session.Get("login", c)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
v, ok := sess.Values["password"]
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
return v == conf.Conf.Password, nil
|
||||
}
|
27
cmd/server/server.go
Normal file
27
cmd/server/server.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
|
||||
"github.com/0x2e/fusion/api"
|
||||
"github.com/0x2e/fusion/conf"
|
||||
"github.com/0x2e/fusion/repo"
|
||||
"github.com/0x2e/fusion/service/pull"
|
||||
)
|
||||
|
||||
// TODO: refactor all loggers
|
||||
func main() {
|
||||
go func() {
|
||||
log.Println(http.ListenAndServe("localhost:6060", nil))
|
||||
}()
|
||||
|
||||
conf.Load()
|
||||
repo.Init()
|
||||
|
||||
go pull.NewPuller(repo.NewFeed(repo.DB), repo.NewItem(repo.DB)).Run()
|
||||
|
||||
// TODO: pprof
|
||||
api.Run()
|
||||
}
|
40
conf/conf.go
Normal file
40
conf/conf.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var Conf struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
Password string `mapstructure:"password"`
|
||||
DB string
|
||||
}
|
||||
|
||||
func Load() {
|
||||
// default conf
|
||||
Conf.Host = "0.0.0.0"
|
||||
Conf.Port = 8080
|
||||
Conf.DB = "data.db"
|
||||
|
||||
viper.SetConfigFile(".env")
|
||||
viper.AutomaticEnv()
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.Unmarshal(&Conf); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := validate(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func validate() error {
|
||||
if Conf.Password == "" {
|
||||
return errors.New("password is required")
|
||||
}
|
||||
return nil
|
||||
}
|
0
docker-compose.yml
Normal file
0
docker-compose.yml
Normal file
13
frontend/.eslintignore
Normal file
13
frontend/.eslintignore
Normal file
|
@ -0,0 +1,13 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
31
frontend/.eslintrc.cjs
Normal file
31
frontend/.eslintrc.cjs
Normal file
|
@ -0,0 +1,31 @@
|
|||
/** @type { import("eslint").Linter.Config } */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:svelte/recommended',
|
||||
'prettier'
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020,
|
||||
extraFileExtensions: ['.svelte']
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.svelte'],
|
||||
parser: 'svelte-eslint-parser',
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
10
frontend/.gitignore
vendored
Normal file
10
frontend/.gitignore
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
1
frontend/.npmrc
Normal file
1
frontend/.npmrc
Normal file
|
@ -0,0 +1 @@
|
|||
engine-strict=true
|
4
frontend/.prettierignore
Normal file
4
frontend/.prettierignore
Normal file
|
@ -0,0 +1,4 @@
|
|||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
8
frontend/.prettierrc
Normal file
8
frontend/.prettierrc
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
14
frontend/components.json
Normal file
14
frontend/components.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||
"style": "default",
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/app.css",
|
||||
"baseColor": "slate"
|
||||
},
|
||||
"aliases": {
|
||||
"components": "$lib/components",
|
||||
"utils": "$lib/utils"
|
||||
},
|
||||
"typescript": true
|
||||
}
|
4519
frontend/package-lock.json
generated
Normal file
4519
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
52
frontend/package.json
Normal file
52
frontend/package.json
Normal file
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"name": "frontend",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^3.1.1",
|
||||
"@sveltejs/adapter-static": "^3.0.1",
|
||||
"@sveltejs/kit": "^2.5.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@types/eslint": "^8.56.5",
|
||||
"@typescript-eslint/eslint-plugin": "^7.1.0",
|
||||
"@typescript-eslint/parser": "^7.1.0",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.35.1",
|
||||
"postcss": "^8.4.35",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-svelte": "^3.2.2",
|
||||
"svelte": "^4.2.12",
|
||||
"svelte-check": "^3.6.6",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.1.4"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"bits-ui": "^0.18.4",
|
||||
"clsx": "^2.1.0",
|
||||
"dompurify": "^3.0.9",
|
||||
"ky": "^1.2.2",
|
||||
"lucide-svelte": "^0.344.0",
|
||||
"mode-watcher": "^0.2.1",
|
||||
"moment": "^2.30.1",
|
||||
"svelte-sonner": "^0.3.19",
|
||||
"tailwind-merge": "^2.2.1",
|
||||
"tailwind-variants": "^0.2.0",
|
||||
"vaul-svelte": "^0.2.4"
|
||||
}
|
||||
}
|
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
78
frontend/src/app.css
Normal file
78
frontend/src/app.css
Normal file
|
@ -0,0 +1,78 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--destructive: 0 72.2% 50.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--ring: 222.2 84% 4.9%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--ring: hsl(212.7,26.8%,83.9);
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
13
frontend/src/app.d.ts
vendored
Normal file
13
frontend/src/app.d.ts
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
12
frontend/src/app.html
Normal file
12
frontend/src/app.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
16
frontend/src/hooks.client.ts
Normal file
16
frontend/src/hooks.client.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import type { HandleClientError } from '@sveltejs/kit';
|
||||
import { HTTPError } from 'ky';
|
||||
|
||||
export const handleError: HandleClientError = async ({ error, event, status, message }) => {
|
||||
console.log(error);
|
||||
if (error instanceof HTTPError) {
|
||||
if (error.response.status === 401) {
|
||||
console.log('goto login');
|
||||
goto('/login');
|
||||
return;
|
||||
}
|
||||
return { message: error.message };
|
||||
}
|
||||
return error;
|
||||
};
|
26
frontend/src/lib/api/api.ts
Normal file
26
frontend/src/lib/api/api.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import ky from 'ky';
|
||||
|
||||
export const api = ky.create({
|
||||
prefixUrl: '/api',
|
||||
retry: 0,
|
||||
throwHttpErrors: true,
|
||||
credentials: 'same-origin',
|
||||
hooks: {
|
||||
beforeError: [
|
||||
// https://github.com/sindresorhus/ky/issues/412
|
||||
async (error) => {
|
||||
const { response } = error;
|
||||
switch (response.status) {
|
||||
default:
|
||||
try {
|
||||
const data = await response.json();
|
||||
error.message = data.message;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
return error;
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
51
frontend/src/lib/api/feed.ts
Normal file
51
frontend/src/lib/api/feed.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
import { api } from './api';
|
||||
import type { Feed } from './model';
|
||||
|
||||
export async function allFeeds() {
|
||||
const resp = await api.get('feeds').json<{ feeds: Feed[] }>();
|
||||
return resp.feeds;
|
||||
}
|
||||
|
||||
export async function getFeed(id: number) {
|
||||
return await api.get('feed/' + id).json<Feed>();
|
||||
}
|
||||
|
||||
export async function checkValidity(link: string) {
|
||||
const resp = await api
|
||||
.post('feeds/validation', {
|
||||
json: { link: link }
|
||||
})
|
||||
.json<{ feed_links: { title: string; link: string }[] }>();
|
||||
return resp.feed_links;
|
||||
}
|
||||
|
||||
export async function createFeed(data: {
|
||||
group_id: number;
|
||||
feeds: { name: string; link: string }[];
|
||||
}) {
|
||||
const feeds = data.feeds.map((v) => {
|
||||
return { name: v.name, link: v.link };
|
||||
});
|
||||
return await api.post('feeds', {
|
||||
json: { feeds: feeds, group_id: data.group_id }
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateFeed(data: Feed) {
|
||||
return await api.patch('feeds/' + data.id, {
|
||||
json: { name: data.name, link: data.link, group_id: data.group.id }
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteFeed(id: number) {
|
||||
return await api.delete('feeds/' + id);
|
||||
}
|
||||
|
||||
export async function refreshFeeds(options: { id?: number; all?: boolean }) {
|
||||
return await api.post('feeds/refresh', {
|
||||
json: {
|
||||
id: options.id,
|
||||
all: options.all
|
||||
}
|
||||
});
|
||||
}
|
27
frontend/src/lib/api/group.ts
Normal file
27
frontend/src/lib/api/group.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { api } from './api';
|
||||
import type { Group } from './model';
|
||||
|
||||
export async function allGroups() {
|
||||
const resp = await api.get('groups').json<{ groups: Group[] }>();
|
||||
return resp.groups;
|
||||
}
|
||||
|
||||
export async function createGroup(name: string) {
|
||||
return await api.post('groups', {
|
||||
json: {
|
||||
name: name
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateGroup(id: number, name: string) {
|
||||
return await api.patch('groups/' + id, {
|
||||
json: {
|
||||
name: name
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteGroup(id: number) {
|
||||
return await api.delete('groups/' + id);
|
||||
}
|
30
frontend/src/lib/api/item.ts
Normal file
30
frontend/src/lib/api/item.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { api } from './api';
|
||||
import type { Item } from './model';
|
||||
|
||||
type listOptions = {
|
||||
count?: number;
|
||||
offset?: number;
|
||||
keyword?: string;
|
||||
feed_id?: number;
|
||||
unread?: boolean;
|
||||
};
|
||||
|
||||
export async function listItems(options?: listOptions) {
|
||||
return api
|
||||
.get('items', {
|
||||
searchParams: options
|
||||
})
|
||||
.json<{ total: number; items: Item[] }>();
|
||||
}
|
||||
|
||||
export async function getItem(id: number) {
|
||||
return api.get('items/' + id).json<Item>();
|
||||
}
|
||||
|
||||
export async function updateItem(id: number, unread: boolean) {
|
||||
return api.patch('items/' + id, {
|
||||
json: {
|
||||
unread: unread
|
||||
}
|
||||
});
|
||||
}
|
9
frontend/src/lib/api/login.ts
Normal file
9
frontend/src/lib/api/login.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { api } from './api';
|
||||
|
||||
export async function login(password: string) {
|
||||
return api.post('sessions', {
|
||||
json: {
|
||||
password: password
|
||||
}
|
||||
});
|
||||
}
|
23
frontend/src/lib/api/model.ts
Normal file
23
frontend/src/lib/api/model.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
export type Group = {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type Feed = {
|
||||
id: number;
|
||||
name: string;
|
||||
link: string;
|
||||
failure: string;
|
||||
updated_at: Date;
|
||||
group: Group;
|
||||
};
|
||||
|
||||
export type Item = {
|
||||
id: number;
|
||||
title: string;
|
||||
link: string;
|
||||
content: string;
|
||||
pub_date: Date;
|
||||
unread: boolean;
|
||||
feed: { id: number; name: string };
|
||||
};
|
7
frontend/src/lib/components/Footer.svelte
Normal file
7
frontend/src/lib/components/Footer.svelte
Normal file
|
@ -0,0 +1,7 @@
|
|||
<footer class="mt-4 py-3 bg-muted">
|
||||
<p class="text-muted-foreground text-center">
|
||||
Fusion is an <a href="https://github.com/0x2e/fusion" target="_blank" class="underline">
|
||||
Open Source Project</a
|
||||
>. Logo by <a href="https://icons8.com" target="_blank" class="underline">Icons8</a>
|
||||
</p>
|
||||
</footer>
|
78
frontend/src/lib/components/ItemAction.svelte
Normal file
78
frontend/src/lib/components/ItemAction.svelte
Normal file
|
@ -0,0 +1,78 @@
|
|||
<script lang="ts">
|
||||
import { CheckIcon, ExternalLinkIcon, UndoIcon } from 'lucide-svelte';
|
||||
import type { ComponentType } from 'svelte';
|
||||
import type { Icon } from 'lucide-svelte';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import Button from './ui/button/button.svelte';
|
||||
import { updateItem } from '$lib/api/item';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
|
||||
export let data: {
|
||||
id: number;
|
||||
link: string;
|
||||
unread: boolean;
|
||||
};
|
||||
|
||||
function getActions(
|
||||
unread: boolean
|
||||
): { icon: ComponentType<Icon>; tooltip: string; handler: (e: Event) => void }[] {
|
||||
const list = [
|
||||
// { icon: BookmarkIcon, tooltip: 'Save to Bookmark', handler: handleSaveToBookmark },
|
||||
{ icon: ExternalLinkIcon, tooltip: 'Visit Original Link', handler: handleExternalLink }
|
||||
];
|
||||
const unreadAction = unread
|
||||
? { icon: CheckIcon, tooltip: 'Mark as Read', handler: handleMarkAsRead }
|
||||
: { icon: UndoIcon, tooltip: 'Mark as Unread', handler: handleMarkAsUnread };
|
||||
list.unshift(unreadAction);
|
||||
return list;
|
||||
}
|
||||
$: actions = getActions(data.unread);
|
||||
|
||||
async function handleMarkAsRead(e: Event) {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await updateItem(data.id, false);
|
||||
} catch (e) {
|
||||
toast.error((e as Error).message);
|
||||
}
|
||||
invalidateAll();
|
||||
}
|
||||
|
||||
async function handleMarkAsUnread(e: Event) {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await updateItem(data.id, true);
|
||||
} catch (e) {
|
||||
toast.error((e as Error).message);
|
||||
}
|
||||
invalidateAll();
|
||||
}
|
||||
|
||||
function handleExternalLink(e: Event) {
|
||||
e.preventDefault();
|
||||
handleMarkAsRead(e);
|
||||
window.open(data.link, '_target');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#each actions as action}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild let:builder>
|
||||
<Button
|
||||
builders={[builder]}
|
||||
variant="ghost"
|
||||
on:click={action.handler}
|
||||
class="hover:bg-gray-300 dark:hover:bg-gray-700"
|
||||
size="icon"
|
||||
>
|
||||
<svelte:component this={action.icon} size="18" />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>{action.tooltip}</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/each}
|
||||
</div>
|
71
frontend/src/lib/components/ItemList.svelte
Normal file
71
frontend/src/lib/components/ItemList.svelte
Normal file
|
@ -0,0 +1,71 @@
|
|||
<script lang="ts">
|
||||
import moment from 'moment';
|
||||
import { Button } from './ui/button';
|
||||
import ItemAction from './ItemAction.svelte';
|
||||
import * as Select from '$lib/components/ui/select';
|
||||
import type { Item } from '$lib/api/model';
|
||||
|
||||
export let data: Item[];
|
||||
$: allFeeds = getFeeds(data);
|
||||
let selectedFeed = 'all';
|
||||
$: filteredItems = filterFeed(data, selectedFeed);
|
||||
|
||||
function getFeeds(allItems: Item[]) {
|
||||
const feeds = new Map<number, { id: number; name: string }>();
|
||||
allItems.map((v) => feeds.set(v.feed.id, v.feed));
|
||||
return Array.from(feeds.values());
|
||||
}
|
||||
|
||||
function filterFeed(allItems: Item[], feedID: string) {
|
||||
if (feedID === 'all') return allItems;
|
||||
return allItems.filter((v) => v.feed.id === parseInt(feedID));
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<Select.Root
|
||||
items={allFeeds.map((v) => {
|
||||
return { value: v.id.toString(), label: v.name };
|
||||
})}
|
||||
onSelectedChange={(v) => v && (selectedFeed = v.value)}
|
||||
>
|
||||
<Select.Trigger class="w-[180px]">
|
||||
<Select.Value placeholder="Filter by Feed" />
|
||||
</Select.Trigger>
|
||||
<Select.Content class="max-h-40 overflow-scroll">
|
||||
<Select.Item value="all">All Feeds</Select.Item>
|
||||
{#each allFeeds as feed}
|
||||
<Select.Item value={feed.id}>{feed.name}</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
<ul class="mt-4">
|
||||
{#each filteredItems as item}
|
||||
<li class="group rounded-md">
|
||||
<Button
|
||||
href={'/items?id=' + item.id}
|
||||
class="flex justify-between items-center gap-8 py-6"
|
||||
variant="ghost"
|
||||
>
|
||||
<div class="w-3/4 truncate text-lg font-medium">
|
||||
{item.title}
|
||||
</div>
|
||||
<div class="flex justify-between items-center w-1/4">
|
||||
<div class="flex w-full justify-between text-sm text-muted-foreground group-hover:hidden">
|
||||
<div class="truncate">{item.feed.name}</div>
|
||||
<div class="truncate">
|
||||
{moment(item.pub_date).fromNow(true)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full hidden group-hover:inline-flex justify-end">
|
||||
<ItemAction data={{ id: item.id, link: item.link, unread: item.unread }} />
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</li>
|
||||
{:else}
|
||||
No data
|
||||
{/each}
|
||||
</ul>
|
51
frontend/src/lib/components/Navbar.svelte
Normal file
51
frontend/src/lib/components/Navbar.svelte
Normal file
|
@ -0,0 +1,51 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import ThemeToggler from './ThemeToggler.svelte';
|
||||
interface link {
|
||||
label: string;
|
||||
url: string;
|
||||
highlight?: boolean;
|
||||
}
|
||||
let links: link[] = [
|
||||
{ label: 'Unread', url: '/' },
|
||||
{ label: 'All', url: '/all' },
|
||||
{ label: 'Feeds', url: '/feeds' }
|
||||
];
|
||||
$: {
|
||||
let path = $page.url.pathname;
|
||||
for (const l of links) {
|
||||
l.highlight = false;
|
||||
let p = path.split('/');
|
||||
while (p.length > 1) {
|
||||
if (p.join('/') === l.url) {
|
||||
l.highlight = true;
|
||||
break;
|
||||
}
|
||||
p.pop();
|
||||
}
|
||||
}
|
||||
links = links;
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav class="block w-full sm:mt-3 mb-6">
|
||||
<div
|
||||
class="flex justify-around items-center w-full sm:max-w-sm mx-auto px-6 py-4 sm:rounded-2xl shadow-md sm:border bg-background"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<img src="/favicon.png" alt="logo" class="w-10" />
|
||||
<!-- <h2 class="font-bold text-xl">Fusion</h2> -->
|
||||
</div>
|
||||
<div>
|
||||
{#each links as l}
|
||||
<Button
|
||||
variant="ghost"
|
||||
href={l.url}
|
||||
class={l.highlight ? 'bg-accent text-accent-foreground' : ''}>{l.label}</Button
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
<ThemeToggler />
|
||||
</div>
|
||||
</nav>
|
9
frontend/src/lib/components/PageHead.svelte
Normal file
9
frontend/src/lib/components/PageHead.svelte
Normal file
|
@ -0,0 +1,9 @@
|
|||
<script lang="ts">
|
||||
export let title: string;
|
||||
export let className = '';
|
||||
</script>
|
||||
|
||||
<div class="flex items-center w-full {className}">
|
||||
<h1 class="text-3xl font-bold mb-4 ml-4">{title}</h1>
|
||||
<slot />
|
||||
</div>
|
27
frontend/src/lib/components/ThemeToggler.svelte
Normal file
27
frontend/src/lib/components/ThemeToggler.svelte
Normal file
|
@ -0,0 +1,27 @@
|
|||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import Sun from 'lucide-svelte/icons/sun';
|
||||
import Moon from 'lucide-svelte/icons/moon';
|
||||
|
||||
import { setMode, resetMode } from 'mode-watcher';
|
||||
</script>
|
||||
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild let:builder>
|
||||
<Button builders={[builder]} variant="ghost" size="icon">
|
||||
<Sun
|
||||
class="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"
|
||||
/>
|
||||
<Moon
|
||||
class="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100"
|
||||
/>
|
||||
<span class="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu.Item on:click={() => setMode('light')}>Light</DropdownMenu.Item>
|
||||
<DropdownMenu.Item on:click={() => setMode('dark')}>Dark</DropdownMenu.Item>
|
||||
<DropdownMenu.Item on:click={() => resetMode()}>System</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
|
@ -0,0 +1,21 @@
|
|||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
import { buttonVariants } from "$lib/components/ui/button";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = AlertDialogPrimitive.ActionProps;
|
||||
type $$Events = AlertDialogPrimitive.ActionEvents;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<AlertDialogPrimitive.Action
|
||||
class={cn(buttonVariants(), className)}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:keydown
|
||||
let:builder
|
||||
>
|
||||
<slot {builder} />
|
||||
</AlertDialogPrimitive.Action>
|
|
@ -0,0 +1,21 @@
|
|||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
import { buttonVariants } from "$lib/components/ui/button";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = AlertDialogPrimitive.CancelProps;
|
||||
type $$Events = AlertDialogPrimitive.CancelEvents;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<AlertDialogPrimitive.Cancel
|
||||
class={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:keydown
|
||||
let:builder
|
||||
>
|
||||
<slot {builder} />
|
||||
</AlertDialogPrimitive.Cancel>
|
|
@ -0,0 +1,28 @@
|
|||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
import * as AlertDialog from ".";
|
||||
import { cn, flyAndScale } from "$lib/utils";
|
||||
|
||||
type $$Props = AlertDialogPrimitive.ContentProps;
|
||||
|
||||
export let transition: $$Props["transition"] = flyAndScale;
|
||||
export let transitionConfig: $$Props["transitionConfig"] = undefined;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<AlertDialog.Portal>
|
||||
<AlertDialog.Overlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
{transition}
|
||||
{transitionConfig}
|
||||
class={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg sm:rounded-lg md:w-full",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</AlertDialogPrimitive.Content>
|
||||
</AlertDialog.Portal>
|
|
@ -0,0 +1,16 @@
|
|||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = AlertDialogPrimitive.DescriptionProps;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<AlertDialogPrimitive.Description
|
||||
class={cn("text-sm text-muted-foreground", className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</AlertDialogPrimitive.Description>
|
|
@ -0,0 +1,16 @@
|
|||
<script lang="ts">
|
||||
import { cn } from "$lib/utils";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { cn } from "$lib/utils";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<div class={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...$$restProps}>
|
||||
<slot />
|
||||
</div>
|
|
@ -0,0 +1,21 @@
|
|||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
import { fade } from "svelte/transition";
|
||||
|
||||
type $$Props = AlertDialogPrimitive.OverlayProps;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let transition: $$Props["transition"] = fade;
|
||||
export let transitionConfig: $$Props["transitionConfig"] = {
|
||||
duration: 150,
|
||||
};
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<AlertDialogPrimitive.Overlay
|
||||
{transition}
|
||||
{transitionConfig}
|
||||
class={cn("fixed inset-0 z-50 bg-background/80 backdrop-blur-sm ", className)}
|
||||
{...$$restProps}
|
||||
/>
|
|
@ -0,0 +1,9 @@
|
|||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
|
||||
type $$Props = AlertDialogPrimitive.PortalProps;
|
||||
</script>
|
||||
|
||||
<AlertDialogPrimitive.Portal {...$$restProps}>
|
||||
<slot />
|
||||
</AlertDialogPrimitive.Portal>
|
|
@ -0,0 +1,14 @@
|
|||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = AlertDialogPrimitive.TitleProps;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let level: $$Props["level"] = "h3";
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<AlertDialogPrimitive.Title class={cn("text-lg font-semibold", className)} {level} {...$$restProps}>
|
||||
<slot />
|
||||
</AlertDialogPrimitive.Title>
|
40
frontend/src/lib/components/ui/alert-dialog/index.ts
Normal file
40
frontend/src/lib/components/ui/alert-dialog/index.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
|
||||
const Root = AlertDialogPrimitive.Root;
|
||||
const Trigger = AlertDialogPrimitive.Trigger;
|
||||
|
||||
import Title from "./alert-dialog-title.svelte";
|
||||
import Action from "./alert-dialog-action.svelte";
|
||||
import Cancel from "./alert-dialog-cancel.svelte";
|
||||
import Portal from "./alert-dialog-portal.svelte";
|
||||
import Footer from "./alert-dialog-footer.svelte";
|
||||
import Header from "./alert-dialog-header.svelte";
|
||||
import Overlay from "./alert-dialog-overlay.svelte";
|
||||
import Content from "./alert-dialog-content.svelte";
|
||||
import Description from "./alert-dialog-description.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Title,
|
||||
Action,
|
||||
Cancel,
|
||||
Portal,
|
||||
Footer,
|
||||
Header,
|
||||
Trigger,
|
||||
Overlay,
|
||||
Content,
|
||||
Description,
|
||||
//
|
||||
Root as AlertDialog,
|
||||
Title as AlertDialogTitle,
|
||||
Action as AlertDialogAction,
|
||||
Cancel as AlertDialogCancel,
|
||||
Portal as AlertDialogPortal,
|
||||
Footer as AlertDialogFooter,
|
||||
Header as AlertDialogHeader,
|
||||
Trigger as AlertDialogTrigger,
|
||||
Overlay as AlertDialogOverlay,
|
||||
Content as AlertDialogContent,
|
||||
Description as AlertDialogDescription,
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { cn } from "$lib/utils";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<div class={cn("text-sm [&_p]:leading-relaxed", className)} {...$$restProps}>
|
||||
<slot />
|
||||
</div>
|
21
frontend/src/lib/components/ui/alert/alert-title.svelte
Normal file
21
frontend/src/lib/components/ui/alert/alert-title.svelte
Normal file
|
@ -0,0 +1,21 @@
|
|||
<script lang="ts">
|
||||
import { cn } from "$lib/utils";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import type { HeadingLevel } from ".";
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLHeadingElement> & {
|
||||
level?: HeadingLevel;
|
||||
};
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let level: $$Props["level"] = "h5";
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<svelte:element
|
||||
this={level}
|
||||
class={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</svelte:element>
|
17
frontend/src/lib/components/ui/alert/alert.svelte
Normal file
17
frontend/src/lib/components/ui/alert/alert.svelte
Normal file
|
@ -0,0 +1,17 @@
|
|||
<script lang="ts">
|
||||
import { cn } from "$lib/utils";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { alertVariants, type Variant } from ".";
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLDivElement> & {
|
||||
variant?: Variant;
|
||||
};
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let variant: $$Props["variant"] = "default";
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<div class={cn(alertVariants({ variant }), className)} {...$$restProps} role="alert">
|
||||
<slot />
|
||||
</div>
|
33
frontend/src/lib/components/ui/alert/index.ts
Normal file
33
frontend/src/lib/components/ui/alert/index.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { tv, type VariantProps } from "tailwind-variants";
|
||||
|
||||
import Root from "./alert.svelte";
|
||||
import Description from "./alert-description.svelte";
|
||||
import Title from "./alert-title.svelte";
|
||||
|
||||
export const alertVariants = tv({
|
||||
base: "relative w-full rounded-lg border p-4 [&>svg]:absolute [&>svg]:text-foreground [&>svg]:left-4 [&>svg]:top-4 [&>svg+div]:translate-y-[-3px] [&:has(svg)]:pl-11",
|
||||
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground",
|
||||
destructive:
|
||||
"text-destructive border-destructive/50 dark:border-destructive [&>svg]:text-destructive text-destructive",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
});
|
||||
|
||||
export type Variant = VariantProps<typeof alertVariants>["variant"];
|
||||
export type HeadingLevel = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Description,
|
||||
Title,
|
||||
//
|
||||
Root as Alert,
|
||||
Description as AlertDescription,
|
||||
Title as AlertTitle,
|
||||
};
|
18
frontend/src/lib/components/ui/badge/badge.svelte
Normal file
18
frontend/src/lib/components/ui/badge/badge.svelte
Normal file
|
@ -0,0 +1,18 @@
|
|||
<script lang="ts">
|
||||
import { cn } from "$lib/utils";
|
||||
import { badgeVariants, type Variant } from ".";
|
||||
|
||||
let className: string | undefined | null = undefined;
|
||||
export let href: string | undefined = undefined;
|
||||
export let variant: Variant = "default";
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<svelte:element
|
||||
this={href ? "a" : "span"}
|
||||
{href}
|
||||
class={cn(badgeVariants({ variant, className }))}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</svelte:element>
|
21
frontend/src/lib/components/ui/badge/index.ts
Normal file
21
frontend/src/lib/components/ui/badge/index.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { tv, type VariantProps } from "tailwind-variants";
|
||||
export { default as Badge } from "./badge.svelte";
|
||||
|
||||
export const badgeVariants = tv({
|
||||
base: "inline-flex items-center border rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none select-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary hover:bg-primary/80 border-transparent text-primary-foreground",
|
||||
secondary:
|
||||
"bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground",
|
||||
destructive:
|
||||
"bg-destructive hover:bg-destructive/80 border-transparent text-destructive-foreground",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
});
|
||||
|
||||
export type Variant = VariantProps<typeof badgeVariants>["variant"];
|
25
frontend/src/lib/components/ui/button/button.svelte
Normal file
25
frontend/src/lib/components/ui/button/button.svelte
Normal file
|
@ -0,0 +1,25 @@
|
|||
<script lang="ts">
|
||||
import { Button as ButtonPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
import { buttonVariants, type Props, type Events } from ".";
|
||||
|
||||
type $$Props = Props;
|
||||
type $$Events = Events;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let variant: $$Props["variant"] = "default";
|
||||
export let size: $$Props["size"] = "default";
|
||||
export let builders: $$Props["builders"] = [];
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<ButtonPrimitive.Root
|
||||
{builders}
|
||||
class={cn(buttonVariants({ variant, size, className }))}
|
||||
type="button"
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:keydown
|
||||
>
|
||||
<slot />
|
||||
</ButtonPrimitive.Root>
|
49
frontend/src/lib/components/ui/button/index.ts
Normal file
49
frontend/src/lib/components/ui/button/index.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import Root from "./button.svelte";
|
||||
import { tv, type VariantProps } from "tailwind-variants";
|
||||
import type { Button as ButtonPrimitive } from "bits-ui";
|
||||
|
||||
const buttonVariants = tv({
|
||||
base: "inline-flex items-center justify-center rounded-md text-sm font-medium whitespace-nowrap ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
});
|
||||
|
||||
type Variant = VariantProps<typeof buttonVariants>["variant"];
|
||||
type Size = VariantProps<typeof buttonVariants>["size"];
|
||||
|
||||
type Props = ButtonPrimitive.Props & {
|
||||
variant?: Variant;
|
||||
size?: Size;
|
||||
};
|
||||
|
||||
type Events = ButtonPrimitive.Events;
|
||||
|
||||
export {
|
||||
Root,
|
||||
type Props,
|
||||
type Events,
|
||||
//
|
||||
Root as Button,
|
||||
type Props as ButtonProps,
|
||||
type Events as ButtonEvents,
|
||||
buttonVariants,
|
||||
};
|
36
frontend/src/lib/components/ui/dialog/dialog-content.svelte
Normal file
36
frontend/src/lib/components/ui/dialog/dialog-content.svelte
Normal file
|
@ -0,0 +1,36 @@
|
|||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import X from "lucide-svelte/icons/x";
|
||||
import * as Dialog from ".";
|
||||
import { cn, flyAndScale } from "$lib/utils";
|
||||
|
||||
type $$Props = DialogPrimitive.ContentProps;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let transition: $$Props["transition"] = flyAndScale;
|
||||
export let transitionConfig: $$Props["transitionConfig"] = {
|
||||
duration: 200,
|
||||
};
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay />
|
||||
<DialogPrimitive.Content
|
||||
{transition}
|
||||
{transitionConfig}
|
||||
class={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg sm:rounded-lg md:w-full",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
<DialogPrimitive.Close
|
||||
class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
|
||||
>
|
||||
<X class="h-4 w-4" />
|
||||
<span class="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</Dialog.Portal>
|
|
@ -0,0 +1,16 @@
|
|||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = DialogPrimitive.DescriptionProps;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Description
|
||||
class={cn("text-sm text-muted-foreground", className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</DialogPrimitive.Description>
|
16
frontend/src/lib/components/ui/dialog/dialog-footer.svelte
Normal file
16
frontend/src/lib/components/ui/dialog/dialog-footer.svelte
Normal file
|
@ -0,0 +1,16 @@
|
|||
<script lang="ts">
|
||||
import { cn } from "$lib/utils";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
13
frontend/src/lib/components/ui/dialog/dialog-header.svelte
Normal file
13
frontend/src/lib/components/ui/dialog/dialog-header.svelte
Normal file
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { cn } from "$lib/utils";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<div class={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...$$restProps}>
|
||||
<slot />
|
||||
</div>
|
21
frontend/src/lib/components/ui/dialog/dialog-overlay.svelte
Normal file
21
frontend/src/lib/components/ui/dialog/dialog-overlay.svelte
Normal file
|
@ -0,0 +1,21 @@
|
|||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
import { fade } from "svelte/transition";
|
||||
|
||||
type $$Props = DialogPrimitive.OverlayProps;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let transition: $$Props["transition"] = fade;
|
||||
export let transitionConfig: $$Props["transitionConfig"] = {
|
||||
duration: 150,
|
||||
};
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Overlay
|
||||
{transition}
|
||||
{transitionConfig}
|
||||
class={cn("fixed inset-0 z-50 bg-background/80 backdrop-blur-sm", className)}
|
||||
{...$$restProps}
|
||||
/>
|
|
@ -0,0 +1,8 @@
|
|||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
type $$Props = DialogPrimitive.PortalProps;
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Portal {...$$restProps}>
|
||||
<slot />
|
||||
</DialogPrimitive.Portal>
|
16
frontend/src/lib/components/ui/dialog/dialog-title.svelte
Normal file
16
frontend/src/lib/components/ui/dialog/dialog-title.svelte
Normal file
|
@ -0,0 +1,16 @@
|
|||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = DialogPrimitive.TitleProps;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Title
|
||||
class={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</DialogPrimitive.Title>
|
34
frontend/src/lib/components/ui/dialog/index.ts
Normal file
34
frontend/src/lib/components/ui/dialog/index.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
|
||||
const Root = DialogPrimitive.Root;
|
||||
const Trigger = DialogPrimitive.Trigger;
|
||||
|
||||
import Title from "./dialog-title.svelte";
|
||||
import Portal from "./dialog-portal.svelte";
|
||||
import Footer from "./dialog-footer.svelte";
|
||||
import Header from "./dialog-header.svelte";
|
||||
import Overlay from "./dialog-overlay.svelte";
|
||||
import Content from "./dialog-content.svelte";
|
||||
import Description from "./dialog-description.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Title,
|
||||
Portal,
|
||||
Footer,
|
||||
Header,
|
||||
Trigger,
|
||||
Overlay,
|
||||
Content,
|
||||
Description,
|
||||
//
|
||||
Root as Dialog,
|
||||
Title as DialogTitle,
|
||||
Portal as DialogPortal,
|
||||
Footer as DialogFooter,
|
||||
Header as DialogHeader,
|
||||
Trigger as DialogTrigger,
|
||||
Overlay as DialogOverlay,
|
||||
Content as DialogContent,
|
||||
Description as DialogDescription,
|
||||
};
|
24
frontend/src/lib/components/ui/drawer/drawer-content.svelte
Normal file
24
frontend/src/lib/components/ui/drawer/drawer-content.svelte
Normal file
|
@ -0,0 +1,24 @@
|
|||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
import DrawerOverlay from "./drawer-overlay.svelte";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = DrawerPrimitive.ContentProps;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Portal>
|
||||
<DrawerOverlay />
|
||||
<DrawerPrimitive.Content
|
||||
class={cn(
|
||||
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<div class="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
|
||||
<slot />
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPrimitive.Portal>
|
|
@ -0,0 +1,18 @@
|
|||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = DrawerPrimitive.DescriptionProps;
|
||||
|
||||
export let el: $$Props["el"] = undefined;
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Description
|
||||
bind:el
|
||||
class={cn("text-sm text-muted-foreground", className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</DrawerPrimitive.Description>
|
16
frontend/src/lib/components/ui/drawer/drawer-footer.svelte
Normal file
16
frontend/src/lib/components/ui/drawer/drawer-footer.svelte
Normal file
|
@ -0,0 +1,16 @@
|
|||
<script lang="ts">
|
||||
import { cn } from "$lib/utils";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLDivElement> & {
|
||||
el?: HTMLDivElement;
|
||||
};
|
||||
|
||||
export let el: $$Props["el"] = undefined;
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<div bind:this={el} class={cn("mt-auto flex flex-col gap-2 p-4", className)} {...$$restProps}>
|
||||
<slot />
|
||||
</div>
|
19
frontend/src/lib/components/ui/drawer/drawer-header.svelte
Normal file
19
frontend/src/lib/components/ui/drawer/drawer-header.svelte
Normal file
|
@ -0,0 +1,19 @@
|
|||
<script lang="ts">
|
||||
import { cn } from "$lib/utils";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLDivElement> & {
|
||||
el?: HTMLDivElement;
|
||||
};
|
||||
export let el: $$Props["el"] = undefined;
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={el}
|
||||
class={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
12
frontend/src/lib/components/ui/drawer/drawer-nested.svelte
Normal file
12
frontend/src/lib/components/ui/drawer/drawer-nested.svelte
Normal file
|
@ -0,0 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
|
||||
type $$Props = DrawerPrimitive.Props;
|
||||
export let shouldScaleBackground: $$Props["shouldScaleBackground"] = true;
|
||||
export let open: $$Props["open"] = false;
|
||||
export let activeSnapPoint: $$Props["activeSnapPoint"] = undefined;
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.NestedRoot {shouldScaleBackground} bind:open bind:activeSnapPoint {...$$restProps}>
|
||||
<slot />
|
||||
</DrawerPrimitive.NestedRoot>
|
18
frontend/src/lib/components/ui/drawer/drawer-overlay.svelte
Normal file
18
frontend/src/lib/components/ui/drawer/drawer-overlay.svelte
Normal file
|
@ -0,0 +1,18 @@
|
|||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = DrawerPrimitive.OverlayProps;
|
||||
|
||||
export let el: $$Props["el"] = undefined;
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Overlay
|
||||
bind:el
|
||||
class={cn("fixed inset-0 z-50 bg-black/80", className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</DrawerPrimitive.Overlay>
|
18
frontend/src/lib/components/ui/drawer/drawer-title.svelte
Normal file
18
frontend/src/lib/components/ui/drawer/drawer-title.svelte
Normal file
|
@ -0,0 +1,18 @@
|
|||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = DrawerPrimitive.TitleProps;
|
||||
|
||||
export let el: $$Props["el"] = undefined;
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Title
|
||||
bind:el
|
||||
class={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</DrawerPrimitive.Title>
|
12
frontend/src/lib/components/ui/drawer/drawer.svelte
Normal file
12
frontend/src/lib/components/ui/drawer/drawer.svelte
Normal file
|
@ -0,0 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
|
||||
type $$Props = DrawerPrimitive.Props;
|
||||
export let shouldScaleBackground: $$Props["shouldScaleBackground"] = true;
|
||||
export let open: $$Props["open"] = false;
|
||||
export let activeSnapPoint: $$Props["activeSnapPoint"] = undefined;
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Root {shouldScaleBackground} bind:open bind:activeSnapPoint {...$$restProps}>
|
||||
<slot />
|
||||
</DrawerPrimitive.Root>
|
41
frontend/src/lib/components/ui/drawer/index.ts
Normal file
41
frontend/src/lib/components/ui/drawer/index.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
|
||||
import Root from "./drawer.svelte";
|
||||
import Content from "./drawer-content.svelte";
|
||||
import Description from "./drawer-description.svelte";
|
||||
import Overlay from "./drawer-overlay.svelte";
|
||||
import Footer from "./drawer-footer.svelte";
|
||||
import Header from "./drawer-header.svelte";
|
||||
import Title from "./drawer-title.svelte";
|
||||
import NestedRoot from "./drawer-nested.svelte";
|
||||
|
||||
const Trigger = DrawerPrimitive.Trigger;
|
||||
const Portal = DrawerPrimitive.Portal;
|
||||
const Close = DrawerPrimitive.Close;
|
||||
|
||||
export {
|
||||
Root,
|
||||
NestedRoot,
|
||||
Content,
|
||||
Description,
|
||||
Overlay,
|
||||
Footer,
|
||||
Header,
|
||||
Title,
|
||||
Trigger,
|
||||
Portal,
|
||||
Close,
|
||||
|
||||
//
|
||||
Root as Drawer,
|
||||
NestedRoot as DrawerNestedRoot,
|
||||
Content as DrawerContent,
|
||||
Description as DrawerDescription,
|
||||
Overlay as DrawerOverlay,
|
||||
Footer as DrawerFooter,
|
||||
Header as DrawerHeader,
|
||||
Title as DrawerTitle,
|
||||
Trigger as DrawerTrigger,
|
||||
Portal as DrawerPortal,
|
||||
Close as DrawerClose,
|
||||
};
|
|
@ -0,0 +1,35 @@
|
|||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import Check from "lucide-svelte/icons/check";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = DropdownMenuPrimitive.CheckboxItemProps;
|
||||
type $$Events = DropdownMenuPrimitive.CheckboxItemEvents;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let checked: $$Props["checked"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
bind:checked
|
||||
class={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:keydown
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:pointerdown
|
||||
on:pointerleave
|
||||
on:pointermove
|
||||
>
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.CheckboxIndicator>
|
||||
<Check class="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.CheckboxIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
|
@ -0,0 +1,27 @@
|
|||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn, flyAndScale } from "$lib/utils";
|
||||
|
||||
type $$Props = DropdownMenuPrimitive.ContentProps;
|
||||
type $$Events = DropdownMenuPrimitive.ContentEvents;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let sideOffset: $$Props["sideOffset"] = 4;
|
||||
export let transition: $$Props["transition"] = flyAndScale;
|
||||
export let transitionConfig: $$Props["transitionConfig"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Content
|
||||
{transition}
|
||||
{transitionConfig}
|
||||
{sideOffset}
|
||||
class={cn(
|
||||
"z-50 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md focus:outline-none",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:keydown
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuPrimitive.Content>
|
|
@ -0,0 +1,31 @@
|
|||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = DropdownMenuPrimitive.ItemProps & {
|
||||
inset?: boolean;
|
||||
};
|
||||
type $$Events = DropdownMenuPrimitive.ItemEvents;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let inset: $$Props["inset"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Item
|
||||
class={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:keydown
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:pointerdown
|
||||
on:pointerleave
|
||||
on:pointermove
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuPrimitive.Item>
|
|
@ -0,0 +1,19 @@
|
|||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = DropdownMenuPrimitive.LabelProps & {
|
||||
inset?: boolean;
|
||||
};
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let inset: $$Props["inset"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Label
|
||||
class={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuPrimitive.Label>
|
|
@ -0,0 +1,11 @@
|
|||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
|
||||
type $$Props = DropdownMenuPrimitive.RadioGroupProps;
|
||||
|
||||
export let value: $$Props["value"] = undefined;
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.RadioGroup {...$$restProps} bind:value>
|
||||
<slot />
|
||||
</DropdownMenuPrimitive.RadioGroup>
|
|
@ -0,0 +1,35 @@
|
|||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import Circle from "lucide-svelte/icons/circle";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = DropdownMenuPrimitive.RadioItemProps;
|
||||
type $$Events = DropdownMenuPrimitive.RadioItemEvents;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let value: $$Props["value"];
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
class={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{value}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:keydown
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:pointerdown
|
||||
on:pointerleave
|
||||
on:pointermove
|
||||
>
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.RadioIndicator>
|
||||
<Circle class="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.RadioIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
</DropdownMenuPrimitive.RadioItem>
|
|
@ -0,0 +1,14 @@
|
|||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = DropdownMenuPrimitive.SeparatorProps;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Separator
|
||||
class={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...$$restProps}
|
||||
/>
|
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { cn } from "$lib/utils";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLSpanElement>;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<span class={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...$$restProps}>
|
||||
<slot />
|
||||
</span>
|
|
@ -0,0 +1,30 @@
|
|||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn, flyAndScale } from "$lib/utils";
|
||||
|
||||
type $$Props = DropdownMenuPrimitive.SubContentProps;
|
||||
type $$Events = DropdownMenuPrimitive.SubContentEvents;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let transition: $$Props["transition"] = flyAndScale;
|
||||
export let transitionConfig: $$Props["transitionConfig"] = {
|
||||
x: -10,
|
||||
y: 0,
|
||||
};
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
{transition}
|
||||
{transitionConfig}
|
||||
class={cn(
|
||||
"z-50 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-lg focus:outline-none",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:keydown
|
||||
on:focusout
|
||||
on:pointermove
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuPrimitive.SubContent>
|
|
@ -0,0 +1,32 @@
|
|||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import ChevronRight from "lucide-svelte/icons/chevron-right";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = DropdownMenuPrimitive.SubTriggerProps & {
|
||||
inset?: boolean;
|
||||
};
|
||||
type $$Events = DropdownMenuPrimitive.SubTriggerEvents;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let inset: $$Props["inset"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
class={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[state=open]:bg-accent data-[highlighted]:text-accent-foreground data-[state=open]:text-accent-foreground",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:keydown
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:pointerleave
|
||||
on:pointermove
|
||||
>
|
||||
<slot />
|
||||
<ChevronRight class="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
48
frontend/src/lib/components/ui/dropdown-menu/index.ts
Normal file
48
frontend/src/lib/components/ui/dropdown-menu/index.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import Item from "./dropdown-menu-item.svelte";
|
||||
import Label from "./dropdown-menu-label.svelte";
|
||||
import Content from "./dropdown-menu-content.svelte";
|
||||
import Shortcut from "./dropdown-menu-shortcut.svelte";
|
||||
import RadioItem from "./dropdown-menu-radio-item.svelte";
|
||||
import Separator from "./dropdown-menu-separator.svelte";
|
||||
import RadioGroup from "./dropdown-menu-radio-group.svelte";
|
||||
import SubContent from "./dropdown-menu-sub-content.svelte";
|
||||
import SubTrigger from "./dropdown-menu-sub-trigger.svelte";
|
||||
import CheckboxItem from "./dropdown-menu-checkbox-item.svelte";
|
||||
|
||||
const Sub = DropdownMenuPrimitive.Sub;
|
||||
const Root = DropdownMenuPrimitive.Root;
|
||||
const Trigger = DropdownMenuPrimitive.Trigger;
|
||||
const Group = DropdownMenuPrimitive.Group;
|
||||
|
||||
export {
|
||||
Sub,
|
||||
Root,
|
||||
Item,
|
||||
Label,
|
||||
Group,
|
||||
Trigger,
|
||||
Content,
|
||||
Shortcut,
|
||||
Separator,
|
||||
RadioItem,
|
||||
SubContent,
|
||||
SubTrigger,
|
||||
RadioGroup,
|
||||
CheckboxItem,
|
||||
//
|
||||
Root as DropdownMenu,
|
||||
Sub as DropdownMenuSub,
|
||||
Item as DropdownMenuItem,
|
||||
Label as DropdownMenuLabel,
|
||||
Group as DropdownMenuGroup,
|
||||
Content as DropdownMenuContent,
|
||||
Trigger as DropdownMenuTrigger,
|
||||
Shortcut as DropdownMenuShortcut,
|
||||
RadioItem as DropdownMenuRadioItem,
|
||||
Separator as DropdownMenuSeparator,
|
||||
RadioGroup as DropdownMenuRadioGroup,
|
||||
SubContent as DropdownMenuSubContent,
|
||||
SubTrigger as DropdownMenuSubTrigger,
|
||||
CheckboxItem as DropdownMenuCheckboxItem,
|
||||
};
|
27
frontend/src/lib/components/ui/input/index.ts
Normal file
27
frontend/src/lib/components/ui/input/index.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import Root from "./input.svelte";
|
||||
|
||||
type FormInputEvent<T extends Event = Event> = T & {
|
||||
currentTarget: EventTarget & HTMLInputElement;
|
||||
};
|
||||
export type InputEvents = {
|
||||
blur: FormInputEvent<FocusEvent>;
|
||||
change: FormInputEvent<Event>;
|
||||
click: FormInputEvent<MouseEvent>;
|
||||
focus: FormInputEvent<FocusEvent>;
|
||||
focusin: FormInputEvent<FocusEvent>;
|
||||
focusout: FormInputEvent<FocusEvent>;
|
||||
keydown: FormInputEvent<KeyboardEvent>;
|
||||
keypress: FormInputEvent<KeyboardEvent>;
|
||||
keyup: FormInputEvent<KeyboardEvent>;
|
||||
mouseover: FormInputEvent<MouseEvent>;
|
||||
mouseenter: FormInputEvent<MouseEvent>;
|
||||
mouseleave: FormInputEvent<MouseEvent>;
|
||||
paste: FormInputEvent<ClipboardEvent>;
|
||||
input: FormInputEvent<InputEvent>;
|
||||
};
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Input,
|
||||
};
|
35
frontend/src/lib/components/ui/input/input.svelte
Normal file
35
frontend/src/lib/components/ui/input/input.svelte
Normal file
|
@ -0,0 +1,35 @@
|
|||
<script lang="ts">
|
||||
import type { HTMLInputAttributes } from "svelte/elements";
|
||||
import { cn } from "$lib/utils";
|
||||
import type { InputEvents } from ".";
|
||||
|
||||
type $$Props = HTMLInputAttributes;
|
||||
type $$Events = InputEvents;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let value: $$Props["value"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<input
|
||||
class={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
bind:value
|
||||
on:blur
|
||||
on:change
|
||||
on:click
|
||||
on:focus
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:keydown
|
||||
on:keypress
|
||||
on:keyup
|
||||
on:mouseover
|
||||
on:mouseenter
|
||||
on:mouseleave
|
||||
on:paste
|
||||
on:input
|
||||
{...$$restProps}
|
||||
/>
|
7
frontend/src/lib/components/ui/label/index.ts
Normal file
7
frontend/src/lib/components/ui/label/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import Root from "./label.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Label,
|
||||
};
|
21
frontend/src/lib/components/ui/label/label.svelte
Normal file
21
frontend/src/lib/components/ui/label/label.svelte
Normal file
|
@ -0,0 +1,21 @@
|
|||
<script lang="ts">
|
||||
import { Label as LabelPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = LabelPrimitive.Props;
|
||||
type $$Events = LabelPrimitive.Events;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<LabelPrimitive.Root
|
||||
class={cn(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:mousedown
|
||||
>
|
||||
<slot />
|
||||
</LabelPrimitive.Root>
|
15
frontend/src/lib/components/ui/radio-group/index.ts
Normal file
15
frontend/src/lib/components/ui/radio-group/index.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { RadioGroup as RadioGroupPrimitive } from "bits-ui";
|
||||
|
||||
import Root from "./radio-group.svelte";
|
||||
import Item from "./radio-group-item.svelte";
|
||||
const Input = RadioGroupPrimitive.Input;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Input,
|
||||
Item,
|
||||
//
|
||||
Root as RadioGroup,
|
||||
Input as RadioGroupInput,
|
||||
Item as RadioGroupItem,
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
<script lang="ts">
|
||||
import { RadioGroup as RadioGroupPrimitive } from "bits-ui";
|
||||
import Circle from "lucide-svelte/icons/circle";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = RadioGroupPrimitive.ItemProps;
|
||||
type $$Events = RadioGroupPrimitive.ItemEvents;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let value: $$Props["value"];
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<RadioGroupPrimitive.Item
|
||||
{value}
|
||||
class={cn(
|
||||
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
>
|
||||
<div class="flex items-center justify-center">
|
||||
<RadioGroupPrimitive.ItemIndicator>
|
||||
<Circle class="h-2.5 w-2.5 fill-current text-current" />
|
||||
</RadioGroupPrimitive.ItemIndicator>
|
||||
</div>
|
||||
</RadioGroupPrimitive.Item>
|
|
@ -0,0 +1,14 @@
|
|||
<script lang="ts">
|
||||
import { RadioGroup as RadioGroupPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = RadioGroupPrimitive.Props;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let value: $$Props["value"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<RadioGroupPrimitive.Root bind:value class={cn("grid gap-2", className)} {...$$restProps}>
|
||||
<slot />
|
||||
</RadioGroupPrimitive.Root>
|
34
frontend/src/lib/components/ui/select/index.ts
Normal file
34
frontend/src/lib/components/ui/select/index.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
|
||||
import Label from "./select-label.svelte";
|
||||
import Item from "./select-item.svelte";
|
||||
import Content from "./select-content.svelte";
|
||||
import Trigger from "./select-trigger.svelte";
|
||||
import Separator from "./select-separator.svelte";
|
||||
|
||||
const Root = SelectPrimitive.Root;
|
||||
const Group = SelectPrimitive.Group;
|
||||
const Input = SelectPrimitive.Input;
|
||||
const Value = SelectPrimitive.Value;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Group,
|
||||
Input,
|
||||
Label,
|
||||
Item,
|
||||
Value,
|
||||
Content,
|
||||
Trigger,
|
||||
Separator,
|
||||
//
|
||||
Root as Select,
|
||||
Group as SelectGroup,
|
||||
Input as SelectInput,
|
||||
Label as SelectLabel,
|
||||
Item as SelectItem,
|
||||
Value as SelectValue,
|
||||
Content as SelectContent,
|
||||
Trigger as SelectTrigger,
|
||||
Separator as SelectSeparator,
|
||||
};
|
39
frontend/src/lib/components/ui/select/select-content.svelte
Normal file
39
frontend/src/lib/components/ui/select/select-content.svelte
Normal file
|
@ -0,0 +1,39 @@
|
|||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn, flyAndScale } from "$lib/utils";
|
||||
import { scale } from "svelte/transition";
|
||||
|
||||
type $$Props = SelectPrimitive.ContentProps;
|
||||
type $$Events = SelectPrimitive.ContentEvents;
|
||||
|
||||
export let sideOffset: $$Props["sideOffset"] = 4;
|
||||
export let inTransition: $$Props["inTransition"] = flyAndScale;
|
||||
export let inTransitionConfig: $$Props["inTransitionConfig"] = undefined;
|
||||
export let outTransition: $$Props["outTransition"] = scale;
|
||||
export let outTransitionConfig: $$Props["outTransitionConfig"] = {
|
||||
start: 0.95,
|
||||
opacity: 0,
|
||||
duration: 50,
|
||||
};
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Content
|
||||
{inTransition}
|
||||
{inTransitionConfig}
|
||||
{outTransition}
|
||||
{outTransitionConfig}
|
||||
{sideOffset}
|
||||
class={cn(
|
||||
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md outline-none",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:keydown
|
||||
>
|
||||
<div class="w-full p-1">
|
||||
<slot />
|
||||
</div>
|
||||
</SelectPrimitive.Content>
|
40
frontend/src/lib/components/ui/select/select-item.svelte
Normal file
40
frontend/src/lib/components/ui/select/select-item.svelte
Normal file
|
@ -0,0 +1,40 @@
|
|||
<script lang="ts">
|
||||
import { cn } from "$lib/utils";
|
||||
import Check from "lucide-svelte/icons/check";
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
|
||||
type $$Props = SelectPrimitive.ItemProps;
|
||||
type $$Events = SelectPrimitive.ItemEvents;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let value: $$Props["value"];
|
||||
export let label: $$Props["label"] = undefined;
|
||||
export let disabled: $$Props["disabled"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Item
|
||||
{value}
|
||||
{disabled}
|
||||
{label}
|
||||
class={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:keydown
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:pointerleave
|
||||
on:pointermove
|
||||
>
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check class="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<slot>
|
||||
{label ? label : value}
|
||||
</slot>
|
||||
</SelectPrimitive.Item>
|
16
frontend/src/lib/components/ui/select/select-label.svelte
Normal file
16
frontend/src/lib/components/ui/select/select-label.svelte
Normal file
|
@ -0,0 +1,16 @@
|
|||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = SelectPrimitive.LabelProps;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Label
|
||||
class={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</SelectPrimitive.Label>
|
|
@ -0,0 +1,11 @@
|
|||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = SelectPrimitive.SeparatorProps;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Separator class={cn("-mx-1 my-1 h-px bg-muted", className)} {...$$restProps} />
|
27
frontend/src/lib/components/ui/select/select-trigger.svelte
Normal file
27
frontend/src/lib/components/ui/select/select-trigger.svelte
Normal file
|
@ -0,0 +1,27 @@
|
|||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import ChevronDown from "lucide-svelte/icons/chevron-down";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = SelectPrimitive.TriggerProps;
|
||||
type $$Events = SelectPrimitive.TriggerEvents;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Trigger
|
||||
class={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
let:builder
|
||||
on:click
|
||||
on:keydown
|
||||
>
|
||||
<slot {builder} />
|
||||
<div>
|
||||
<ChevronDown class="h-4 w-4 opacity-50" />
|
||||
</div>
|
||||
</SelectPrimitive.Trigger>
|
7
frontend/src/lib/components/ui/separator/index.ts
Normal file
7
frontend/src/lib/components/ui/separator/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import Root from "./separator.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Separator,
|
||||
};
|
22
frontend/src/lib/components/ui/separator/separator.svelte
Normal file
22
frontend/src/lib/components/ui/separator/separator.svelte
Normal file
|
@ -0,0 +1,22 @@
|
|||
<script lang="ts">
|
||||
import { Separator as SeparatorPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = SeparatorPrimitive.Props;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let orientation: $$Props["orientation"] = "horizontal";
|
||||
export let decorative: $$Props["decorative"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<SeparatorPrimitive.Root
|
||||
class={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className
|
||||
)}
|
||||
{orientation}
|
||||
{decorative}
|
||||
{...$$restProps}
|
||||
/>
|
106
frontend/src/lib/components/ui/sheet/index.ts
Normal file
106
frontend/src/lib/components/ui/sheet/index.ts
Normal file
|
@ -0,0 +1,106 @@
|
|||
import { Dialog as SheetPrimitive } from "bits-ui";
|
||||
import { tv, type VariantProps } from "tailwind-variants";
|
||||
|
||||
import Portal from "./sheet-portal.svelte";
|
||||
import Overlay from "./sheet-overlay.svelte";
|
||||
import Content from "./sheet-content.svelte";
|
||||
import Header from "./sheet-header.svelte";
|
||||
import Footer from "./sheet-footer.svelte";
|
||||
import Title from "./sheet-title.svelte";
|
||||
import Description from "./sheet-description.svelte";
|
||||
|
||||
const Root = SheetPrimitive.Root;
|
||||
const Close = SheetPrimitive.Close;
|
||||
const Trigger = SheetPrimitive.Trigger;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Close,
|
||||
Trigger,
|
||||
Portal,
|
||||
Overlay,
|
||||
Content,
|
||||
Header,
|
||||
Footer,
|
||||
Title,
|
||||
Description,
|
||||
//
|
||||
Root as Sheet,
|
||||
Close as SheetClose,
|
||||
Trigger as SheetTrigger,
|
||||
Portal as SheetPortal,
|
||||
Overlay as SheetOverlay,
|
||||
Content as SheetContent,
|
||||
Header as SheetHeader,
|
||||
Footer as SheetFooter,
|
||||
Title as SheetTitle,
|
||||
Description as SheetDescription,
|
||||
};
|
||||
|
||||
export const sheetVariants = tv({
|
||||
base: "fixed z-50 gap-4 bg-background p-6 shadow-lg",
|
||||
variants: {
|
||||
side: {
|
||||
top: "inset-x-0 top-0 border-b",
|
||||
bottom: "inset-x-0 bottom-0 border-t",
|
||||
left: "inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
|
||||
right: "inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: "right",
|
||||
},
|
||||
});
|
||||
|
||||
export const sheetTransitions = {
|
||||
top: {
|
||||
in: {
|
||||
y: "-100%",
|
||||
duration: 500,
|
||||
opacity: 1,
|
||||
},
|
||||
out: {
|
||||
y: "-100%",
|
||||
duration: 300,
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
bottom: {
|
||||
in: {
|
||||
y: "100%",
|
||||
duration: 500,
|
||||
opacity: 1,
|
||||
},
|
||||
out: {
|
||||
y: "100%",
|
||||
duration: 300,
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
left: {
|
||||
in: {
|
||||
x: "-100%",
|
||||
duration: 500,
|
||||
opacity: 1,
|
||||
},
|
||||
out: {
|
||||
x: "-100%",
|
||||
duration: 300,
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
right: {
|
||||
in: {
|
||||
x: "100%",
|
||||
duration: 500,
|
||||
opacity: 1,
|
||||
},
|
||||
out: {
|
||||
x: "100%",
|
||||
duration: 300,
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export type Side = VariantProps<typeof sheetVariants>["side"];
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue