1
0
Fork 0
forked from 0x2E/fusion
This commit is contained in:
rook1e 2024-03-06 16:54:13 +08:00
parent 245c8933cb
commit e9b065e9fb
No known key found for this signature in database
GPG key ID: C63289D731719BC0
170 changed files with 10768 additions and 0 deletions

6
.env Normal file
View file

@ -0,0 +1,6 @@
# web server
host="127.0.0.1"
port=8080
# web ui password
password="123"

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
.DS_*
*.db
*.paw
tmp/
build/

0
Dockerfile Normal file
View file

22
README.md Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View file

13
frontend/.eslintignore Normal file
View 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
View 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
View 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
View file

@ -0,0 +1 @@
engine-strict=true

4
frontend/.prettierignore Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load diff

52
frontend/package.json Normal file
View 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"
}
}

View file

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

78
frontend/src/app.css Normal file
View 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
View 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
View 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>

View 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;
};

View 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;
}
]
}
});

View 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
}
});
}

View 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);
}

View 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
}
});
}

View file

@ -0,0 +1,9 @@
import { api } from './api';
export async function login(password: string) {
return api.post('sessions', {
json: {
password: password
}
});
}

View 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 };
};

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View 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>

View 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-2 text-center sm:text-left", className)} {...$$restProps}>
<slot />
</div>

View file

@ -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}
/>

View file

@ -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>

View file

@ -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>

View 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,
};

View 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("text-sm [&_p]:leading-relaxed", className)} {...$$restProps}>
<slot />
</div>

View 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>

View 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>

View 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,
};

View 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>

View 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"];

View 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>

View 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,
};

View 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>

View file

@ -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>

View 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>

View 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>

View 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}
/>

View file

@ -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>

View 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>

View 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,
};

View 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>

View file

@ -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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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,
};

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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}
/>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View 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,
};

View 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,
};

View 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}
/>

View file

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

View 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>

View 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,
};

View file

@ -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>

View file

@ -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>

View 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,
};

View 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>

View 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>

View 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>

View file

@ -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} />

View 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>

View file

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

View 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}
/>

View 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