mirror of
https://github.com/maxdorninger/MediaManager.git
synced 2026-04-17 15:43:28 +02:00
working on the frontend, adding torrent page, working on show directory, working on the api
This commit is contained in:
24
.gitignore
vendored
24
.gitignore
vendored
@@ -11,3 +11,27 @@ tv/*
|
||||
log.txt
|
||||
res/*
|
||||
/backend/src/indexer/indexers/prowlarr.http
|
||||
/web/cache/
|
||||
web/node_modules
|
||||
|
||||
# Output
|
||||
web/.output
|
||||
web/.vercel
|
||||
web/.netlify
|
||||
web/.wrangler
|
||||
web/.svelte-kit
|
||||
web/build
|
||||
|
||||
# OS
|
||||
web/.DS_Store
|
||||
web/Thumbs.db
|
||||
|
||||
# Env
|
||||
web/.env
|
||||
web/.env.*
|
||||
web/!.env.example
|
||||
web/!.env.test
|
||||
|
||||
# Vite
|
||||
web/vite.config.js.timestamp-*
|
||||
web/vite.config.ts.timestamp-*
|
||||
|
||||
@@ -47,7 +47,7 @@ class Prowlarr(GenericIndexer):
|
||||
title=result['sortTitle'],
|
||||
seeders=result['seeders'],
|
||||
flags=result['indexerFlags'],
|
||||
)
|
||||
size=result['size'], )
|
||||
)
|
||||
return result_list
|
||||
else:
|
||||
|
||||
@@ -16,3 +16,4 @@ class IndexerQueryResult(Base):
|
||||
flags = mapped_column(ARRAY(String))
|
||||
quality: Mapped[Quality]
|
||||
season = mapped_column(ARRAY(Integer))
|
||||
size = Mapped[int]
|
||||
|
||||
@@ -19,6 +19,7 @@ class IndexerQueryResult(BaseModel):
|
||||
download_url: str
|
||||
seeders: int
|
||||
flags: list[str]
|
||||
size: int
|
||||
|
||||
@computed_field(return_type=Quality)
|
||||
@property
|
||||
@@ -64,18 +65,6 @@ class IndexerQueryResult(BaseModel):
|
||||
return self.quality.value < other.quality.value
|
||||
return self.seeders > other.seeders
|
||||
|
||||
# def download(self) -> SeasonTorrent:
|
||||
# """
|
||||
# downloads a torrent file and returns the filepath
|
||||
# """
|
||||
# import requests
|
||||
# url = self.download_url
|
||||
# torrent_filepath = self.title + ".torrent"
|
||||
# with open(torrent_filepath, 'wb') as out_file:
|
||||
# content = requests.get(url).content
|
||||
# out_file.write(content)
|
||||
# return SeasonTorrent(status=None, title=self.title, quality=self.quality, id=self.id)
|
||||
|
||||
|
||||
class PublicIndexerQueryResult(BaseModel):
|
||||
title: str
|
||||
@@ -84,3 +73,4 @@ class PublicIndexerQueryResult(BaseModel):
|
||||
seeders: int
|
||||
flags: list[str]
|
||||
season: list[int]
|
||||
size: int
|
||||
|
||||
@@ -11,10 +11,11 @@ def search(query: str, db: Session) -> list[IndexerQueryResult]:
|
||||
|
||||
log.debug(f"Searching for Torrent: {query}")
|
||||
|
||||
for indexer in indexers:
|
||||
results.extend(indexer.get_search_results(query))
|
||||
for i in indexers:
|
||||
results.extend(i.get_search_results(query))
|
||||
for result in results:
|
||||
save_result(result=result, db=db)
|
||||
log.debug(f"Found Torrents: {results}")
|
||||
return results
|
||||
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ from auth.users import oauth_client
|
||||
import uvicorn
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
import tv.router
|
||||
import torrent.router
|
||||
@@ -75,8 +76,7 @@ app = FastAPI(root_path="/api/v1")
|
||||
|
||||
if basic_config.DEVELOPMENT:
|
||||
origins = [
|
||||
"http://localhost:5173",
|
||||
"http://127.0.0.1:5173",
|
||||
"*",
|
||||
]
|
||||
|
||||
app.add_middleware(
|
||||
@@ -153,5 +153,9 @@ app.include_router(
|
||||
prefix="/torrent",
|
||||
tags=["torrent"]
|
||||
)
|
||||
|
||||
# static file routers
|
||||
app.mount("/static/image", StaticFiles(directory=basic_config.image_directory), name="static-images")
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(app, host="127.0.0.1", port=5049, log_config=LOGGING_CONFIG)
|
||||
|
||||
@@ -2,6 +2,7 @@ import logging
|
||||
|
||||
import metadataProvider.tmdb
|
||||
from metadataProvider.abstractMetaDataProvider import metadata_providers
|
||||
from metadataProvider.schemas import MetaDataProviderShowSearchResult
|
||||
from tv.schemas import Show
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -13,5 +14,5 @@ def get_show_metadata(id: int = None, provider: str = "tmdb") -> Show:
|
||||
return metadata_providers[provider].get_show_metadata(id)
|
||||
|
||||
|
||||
def search_show(query: str, provider: str = "tmdb"):
|
||||
def search_show(query: str, provider: str = "tmdb") -> list[MetaDataProviderShowSearchResult]:
|
||||
return metadata_providers[provider].search_show(query)
|
||||
|
||||
@@ -2,6 +2,7 @@ import logging
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
import config
|
||||
from metadataProvider.schemas import MetaDataProviderShowSearchResult
|
||||
from tv.schemas import Show
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -19,7 +20,7 @@ class AbstractMetadataProvider(ABC):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def search_show(self, query):
|
||||
def search_show(self, query) -> list[MetaDataProviderShowSearchResult]:
|
||||
pass
|
||||
|
||||
|
||||
|
||||
11
backend/src/metadataProvider/schemas.py
Normal file
11
backend/src/metadataProvider/schemas.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class MetaDataProviderShowSearchResult(BaseModel):
|
||||
poster_path: str | None
|
||||
overview: str | None
|
||||
name: str
|
||||
external_id: int
|
||||
year: int | None
|
||||
metadata_provider: str
|
||||
added: bool
|
||||
@@ -6,7 +6,9 @@ import tmdbsimple
|
||||
from pydantic_settings import BaseSettings
|
||||
from tmdbsimple import TV, TV_Seasons
|
||||
|
||||
import metadataProvider.utils
|
||||
from metadataProvider.abstractMetaDataProvider import AbstractMetadataProvider, register_metadata_provider
|
||||
from metadataProvider.schemas import MetaDataProviderShowSearchResult
|
||||
from tv.schemas import Episode, Season, Show, SeasonNumber, EpisodeNumber
|
||||
|
||||
|
||||
@@ -56,11 +58,7 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
|
||||
)
|
||||
)
|
||||
|
||||
year: str | None = show_metadata["first_air_date"]
|
||||
if year:
|
||||
year: int = int(year.split('-')[0])
|
||||
else:
|
||||
year = None
|
||||
year = metadataProvider.utils.get_year_from_first_air_date(show_metadata["first_air_date"])
|
||||
|
||||
show = Show(
|
||||
external_id=id,
|
||||
@@ -71,23 +69,42 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
|
||||
metadata_provider=self.name,
|
||||
)
|
||||
|
||||
# TODO: convert images automatically to .jpg
|
||||
# downloading the poster
|
||||
poster_url = "https://image.tmdb.org/t/p/original" + show_metadata["poster_path"]
|
||||
res = requests.get(poster_url, stream=True)
|
||||
content_type = res.headers["content-type"]
|
||||
file_extension = mimetypes.guess_extension(content_type)
|
||||
if res.status_code == 200:
|
||||
with open(self.storage_path.joinpath(str(show.id) + file_extension), 'wb') as f:
|
||||
f.write(res.content)
|
||||
log.info(f"image for show {show.name} successfully downloaded")
|
||||
|
||||
if show_metadata["poster_path"] is not None:
|
||||
poster_url = "https://image.tmdb.org/t/p/original" + show_metadata["poster_path"]
|
||||
res = requests.get(poster_url, stream=True)
|
||||
content_type = res.headers["content-type"]
|
||||
file_extension = mimetypes.guess_extension(content_type)
|
||||
if res.status_code == 200:
|
||||
with open(self.storage_path.joinpath(str(show.id) + file_extension), 'wb') as f:
|
||||
f.write(res.content)
|
||||
log.info(f"image for show {show.name} successfully downloaded")
|
||||
else:
|
||||
log.warning(f"image for show {show.name} could not be downloaded")
|
||||
|
||||
return show
|
||||
|
||||
def search_show(self, query: str):
|
||||
return tmdbsimple.Search().tv(query=query)
|
||||
def search_show(self, query: str) -> list[MetaDataProviderShowSearchResult]:
|
||||
results = tmdbsimple.Search().tv(query=query)
|
||||
formatted_results = []
|
||||
for result in results["results"]:
|
||||
if result["poster_path"] is not None:
|
||||
poster_url = "https://image.tmdb.org/t/p/original" + result["poster_path"]
|
||||
else:
|
||||
poster_url = None
|
||||
formatted_results.append(
|
||||
MetaDataProviderShowSearchResult(
|
||||
poster_path=poster_url,
|
||||
overview=result["overview"],
|
||||
name=result["name"],
|
||||
external_id=result["id"],
|
||||
year=metadataProvider.utils.get_year_from_first_air_date(result["first_air_date"]),
|
||||
metadata_provider=self.name,
|
||||
added=False,
|
||||
)
|
||||
)
|
||||
return formatted_results
|
||||
|
||||
def __init__(self, api_key: str = None):
|
||||
tmdbsimple.API_KEY = api_key
|
||||
|
||||
5
backend/src/metadataProvider/utils.py
Normal file
5
backend/src/metadataProvider/utils.py
Normal file
@@ -0,0 +1,5 @@
|
||||
def get_year_from_first_air_date(first_air_date: str | None) -> int | None:
|
||||
if first_air_date:
|
||||
return int(first_air_date.split('-')[0])
|
||||
else:
|
||||
return None
|
||||
@@ -6,6 +6,8 @@ import tv.service
|
||||
from auth.users import current_active_user, current_superuser
|
||||
from database import DbSessionDependency
|
||||
from indexer.schemas import PublicIndexerQueryResult, IndexerQueryResultId
|
||||
from metadataProvider.schemas import MetaDataProviderShowSearchResult
|
||||
from torrent.schemas import Torrent
|
||||
from tv.exceptions import MediaAlreadyExists
|
||||
from tv.schemas import Show, SeasonRequest, ShowId
|
||||
|
||||
@@ -40,10 +42,12 @@ def delete_a_show(db: DbSessionDependency, show_id: ShowId):
|
||||
# --------------------------------
|
||||
|
||||
@router.get("/shows", dependencies=[Depends(current_active_user)], response_model=list[Show])
|
||||
def get_all_shows(db: DbSessionDependency):
|
||||
def get_all_shows(db: DbSessionDependency, external_id: int = None, metadata_provider: str = "tmdb"):
|
||||
""""""
|
||||
return tv.service.get_all_shows(db=db)
|
||||
|
||||
if external_id is not None:
|
||||
return tv.service.get_show_by_external_id(db=db, external_id=external_id, metadata_provider=metadata_provider)
|
||||
else:
|
||||
return tv.service.get_all_shows(db=db)
|
||||
|
||||
@router.get("/shows/{show_id}", dependencies=[Depends(current_active_user)], response_model=Show)
|
||||
def get_a_show(db: DbSessionDependency, show_id: ShowId):
|
||||
@@ -94,7 +98,8 @@ def get_torrents_for_a_season(db: DbSessionDependency, show_id: ShowId, season_n
|
||||
|
||||
|
||||
# download a torrent
|
||||
@router.post("/torrents", status_code=status.HTTP_200_OK, dependencies=[Depends(current_superuser)])
|
||||
@router.post("/torrents", status_code=status.HTTP_200_OK, response_model=Torrent,
|
||||
dependencies=[Depends(current_superuser)])
|
||||
def download_a_torrent(db: DbSessionDependency, public_indexer_result_id: IndexerQueryResultId, show_id: ShowId,
|
||||
override_file_path_suffix: str = ""):
|
||||
return tv.service.download_torrent(db=db, public_indexer_result_id=public_indexer_result_id, show_id=show_id,
|
||||
@@ -104,6 +109,7 @@ def download_a_torrent(db: DbSessionDependency, public_indexer_result_id: Indexe
|
||||
# SEARCH SHOWS ON METADATA PROVIDERS
|
||||
# --------------------------------
|
||||
|
||||
@router.get("/search", dependencies=[Depends(current_active_user)])
|
||||
def search_metadata_providers_for_a_show(query: str, metadata_provider: str = "tmdb"):
|
||||
return metadataProvider.search_show(query, metadata_provider)
|
||||
@router.get("/search", dependencies=[Depends(current_active_user)],
|
||||
response_model=list[MetaDataProviderShowSearchResult])
|
||||
def search_metadata_providers_for_a_show(db: DbSessionDependency, query: str, metadata_provider: str = "tmdb"):
|
||||
return tv.service.search_for_show(query=query, metadata_provider=metadata_provider, db=db)
|
||||
|
||||
@@ -45,7 +45,7 @@ class Show(BaseModel):
|
||||
|
||||
name: str
|
||||
overview: str
|
||||
year: int
|
||||
year: int | None
|
||||
|
||||
external_id: int
|
||||
metadata_provider: str
|
||||
|
||||
@@ -5,6 +5,7 @@ import metadataProvider
|
||||
import tv.repository
|
||||
from indexer import IndexerQueryResult
|
||||
from indexer.schemas import IndexerQueryResultId
|
||||
from metadataProvider.schemas import MetaDataProviderShowSearchResult
|
||||
from torrent.schemas import Torrent
|
||||
from torrent.service import TorrentService
|
||||
from tv import log
|
||||
@@ -53,11 +54,15 @@ def get_all_available_torrents_for_a_season(db: Session, season_number: int, sho
|
||||
IndexerQueryResult]:
|
||||
log.debug(f"getting all available torrents for season {season_number} for show {show_id}")
|
||||
show = tv.repository.get_show(show_id=show_id, db=db)
|
||||
if search_query_override is not None:
|
||||
if search_query_override:
|
||||
search_query = search_query_override
|
||||
else:
|
||||
search_query = show.name + " Season " + str(season_number)
|
||||
# TODO: add more Search query strings and combine all the results, like "season 3", "s03", "s3"
|
||||
search_query = show.name + " s" + str(season_number).zfill(2)
|
||||
torrents: list[IndexerQueryResult] = indexer.service.search(query=search_query, db=db)
|
||||
if search_query_override:
|
||||
log.debug(f"Found with search query override {torrents.__len__()} torrents: {torrents}")
|
||||
return torrents
|
||||
result: list[IndexerQueryResult] = []
|
||||
for torrent in torrents:
|
||||
if season_number in torrent.season:
|
||||
@@ -70,10 +75,20 @@ def get_all_shows(db: Session) -> list[Show]:
|
||||
return tv.repository.get_shows(db=db)
|
||||
|
||||
|
||||
def search_for_show(query: str, metadata_provider: str, db: Session) -> list[MetaDataProviderShowSearchResult]:
|
||||
results = metadataProvider.search_show(query, metadata_provider)
|
||||
for result in results:
|
||||
if check_if_show_exists(db=db, external_id=result.external_id, metadata_provider=metadata_provider):
|
||||
result.added = True
|
||||
return results
|
||||
|
||||
def get_show_by_id(db: Session, show_id: ShowId) -> Show | None:
|
||||
return tv.repository.get_show(show_id=show_id, db=db)
|
||||
|
||||
|
||||
def get_show_by_external_id(db: Session, external_id: int, metadata_provider: str) -> Show | None:
|
||||
return tv.repository.get_show_by_external_id(external_id=external_id, metadata_provider=metadata_provider, db=db)
|
||||
|
||||
def get_season(db: Session, season_id: SeasonId) -> Season:
|
||||
return tv.repository.get_season(season_id=season_id, db=db)
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ services:
|
||||
- TZ=Etc/UTC
|
||||
volumes:
|
||||
- .\res\prowlarr:/config
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "9696:9696"
|
||||
qbittorrent:
|
||||
|
||||
23
web/.gitignore
vendored
23
web/.gitignore
vendored
@@ -1,23 +0,0 @@
|
||||
node_modules
|
||||
|
||||
# Output
|
||||
.output
|
||||
.vercel
|
||||
.netlify
|
||||
.wrangler
|
||||
/.svelte-kit
|
||||
/build
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Env
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.test
|
||||
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": [
|
||||
"prettier-plugin-svelte",
|
||||
"prettier-plugin-tailwindcss"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.svelte",
|
||||
"options": {
|
||||
"parser": "svelte"
|
||||
}
|
||||
}
|
||||
]
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": [
|
||||
"prettier-plugin-svelte",
|
||||
"prettier-plugin-tailwindcss"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.svelte",
|
||||
"options": {
|
||||
"parser": "svelte"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,10 +5,35 @@ import svelte from 'eslint-plugin-svelte';
|
||||
import globals from 'globals';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
import ts from 'typescript-eslint';
|
||||
import unusedImports from 'eslint-plugin-unused-imports';
|
||||
|
||||
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
|
||||
|
||||
export default ts.config(
|
||||
{
|
||||
plugins: {
|
||||
'unused-imports': unusedImports
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'unused-imports/no-unused-imports': 'error',
|
||||
'unused-imports/no-unused-vars': [
|
||||
'warn',
|
||||
{
|
||||
vars: 'all',
|
||||
varsIgnorePattern: '^_',
|
||||
args: 'after-used',
|
||||
argsIgnorePattern: '^_'
|
||||
}
|
||||
],
|
||||
"sort-imports": [
|
||||
"error",
|
||||
{
|
||||
"ignoreDeclarationSort": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
includeIgnoreFile(gitignorePath),
|
||||
js.configs.recommended,
|
||||
...ts.configs.recommended,
|
||||
|
||||
2472
web/package-lock.json
generated
2472
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
101
web/package.json
101
web/package.json
@@ -1,48 +1,57 @@
|
||||
{
|
||||
"name": "web",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "eslint . && prettier --check .",
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.2.5",
|
||||
"@eslint/js": "^9.18.0",
|
||||
"@fontsource/fira-mono": "^5.0.0",
|
||||
"@lucide/svelte": "^0.503.0",
|
||||
"@neoconfetti/svelte": "^2.0.0",
|
||||
"@sveltejs/adapter-static": "^3.0.8",
|
||||
"@sveltejs/kit": "^2.16.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"bits-ui": "^1.3.19",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-svelte": "^2.46.1",
|
||||
"globals": "^15.14.0",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.10",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwind-variants": "^1.0.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "^5.0.0",
|
||||
"typescript-eslint": "^8.20.0",
|
||||
"vite": "^6.0.0"
|
||||
}
|
||||
"name": "web",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "eslint --fix . && prettier --check .",
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.2.5",
|
||||
"@eslint/js": "^9.18.0",
|
||||
"@fontsource/fira-mono": "^5.0.0",
|
||||
"@lucide/svelte": "^0.503.0",
|
||||
"@neoconfetti/svelte": "^2.0.0",
|
||||
"@sveltejs/adapter-static": "^3.0.8",
|
||||
"@sveltejs/kit": "^2.16.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"bits-ui": "^1.4.6",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-svelte": "^2.46.1",
|
||||
"globals": "^15.14.0",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.10",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwind-variants": "^1.0.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "^5.0.0",
|
||||
"typescript-eslint": "^8.20.0",
|
||||
"vite": "^6.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.806.0",
|
||||
"@sveltejs/adapter-node": "^5.2.12",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"lucide-svelte": "^0.507.0",
|
||||
"sharp": "^0.34.1",
|
||||
"sveltekit-image-optimize": "^0.0.7",
|
||||
"uuid": "^11.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,4 +73,4 @@
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
7
web/src/app.d.ts
vendored
7
web/src/app.d.ts
vendored
@@ -7,13 +7,6 @@ declare global {
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
is_active?: boolean;
|
||||
is_superuser?: boolean;
|
||||
is_verified?: boolean;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
12
web/src/hooks.server.ts
Normal file
12
web/src/hooks.server.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import {createImageOptimizer} from 'sveltekit-image-optimize';
|
||||
import type {Handle} from '@sveltejs/kit';
|
||||
import {createFileSystemCache} from 'sveltekit-image-optimize/cache-adapters';
|
||||
|
||||
const cache = createFileSystemCache('./cache');
|
||||
const imageHandler = createImageOptimizer({
|
||||
cache: cache,
|
||||
fallbackFormat: 'avif',
|
||||
quality: 20
|
||||
});
|
||||
|
||||
export const handle: Handle = imageHandler;
|
||||
@@ -1,122 +1,107 @@
|
||||
<script lang="ts" module>
|
||||
import BookOpen from "@lucide/svelte/icons/book-open";
|
||||
import Bot from "@lucide/svelte/icons/bot";
|
||||
import Settings2 from "@lucide/svelte/icons/settings-2";
|
||||
import SquareTerminal from "@lucide/svelte/icons/square-terminal";
|
||||
import LifeBuoy from '@lucide/svelte/icons/life-buoy';
|
||||
import Send from '@lucide/svelte/icons/send';
|
||||
import TvIcon from '@lucide/svelte/icons/tv';
|
||||
import LayoutPanelLeft from '@lucide/svelte/icons/layout-panel-left';
|
||||
import DownloadIcon from '@lucide/svelte/icons/download';
|
||||
|
||||
const data = {
|
||||
navMain: [
|
||||
{
|
||||
title: "Playground",
|
||||
url: "#",
|
||||
icon: SquareTerminal,
|
||||
isActive: true,
|
||||
items: [
|
||||
{
|
||||
title: "History",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Starred",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Models",
|
||||
url: "#",
|
||||
icon: Bot,
|
||||
items: [
|
||||
{
|
||||
title: "Genesis",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Explorer",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Quantum",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Documentation",
|
||||
url: "#",
|
||||
icon: BookOpen,
|
||||
items: [
|
||||
{
|
||||
title: "Introduction",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Get Started",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Tutorials",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Changelog",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
url: "#",
|
||||
icon: Settings2,
|
||||
items: [
|
||||
{
|
||||
title: "General",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Team",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Billing",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Limits",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const data = {
|
||||
navMain: [
|
||||
{
|
||||
title: 'TV',
|
||||
url: '#',
|
||||
icon: TvIcon,
|
||||
isActive: true,
|
||||
items: [
|
||||
{
|
||||
title: 'Shows',
|
||||
url: '/dashboard/tv'
|
||||
},
|
||||
{
|
||||
title: 'Add Show',
|
||||
url: '/dashboard/tv/add-show'
|
||||
},
|
||||
{
|
||||
title: 'Settings',
|
||||
url: '#'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Torrents',
|
||||
url: '#',
|
||||
icon: DownloadIcon,
|
||||
isActive: true,
|
||||
items: [
|
||||
{
|
||||
title: 'Show Torrents',
|
||||
url: '/dashboard/torrents'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
navSecondary: [
|
||||
{
|
||||
title: 'Support',
|
||||
url: '#',
|
||||
icon: LifeBuoy
|
||||
},
|
||||
{
|
||||
title: 'Feedback',
|
||||
url: '#',
|
||||
icon: Send
|
||||
}
|
||||
],
|
||||
projects: [
|
||||
{
|
||||
name: 'Dashboard',
|
||||
url: '/dashboard',
|
||||
icon: LayoutPanelLeft
|
||||
}
|
||||
]
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import NavMain from "$lib/components/nav-main.svelte";
|
||||
import NavUser from "$lib/components/nav-user.svelte";
|
||||
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
|
||||
import type {ComponentProps} from "svelte";
|
||||
import NavMain from '$lib/components/nav-main.svelte';
|
||||
import NavProjects from '$lib/components/nav-projects.svelte';
|
||||
import NavSecondary from '$lib/components/nav-secondary.svelte';
|
||||
import NavUser from '$lib/components/nav-user.svelte';
|
||||
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
||||
import Command from '@lucide/svelte/icons/command';
|
||||
import type {ComponentProps} from 'svelte';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
collapsible = "icon",
|
||||
...restProps
|
||||
}: ComponentProps<typeof Sidebar.Root> = $props();
|
||||
let {ref = $bindable(null), ...restProps}: ComponentProps<typeof Sidebar.Root> = $props();
|
||||
</script>
|
||||
|
||||
<Sidebar.Root {...restProps} bind:ref {collapsible}>
|
||||
<Sidebar.Header>
|
||||
<h1 class="font-bold">MediaManager</h1>
|
||||
</Sidebar.Header>
|
||||
<Sidebar.Content>
|
||||
<NavMain items={data.navMain}/>
|
||||
</Sidebar.Content>
|
||||
<Sidebar.Footer>
|
||||
<NavUser user={data.user}/>
|
||||
</Sidebar.Footer>
|
||||
<Sidebar.Rail/>
|
||||
<Sidebar.Root bind:ref variant="inset" {...restProps}>
|
||||
<Sidebar.Header>
|
||||
<Sidebar.Menu>
|
||||
<Sidebar.MenuItem>
|
||||
<Sidebar.MenuButton size="lg">
|
||||
{#snippet child({props})}
|
||||
<a href="##" {...props}>
|
||||
<div
|
||||
class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"
|
||||
>
|
||||
<Command class="size-4"/>
|
||||
</div>
|
||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||
<span class="truncate font-semibold">Acme Inc</span>
|
||||
<span class="truncate text-xs">Enterprise</span>
|
||||
</div>
|
||||
</a>
|
||||
{/snippet}
|
||||
</Sidebar.MenuButton>
|
||||
</Sidebar.MenuItem>
|
||||
</Sidebar.Menu>
|
||||
</Sidebar.Header>
|
||||
<Sidebar.Content>
|
||||
<NavMain items={data.navMain}/>
|
||||
<NavProjects projects={data.projects}/>
|
||||
<NavSecondary items={data.navSecondary} class="mt-auto"/>
|
||||
</Sidebar.Content>
|
||||
<Sidebar.Footer>
|
||||
<NavUser/>
|
||||
</Sidebar.Footer>
|
||||
</Sidebar.Root>
|
||||
|
||||
67
web/src/lib/components/chip.svelte
Normal file
67
web/src/lib/components/chip.svelte
Normal file
@@ -0,0 +1,67 @@
|
||||
<script lang="ts">
|
||||
import {cn} from '$lib/utils'; // Assuming you have the cn utility from shadcn-svelte
|
||||
|
||||
let {
|
||||
label,
|
||||
variant = 'default',
|
||||
size = 'default',
|
||||
onClose = undefined,
|
||||
class: className = ''
|
||||
} = $props<{
|
||||
label: string;
|
||||
variant?: 'default' | 'secondary' | 'outline' | 'destructive';
|
||||
size?: 'default' | 'sm' | 'lg';
|
||||
onClose?: () => void;
|
||||
class?: string;
|
||||
}>();
|
||||
|
||||
// Base styles for the chip
|
||||
const baseStyles =
|
||||
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50';
|
||||
|
||||
// Variant styles
|
||||
const variantStyles = {
|
||||
default:
|
||||
'border bg-background text-foreground shadow-sm hover:bg-accent hover:text-accent-foreground',
|
||||
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
||||
outline:
|
||||
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
|
||||
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90'
|
||||
};
|
||||
|
||||
// Size styles
|
||||
const sizeStyles = {
|
||||
default: 'h-9 px-3 py-0.5', // Adjusted height for New York style
|
||||
sm: 'h-7 px-2 py-0.5 text-xs', // Adjusted height for New York style
|
||||
lg: 'h-10 px-4 py-0.5' // Adjusted height for New York style
|
||||
};
|
||||
|
||||
// Styles for the close button
|
||||
const closeButtonStyles =
|
||||
'ml-1 inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full';
|
||||
</script>
|
||||
|
||||
<div class={cn(baseStyles, variantStyles[variant], sizeStyles[size], className)}>
|
||||
{label}
|
||||
{#if onClose}
|
||||
<button
|
||||
class={cn(closeButtonStyles, 'hover:bg-accent-foreground/20')}
|
||||
onclick={onClose}
|
||||
aria-label="remove tag"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="h-3 w-3"
|
||||
>
|
||||
<path d="M18 6L6 18"/>
|
||||
<path d="M6 6L18 18"/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,102 +1,99 @@
|
||||
<script lang="ts">
|
||||
import {Button} from "$lib/components/ui/button/index.js";
|
||||
import {Input} from "$lib/components/ui/input/index.js";
|
||||
import {Label} from "$lib/components/ui/label/index.js";
|
||||
import {goto} from "$app/navigation";
|
||||
import {env} from "$env/dynamic/public"
|
||||
import {Button} from '$lib/components/ui/button/index.js';
|
||||
import * as Card from '$lib/components/ui/card/index.js';
|
||||
import {Input} from '$lib/components/ui/input/index.js';
|
||||
import {Label} from '$lib/components/ui/label/index.js';
|
||||
import {goto} from '$app/navigation';
|
||||
import {env} from '$env/dynamic/public';
|
||||
|
||||
let apiUrl = env.PUBLIC_API_URL
|
||||
let apiUrl = env.PUBLIC_API_URL;
|
||||
|
||||
let email = '';
|
||||
let password = '';
|
||||
let errorMessage = '';
|
||||
let isLoading = false;
|
||||
let email = '';
|
||||
let password = '';
|
||||
let errorMessage = '';
|
||||
let isLoading = false;
|
||||
|
||||
async function handleLogin(event: Event) {
|
||||
event.preventDefault();
|
||||
async function handleLogin(event: Event) {
|
||||
event.preventDefault();
|
||||
|
||||
isLoading = true;
|
||||
errorMessage = '';
|
||||
isLoading = true;
|
||||
errorMessage = '';
|
||||
|
||||
const formData = new URLSearchParams();
|
||||
formData.append('username', email);
|
||||
formData.append('password', password);
|
||||
console.log(apiUrl + '/auth/cookie/login')
|
||||
try {
|
||||
const response = await fetch(apiUrl + '/auth/cookie/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: formData.toString(),
|
||||
credentials: 'include',
|
||||
});
|
||||
const formData = new URLSearchParams();
|
||||
formData.append('username', email);
|
||||
formData.append('password', password);
|
||||
console.log(apiUrl + '/auth/cookie/login');
|
||||
try {
|
||||
const response = await fetch(apiUrl + '/auth/cookie/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: formData.toString(),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.log('Login successful!');
|
||||
console.log("Received User Data: ", response);
|
||||
await goto('/dashboard');
|
||||
errorMessage = 'Login successful! Redirecting...';
|
||||
|
||||
} else {
|
||||
let errorText = await response.text();
|
||||
try {
|
||||
const errorData = JSON.parse(errorText);
|
||||
errorMessage = errorData.message || 'Login failed. Please check your credentials.';
|
||||
} catch {
|
||||
errorMessage = errorText || 'Login failed. Please check your credentials.';
|
||||
}
|
||||
console.error('Login failed:', response.status, errorText);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Login request failed:', error);
|
||||
errorMessage = 'An error occurred during the login request.';
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
if (response.ok) {
|
||||
console.log('Login successful!');
|
||||
console.log('Received User Data: ', response);
|
||||
await goto('/dashboard');
|
||||
errorMessage = 'Login successful! Redirecting...';
|
||||
} else {
|
||||
let errorText = await response.text();
|
||||
try {
|
||||
const errorData = JSON.parse(errorText);
|
||||
errorMessage = errorData.message || 'Login failed. Please check your credentials.';
|
||||
} catch {
|
||||
errorMessage = errorText || 'Login failed. Please check your credentials.';
|
||||
}
|
||||
console.error('Login failed:', response.status, errorText);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Login request failed:', error);
|
||||
errorMessage = 'An error occurred during the login request.';
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card.Root class="mx-auto max-w-sm">
|
||||
<Card.Header>
|
||||
<Card.Title class="text-2xl">Login</Card.Title>
|
||||
<Card.Description>Enter your email below to login to your account</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<form class="grid gap-4" on:submit={handleLogin}>
|
||||
<div class="grid gap-2">
|
||||
<Label for="email">Email</Label>
|
||||
<Input bind:value={email} id="email" placeholder="m@example.com" required type="email"/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<div class="flex items-center">
|
||||
<Label for="password">Password</Label>
|
||||
<a class="ml-auto inline-block text-sm underline" href="##">
|
||||
Forgot your password?
|
||||
</a>
|
||||
</div>
|
||||
<Input bind:value={password} id="password" required type="password"/>
|
||||
</div>
|
||||
<Card.Header>
|
||||
<Card.Title class="text-2xl">Login</Card.Title>
|
||||
<Card.Description>Enter your email below to login to your account</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<form class="grid gap-4" on:submit={handleLogin}>
|
||||
<div class="grid gap-2">
|
||||
<Label for="email">Email</Label>
|
||||
<Input bind:value={email} id="email" placeholder="m@example.com" required type="email"/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<div class="flex items-center">
|
||||
<Label for="password">Password</Label>
|
||||
<a class="ml-auto inline-block text-sm underline" href="##"> Forgot your password? </a>
|
||||
</div>
|
||||
<Input bind:value={password} id="password" required type="password"/>
|
||||
</div>
|
||||
|
||||
{#if errorMessage}
|
||||
<p class="text-sm text-red-500">{errorMessage}</p>
|
||||
{/if}
|
||||
{#if errorMessage}
|
||||
<p class="text-sm text-red-500">{errorMessage}</p>
|
||||
{/if}
|
||||
|
||||
<Button class="w-full" disabled={isLoading} type="submit">
|
||||
{#if isLoading}
|
||||
Logging in...
|
||||
{:else}
|
||||
Login
|
||||
{/if}
|
||||
</Button>
|
||||
</form>
|
||||
<Button class="w-full" disabled={isLoading} type="submit">
|
||||
{#if isLoading}
|
||||
Logging in...
|
||||
{:else}
|
||||
Login
|
||||
{/if}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<Button class="w-full mt-2" variant="outline">Login with Google</Button>
|
||||
<Button class="mt-2 w-full" variant="outline">Login with Google</Button>
|
||||
|
||||
|
||||
<div class="mt-4 text-center text-sm">
|
||||
Don't have an account?
|
||||
<a class="underline" href="##"> Sign up </a>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
<div class="mt-4 text-center text-sm">
|
||||
Don't have an account?
|
||||
<a class="underline" href="##"> Sign up </a>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
@@ -1,66 +1,68 @@
|
||||
<script lang="ts">
|
||||
import * as Collapsible from '$lib/components/ui/collapsible/index.js';
|
||||
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
||||
import ChevronRight from '@lucide/svelte/icons/chevron-right';
|
||||
|
||||
let {
|
||||
items,
|
||||
}: {
|
||||
items: {
|
||||
title: string;
|
||||
url: string;
|
||||
// this should be `Component` after @lucide/svelte updates types
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
icon?: any;
|
||||
isActive?: boolean;
|
||||
items?: {
|
||||
title: string;
|
||||
url: string;
|
||||
}[];
|
||||
}[];
|
||||
} = $props();
|
||||
let {
|
||||
items
|
||||
}: {
|
||||
items: {
|
||||
title: string;
|
||||
url: string;
|
||||
// This should be `Component` after @lucide/svelte updates types
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
icon: any;
|
||||
isActive?: boolean;
|
||||
items?: {
|
||||
title: string;
|
||||
url: string;
|
||||
}[];
|
||||
}[];
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<Sidebar.Group>
|
||||
<Sidebar.GroupLabel>Platform</Sidebar.GroupLabel>
|
||||
<Sidebar.Menu>
|
||||
{#each items as mainItem (mainItem.title)}
|
||||
<Collapsible.Root open={mainItem.isActive} class="group/collapsible">
|
||||
{#snippet child({props})}
|
||||
<Sidebar.MenuItem {...props}>
|
||||
<Collapsible.Trigger>
|
||||
{#snippet child({props})}
|
||||
<Sidebar.MenuButton {...props}>
|
||||
{#snippet tooltipContent()}
|
||||
{mainItem.title}
|
||||
{/snippet}
|
||||
{#if mainItem.icon}
|
||||
<mainItem.icon/>
|
||||
{/if}
|
||||
<span>{mainItem.title}</span>
|
||||
<ChevronRight
|
||||
class="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
|
||||
/>
|
||||
</Sidebar.MenuButton>
|
||||
{/snippet}
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
{#if mainItem.items}
|
||||
<Sidebar.MenuSub>
|
||||
{#each mainItem.items as subItem (subItem.title)}
|
||||
<Sidebar.MenuSubItem>
|
||||
<Sidebar.MenuSubButton>
|
||||
{#snippet child({props})}
|
||||
<a href={subItem.url} {...props}>
|
||||
<span>{subItem.title}</span>
|
||||
</a>
|
||||
{/snippet}
|
||||
</Sidebar.MenuSubButton>
|
||||
</Sidebar.MenuSubItem>
|
||||
{/each}
|
||||
</Sidebar.MenuSub>
|
||||
{/if}
|
||||
</Collapsible.Content>
|
||||
</Sidebar.MenuItem>
|
||||
{/snippet}
|
||||
</Collapsible.Root>
|
||||
{/each}
|
||||
</Sidebar.Menu>
|
||||
<Sidebar.GroupLabel>Platform</Sidebar.GroupLabel>
|
||||
<Sidebar.Menu>
|
||||
{#each items as mainItem (mainItem.title)}
|
||||
<Collapsible.Root open={mainItem.isActive}>
|
||||
{#snippet child({props})}
|
||||
<Sidebar.MenuItem {...props}>
|
||||
<Sidebar.MenuButton>
|
||||
{#snippet tooltipContent()}
|
||||
{mainItem.title}
|
||||
{/snippet}
|
||||
{#snippet child({props})}
|
||||
<a href={mainItem.url} {...props}>
|
||||
<mainItem.icon/>
|
||||
<span>{mainItem.title}</span>
|
||||
</a>
|
||||
{/snippet}
|
||||
</Sidebar.MenuButton>
|
||||
{#if mainItem.items?.length}
|
||||
<Collapsible.Trigger>
|
||||
{#snippet child({props})}
|
||||
<Sidebar.MenuAction {...props} class="data-[state=open]:rotate-90">
|
||||
<ChevronRight/>
|
||||
<span class="sr-only">Toggle</span>
|
||||
</Sidebar.MenuAction>
|
||||
{/snippet}
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
<Sidebar.MenuSub>
|
||||
{#each mainItem.items as subItem (subItem.title)}
|
||||
<Sidebar.MenuSubItem>
|
||||
<Sidebar.MenuSubButton href={subItem.url}>
|
||||
<span>{subItem.title}</span>
|
||||
</Sidebar.MenuSubButton>
|
||||
</Sidebar.MenuSubItem>
|
||||
{/each}
|
||||
</Sidebar.MenuSub>
|
||||
</Collapsible.Content>
|
||||
{/if}
|
||||
</Sidebar.MenuItem>
|
||||
{/snippet}
|
||||
</Collapsible.Root>
|
||||
{/each}
|
||||
</Sidebar.Menu>
|
||||
</Sidebar.Group>
|
||||
|
||||
@@ -1,70 +1,76 @@
|
||||
<script lang="ts">
|
||||
import {useSidebar} from "$lib/components/ui/sidebar/context.svelte.js";
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
|
||||
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
||||
import {useSidebar} from '$lib/components/ui/sidebar/index.js';
|
||||
import Ellipsis from '@lucide/svelte/icons/ellipsis';
|
||||
import Folder from '@lucide/svelte/icons/folder';
|
||||
import Share from '@lucide/svelte/icons/share';
|
||||
import Trash2 from '@lucide/svelte/icons/trash-2';
|
||||
|
||||
let {
|
||||
projects,
|
||||
}: {
|
||||
projects: {
|
||||
name: string;
|
||||
url: string;
|
||||
// This should be `Component` after @lucide/svelte updates types
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
icon: any;
|
||||
}[];
|
||||
} = $props();
|
||||
let {
|
||||
projects
|
||||
}: {
|
||||
projects: {
|
||||
name: string;
|
||||
url: string;
|
||||
// This should be `Component` after @lucide/svelte updates types
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
icon: any;
|
||||
}[];
|
||||
} = $props();
|
||||
|
||||
const sidebar = useSidebar();
|
||||
const sidebar = useSidebar();
|
||||
</script>
|
||||
|
||||
<Sidebar.Group class="group-data-[collapsible=icon]:hidden">
|
||||
<Sidebar.GroupLabel>Projects</Sidebar.GroupLabel>
|
||||
<Sidebar.Menu>
|
||||
{#each projects as item (item.name)}
|
||||
<Sidebar.MenuItem>
|
||||
<Sidebar.MenuButton>
|
||||
{#snippet child({props})}
|
||||
<a href={item.url} {...props}>
|
||||
<item.icon/>
|
||||
<span>{item.name}</span>
|
||||
</a>
|
||||
{/snippet}
|
||||
</Sidebar.MenuButton>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({props})}
|
||||
<Sidebar.MenuAction showOnHover {...props}>
|
||||
<Ellipsis/>
|
||||
<span class="sr-only">More</span>
|
||||
</Sidebar.MenuAction>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content
|
||||
class="w-48 rounded-lg"
|
||||
side={sidebar.isMobile ? "bottom" : "right"}
|
||||
align={sidebar.isMobile ? "end" : "start"}
|
||||
>
|
||||
<DropdownMenu.Item>
|
||||
<Folder class="text-muted-foreground"/>
|
||||
<span>View Project</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item>
|
||||
<Forward class="text-muted-foreground"/>
|
||||
<span>Share Project</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator/>
|
||||
<DropdownMenu.Item>
|
||||
<Trash2 class="text-muted-foreground"/>
|
||||
<span>Delete Project</span>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</Sidebar.MenuItem>
|
||||
{/each}
|
||||
<Sidebar.MenuItem>
|
||||
<Sidebar.MenuButton class="text-sidebar-foreground/70">
|
||||
<Ellipsis class="text-sidebar-foreground/70"/>
|
||||
<span>More</span>
|
||||
</Sidebar.MenuButton>
|
||||
</Sidebar.MenuItem>
|
||||
</Sidebar.Menu>
|
||||
<Sidebar.GroupLabel>Projects</Sidebar.GroupLabel>
|
||||
<Sidebar.Menu>
|
||||
{#each projects as item (item.name)}
|
||||
<Sidebar.MenuItem>
|
||||
<Sidebar.MenuButton>
|
||||
{#snippet child({props})}
|
||||
<a href={item.url} {...props}>
|
||||
<item.icon/>
|
||||
<span>{item.name}</span>
|
||||
</a>
|
||||
{/snippet}
|
||||
</Sidebar.MenuButton>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({props})}
|
||||
<Sidebar.MenuAction showOnHover {...props}>
|
||||
<Ellipsis/>
|
||||
<span class="sr-only">More</span>
|
||||
</Sidebar.MenuAction>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content
|
||||
class="w-48"
|
||||
side={sidebar.isMobile ? 'bottom' : 'right'}
|
||||
align={sidebar.isMobile ? 'end' : 'start'}
|
||||
>
|
||||
<DropdownMenu.Item>
|
||||
<Folder class="text-muted-foreground"/>
|
||||
<span>View Project</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item>
|
||||
<Share class="text-muted-foreground"/>
|
||||
<span>Share Project</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator/>
|
||||
<DropdownMenu.Item>
|
||||
<Trash2 class="text-muted-foreground"/>
|
||||
<span>Delete Project</span>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</Sidebar.MenuItem>
|
||||
{/each}
|
||||
<Sidebar.MenuItem>
|
||||
<Sidebar.MenuButton>
|
||||
<Ellipsis/>
|
||||
<span>More</span>
|
||||
</Sidebar.MenuButton>
|
||||
</Sidebar.MenuItem>
|
||||
</Sidebar.Menu>
|
||||
</Sidebar.Group>
|
||||
|
||||
37
web/src/lib/components/nav-secondary.svelte
Normal file
37
web/src/lib/components/nav-secondary.svelte
Normal file
@@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
||||
import type {ComponentProps} from 'svelte';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
items,
|
||||
...restProps
|
||||
}: {
|
||||
items: {
|
||||
title: string;
|
||||
url: string;
|
||||
// This should be `Component` after @lucide/svelte updates types
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
icon: any;
|
||||
}[];
|
||||
} & ComponentProps<typeof Sidebar.Group> = $props();
|
||||
</script>
|
||||
|
||||
<Sidebar.Group bind:ref {...restProps}>
|
||||
<Sidebar.GroupContent>
|
||||
<Sidebar.Menu>
|
||||
{#each items as item (item.title)}
|
||||
<Sidebar.MenuItem>
|
||||
<Sidebar.MenuButton size="sm">
|
||||
{#snippet child({props})}
|
||||
<a href={item.url} {...props}>
|
||||
<item.icon/>
|
||||
<span>{item.title}</span>
|
||||
</a>
|
||||
{/snippet}
|
||||
</Sidebar.MenuButton>
|
||||
</Sidebar.MenuItem>
|
||||
{/each}
|
||||
</Sidebar.Menu>
|
||||
</Sidebar.GroupContent>
|
||||
</Sidebar.Group>
|
||||
@@ -1,69 +1,89 @@
|
||||
<script lang="ts">
|
||||
import {useSidebar} from "$lib/components/ui/sidebar/index.js";
|
||||
import UserDetails from "$lib/components/user-details.svelte"
|
||||
import BadgeCheck from '@lucide/svelte/icons/badge-check';
|
||||
import Bell from '@lucide/svelte/icons/bell';
|
||||
import ChevronsUpDown from '@lucide/svelte/icons/chevrons-up-down';
|
||||
import CreditCard from '@lucide/svelte/icons/credit-card';
|
||||
import LogOut from '@lucide/svelte/icons/log-out';
|
||||
import Sparkles from '@lucide/svelte/icons/sparkles';
|
||||
|
||||
//let { user }: { user: { name: string; email: string; avatar: string } } = $props();
|
||||
const sidebar = useSidebar();
|
||||
import * as Avatar from '$lib/components/ui/avatar/index.js';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
|
||||
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
||||
import {useSidebar} from '$lib/components/ui/sidebar/index.js';
|
||||
import {getContext} from 'svelte';
|
||||
import UserDetails from './user-details.svelte';
|
||||
import type {User} from '$lib/types';
|
||||
|
||||
const user: () => User = getContext('user');
|
||||
const sidebar = useSidebar();
|
||||
</script>
|
||||
|
||||
<Sidebar.Menu>
|
||||
<Sidebar.MenuItem>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({props})}
|
||||
<Sidebar.MenuButton
|
||||
size="lg"
|
||||
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
{...props}
|
||||
>
|
||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||
<UserDetails/>
|
||||
</div>
|
||||
<ChevronsUpDown class="ml-auto size-4"/>
|
||||
</Sidebar.MenuButton>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content
|
||||
align="end"
|
||||
class="w-[var(--bits-dropdown-menu-anchor-width)] min-w-56 rounded-lg"
|
||||
side={sidebar.isMobile ? "bottom" : "right"}
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenu.Label class="p-0 font-normal">
|
||||
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||
<UserDetails/>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenu.Label>
|
||||
<DropdownMenu.Separator/>
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item>
|
||||
<Sparkles/>
|
||||
Upgrade to Pro
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Separator/>
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item>
|
||||
<BadgeCheck/>
|
||||
Account
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item>
|
||||
<CreditCard/>
|
||||
Billing
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item>
|
||||
<Bell/>
|
||||
Notifications
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Separator/>
|
||||
<DropdownMenu.Item>
|
||||
<LogOut/>
|
||||
Log out
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</Sidebar.MenuItem>
|
||||
<Sidebar.MenuItem>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({props})}
|
||||
<Sidebar.MenuButton
|
||||
{...props}
|
||||
size="lg"
|
||||
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
<Avatar.Root class="h-8 w-8 rounded-lg">
|
||||
<!--<Avatar.Image src={user.avatar} alt={user.name} />-->
|
||||
<Avatar.Fallback class="rounded-lg">CN</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||
<UserDetails/>
|
||||
</div>
|
||||
<ChevronsUpDown class="ml-auto size-4"/>
|
||||
</Sidebar.MenuButton>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content
|
||||
class="w-[var(--bits-dropdown-menu-anchor-width)] min-w-56 rounded-lg"
|
||||
side={sidebar.isMobile ? 'bottom' : 'right'}
|
||||
align="end"
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenu.Label class="p-0 font-normal">
|
||||
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar.Root class="h-8 w-8 rounded-lg">
|
||||
<!--<Avatar.Image src={user.avatar} alt={user.name} />-->
|
||||
<Avatar.Fallback class="rounded-lg">CN</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||
<UserDetails/>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenu.Label>
|
||||
<DropdownMenu.Separator/>
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item>
|
||||
<Sparkles/>
|
||||
Upgrade to Pro
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Separator/>
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item>
|
||||
<BadgeCheck/>
|
||||
Account
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item>
|
||||
<CreditCard/>
|
||||
Billing
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item>
|
||||
<Bell/>
|
||||
Notifications
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Separator/>
|
||||
<DropdownMenu.Item>
|
||||
<LogOut/>
|
||||
Log out
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</Sidebar.MenuItem>
|
||||
</Sidebar.Menu>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {Avatar as AvatarPrimitive} from "bits-ui";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import {Avatar as AvatarPrimitive} from 'bits-ui';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
class: className,
|
||||
@@ -10,7 +10,7 @@
|
||||
</script>
|
||||
|
||||
<AvatarPrimitive.Fallback
|
||||
{...restProps}
|
||||
bind:ref
|
||||
class={cn("bg-muted flex size-full items-center justify-center", className)}
|
||||
class={cn('flex size-full items-center justify-center bg-muted', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {Avatar as AvatarPrimitive} from "bits-ui";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import {Avatar as AvatarPrimitive} from 'bits-ui';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
class: className,
|
||||
@@ -12,9 +12,9 @@
|
||||
</script>
|
||||
|
||||
<AvatarPrimitive.Image
|
||||
{...restProps}
|
||||
{alt}
|
||||
bind:ref
|
||||
class={cn("aspect-square size-full", className)}
|
||||
{src}
|
||||
{alt}
|
||||
class={cn('aspect-square size-full', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<script lang="ts">
|
||||
import {Avatar as AvatarPrimitive} from "bits-ui";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import {Avatar as AvatarPrimitive} from 'bits-ui';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
class: className,
|
||||
ref = $bindable(null),
|
||||
loadingStatus = $bindable("loading"),
|
||||
loadingStatus = $bindable('loading'),
|
||||
...restProps
|
||||
}: AvatarPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<AvatarPrimitive.Root
|
||||
{...restProps}
|
||||
bind:loadingStatus
|
||||
bind:ref
|
||||
class={cn("relative flex size-10 shrink-0 overflow-hidden rounded-full", className)}
|
||||
class={cn('relative flex size-10 shrink-0 overflow-hidden rounded-full', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Root from "./avatar.svelte";
|
||||
import Image from "./avatar-image.svelte";
|
||||
import Fallback from "./avatar-fallback.svelte";
|
||||
import Root from './avatar.svelte';
|
||||
import Image from './avatar-image.svelte';
|
||||
import Fallback from './avatar-fallback.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
@@ -9,5 +9,5 @@ export {
|
||||
//
|
||||
Root as Avatar,
|
||||
Image as AvatarImage,
|
||||
Fallback as AvatarFallback,
|
||||
Fallback as AvatarFallback
|
||||
};
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import type {WithElementRef, WithoutChildren} from "bits-ui";
|
||||
import type {HTMLAttributes} from "svelte/elements";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import Ellipsis from '@lucide/svelte/icons/ellipsis';
|
||||
import type {WithElementRef, WithoutChildren} from 'bits-ui';
|
||||
import type {HTMLAttributes} from 'svelte/elements';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -11,11 +12,11 @@
|
||||
</script>
|
||||
|
||||
<span
|
||||
{...restProps}
|
||||
aria-hidden="true"
|
||||
bind:this={ref}
|
||||
class={cn("flex size-9 items-center justify-center", className)}
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
class={cn('flex size-9 items-center justify-center', className)}
|
||||
{...restProps}
|
||||
>
|
||||
<Ellipsis class="size-4"/>
|
||||
<span class="sr-only">More</span>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type {WithElementRef} from "bits-ui";
|
||||
import type {HTMLLiAttributes} from "svelte/elements";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import type {WithElementRef} from 'bits-ui';
|
||||
import type {HTMLLiAttributes} from 'svelte/elements';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -11,6 +11,6 @@
|
||||
}: WithElementRef<HTMLLiAttributes> = $props();
|
||||
</script>
|
||||
|
||||
<li {...restProps} bind:this={ref} class={cn("inline-flex items-center gap-1.5", className)}>
|
||||
<li bind:this={ref} class={cn('inline-flex items-center gap-1.5', className)} {...restProps}>
|
||||
{@render children?.()}
|
||||
</li>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import type {HTMLAnchorAttributes} from "svelte/elements";
|
||||
import type {Snippet} from "svelte";
|
||||
import type {WithElementRef} from "bits-ui";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import type {HTMLAnchorAttributes} from 'svelte/elements';
|
||||
import type {Snippet} from 'svelte';
|
||||
import type {WithElementRef} from 'bits-ui';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -16,9 +16,9 @@
|
||||
} = $props();
|
||||
|
||||
const attrs = $derived({
|
||||
class: cn("hover:text-foreground transition-colors", className),
|
||||
class: cn('hover:text-foreground transition-colors', className),
|
||||
href,
|
||||
...restProps,
|
||||
...restProps
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type {WithElementRef} from "bits-ui";
|
||||
import type {HTMLOlAttributes} from "svelte/elements";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import type {WithElementRef} from 'bits-ui';
|
||||
import type {HTMLOlAttributes} from 'svelte/elements';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -12,12 +12,12 @@
|
||||
</script>
|
||||
|
||||
<ol
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
class={cn(
|
||||
"text-muted-foreground flex flex-wrap items-center gap-1.5 break-words text-sm sm:gap-2.5",
|
||||
'flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</ol>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type {WithElementRef} from "bits-ui";
|
||||
import type {HTMLAttributes} from "svelte/elements";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import type {WithElementRef} from 'bits-ui';
|
||||
import type {HTMLAttributes} from 'svelte/elements';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -12,12 +12,12 @@
|
||||
</script>
|
||||
|
||||
<span
|
||||
{...restProps}
|
||||
aria-current="page"
|
||||
aria-disabled="true"
|
||||
bind:this={ref}
|
||||
class={cn("text-foreground font-normal", className)}
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
class={cn('font-normal text-foreground', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</span>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import type {WithElementRef} from "bits-ui";
|
||||
import type {HTMLLiAttributes} from "svelte/elements";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import ChevronRight from '@lucide/svelte/icons/chevron-right';
|
||||
import type {WithElementRef} from 'bits-ui';
|
||||
import type {HTMLLiAttributes} from 'svelte/elements';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -12,11 +13,11 @@
|
||||
</script>
|
||||
|
||||
<li
|
||||
{...restProps}
|
||||
aria-hidden="true"
|
||||
bind:this={ref}
|
||||
class={cn("[&>svg]:size-3.5", className)}
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
class={cn('[&>svg]:size-3.5', className)}
|
||||
bind:this={ref}
|
||||
{...restProps}
|
||||
>
|
||||
{#if children}
|
||||
{@render children?.()}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type {WithElementRef} from "bits-ui";
|
||||
import type {HTMLAttributes} from "svelte/elements";
|
||||
import type {WithElementRef} from 'bits-ui';
|
||||
import type {HTMLAttributes} from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(),
|
||||
@@ -10,6 +10,6 @@
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||
</script>
|
||||
|
||||
<nav {...restProps} aria-label="breadcrumb" bind:this={ref} class={className}>
|
||||
<nav class={className} bind:this={ref} aria-label="breadcrumb" {...restProps}>
|
||||
{@render children?.()}
|
||||
</nav>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import Root from "./breadcrumb.svelte";
|
||||
import Ellipsis from "./breadcrumb-ellipsis.svelte";
|
||||
import Item from "./breadcrumb-item.svelte";
|
||||
import Separator from "./breadcrumb-separator.svelte";
|
||||
import Link from "./breadcrumb-link.svelte";
|
||||
import List from "./breadcrumb-list.svelte";
|
||||
import Page from "./breadcrumb-page.svelte";
|
||||
import Root from './breadcrumb.svelte';
|
||||
import Ellipsis from './breadcrumb-ellipsis.svelte';
|
||||
import Item from './breadcrumb-item.svelte';
|
||||
import Separator from './breadcrumb-separator.svelte';
|
||||
import Link from './breadcrumb-link.svelte';
|
||||
import List from './breadcrumb-list.svelte';
|
||||
import Page from './breadcrumb-page.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
@@ -21,5 +21,5 @@ export {
|
||||
Separator as BreadcrumbSeparator,
|
||||
Link as BreadcrumbLink,
|
||||
List as BreadcrumbList,
|
||||
Page as BreadcrumbPage,
|
||||
Page as BreadcrumbPage
|
||||
};
|
||||
|
||||
@@ -1,36 +1,35 @@
|
||||
<script lang="ts" module>
|
||||
import type {WithElementRef} from "bits-ui";
|
||||
import type {HTMLAnchorAttributes, HTMLButtonAttributes} from "svelte/elements";
|
||||
import {tv, type VariantProps} from "tailwind-variants";
|
||||
import type {WithElementRef} from 'bits-ui';
|
||||
import type {HTMLAnchorAttributes, HTMLButtonAttributes} from 'svelte/elements';
|
||||
import {type VariantProps, tv} from 'tailwind-variants';
|
||||
|
||||
export const buttonVariants = tv({
|
||||
base: "focus-visible:ring-ring inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
base: 'focus-visible:ring-ring inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90 shadow",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90 shadow-sm",
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90 shadow',
|
||||
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90 shadow-sm',
|
||||
outline:
|
||||
"border-input bg-background hover:bg-accent hover:text-accent-foreground border shadow-sm",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-sm",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
'border-input bg-background hover:bg-accent hover:text-accent-foreground border shadow-sm',
|
||||
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-sm',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline'
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9",
|
||||
},
|
||||
default: 'h-9 px-4 py-2',
|
||||
sm: 'h-8 rounded-md px-3 text-xs',
|
||||
lg: 'h-10 rounded-md px-8',
|
||||
icon: 'h-9 w-9'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
variant: 'default',
|
||||
size: 'default'
|
||||
}
|
||||
});
|
||||
|
||||
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
|
||||
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
|
||||
export type ButtonVariant = VariantProps<typeof buttonVariants>['variant'];
|
||||
export type ButtonSize = VariantProps<typeof buttonVariants>['size'];
|
||||
|
||||
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||
WithElementRef<HTMLAnchorAttributes> & {
|
||||
@@ -40,27 +39,22 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import {cn} from "$lib/utils.js";
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
class: className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
variant = 'default',
|
||||
size = 'default',
|
||||
ref = $bindable(null),
|
||||
href = undefined,
|
||||
type = "button",
|
||||
type = 'button',
|
||||
children,
|
||||
...restProps
|
||||
}: ButtonProps = $props();
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<a
|
||||
bind:this={ref}
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
{href}
|
||||
{...restProps}
|
||||
>
|
||||
<a bind:this={ref} class={cn(buttonVariants({ variant, size }), className)} {href} {...restProps}>
|
||||
{@render children?.()}
|
||||
</a>
|
||||
{:else}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import Root, {type ButtonProps, type ButtonSize, type ButtonVariant, buttonVariants,} from "./button.svelte";
|
||||
import Root, {
|
||||
type ButtonProps,
|
||||
type ButtonSize,
|
||||
type ButtonVariant,
|
||||
buttonVariants
|
||||
} from './button.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
@@ -8,5 +13,5 @@ export {
|
||||
buttonVariants,
|
||||
type ButtonProps,
|
||||
type ButtonSize,
|
||||
type ButtonVariant,
|
||||
type ButtonVariant
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type {WithElementRef} from "bits-ui";
|
||||
import type {HTMLAttributes} from "svelte/elements";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import type {WithElementRef} from 'bits-ui';
|
||||
import type {HTMLAttributes} from 'svelte/elements';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -11,6 +11,6 @@
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div {...restProps} bind:this={ref} class={cn("p-6", className)}>
|
||||
<div {...restProps} bind:this={ref} class={cn('p-6', className)}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type {WithElementRef} from "bits-ui";
|
||||
import type {HTMLAttributes} from "svelte/elements";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import type {WithElementRef} from 'bits-ui';
|
||||
import type {HTMLAttributes} from 'svelte/elements';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -11,6 +11,6 @@
|
||||
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
|
||||
</script>
|
||||
|
||||
<p {...restProps} bind:this={ref} class={cn("text-muted-foreground text-sm", className)}>
|
||||
<p {...restProps} bind:this={ref} class={cn('text-sm text-muted-foreground', className)}>
|
||||
{@render children?.()}
|
||||
</p>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type {WithElementRef} from "bits-ui";
|
||||
import type {HTMLAttributes} from "svelte/elements";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import type {WithElementRef} from 'bits-ui';
|
||||
import type {HTMLAttributes} from 'svelte/elements';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -11,6 +11,6 @@
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div {...restProps} bind:this={ref} class={cn("flex items-center p-6 pt-0", className)}>
|
||||
<div {...restProps} bind:this={ref} class={cn('flex items-center p-6 pt-0', className)}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type {WithElementRef} from "bits-ui";
|
||||
import type {HTMLAttributes} from "svelte/elements";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import type {WithElementRef} from 'bits-ui';
|
||||
import type {HTMLAttributes} from 'svelte/elements';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -11,6 +11,6 @@
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div {...restProps} bind:this={ref} class={cn("flex flex-col space-y-1.5 p-6 pb-0", className)}>
|
||||
<div {...restProps} bind:this={ref} class={cn('flex flex-col space-y-1.5 p-6 pb-0', className)}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type {WithElementRef} from "bits-ui";
|
||||
import type {HTMLAttributes} from "svelte/elements";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import type {WithElementRef} from 'bits-ui';
|
||||
import type {HTMLAttributes} from 'svelte/elements';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -18,7 +18,7 @@
|
||||
{...restProps}
|
||||
aria-level={level}
|
||||
bind:this={ref}
|
||||
class={cn("font-semibold leading-none tracking-tight", className)}
|
||||
class={cn('font-semibold leading-none tracking-tight', className)}
|
||||
role="heading"
|
||||
>
|
||||
{@render children?.()}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type {WithElementRef} from "bits-ui";
|
||||
import type {HTMLAttributes} from "svelte/elements";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import type {WithElementRef} from 'bits-ui';
|
||||
import type {HTMLAttributes} from 'svelte/elements';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -14,7 +14,7 @@
|
||||
<div
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
class={cn("bg-card text-card-foreground rounded-xl border shadow", className)}
|
||||
class={cn('rounded-xl border bg-card text-card-foreground shadow', className)}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import Root from "./card.svelte";
|
||||
import Content from "./card-content.svelte";
|
||||
import Description from "./card-description.svelte";
|
||||
import Footer from "./card-footer.svelte";
|
||||
import Header from "./card-header.svelte";
|
||||
import Title from "./card-title.svelte";
|
||||
import Root from './card.svelte';
|
||||
import Content from './card-content.svelte';
|
||||
import Description from './card-description.svelte';
|
||||
import Footer from './card-footer.svelte';
|
||||
import Header from './card-header.svelte';
|
||||
import Title from './card-title.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
@@ -18,5 +18,5 @@ export {
|
||||
Description as CardDescription,
|
||||
Footer as CardFooter,
|
||||
Header as CardHeader,
|
||||
Title as CardTitle,
|
||||
Title as CardTitle
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Collapsible as CollapsiblePrimitive} from "bits-ui";
|
||||
import {Collapsible as CollapsiblePrimitive} from 'bits-ui';
|
||||
|
||||
const Root: typeof CollapsiblePrimitive.Root = CollapsiblePrimitive.Root;
|
||||
const Trigger: typeof CollapsiblePrimitive.Trigger = CollapsiblePrimitive.Trigger;
|
||||
@@ -11,5 +11,5 @@ export {
|
||||
//
|
||||
Root as Collapsible,
|
||||
Content as CollapsibleContent,
|
||||
Trigger as CollapsibleTrigger,
|
||||
Trigger as CollapsibleTrigger
|
||||
};
|
||||
|
||||
38
web/src/lib/components/ui/dialog/dialog-content.svelte
Normal file
38
web/src/lib/components/ui/dialog/dialog-content.svelte
Normal file
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import {Dialog as DialogPrimitive, type WithoutChildrenOrChild} from 'bits-ui';
|
||||
import X from '@lucide/svelte/icons/x';
|
||||
import type {Snippet} from 'svelte';
|
||||
import * as Dialog from './index.js';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
portalProps,
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
|
||||
portalProps?: DialogPrimitive.PortalProps;
|
||||
children: Snippet;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<Dialog.Portal {...portalProps}>
|
||||
<Dialog.Overlay/>
|
||||
<DialogPrimitive.Content
|
||||
bind:ref
|
||||
class={cn(
|
||||
'fixed left-[50%] top-[50%] z-50 grid translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
<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="size-4"/>
|
||||
<span class="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</Dialog.Portal>
|
||||
16
web/src/lib/components/ui/dialog/dialog-description.svelte
Normal file
16
web/src/lib/components/ui/dialog/dialog-description.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import {Dialog as DialogPrimitive} from 'bits-ui';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.DescriptionProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Description
|
||||
bind:ref
|
||||
class={cn('text-sm text-muted-foreground', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
20
web/src/lib/components/ui/dialog/dialog-footer.svelte
Normal file
20
web/src/lib/components/ui/dialog/dialog-footer.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type {WithElementRef} from 'bits-ui';
|
||||
import type {HTMLAttributes} from 'svelte/elements';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
class={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
20
web/src/lib/components/ui/dialog/dialog-header.svelte
Normal file
20
web/src/lib/components/ui/dialog/dialog-header.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type {HTMLAttributes} from 'svelte/elements';
|
||||
import type {WithElementRef} from 'bits-ui';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
class={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
19
web/src/lib/components/ui/dialog/dialog-overlay.svelte
Normal file
19
web/src/lib/components/ui/dialog/dialog-overlay.svelte
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import {Dialog as DialogPrimitive} from 'bits-ui';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.OverlayProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Overlay
|
||||
bind:ref
|
||||
class={cn(
|
||||
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
16
web/src/lib/components/ui/dialog/dialog-title.svelte
Normal file
16
web/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.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.TitleProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Title
|
||||
bind:ref
|
||||
class={cn('text-lg font-semibold leading-none tracking-tight', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
37
web/src/lib/components/ui/dialog/index.ts
Normal file
37
web/src/lib/components/ui/dialog/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import {Dialog as DialogPrimitive} from 'bits-ui';
|
||||
|
||||
import Title from './dialog-title.svelte';
|
||||
import Footer from './dialog-footer.svelte';
|
||||
import Header from './dialog-header.svelte';
|
||||
import Overlay from './dialog-overlay.svelte';
|
||||
import Content from './dialog-content.svelte';
|
||||
import Description from './dialog-description.svelte';
|
||||
|
||||
const Root: typeof DialogPrimitive.Root = DialogPrimitive.Root;
|
||||
const Trigger: typeof DialogPrimitive.Trigger = DialogPrimitive.Trigger;
|
||||
const Close: typeof DialogPrimitive.Close = DialogPrimitive.Close;
|
||||
const Portal: typeof DialogPrimitive.Portal = DialogPrimitive.Portal;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Title,
|
||||
Portal,
|
||||
Footer,
|
||||
Header,
|
||||
Trigger,
|
||||
Overlay,
|
||||
Content,
|
||||
Description,
|
||||
Close,
|
||||
//
|
||||
Root as Dialog,
|
||||
Title as DialogTitle,
|
||||
Portal as DialogPortal,
|
||||
Footer as DialogFooter,
|
||||
Header as DialogHeader,
|
||||
Trigger as DialogTrigger,
|
||||
Overlay as DialogOverlay,
|
||||
Content as DialogContent,
|
||||
Description as DialogDescription,
|
||||
Close as DialogClose
|
||||
};
|
||||
@@ -1,7 +1,9 @@
|
||||
<script lang="ts">
|
||||
import {DropdownMenu as DropdownMenuPrimitive, type WithoutChildrenOrChild} from "bits-ui";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import type {Snippet} from "svelte";
|
||||
import {DropdownMenu as DropdownMenuPrimitive, type WithoutChildrenOrChild} from 'bits-ui';
|
||||
import Check from '@lucide/svelte/icons/check';
|
||||
import Minus from '@lucide/svelte/icons/minus';
|
||||
import {cn} from '$lib/utils.js';
|
||||
import type {Snippet} from 'svelte';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -16,21 +18,21 @@
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
{...restProps}
|
||||
bind:ref
|
||||
bind:checked
|
||||
bind:indeterminate
|
||||
bind:ref
|
||||
class={cn(
|
||||
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground 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-[disabled]:opacity-50",
|
||||
'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}
|
||||
>
|
||||
{#snippet children({checked, indeterminate})}
|
||||
<span class="absolute left-2 flex size-3.5 items-center justify-center">
|
||||
{#if indeterminate}
|
||||
<Minus class="size-4"/>
|
||||
{:else}
|
||||
<Check class={cn("size-4", !checked && "text-transparent")}/>
|
||||
<Check class={cn('size-4', !checked && 'text-transparent')}/>
|
||||
{/if}
|
||||
</span>
|
||||
{@render childrenProp?.()}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {cn} from "$lib/utils.js";
|
||||
import {DropdownMenu as DropdownMenuPrimitive} from "bits-ui";
|
||||
import {cn} from '$lib/utils.js';
|
||||
import {DropdownMenu as DropdownMenuPrimitive} from 'bits-ui';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -15,13 +15,13 @@
|
||||
|
||||
<DropdownMenuPrimitive.Portal {...portalProps}>
|
||||
<DropdownMenuPrimitive.Content
|
||||
{...restProps}
|
||||
bind:ref
|
||||
{sideOffset}
|
||||
class={cn(
|
||||
"bg-popover text-popover-foreground z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 outline-none",
|
||||
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',
|
||||
'outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className
|
||||
)}
|
||||
{sideOffset}
|
||||
{...restProps}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {DropdownMenu as DropdownMenuPrimitive} from "bits-ui";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import {DropdownMenu as DropdownMenuPrimitive} from 'bits-ui';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -13,7 +13,7 @@
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.GroupHeading
|
||||
{...restProps}
|
||||
bind:ref
|
||||
class={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
||||
class={cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {cn} from "$lib/utils.js";
|
||||
import {DropdownMenu as DropdownMenuPrimitive} from "bits-ui";
|
||||
import {cn} from '$lib/utils.js';
|
||||
import {DropdownMenu as DropdownMenuPrimitive} from 'bits-ui';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -13,11 +13,11 @@
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Item
|
||||
{...restProps}
|
||||
bind:ref
|
||||
class={cn(
|
||||
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
'relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0',
|
||||
inset && 'pl-8',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import {cn} from "$lib/utils.js";
|
||||
import {type WithElementRef} from "bits-ui";
|
||||
import type {HTMLAttributes} from "svelte/elements";
|
||||
import {cn} from '$lib/utils.js';
|
||||
import {type WithElementRef} from 'bits-ui';
|
||||
import type {HTMLAttributes} from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -15,9 +15,9 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
class={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
||||
class={cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import {DropdownMenu as DropdownMenuPrimitive, type WithoutChild} from "bits-ui";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import {DropdownMenu as DropdownMenuPrimitive, type WithoutChild} from 'bits-ui';
|
||||
import Circle from '@lucide/svelte/icons/circle';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -11,12 +12,12 @@
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
{...restProps}
|
||||
bind:ref
|
||||
class={cn(
|
||||
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground 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-[disabled]:opacity-50",
|
||||
'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}
|
||||
>
|
||||
{#snippet children({checked})}
|
||||
<span class="absolute left-2 flex size-3.5 items-center justify-center">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {DropdownMenu as DropdownMenuPrimitive} from "bits-ui";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import {DropdownMenu as DropdownMenuPrimitive} from 'bits-ui';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -10,7 +10,7 @@
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Separator
|
||||
{...restProps}
|
||||
bind:ref
|
||||
class={cn("bg-muted -mx-1 my-1 h-px", className)}
|
||||
class={cn('-mx-1 my-1 h-px bg-muted', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type {HTMLAttributes} from "svelte/elements";
|
||||
import {type WithElementRef} from "bits-ui";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import type {HTMLAttributes} from 'svelte/elements';
|
||||
import {type WithElementRef} from 'bits-ui';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -12,9 +12,9 @@
|
||||
</script>
|
||||
|
||||
<span
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
class={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
class={cn('ml-auto text-xs tracking-widest opacity-60', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</span>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {DropdownMenu as DropdownMenuPrimitive} from "bits-ui";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import {DropdownMenu as DropdownMenuPrimitive} from 'bits-ui';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -10,10 +10,10 @@
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
{...restProps}
|
||||
bind:ref
|
||||
class={cn(
|
||||
"bg-popover text-popover-foreground z-50 min-w-[8rem] rounded-md border p-1 shadow-lg focus:outline-none",
|
||||
'z-50 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-lg focus:outline-none',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import {DropdownMenu as DropdownMenuPrimitive, type WithoutChild} from "bits-ui";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import {DropdownMenu as DropdownMenuPrimitive, type WithoutChild} from 'bits-ui';
|
||||
import ChevronRight from '@lucide/svelte/icons/chevron-right';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -14,13 +15,13 @@
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
{...restProps}
|
||||
bind:ref
|
||||
class={cn(
|
||||
"data-[highlighted]:bg-accent data-[state=open]:bg-accent flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
'flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
inset && 'pl-8',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
<ChevronRight class="ml-auto"/>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import {DropdownMenu as DropdownMenuPrimitive} from "bits-ui";
|
||||
import CheckboxItem from "./dropdown-menu-checkbox-item.svelte";
|
||||
import Content from "./dropdown-menu-content.svelte";
|
||||
import GroupHeading from "./dropdown-menu-group-heading.svelte";
|
||||
import Item from "./dropdown-menu-item.svelte";
|
||||
import Label from "./dropdown-menu-label.svelte";
|
||||
import RadioItem from "./dropdown-menu-radio-item.svelte";
|
||||
import Separator from "./dropdown-menu-separator.svelte";
|
||||
import Shortcut from "./dropdown-menu-shortcut.svelte";
|
||||
import SubContent from "./dropdown-menu-sub-content.svelte";
|
||||
import SubTrigger from "./dropdown-menu-sub-trigger.svelte";
|
||||
import {DropdownMenu as DropdownMenuPrimitive} from 'bits-ui';
|
||||
import CheckboxItem from './dropdown-menu-checkbox-item.svelte';
|
||||
import Content from './dropdown-menu-content.svelte';
|
||||
import GroupHeading from './dropdown-menu-group-heading.svelte';
|
||||
import Item from './dropdown-menu-item.svelte';
|
||||
import Label from './dropdown-menu-label.svelte';
|
||||
import RadioItem from './dropdown-menu-radio-item.svelte';
|
||||
import Separator from './dropdown-menu-separator.svelte';
|
||||
import Shortcut from './dropdown-menu-shortcut.svelte';
|
||||
import SubContent from './dropdown-menu-sub-content.svelte';
|
||||
import SubTrigger from './dropdown-menu-sub-trigger.svelte';
|
||||
|
||||
const Sub = DropdownMenuPrimitive.Sub;
|
||||
const Root = DropdownMenuPrimitive.Root;
|
||||
@@ -46,5 +46,5 @@ export {
|
||||
Sub,
|
||||
SubContent,
|
||||
SubTrigger,
|
||||
Trigger,
|
||||
Trigger
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Root from "./input.svelte";
|
||||
import Root from './input.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Input,
|
||||
Root as Input
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script lang="ts">
|
||||
import type {HTMLInputAttributes, HTMLInputTypeAttribute} from "svelte/elements";
|
||||
import type {WithElementRef} from "bits-ui";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import type {HTMLInputAttributes, HTMLInputTypeAttribute} from 'svelte/elements';
|
||||
import type {WithElementRef} from 'bits-ui';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
type InputType = Exclude<HTMLInputTypeAttribute, "file">;
|
||||
type InputType = Exclude<HTMLInputTypeAttribute, 'file'>;
|
||||
|
||||
type Props = WithElementRef<
|
||||
Omit<HTMLInputAttributes, "type"> &
|
||||
({ type: "file"; files?: FileList } | { type?: InputType; files?: undefined })
|
||||
Omit<HTMLInputAttributes, 'type'> &
|
||||
({ type: 'file'; files?: FileList } | { type?: InputType; files?: undefined })
|
||||
>;
|
||||
|
||||
let {
|
||||
@@ -20,11 +20,11 @@
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if type === "file"}
|
||||
{#if type === 'file'}
|
||||
<input
|
||||
bind:this={ref}
|
||||
class={cn(
|
||||
"border-input placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
className
|
||||
)}
|
||||
type="file"
|
||||
@@ -36,7 +36,7 @@
|
||||
<input
|
||||
bind:this={ref}
|
||||
class={cn(
|
||||
"border-input placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
className
|
||||
)}
|
||||
{type}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Root from "./label.svelte";
|
||||
import Root from './label.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Label,
|
||||
Root as Label
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {Label as LabelPrimitive} from "bits-ui";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import {Label as LabelPrimitive} from 'bits-ui';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -13,7 +13,7 @@
|
||||
{...restProps}
|
||||
bind:ref
|
||||
class={cn(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
||||
className
|
||||
)}
|
||||
/>
|
||||
|
||||
10
web/src/lib/components/ui/radio-group/index.ts
Normal file
10
web/src/lib/components/ui/radio-group/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import Root from './radio-group.svelte';
|
||||
import Item from './radio-group-item.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
Item,
|
||||
//
|
||||
Root as RadioGroup,
|
||||
Item as RadioGroupItem
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import {RadioGroup as RadioGroupPrimitive, type WithoutChildrenOrChild} from 'bits-ui';
|
||||
import Circle from '@lucide/svelte/icons/circle';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<RadioGroupPrimitive.ItemProps> & {
|
||||
value: string;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<RadioGroupPrimitive.Item
|
||||
bind:ref
|
||||
class={cn(
|
||||
'aspect-square size-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({checked})}
|
||||
<div class="flex items-center justify-center">
|
||||
{#if checked}
|
||||
<Circle class="size-3.5 fill-primary"/>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</RadioGroupPrimitive.Item>
|
||||
13
web/src/lib/components/ui/radio-group/radio-group.svelte
Normal file
13
web/src/lib/components/ui/radio-group/radio-group.svelte
Normal file
@@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
import {RadioGroup as RadioGroupPrimitive} from 'bits-ui';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
value = $bindable(),
|
||||
...restProps
|
||||
}: RadioGroupPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<RadioGroupPrimitive.Root bind:value class={cn('grid gap-2', className)} {...restProps} bind:ref/>
|
||||
34
web/src/lib/components/ui/select/index.ts
Normal file
34
web/src/lib/components/ui/select/index.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import {Select as SelectPrimitive} from 'bits-ui';
|
||||
|
||||
import GroupHeading from './select-group-heading.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';
|
||||
import ScrollDownButton from './select-scroll-down-button.svelte';
|
||||
import ScrollUpButton from './select-scroll-up-button.svelte';
|
||||
|
||||
const Root = SelectPrimitive.Root;
|
||||
const Group = SelectPrimitive.Group;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Item,
|
||||
Group,
|
||||
GroupHeading,
|
||||
Content,
|
||||
Trigger,
|
||||
Separator,
|
||||
ScrollDownButton,
|
||||
ScrollUpButton,
|
||||
//
|
||||
Root as Select,
|
||||
Item as SelectItem,
|
||||
Group as SelectGroup,
|
||||
GroupHeading as SelectGroupHeading,
|
||||
Content as SelectContent,
|
||||
Trigger as SelectTrigger,
|
||||
Separator as SelectSeparator,
|
||||
ScrollDownButton as SelectScrollDownButton,
|
||||
ScrollUpButton as SelectScrollUpButton
|
||||
};
|
||||
38
web/src/lib/components/ui/select/select-content.svelte
Normal file
38
web/src/lib/components/ui/select/select-content.svelte
Normal file
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import {Select as SelectPrimitive, type WithoutChild} from 'bits-ui';
|
||||
import * as Select from './index.js';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
sideOffset = 4,
|
||||
portalProps,
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChild<SelectPrimitive.ContentProps> & {
|
||||
portalProps?: SelectPrimitive.PortalProps;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Portal {...portalProps}>
|
||||
<SelectPrimitive.Content
|
||||
bind:ref
|
||||
{sideOffset}
|
||||
class={cn(
|
||||
'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
<Select.ScrollUpButton/>
|
||||
<SelectPrimitive.Viewport
|
||||
class={cn(
|
||||
'h-[var(--bits-select-anchor-height)] w-full min-w-[var(--bits-select-anchor-width)] p-1'
|
||||
)}
|
||||
>
|
||||
{@render children?.()}
|
||||
</SelectPrimitive.Viewport>
|
||||
<Select.ScrollDownButton/>
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
16
web/src/lib/components/ui/select/select-group-heading.svelte
Normal file
16
web/src/lib/components/ui/select/select-group-heading.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import {Select as SelectPrimitive} from 'bits-ui';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: SelectPrimitive.GroupHeadingProps = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.GroupHeading
|
||||
bind:ref
|
||||
class={cn('px-2 py-1.5 text-sm font-semibold', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
37
web/src/lib/components/ui/select/select-item.svelte
Normal file
37
web/src/lib/components/ui/select/select-item.svelte
Normal file
@@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import {Select as SelectPrimitive, type WithoutChild} from 'bits-ui';
|
||||
import Check from '@lucide/svelte/icons/check';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
value,
|
||||
label,
|
||||
children: childrenProp,
|
||||
...restProps
|
||||
}: WithoutChild<SelectPrimitive.ItemProps> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Item
|
||||
bind:ref
|
||||
{value}
|
||||
class={cn(
|
||||
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({selected, highlighted})}
|
||||
<span class="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
{#if selected}
|
||||
<Check class="size-4"/>
|
||||
{/if}
|
||||
</span>
|
||||
{#if childrenProp}
|
||||
{@render childrenProp({selected, highlighted})}
|
||||
{:else}
|
||||
{label || value}
|
||||
{/if}
|
||||
{/snippet}
|
||||
</SelectPrimitive.Item>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import ChevronDown from '@lucide/svelte/icons/chevron-down';
|
||||
import {Select as SelectPrimitive, type WithoutChildrenOrChild} from 'bits-ui';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SelectPrimitive.ScrollDownButtonProps> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
bind:ref
|
||||
class={cn('flex cursor-default items-center justify-center py-1', className)}
|
||||
{...restProps}
|
||||
>
|
||||
<ChevronDown class="size-4"/>
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import ChevronUp from '@lucide/svelte/icons/chevron-up';
|
||||
import {Select as SelectPrimitive, type WithoutChildrenOrChild} from 'bits-ui';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SelectPrimitive.ScrollDownButtonProps> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
bind:ref
|
||||
class={cn('flex cursor-default items-center justify-center py-1', className)}
|
||||
{...restProps}
|
||||
>
|
||||
<ChevronUp class="size-4"/>
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
13
web/src/lib/components/ui/select/select-separator.svelte
Normal file
13
web/src/lib/components/ui/select/select-separator.svelte
Normal file
@@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
import type {Separator as SeparatorPrimitive} from 'bits-ui';
|
||||
import {Separator} from '$lib/components/ui/separator/index.js';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: SeparatorPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<Separator bind:ref class={cn('-mx-1 my-1 h-px bg-muted', className)} {...restProps}/>
|
||||
24
web/src/lib/components/ui/select/select-trigger.svelte
Normal file
24
web/src/lib/components/ui/select/select-trigger.svelte
Normal file
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import {Select as SelectPrimitive, type WithoutChild} from 'bits-ui';
|
||||
import ChevronDown from '@lucide/svelte/icons/chevron-down';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChild<SelectPrimitive.TriggerProps> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Trigger
|
||||
bind:ref
|
||||
class={cn(
|
||||
'flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[placeholder]:text-muted-foreground [&>span]:line-clamp-1',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
<ChevronDown class="size-4 opacity-50"/>
|
||||
</SelectPrimitive.Trigger>
|
||||
@@ -1,7 +1,7 @@
|
||||
import Root from "./separator.svelte";
|
||||
import Root from './separator.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Separator,
|
||||
Root as Separator
|
||||
};
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
<script lang="ts">
|
||||
import {Separator as SeparatorPrimitive} from "bits-ui";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import {Separator as SeparatorPrimitive} from 'bits-ui';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
orientation = "horizontal",
|
||||
orientation = 'horizontal',
|
||||
...restProps
|
||||
}: SeparatorPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<SeparatorPrimitive.Root
|
||||
{...restProps}
|
||||
bind:ref
|
||||
class={cn(
|
||||
"bg-border shrink-0",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "min-h-full w-[1px]",
|
||||
'shrink-0 bg-border',
|
||||
orientation === 'horizontal' ? 'h-[1px] w-full' : 'min-h-full w-[1px]',
|
||||
className
|
||||
)}
|
||||
{orientation}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {Dialog as SheetPrimitive} from "bits-ui";
|
||||
import {Dialog as SheetPrimitive} from 'bits-ui';
|
||||
|
||||
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";
|
||||
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;
|
||||
@@ -33,5 +33,5 @@ export {
|
||||
Header as SheetHeader,
|
||||
Footer as SheetFooter,
|
||||
Title as SheetTitle,
|
||||
Description as SheetDescription,
|
||||
Description as SheetDescription
|
||||
};
|
||||
|
||||
@@ -1,36 +1,38 @@
|
||||
<script lang="ts" module>
|
||||
import {tv, type VariantProps} from "tailwind-variants";
|
||||
import {type VariantProps, tv} from 'tailwind-variants';
|
||||
|
||||
export const sheetVariants = tv({
|
||||
base: "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 gap-4 p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||
base: 'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 gap-4 p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
|
||||
variants: {
|
||||
side: {
|
||||
top: "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 border-b",
|
||||
bottom: "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 border-t",
|
||||
left: "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
|
||||
right: "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
|
||||
},
|
||||
top: 'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 border-b',
|
||||
bottom:
|
||||
'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 border-t',
|
||||
left: 'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm',
|
||||
right:
|
||||
'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
side: "right",
|
||||
},
|
||||
side: 'right'
|
||||
}
|
||||
});
|
||||
|
||||
export type Side = VariantProps<typeof sheetVariants>["side"];
|
||||
export type Side = VariantProps<typeof sheetVariants>['side'];
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import {Dialog as SheetPrimitive, type WithoutChildrenOrChild} from "bits-ui";
|
||||
import X from "@lucide/svelte/icons/x";
|
||||
import type {Snippet} from "svelte";
|
||||
import SheetOverlay from "./sheet-overlay.svelte";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import {Dialog as SheetPrimitive, type WithoutChildrenOrChild} from 'bits-ui';
|
||||
import X from '@lucide/svelte/icons/x';
|
||||
import type {Snippet} from 'svelte';
|
||||
import SheetOverlay from './sheet-overlay.svelte';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
portalProps,
|
||||
side = "right",
|
||||
side = 'right',
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SheetPrimitive.ContentProps> & {
|
||||
@@ -42,10 +44,10 @@
|
||||
|
||||
<SheetPrimitive.Portal {...portalProps}>
|
||||
<SheetOverlay/>
|
||||
<SheetPrimitive.Content {...restProps} bind:ref class={cn(sheetVariants({ side }), className)}>
|
||||
<SheetPrimitive.Content bind:ref class={cn(sheetVariants({ side }), className)} {...restProps}>
|
||||
{@render children?.()}
|
||||
<SheetPrimitive.Close
|
||||
class="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none"
|
||||
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-secondary"
|
||||
>
|
||||
<X class="size-4"/>
|
||||
<span class="sr-only">Close</span>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {Dialog as SheetPrimitive} from "bits-ui";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import {Dialog as SheetPrimitive} from 'bits-ui';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -10,7 +10,7 @@
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Description
|
||||
{...restProps}
|
||||
bind:ref
|
||||
class={cn("text-muted-foreground text-sm", className)}
|
||||
class={cn('text-sm text-muted-foreground', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type {WithElementRef} from "bits-ui";
|
||||
import type {HTMLAttributes} from "svelte/elements";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import type {WithElementRef} from 'bits-ui';
|
||||
import type {HTMLAttributes} from 'svelte/elements';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -12,9 +12,9 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||
class={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type {HTMLAttributes} from "svelte/elements";
|
||||
import type {WithElementRef} from "bits-ui";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import type {HTMLAttributes} from 'svelte/elements';
|
||||
import type {WithElementRef} from 'bits-ui';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -12,9 +12,9 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
class={cn("flex flex-col space-y-2 text-center sm:text-left", className)}
|
||||
class={cn('flex flex-col space-y-2 text-center sm:text-left', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {Dialog as SheetPrimitive} from "bits-ui";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import {Dialog as SheetPrimitive} from 'bits-ui';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -10,10 +10,10 @@
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Overlay
|
||||
{...restProps}
|
||||
bind:ref
|
||||
class={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
|
||||
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {Dialog as SheetPrimitive} from "bits-ui";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import {Dialog as SheetPrimitive} from 'bits-ui';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -10,7 +10,7 @@
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Title
|
||||
{...restProps}
|
||||
bind:ref
|
||||
class={cn("text-foreground text-lg font-semibold", className)}
|
||||
class={cn('text-lg font-semibold text-foreground', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const SIDEBAR_COOKIE_NAME = "sidebar:state";
|
||||
export const SIDEBAR_COOKIE_NAME = 'sidebar:state';
|
||||
export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
||||
export const SIDEBAR_WIDTH = "16rem";
|
||||
export const SIDEBAR_WIDTH_MOBILE = "18rem";
|
||||
export const SIDEBAR_WIDTH_ICON = "3rem";
|
||||
export const SIDEBAR_KEYBOARD_SHORTCUT = "b";
|
||||
export const SIDEBAR_WIDTH = '16rem';
|
||||
export const SIDEBAR_WIDTH_MOBILE = '18rem';
|
||||
export const SIDEBAR_WIDTH_ICON = '3rem';
|
||||
export const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {IsMobile} from "$lib/hooks/is-mobile.svelte.js";
|
||||
import {getContext, setContext} from "svelte";
|
||||
import {SIDEBAR_KEYBOARD_SHORTCUT} from "./constants.js";
|
||||
import {IsMobile} from '$lib/hooks/is-mobile.svelte.js';
|
||||
import {getContext, setContext} from 'svelte';
|
||||
import {SIDEBAR_KEYBOARD_SHORTCUT} from './constants.js';
|
||||
|
||||
type Getter<T> = () => T;
|
||||
|
||||
@@ -24,9 +24,9 @@ class SidebarState {
|
||||
readonly props: SidebarStateProps;
|
||||
open = $derived.by(() => this.props.open());
|
||||
openMobile = $state(false);
|
||||
setOpen: SidebarStateProps["setOpen"];
|
||||
state = $derived.by(() => (this.open ? "expanded" : "collapsed"));
|
||||
setOpen: SidebarStateProps['setOpen'];
|
||||
#isMobile: IsMobile;
|
||||
state = $derived.by(() => (this.open ? 'expanded' : 'collapsed'));
|
||||
|
||||
constructor(props: SidebarStateProps) {
|
||||
this.setOpen = props.setOpen;
|
||||
@@ -53,13 +53,11 @@ class SidebarState {
|
||||
};
|
||||
|
||||
toggle = () => {
|
||||
return this.#isMobile.current
|
||||
? (this.openMobile = !this.openMobile)
|
||||
: this.setOpen(!this.open);
|
||||
return this.#isMobile.current ? (this.openMobile = !this.openMobile) : this.setOpen(!this.open);
|
||||
};
|
||||
}
|
||||
|
||||
const SYMBOL_KEY = "scn-sidebar";
|
||||
const SYMBOL_KEY = 'scn-sidebar';
|
||||
|
||||
/**
|
||||
* Instantiates a new `SidebarState` instance and sets it in the context.
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import {useSidebar} from "./context.svelte.js";
|
||||
import Content from "./sidebar-content.svelte";
|
||||
import Footer from "./sidebar-footer.svelte";
|
||||
import GroupAction from "./sidebar-group-action.svelte";
|
||||
import GroupContent from "./sidebar-group-content.svelte";
|
||||
import GroupLabel from "./sidebar-group-label.svelte";
|
||||
import Group from "./sidebar-group.svelte";
|
||||
import Header from "./sidebar-header.svelte";
|
||||
import Input from "./sidebar-input.svelte";
|
||||
import Inset from "./sidebar-inset.svelte";
|
||||
import MenuAction from "./sidebar-menu-action.svelte";
|
||||
import MenuBadge from "./sidebar-menu-badge.svelte";
|
||||
import MenuButton from "./sidebar-menu-button.svelte";
|
||||
import MenuItem from "./sidebar-menu-item.svelte";
|
||||
import MenuSkeleton from "./sidebar-menu-skeleton.svelte";
|
||||
import MenuSubButton from "./sidebar-menu-sub-button.svelte";
|
||||
import MenuSubItem from "./sidebar-menu-sub-item.svelte";
|
||||
import MenuSub from "./sidebar-menu-sub.svelte";
|
||||
import Menu from "./sidebar-menu.svelte";
|
||||
import Provider from "./sidebar-provider.svelte";
|
||||
import Rail from "./sidebar-rail.svelte";
|
||||
import Separator from "./sidebar-separator.svelte";
|
||||
import Trigger from "./sidebar-trigger.svelte";
|
||||
import Root from "./sidebar.svelte";
|
||||
import {useSidebar} from './context.svelte.js';
|
||||
import Content from './sidebar-content.svelte';
|
||||
import Footer from './sidebar-footer.svelte';
|
||||
import GroupAction from './sidebar-group-action.svelte';
|
||||
import GroupContent from './sidebar-group-content.svelte';
|
||||
import GroupLabel from './sidebar-group-label.svelte';
|
||||
import Group from './sidebar-group.svelte';
|
||||
import Header from './sidebar-header.svelte';
|
||||
import Input from './sidebar-input.svelte';
|
||||
import Inset from './sidebar-inset.svelte';
|
||||
import MenuAction from './sidebar-menu-action.svelte';
|
||||
import MenuBadge from './sidebar-menu-badge.svelte';
|
||||
import MenuButton from './sidebar-menu-button.svelte';
|
||||
import MenuItem from './sidebar-menu-item.svelte';
|
||||
import MenuSkeleton from './sidebar-menu-skeleton.svelte';
|
||||
import MenuSubButton from './sidebar-menu-sub-button.svelte';
|
||||
import MenuSubItem from './sidebar-menu-sub-item.svelte';
|
||||
import MenuSub from './sidebar-menu-sub.svelte';
|
||||
import Menu from './sidebar-menu.svelte';
|
||||
import Provider from './sidebar-provider.svelte';
|
||||
import Rail from './sidebar-rail.svelte';
|
||||
import Separator from './sidebar-separator.svelte';
|
||||
import Trigger from './sidebar-trigger.svelte';
|
||||
import Root from './sidebar.svelte';
|
||||
|
||||
export {
|
||||
Content,
|
||||
@@ -71,5 +71,5 @@ export {
|
||||
Separator as SidebarSeparator,
|
||||
Trigger as SidebarTrigger,
|
||||
Trigger,
|
||||
useSidebar,
|
||||
useSidebar
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type {HTMLAttributes} from "svelte/elements";
|
||||
import type {WithElementRef} from "bits-ui";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import type {HTMLAttributes} from 'svelte/elements';
|
||||
import type {WithElementRef} from 'bits-ui';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -12,13 +12,13 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
data-sidebar="content"
|
||||
class={cn(
|
||||
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
||||
'flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden',
|
||||
className
|
||||
)}
|
||||
data-sidebar="content"
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type {HTMLAttributes} from "svelte/elements";
|
||||
import type {WithElementRef} from "bits-ui";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import type {HTMLAttributes} from 'svelte/elements';
|
||||
import type {WithElementRef} from 'bits-ui';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -12,10 +12,10 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
class={cn("flex flex-col gap-2 p-2", className)}
|
||||
data-sidebar="footer"
|
||||
class={cn('flex flex-col gap-2 p-2', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import {cn} from "$lib/utils.js";
|
||||
import type {WithElementRef} from "bits-ui";
|
||||
import type {Snippet} from "svelte";
|
||||
import type {HTMLButtonAttributes} from "svelte/elements";
|
||||
import {cn} from '$lib/utils.js';
|
||||
import type {WithElementRef} from 'bits-ui';
|
||||
import type {Snippet} from 'svelte';
|
||||
import type {HTMLButtonAttributes} from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -16,14 +16,14 @@
|
||||
|
||||
const propObj = $derived({
|
||||
class: cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-none transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-none transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 after:md:hidden",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
'after:absolute after:-inset-2 after:md:hidden',
|
||||
'group-data-[collapsible=icon]:hidden',
|
||||
className
|
||||
),
|
||||
"data-sidebar": "group-action",
|
||||
...restProps,
|
||||
'data-sidebar': 'group-action',
|
||||
...restProps
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user