feature/sidebar #8
2
.gitignore
vendored
2
.gitignore
vendored
@@ -22,6 +22,7 @@ Thumbs.db
|
||||
|
||||
# Yarn
|
||||
.yarn
|
||||
.yarn/**
|
||||
.pnp.*
|
||||
|
||||
# Zed
|
||||
@@ -32,3 +33,4 @@ vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
/docs
|
||||
AGENTS.md
|
||||
|
||||
Binary file not shown.
@@ -5,11 +5,11 @@
|
||||
"baseColor": "zinc"
|
||||
},
|
||||
"aliases": {
|
||||
"components": "$lib/components",
|
||||
"utils": "$lib/utils",
|
||||
"ui": "$lib/components/ui",
|
||||
"hooks": "$lib/hooks",
|
||||
"lib": "$lib"
|
||||
"components": "$shared/shadcn/ui",
|
||||
"utils": "$shared/shadcn/utils/shadcn-utils",
|
||||
"ui": "$shared/shadcn/ui",
|
||||
"hooks": "$shared/shadcn/hooks",
|
||||
"lib": "$shared"
|
||||
},
|
||||
"typescript": true,
|
||||
"registry": "https://shadcn-svelte.com/registry"
|
||||
|
||||
19
dprint.json
19
dprint.json
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "https://dprint.dev/schemas/v0.json",
|
||||
"incremental": true,
|
||||
"includes": ["**/*.{ts,tsx,js,jsx,svelte,json,md}"],
|
||||
"excludes": [
|
||||
@@ -22,7 +23,15 @@
|
||||
"quoteStyle": "preferSingle",
|
||||
"trailingCommas": "onlyMultiLine",
|
||||
"arrowFunction.useParentheses": "preferNone",
|
||||
"importDeclaration.sortNamedImports": "caseInsensitive"
|
||||
|
||||
"module.sortImportDeclarations": "caseSensitive",
|
||||
"module.sortExportDeclarations": "caseSensitive",
|
||||
"importDeclaration.sortNamedImports": "caseSensitive",
|
||||
|
||||
"importDeclaration.forceMultiLine": "whenMultiple",
|
||||
"importDeclaration.forceSingleLine": false,
|
||||
"exportDeclaration.forceMultiLine": "whenMultiple",
|
||||
"exportDeclaration.forceSingleLine": false
|
||||
},
|
||||
"json": {
|
||||
"indentWidth": 2,
|
||||
@@ -36,7 +45,11 @@
|
||||
"indentWidth": 4,
|
||||
"useTabs": false,
|
||||
"quotes": "double",
|
||||
"scriptIndent": true,
|
||||
"styleIndent": true
|
||||
"scriptIndent": false,
|
||||
"styleIndent": false,
|
||||
|
||||
"vBindStyle": "short",
|
||||
"vOnStyle": "short",
|
||||
"formatComments": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import {
|
||||
expect,
|
||||
test,
|
||||
} from '@playwright/test';
|
||||
|
||||
test('home page has expected h1', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
@@ -17,16 +17,20 @@
|
||||
"test": "npm run test:e2e"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lucide/svelte": "^0.562.0",
|
||||
"@internationalized/date": "^3.10.0",
|
||||
"@lucide/svelte": "^0.561.0",
|
||||
"@playwright/test": "^1.57.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tsconfig/svelte": "^5.0.6",
|
||||
"bits-ui": "^2.14.4",
|
||||
"clsx": "^2.1.1",
|
||||
"dprint": "^0.50.2",
|
||||
"lefthook": "^2.0.13",
|
||||
"oxlint": "^1.35.0",
|
||||
"svelte": "^5.45.6",
|
||||
"svelte-check": "^4.3.4",
|
||||
"svelte-language-server": "^0.17.23",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwind-variants": "^3.2.2",
|
||||
"tailwindcss": "^4.1.18",
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
<script lang="ts">
|
||||
import favicon from '$lib/assets/favicon.svg';
|
||||
import './app.css';
|
||||
import Page from './routes/Page.svelte';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<link rel="icon" href={favicon} />
|
||||
</svelte:head>
|
||||
|
||||
<div id="app-root">
|
||||
<Page />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#app-root {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
20
src/ambient.d.ts
vendored
20
src/ambient.d.ts
vendored
@@ -1,20 +0,0 @@
|
||||
declare module '*.svelte' {
|
||||
import type { ComponentType } from 'svelte';
|
||||
const component: ComponentType;
|
||||
export default component;
|
||||
}
|
||||
|
||||
declare module '*.svg' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.png' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.jpg' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
18
src/app/App.svelte
Normal file
18
src/app/App.svelte
Normal file
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* App Component
|
||||
*
|
||||
* Application entry point component. Wraps the main page route within the shared
|
||||
* layout shell. This is the root component mounted by the application.
|
||||
*
|
||||
* Structure:
|
||||
* - Layout provides sidebar, header/footer, and page container
|
||||
* - Page renders the current route content
|
||||
*/
|
||||
import Page from '$routes/Page.svelte';
|
||||
import Layout from './ui/Layout.svelte';
|
||||
</script>
|
||||
|
||||
<Layout>
|
||||
<Page />
|
||||
</Layout>
|
||||
@@ -118,4 +118,23 @@
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Global utility - useful across your app */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Performance optimization for collapsible elements */
|
||||
[data-state="open"] {
|
||||
will-change: height;
|
||||
}
|
||||
|
||||
/* Smooth focus transitions - good globally */
|
||||
.peer:focus-visible ~ * {
|
||||
transition: all 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
37
src/app/types/ambient.d.ts
vendored
Normal file
37
src/app/types/ambient.d.ts
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
declare module '*.svelte' {
|
||||
import type {
|
||||
ComponentProps as SvelteComponentProps,
|
||||
ComponentType,
|
||||
Snippet,
|
||||
} from 'svelte';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
interface Component {
|
||||
new(options: {
|
||||
target: HTMLElement;
|
||||
props?: Record<string, unknown>;
|
||||
intro?: boolean;
|
||||
}): {
|
||||
$on: (event: string, handler: (...args: unknown[]) => unknown) => void;
|
||||
$destroy: () => void;
|
||||
$set: (props: Record<string, unknown>) => void;
|
||||
};
|
||||
}
|
||||
|
||||
export default Component;
|
||||
}
|
||||
|
||||
declare module '*.svg' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.png' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.jpg' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
46
src/app/ui/Layout.svelte
Normal file
46
src/app/ui/Layout.svelte
Normal file
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Layout Component
|
||||
*
|
||||
* Root layout wrapper that provides the application shell structure. Handles favicon,
|
||||
* sidebar provider initialization, and renders child routes with consistent structure.
|
||||
*
|
||||
* Layout structure:
|
||||
* - Header area (currently empty, reserved for future use)
|
||||
* - Collapsible sidebar with main content area
|
||||
* - Footer area (currently empty, reserved for future use)
|
||||
*
|
||||
* Uses Sidebar.Provider to enable mobile-responsive collapsible sidebar behavior
|
||||
* throughout the application.
|
||||
*/
|
||||
import favicon from '$shared/assets/favicon.svg';
|
||||
import * as Sidebar from '$shared/shadcn/ui/sidebar/index';
|
||||
import { FiltersSidebar } from '$widgets/FiltersSidebar';
|
||||
|
||||
/** Slot content for route pages to render */
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<link rel="icon" href={favicon} />
|
||||
</svelte:head>
|
||||
|
||||
<div class="app">
|
||||
<header></header>
|
||||
|
||||
<Sidebar.Provider>
|
||||
<FiltersSidebar />
|
||||
<main>
|
||||
<Sidebar.Trigger />
|
||||
{@render children?.()}
|
||||
</main>
|
||||
</Sidebar.Provider>
|
||||
<footer></footer>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#app-root {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
22
src/entities/Font/index.ts
Normal file
22
src/entities/Font/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export type {
|
||||
FontCategory,
|
||||
FontProvider,
|
||||
FontSubset,
|
||||
} from './model/font';
|
||||
export type {
|
||||
FontshareApiModel,
|
||||
FontshareDesigner,
|
||||
FontshareFeature,
|
||||
FontshareFont,
|
||||
FontsharePublisher,
|
||||
FontshareStyle,
|
||||
FontshareStyleProperties,
|
||||
FontshareTag,
|
||||
FontshareWeight,
|
||||
} from './model/fontshare_fonts';
|
||||
export type {
|
||||
FontFiles,
|
||||
FontItem,
|
||||
FontVariant,
|
||||
GoogleFontsApiModel,
|
||||
} from './model/google_fonts';
|
||||
14
src/entities/Font/model/font.ts
Normal file
14
src/entities/Font/model/font.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Font category
|
||||
*/
|
||||
export type FontCategory = 'sans-serif' | 'serif' | 'display' | 'handwriting' | 'monospace';
|
||||
|
||||
/**
|
||||
* Font provider
|
||||
*/
|
||||
export type FontProvider = 'google' | 'fontshare';
|
||||
|
||||
/**
|
||||
* Font subset
|
||||
*/
|
||||
export type FontSubset = 'latin' | 'latin-ext' | 'cyrillic' | 'greek' | 'arabic' | 'devanagari';
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { CollectionApiModel } from './collection';
|
||||
import type { CollectionApiModel } from '../../../shared/types/collection';
|
||||
|
||||
export const FONTSHARE_API_URL = 'https://api.fontshare.com/v2' as const;
|
||||
|
||||
/**
|
||||
* Model of Fontshare API response
|
||||
5
src/features/FilterFonts/index.ts
Normal file
5
src/features/FilterFonts/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export { categoryFilterStore } from './model/stores/categoryFilterStore';
|
||||
export { providersFilterStore } from './model/stores/providersFilterStore';
|
||||
export { subsetsFilterStore } from './model/stores/subsetsFilterStore';
|
||||
|
||||
export { clearAllFilters } from './model/services/clearAllFilters/clearAllFilters';
|
||||
70
src/features/FilterFonts/model/const/const.ts
Normal file
70
src/features/FilterFonts/model/const/const.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import type { Property } from '$shared/store/createFilterStore';
|
||||
|
||||
export const FONT_CATEGORIES: Property[] = [
|
||||
{
|
||||
id: 'serif',
|
||||
name: 'Serif',
|
||||
},
|
||||
{
|
||||
id: 'sans-serif',
|
||||
name: 'Sans-serif',
|
||||
},
|
||||
{
|
||||
id: 'display',
|
||||
name: 'Display',
|
||||
},
|
||||
{
|
||||
id: 'handwriting',
|
||||
name: 'Handwriting',
|
||||
},
|
||||
{
|
||||
id: 'monospace',
|
||||
name: 'Monospace',
|
||||
},
|
||||
{
|
||||
id: 'script',
|
||||
name: 'Script',
|
||||
},
|
||||
{
|
||||
id: 'slab',
|
||||
name: 'Slab',
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const FONT_PROVIDERS: Property[] = [
|
||||
{
|
||||
id: 'google',
|
||||
name: 'Google Fonts',
|
||||
},
|
||||
{
|
||||
id: 'fontshare',
|
||||
name: 'Fontshare',
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const FONT_SUBSETS: Property[] = [
|
||||
{
|
||||
id: 'latin',
|
||||
name: 'Latin',
|
||||
},
|
||||
{
|
||||
id: 'latin-ext',
|
||||
name: 'Latin Extended',
|
||||
},
|
||||
{
|
||||
id: 'cyrillic',
|
||||
name: 'Cyrillic',
|
||||
},
|
||||
{
|
||||
id: 'greek',
|
||||
name: 'Greek',
|
||||
},
|
||||
{
|
||||
id: 'arabic',
|
||||
name: 'Arabic',
|
||||
},
|
||||
{
|
||||
id: 'devanagari',
|
||||
name: 'Devanagari',
|
||||
},
|
||||
] as const;
|
||||
@@ -0,0 +1,9 @@
|
||||
import { categoryFilterStore } from '../../stores/categoryFilterStore';
|
||||
import { providersFilterStore } from '../../stores/providersFilterStore';
|
||||
import { subsetsFilterStore } from '../../stores/subsetsFilterStore';
|
||||
|
||||
export function clearAllFilters() {
|
||||
categoryFilterStore.deselectAllProperties();
|
||||
providersFilterStore.deselectAllProperties();
|
||||
subsetsFilterStore.deselectAllProperties();
|
||||
}
|
||||
18
src/features/FilterFonts/model/stores/categoryFilterStore.ts
Normal file
18
src/features/FilterFonts/model/stores/categoryFilterStore.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import {
|
||||
type FilterModel,
|
||||
createFilterStore,
|
||||
} from '$shared/store/createFilterStore';
|
||||
import { FONT_CATEGORIES } from '../const/const';
|
||||
|
||||
/**
|
||||
* Initial state for CategoryFilter
|
||||
*/
|
||||
export const initialState: FilterModel = {
|
||||
searchQuery: '',
|
||||
properties: FONT_CATEGORIES,
|
||||
};
|
||||
|
||||
/**
|
||||
* CategoryFilter store
|
||||
*/
|
||||
export const categoryFilterStore = createFilterStore(initialState);
|
||||
@@ -0,0 +1,18 @@
|
||||
import {
|
||||
type FilterModel,
|
||||
createFilterStore,
|
||||
} from '$shared/store/createFilterStore';
|
||||
import { FONT_PROVIDERS } from '../const/const';
|
||||
|
||||
/**
|
||||
* Initial state for ProvidersFilter
|
||||
*/
|
||||
export const initialState: FilterModel = {
|
||||
searchQuery: '',
|
||||
properties: FONT_PROVIDERS,
|
||||
};
|
||||
|
||||
/**
|
||||
* ProvidersFilter store
|
||||
*/
|
||||
export const providersFilterStore = createFilterStore(initialState);
|
||||
18
src/features/FilterFonts/model/stores/subsetsFilterStore.ts
Normal file
18
src/features/FilterFonts/model/stores/subsetsFilterStore.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import {
|
||||
type FilterModel,
|
||||
createFilterStore,
|
||||
} from '$shared/store/createFilterStore';
|
||||
import { FONT_SUBSETS } from '../const/const';
|
||||
|
||||
/**
|
||||
* Initial state for SubsetsFilter
|
||||
*/
|
||||
const initialState: FilterModel = {
|
||||
searchQuery: '',
|
||||
properties: FONT_SUBSETS,
|
||||
};
|
||||
|
||||
/**
|
||||
* SubsetsFilter store
|
||||
*/
|
||||
export const subsetsFilterStore = createFilterStore(initialState);
|
||||
@@ -1 +0,0 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
||||
@@ -1,37 +0,0 @@
|
||||
/**
|
||||
* Model of response with error
|
||||
*/
|
||||
export interface ApiErrorResponse {
|
||||
/**
|
||||
* Error text
|
||||
*/
|
||||
error: string;
|
||||
/**
|
||||
* Status
|
||||
*/
|
||||
status: number;
|
||||
/**
|
||||
* Status text
|
||||
*/
|
||||
statusText: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Model of response with success
|
||||
*/
|
||||
export interface ApiSuccessResponse<T> {
|
||||
/**
|
||||
* Data
|
||||
*/
|
||||
data: T;
|
||||
/**
|
||||
* Status
|
||||
*/
|
||||
status: number;
|
||||
/**
|
||||
* Status text
|
||||
*/
|
||||
statusText: string;
|
||||
}
|
||||
|
||||
export type ApiResponse<T> = ApiErrorResponse | ApiSuccessResponse<T>;
|
||||
@@ -1,13 +0,0 @@
|
||||
import { type ClassValue, clsx } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, 'child'> : T;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, 'children'> : T;
|
||||
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
|
||||
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };
|
||||
@@ -1,5 +1,6 @@
|
||||
import App from '$app/App.svelte';
|
||||
import { mount } from 'svelte';
|
||||
import App from './App.svelte';
|
||||
import '$app/styles/app.css';
|
||||
|
||||
mount(App, {
|
||||
target: document.getElementById('app')!,
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
<script>
|
||||
import Button from '$lib/components/ui/button/button.svelte';
|
||||
/**
|
||||
* Page Component
|
||||
*
|
||||
* Main page route component. This is the default route that users see when
|
||||
* accessing the application. Currently displays a welcome message.
|
||||
*
|
||||
* Note: This is a placeholder component. Replace with actual application content
|
||||
* as the font comparison and filtering features are implemented.
|
||||
*/
|
||||
</script>
|
||||
|
||||
<h1>Welcome to Svelte + Vite</h1>
|
||||
<p>
|
||||
Visit <a href="https://svelte.dev/docs">svelte.dev/docs</a> to read the documentation
|
||||
</p>
|
||||
<Button>Click me!</Button>
|
||||
|
||||
61
src/shared/api/api.ts
Normal file
61
src/shared/api/api.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { ApiResponse } from '$shared/types/common';
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
public status: number,
|
||||
message: string,
|
||||
public response?: Response,
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'ApiError';
|
||||
}
|
||||
}
|
||||
|
||||
async function request<T>(
|
||||
url: string,
|
||||
options?: RequestInit,
|
||||
): Promise<ApiResponse<T>> {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options?.headers,
|
||||
},
|
||||
...options,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new ApiError(
|
||||
response.status,
|
||||
`Request failed: ${response.statusText}`,
|
||||
response,
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json() as T;
|
||||
|
||||
return {
|
||||
data,
|
||||
status: response.status,
|
||||
};
|
||||
}
|
||||
|
||||
export const api = {
|
||||
get: <T>(url: string, options?: RequestInit) => request<T>(url, { ...options, method: 'GET' }),
|
||||
|
||||
post: <T>(url: string, body?: unknown, options?: RequestInit) =>
|
||||
request<T>(url, {
|
||||
...options,
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
|
||||
put: <T>(url: string, body?: unknown, options?: RequestInit) =>
|
||||
request<T>(url, {
|
||||
...options,
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
|
||||
delete: <T>(url: string, options?: RequestInit) =>
|
||||
request<T>(url, { ...options, method: 'DELETE' }),
|
||||
};
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
9
src/shared/shadcn/hooks/is-mobile.svelte.ts
Normal file
9
src/shared/shadcn/hooks/is-mobile.svelte.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { MediaQuery } from 'svelte/reactivity';
|
||||
|
||||
const DEFAULT_MOBILE_BREAKPOINT = 768;
|
||||
|
||||
export class IsMobile extends MediaQuery {
|
||||
constructor(breakpoint: number = DEFAULT_MOBILE_BREAKPOINT) {
|
||||
super(`max-width: ${breakpoint - 1}px`);
|
||||
}
|
||||
}
|
||||
57
src/shared/shadcn/ui/badge/badge.svelte
Normal file
57
src/shared/shadcn/ui/badge/badge.svelte
Normal file
@@ -0,0 +1,57 @@
|
||||
<script lang="ts" module>
|
||||
import {
|
||||
type VariantProps,
|
||||
tv,
|
||||
} from 'tailwind-variants';
|
||||
|
||||
export const badgeVariants = tv({
|
||||
base:
|
||||
'focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3',
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent',
|
||||
destructive:
|
||||
'bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white',
|
||||
outline: 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
});
|
||||
|
||||
export type BadgeVariant = VariantProps<typeof badgeVariants>['variant'];
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
type WithElementRef,
|
||||
cn,
|
||||
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import type { HTMLAnchorAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
href,
|
||||
class: className,
|
||||
variant = 'default',
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAnchorAttributes> & {
|
||||
variant?: BadgeVariant;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<svelte:element
|
||||
this={href ? 'a' : 'span'}
|
||||
bind:this={ref}
|
||||
data-slot="badge"
|
||||
{href}
|
||||
class={cn(badgeVariants({ variant }), className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</svelte:element>
|
||||
5
src/shared/shadcn/ui/badge/index.ts
Normal file
5
src/shared/shadcn/ui/badge/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export { default as Badge } from './badge.svelte';
|
||||
export {
|
||||
type BadgeVariant,
|
||||
badgeVariants,
|
||||
} from './badge.svelte';
|
||||
@@ -1,7 +1,16 @@
|
||||
<script lang="ts" module>
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
|
||||
import { tv, type VariantProps } from 'tailwind-variants';
|
||||
import {
|
||||
type WithElementRef,
|
||||
cn,
|
||||
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import type {
|
||||
HTMLAnchorAttributes,
|
||||
HTMLButtonAttributes,
|
||||
} from 'svelte/elements';
|
||||
import {
|
||||
type VariantProps,
|
||||
tv,
|
||||
} from 'tailwind-variants';
|
||||
|
||||
export const buttonVariants = tv({
|
||||
base:
|
||||
@@ -45,7 +54,7 @@ export type ButtonProps =
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
let {
|
||||
let {
|
||||
class: className,
|
||||
variant = 'default',
|
||||
size = 'default',
|
||||
39
src/shared/shadcn/ui/checkbox/checkbox.svelte
Normal file
39
src/shared/shadcn/ui/checkbox/checkbox.svelte
Normal file
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type WithoutChildrenOrChild,
|
||||
cn,
|
||||
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import CheckIcon from '@lucide/svelte/icons/check';
|
||||
import MinusIcon from '@lucide/svelte/icons/minus';
|
||||
import { Checkbox as CheckboxPrimitive } from 'bits-ui';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
checked = $bindable(false),
|
||||
indeterminate = $bindable(false),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<CheckboxPrimitive.RootProps> = $props();
|
||||
</script>
|
||||
|
||||
<CheckboxPrimitive.Root
|
||||
bind:ref
|
||||
data-slot="checkbox"
|
||||
class={cn(
|
||||
'border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive peer flex size-4 shrink-0 items-center justify-center rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
bind:checked
|
||||
bind:indeterminate
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ checked, indeterminate })}
|
||||
<div data-slot="checkbox-indicator" class="text-current transition-none">
|
||||
{#if checked}
|
||||
<CheckIcon class="size-3.5" />
|
||||
{:else if indeterminate}
|
||||
<MinusIcon class="size-3.5" />
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</CheckboxPrimitive.Root>
|
||||
6
src/shared/shadcn/ui/checkbox/index.ts
Normal file
6
src/shared/shadcn/ui/checkbox/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import Root from './checkbox.svelte';
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Checkbox,
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Collapsible as CollapsiblePrimitive } from 'bits-ui';
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: CollapsiblePrimitive.ContentProps = $props();
|
||||
</script>
|
||||
|
||||
<CollapsiblePrimitive.Content bind:ref data-slot="collapsible-content" {...restProps} />
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Collapsible as CollapsiblePrimitive } from 'bits-ui';
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: CollapsiblePrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<CollapsiblePrimitive.Trigger bind:ref data-slot="collapsible-trigger" {...restProps} />
|
||||
11
src/shared/shadcn/ui/collapsible/collapsible.svelte
Normal file
11
src/shared/shadcn/ui/collapsible/collapsible.svelte
Normal file
@@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { Collapsible as CollapsiblePrimitive } from 'bits-ui';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
open = $bindable(false),
|
||||
...restProps
|
||||
}: CollapsiblePrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<CollapsiblePrimitive.Root bind:ref bind:open data-slot="collapsible" {...restProps} />
|
||||
13
src/shared/shadcn/ui/collapsible/index.ts
Normal file
13
src/shared/shadcn/ui/collapsible/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import Content from './collapsible-content.svelte';
|
||||
import Trigger from './collapsible-trigger.svelte';
|
||||
import Root from './collapsible.svelte';
|
||||
|
||||
export {
|
||||
Content,
|
||||
Content as CollapsibleContent,
|
||||
Root,
|
||||
//
|
||||
Root as Collapsible,
|
||||
Trigger,
|
||||
Trigger as CollapsibleTrigger,
|
||||
};
|
||||
7
src/shared/shadcn/ui/dialog/dialog-close.svelte
Normal file
7
src/shared/shadcn/ui/dialog/dialog-close.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: DialogPrimitive.CloseProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Close bind:ref data-slot="dialog-close" {...restProps} />
|
||||
48
src/shared/shadcn/ui/dialog/dialog-content.svelte
Normal file
48
src/shared/shadcn/ui/dialog/dialog-content.svelte
Normal file
@@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type WithoutChildrenOrChild,
|
||||
cn,
|
||||
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import XIcon from '@lucide/svelte/icons/x';
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
import DialogPortal from './dialog-portal.svelte';
|
||||
import * as Dialog from './index.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
portalProps,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
|
||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DialogPortal>>;
|
||||
children: Snippet;
|
||||
showCloseButton?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DialogPortal {...portalProps}>
|
||||
<Dialog.Overlay />
|
||||
<DialogPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="dialog-content"
|
||||
class={cn(
|
||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{#if showCloseButton}
|
||||
<DialogPrimitive.Close
|
||||
class="ring-offset-background focus:ring-ring absolute end-4 top-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
>
|
||||
<XIcon />
|
||||
<span class="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
{/if}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
17
src/shared/shadcn/ui/dialog/dialog-description.svelte
Normal file
17
src/shared/shadcn/ui/dialog/dialog-description.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.DescriptionProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Description
|
||||
bind:ref
|
||||
data-slot="dialog-description"
|
||||
class={cn('text-muted-foreground text-sm', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
23
src/shared/shadcn/ui/dialog/dialog-footer.svelte
Normal file
23
src/shared/shadcn/ui/dialog/dialog-footer.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type WithElementRef,
|
||||
cn,
|
||||
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="dialog-footer"
|
||||
class={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
23
src/shared/shadcn/ui/dialog/dialog-header.svelte
Normal file
23
src/shared/shadcn/ui/dialog/dialog-header.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type WithElementRef,
|
||||
cn,
|
||||
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="dialog-header"
|
||||
class={cn('flex flex-col gap-2 text-center sm:text-start', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
20
src/shared/shadcn/ui/dialog/dialog-overlay.svelte
Normal file
20
src/shared/shadcn/ui/dialog/dialog-overlay.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.OverlayProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Overlay
|
||||
bind:ref
|
||||
data-slot="dialog-overlay"
|
||||
class={cn(
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
7
src/shared/shadcn/ui/dialog/dialog-portal.svelte
Normal file
7
src/shared/shadcn/ui/dialog/dialog-portal.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
|
||||
let { ...restProps }: DialogPrimitive.PortalProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Portal {...restProps} />
|
||||
17
src/shared/shadcn/ui/dialog/dialog-title.svelte
Normal file
17
src/shared/shadcn/ui/dialog/dialog-title.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.TitleProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Title
|
||||
bind:ref
|
||||
data-slot="dialog-title"
|
||||
class={cn('text-lg leading-none font-semibold', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
7
src/shared/shadcn/ui/dialog/dialog-trigger.svelte
Normal file
7
src/shared/shadcn/ui/dialog/dialog-trigger.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: DialogPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Trigger bind:ref data-slot="dialog-trigger" {...restProps} />
|
||||
7
src/shared/shadcn/ui/dialog/dialog.svelte
Normal file
7
src/shared/shadcn/ui/dialog/dialog.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
|
||||
let { open = $bindable(false), ...restProps }: DialogPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Root bind:open {...restProps} />
|
||||
34
src/shared/shadcn/ui/dialog/index.ts
Normal file
34
src/shared/shadcn/ui/dialog/index.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import Close from './dialog-close.svelte';
|
||||
import Content from './dialog-content.svelte';
|
||||
import Description from './dialog-description.svelte';
|
||||
import Footer from './dialog-footer.svelte';
|
||||
import Header from './dialog-header.svelte';
|
||||
import Overlay from './dialog-overlay.svelte';
|
||||
import Portal from './dialog-portal.svelte';
|
||||
import Title from './dialog-title.svelte';
|
||||
import Trigger from './dialog-trigger.svelte';
|
||||
import Root from './dialog.svelte';
|
||||
|
||||
export {
|
||||
Close,
|
||||
Close as DialogClose,
|
||||
Content,
|
||||
Content as DialogContent,
|
||||
Description,
|
||||
Description as DialogDescription,
|
||||
Footer,
|
||||
Footer as DialogFooter,
|
||||
Header,
|
||||
Header as DialogHeader,
|
||||
Overlay,
|
||||
Overlay as DialogOverlay,
|
||||
Portal,
|
||||
Portal as DialogPortal,
|
||||
Root,
|
||||
//
|
||||
Root as Dialog,
|
||||
Title,
|
||||
Title as DialogTitle,
|
||||
Trigger,
|
||||
Trigger as DialogTrigger,
|
||||
};
|
||||
7
src/shared/shadcn/ui/input/index.ts
Normal file
7
src/shared/shadcn/ui/input/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Root from './input.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Input,
|
||||
};
|
||||
58
src/shared/shadcn/ui/input/input.svelte
Normal file
58
src/shared/shadcn/ui/input/input.svelte
Normal file
@@ -0,0 +1,58 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type WithElementRef,
|
||||
cn,
|
||||
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import type {
|
||||
HTMLInputAttributes,
|
||||
HTMLInputTypeAttribute,
|
||||
} from 'svelte/elements';
|
||||
|
||||
type InputType = Exclude<HTMLInputTypeAttribute, 'file'>;
|
||||
|
||||
type Props = WithElementRef<
|
||||
& Omit<HTMLInputAttributes, 'type'>
|
||||
& ({ type: 'file'; files?: FileList } | { type?: InputType; files?: undefined })
|
||||
>;
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
type,
|
||||
files = $bindable(),
|
||||
class: className,
|
||||
'data-slot': dataSlot = 'input',
|
||||
...restProps
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if type === 'file'}
|
||||
<input
|
||||
bind:this={ref}
|
||||
data-slot={dataSlot}
|
||||
class={cn(
|
||||
'selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 pt-1.5 text-sm font-medium shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
||||
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||
className,
|
||||
)}
|
||||
type="file"
|
||||
bind:files
|
||||
bind:value
|
||||
{...restProps}
|
||||
/>
|
||||
{:else}
|
||||
<input
|
||||
bind:this={ref}
|
||||
data-slot={dataSlot}
|
||||
class={cn(
|
||||
'border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
||||
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||
className,
|
||||
)}
|
||||
{type}
|
||||
bind:value
|
||||
{...restProps}
|
||||
/>
|
||||
{/if}
|
||||
7
src/shared/shadcn/ui/label/index.ts
Normal file
7
src/shared/shadcn/ui/label/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Root from './label.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Label,
|
||||
};
|
||||
20
src/shared/shadcn/ui/label/label.svelte
Normal file
20
src/shared/shadcn/ui/label/label.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import { Label as LabelPrimitive } from 'bits-ui';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: LabelPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<LabelPrimitive.Root
|
||||
bind:ref
|
||||
data-slot="label"
|
||||
class={cn(
|
||||
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
7
src/shared/shadcn/ui/separator/index.ts
Normal file
7
src/shared/shadcn/ui/separator/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Root from './separator.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Separator,
|
||||
};
|
||||
21
src/shared/shadcn/ui/separator/separator.svelte
Normal file
21
src/shared/shadcn/ui/separator/separator.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import { Separator as SeparatorPrimitive } from 'bits-ui';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
'data-slot': dataSlot = 'separator',
|
||||
...restProps
|
||||
}: SeparatorPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<SeparatorPrimitive.Root
|
||||
bind:ref
|
||||
data-slot={dataSlot}
|
||||
class={cn(
|
||||
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
34
src/shared/shadcn/ui/sheet/index.ts
Normal file
34
src/shared/shadcn/ui/sheet/index.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import Close from './sheet-close.svelte';
|
||||
import Content from './sheet-content.svelte';
|
||||
import Description from './sheet-description.svelte';
|
||||
import Footer from './sheet-footer.svelte';
|
||||
import Header from './sheet-header.svelte';
|
||||
import Overlay from './sheet-overlay.svelte';
|
||||
import Portal from './sheet-portal.svelte';
|
||||
import Title from './sheet-title.svelte';
|
||||
import Trigger from './sheet-trigger.svelte';
|
||||
import Root from './sheet.svelte';
|
||||
|
||||
export {
|
||||
Close,
|
||||
Close as SheetClose,
|
||||
Content,
|
||||
Content as SheetContent,
|
||||
Description,
|
||||
Description as SheetDescription,
|
||||
Footer,
|
||||
Footer as SheetFooter,
|
||||
Header,
|
||||
Header as SheetHeader,
|
||||
Overlay,
|
||||
Overlay as SheetOverlay,
|
||||
Portal,
|
||||
Portal as SheetPortal,
|
||||
Root,
|
||||
//
|
||||
Root as Sheet,
|
||||
Title,
|
||||
Title as SheetTitle,
|
||||
Trigger,
|
||||
Trigger as SheetTrigger,
|
||||
};
|
||||
7
src/shared/shadcn/ui/sheet/sheet-close.svelte
Normal file
7
src/shared/shadcn/ui/sheet/sheet-close.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from 'bits-ui';
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: SheetPrimitive.CloseProps = $props();
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Close bind:ref data-slot="sheet-close" {...restProps} />
|
||||
70
src/shared/shadcn/ui/sheet/sheet-content.svelte
Normal file
70
src/shared/shadcn/ui/sheet/sheet-content.svelte
Normal file
@@ -0,0 +1,70 @@
|
||||
<script lang="ts" module>
|
||||
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 flex flex-col gap-4 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 h-auto border-b',
|
||||
bottom:
|
||||
'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t',
|
||||
left:
|
||||
'data-[state=closed]:slide-out-to-start data-[state=open]:slide-in-from-start inset-y-0 start-0 h-full w-3/4 border-e sm:max-w-sm',
|
||||
right:
|
||||
'data-[state=closed]:slide-out-to-end data-[state=open]:slide-in-from-end inset-y-0 end-0 h-full w-3/4 border-s sm:max-w-sm',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: 'right',
|
||||
},
|
||||
});
|
||||
|
||||
export type Side = VariantProps<typeof sheetVariants>['side'];
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
type WithoutChildrenOrChild,
|
||||
cn,
|
||||
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import XIcon from '@lucide/svelte/icons/x';
|
||||
import { Dialog as SheetPrimitive } from 'bits-ui';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
import SheetOverlay from './sheet-overlay.svelte';
|
||||
import SheetPortal from './sheet-portal.svelte';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
side = 'right',
|
||||
portalProps,
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SheetPrimitive.ContentProps> & {
|
||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof SheetPortal>>;
|
||||
side?: Side;
|
||||
children: Snippet;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<SheetPortal {...portalProps}>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="sheet-content"
|
||||
class={cn(sheetVariants({ side }), className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
<SheetPrimitive.Close
|
||||
class="ring-offset-background focus-visible:ring-ring absolute end-4 top-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:pointer-events-none"
|
||||
>
|
||||
<XIcon class="size-4" />
|
||||
<span class="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
17
src/shared/shadcn/ui/sheet/sheet-description.svelte
Normal file
17
src/shared/shadcn/ui/sheet/sheet-description.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import { Dialog as SheetPrimitive } from 'bits-ui';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: SheetPrimitive.DescriptionProps = $props();
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Description
|
||||
bind:ref
|
||||
data-slot="sheet-description"
|
||||
class={cn('text-muted-foreground text-sm', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
23
src/shared/shadcn/ui/sheet/sheet-footer.svelte
Normal file
23
src/shared/shadcn/ui/sheet/sheet-footer.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type WithElementRef,
|
||||
cn,
|
||||
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="sheet-footer"
|
||||
class={cn('mt-auto flex flex-col gap-2 p-4', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
23
src/shared/shadcn/ui/sheet/sheet-header.svelte
Normal file
23
src/shared/shadcn/ui/sheet/sheet-header.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type WithElementRef,
|
||||
cn,
|
||||
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="sheet-header"
|
||||
class={cn('flex flex-col gap-1.5 p-4', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
20
src/shared/shadcn/ui/sheet/sheet-overlay.svelte
Normal file
20
src/shared/shadcn/ui/sheet/sheet-overlay.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import { Dialog as SheetPrimitive } from 'bits-ui';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: SheetPrimitive.OverlayProps = $props();
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Overlay
|
||||
bind:ref
|
||||
data-slot="sheet-overlay"
|
||||
class={cn(
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
7
src/shared/shadcn/ui/sheet/sheet-portal.svelte
Normal file
7
src/shared/shadcn/ui/sheet/sheet-portal.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from 'bits-ui';
|
||||
|
||||
let { ...restProps }: SheetPrimitive.PortalProps = $props();
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Portal {...restProps} />
|
||||
17
src/shared/shadcn/ui/sheet/sheet-title.svelte
Normal file
17
src/shared/shadcn/ui/sheet/sheet-title.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import { Dialog as SheetPrimitive } from 'bits-ui';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: SheetPrimitive.TitleProps = $props();
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Title
|
||||
bind:ref
|
||||
data-slot="sheet-title"
|
||||
class={cn('text-foreground font-semibold', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
7
src/shared/shadcn/ui/sheet/sheet-trigger.svelte
Normal file
7
src/shared/shadcn/ui/sheet/sheet-trigger.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from 'bits-ui';
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: SheetPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Trigger bind:ref data-slot="sheet-trigger" {...restProps} />
|
||||
7
src/shared/shadcn/ui/sheet/sheet.svelte
Normal file
7
src/shared/shadcn/ui/sheet/sheet.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from 'bits-ui';
|
||||
|
||||
let { open = $bindable(false), ...restProps }: SheetPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Root bind:open {...restProps} />
|
||||
6
src/shared/shadcn/ui/sidebar/constants.ts
Normal file
6
src/shared/shadcn/ui/sidebar/constants.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
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';
|
||||
84
src/shared/shadcn/ui/sidebar/context.svelte.ts
Normal file
84
src/shared/shadcn/ui/sidebar/context.svelte.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { IsMobile } from '$shared/shadcn/hooks/is-mobile.svelte.js';
|
||||
import {
|
||||
getContext,
|
||||
setContext,
|
||||
} from 'svelte';
|
||||
import { SIDEBAR_KEYBOARD_SHORTCUT } from './constants.js';
|
||||
|
||||
type Getter<T> = () => T;
|
||||
|
||||
export type SidebarStateProps = {
|
||||
/**
|
||||
* A getter function that returns the current open state of the sidebar.
|
||||
* We use a getter function here to support `bind:open` on the `Sidebar.Provider`
|
||||
* component.
|
||||
*/
|
||||
open: Getter<boolean>;
|
||||
|
||||
/**
|
||||
* A function that sets the open state of the sidebar. To support `bind:open`, we need
|
||||
* a source of truth for changing the open state to ensure it will be synced throughout
|
||||
* the sub-components and any `bind:` references.
|
||||
*/
|
||||
setOpen: (open: boolean) => void;
|
||||
};
|
||||
|
||||
class SidebarState {
|
||||
readonly props: SidebarStateProps;
|
||||
open = $derived.by(() => this.props.open());
|
||||
openMobile = $state(false);
|
||||
setOpen: SidebarStateProps['setOpen'];
|
||||
#isMobile: IsMobile;
|
||||
state = $derived.by(() => (this.open ? 'expanded' : 'collapsed'));
|
||||
|
||||
constructor(props: SidebarStateProps) {
|
||||
this.setOpen = props.setOpen;
|
||||
this.#isMobile = new IsMobile();
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
// Convenience getter for checking if the sidebar is mobile
|
||||
// without this, we would need to use `sidebar.isMobile.current` everywhere
|
||||
get isMobile() {
|
||||
return this.#isMobile.current;
|
||||
}
|
||||
|
||||
// Event handler to apply to the `<svelte:window>`
|
||||
handleShortcutKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key === SIDEBAR_KEYBOARD_SHORTCUT && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
this.toggle();
|
||||
}
|
||||
};
|
||||
|
||||
setOpenMobile = (value: boolean) => {
|
||||
this.openMobile = value;
|
||||
};
|
||||
|
||||
toggle = () => {
|
||||
return this.#isMobile.current
|
||||
? (this.openMobile = !this.openMobile)
|
||||
: this.setOpen(!this.open);
|
||||
};
|
||||
}
|
||||
|
||||
const SYMBOL_KEY = 'scn-sidebar';
|
||||
|
||||
/**
|
||||
* Instantiates a new `SidebarState` instance and sets it in the context.
|
||||
*
|
||||
* @param props The constructor props for the `SidebarState` class.
|
||||
* @returns The `SidebarState` instance.
|
||||
*/
|
||||
export function setSidebar(props: SidebarStateProps): SidebarState {
|
||||
return setContext(Symbol.for(SYMBOL_KEY), new SidebarState(props));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the `SidebarState` instance from the context. This is a class instance,
|
||||
* so you cannot destructure it.
|
||||
* @returns The `SidebarState` instance.
|
||||
*/
|
||||
export function useSidebar(): SidebarState {
|
||||
return getContext(Symbol.for(SYMBOL_KEY));
|
||||
}
|
||||
75
src/shared/shadcn/ui/sidebar/index.ts
Normal file
75
src/shared/shadcn/ui/sidebar/index.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
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,
|
||||
Content as SidebarContent,
|
||||
Footer,
|
||||
Footer as SidebarFooter,
|
||||
Group,
|
||||
Group as SidebarGroup,
|
||||
GroupAction,
|
||||
GroupAction as SidebarGroupAction,
|
||||
GroupContent,
|
||||
GroupContent as SidebarGroupContent,
|
||||
GroupLabel,
|
||||
GroupLabel as SidebarGroupLabel,
|
||||
Header,
|
||||
Header as SidebarHeader,
|
||||
Input,
|
||||
Input as SidebarInput,
|
||||
Inset,
|
||||
Inset as SidebarInset,
|
||||
Menu,
|
||||
Menu as SidebarMenu,
|
||||
MenuAction,
|
||||
MenuAction as SidebarMenuAction,
|
||||
MenuBadge,
|
||||
MenuBadge as SidebarMenuBadge,
|
||||
MenuButton,
|
||||
MenuButton as SidebarMenuButton,
|
||||
MenuItem,
|
||||
MenuItem as SidebarMenuItem,
|
||||
MenuSkeleton,
|
||||
MenuSkeleton as SidebarMenuSkeleton,
|
||||
MenuSub,
|
||||
MenuSub as SidebarMenuSub,
|
||||
MenuSubButton,
|
||||
MenuSubButton as SidebarMenuSubButton,
|
||||
MenuSubItem,
|
||||
MenuSubItem as SidebarMenuSubItem,
|
||||
Provider,
|
||||
Provider as SidebarProvider,
|
||||
Rail,
|
||||
Rail as SidebarRail,
|
||||
Root,
|
||||
//
|
||||
Root as Sidebar,
|
||||
Separator,
|
||||
Separator as SidebarSeparator,
|
||||
Trigger,
|
||||
Trigger as SidebarTrigger,
|
||||
useSidebar,
|
||||
};
|
||||
27
src/shared/shadcn/ui/sidebar/sidebar-content.svelte
Normal file
27
src/shared/shadcn/ui/sidebar/sidebar-content.svelte
Normal file
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type WithElementRef,
|
||||
cn,
|
||||
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="sidebar-content"
|
||||
data-sidebar="content"
|
||||
class={cn(
|
||||
'flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden',
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
24
src/shared/shadcn/ui/sidebar/sidebar-footer.svelte
Normal file
24
src/shared/shadcn/ui/sidebar/sidebar-footer.svelte
Normal file
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type WithElementRef,
|
||||
cn,
|
||||
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="sidebar-footer"
|
||||
data-sidebar="footer"
|
||||
class={cn('flex flex-col gap-2 p-2', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
39
src/shared/shadcn/ui/sidebar/sidebar-group-action.svelte
Normal file
39
src/shared/shadcn/ui/sidebar/sidebar-group-action.svelte
Normal file
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type WithElementRef,
|
||||
cn,
|
||||
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { HTMLButtonAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
child,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLButtonAttributes> & {
|
||||
child?: Snippet<[{ props: Record<string, unknown> }]>;
|
||||
} = $props();
|
||||
|
||||
const mergedProps = $derived({
|
||||
class: cn(
|
||||
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute end-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden 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 md:after:hidden',
|
||||
'group-data-[collapsible=icon]:hidden',
|
||||
className,
|
||||
),
|
||||
'data-slot': 'sidebar-group-action',
|
||||
'data-sidebar': 'group-action',
|
||||
...restProps,
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<button bind:this={ref} {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
{/if}
|
||||
24
src/shared/shadcn/ui/sidebar/sidebar-group-content.svelte
Normal file
24
src/shared/shadcn/ui/sidebar/sidebar-group-content.svelte
Normal file
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type WithElementRef,
|
||||
cn,
|
||||
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="sidebar-group-content"
|
||||
data-sidebar="group-content"
|
||||
class={cn('w-full text-sm', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
37
src/shared/shadcn/ui/sidebar/sidebar-group-label.svelte
Normal file
37
src/shared/shadcn/ui/sidebar/sidebar-group-label.svelte
Normal file
@@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type WithElementRef,
|
||||
cn,
|
||||
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
children,
|
||||
child,
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> & {
|
||||
child?: Snippet<[{ props: Record<string, unknown> }]>;
|
||||
} = $props();
|
||||
|
||||
const mergedProps = $derived({
|
||||
class: cn(
|
||||
'text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
|
||||
'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0',
|
||||
className,
|
||||
),
|
||||
'data-slot': 'sidebar-group-label',
|
||||
'data-sidebar': 'group-label',
|
||||
...restProps,
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<div bind:this={ref} {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
24
src/shared/shadcn/ui/sidebar/sidebar-group.svelte
Normal file
24
src/shared/shadcn/ui/sidebar/sidebar-group.svelte
Normal file
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type WithElementRef,
|
||||
cn,
|
||||
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="sidebar-group"
|
||||
data-sidebar="group"
|
||||
class={cn('relative flex w-full min-w-0 flex-col p-2', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
24
src/shared/shadcn/ui/sidebar/sidebar-header.svelte
Normal file
24
src/shared/shadcn/ui/sidebar/sidebar-header.svelte
Normal file
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type WithElementRef,
|
||||
cn,
|
||||
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="sidebar-header"
|
||||
data-sidebar="header"
|
||||
class={cn('flex flex-col gap-2 p-2', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
21
src/shared/shadcn/ui/sidebar/sidebar-input.svelte
Normal file
21
src/shared/shadcn/ui/sidebar/sidebar-input.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { Input } from '$shared/shadcn/ui/input/index.js';
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(''),
|
||||
class: className,
|
||||
...restProps
|
||||
}: ComponentProps<typeof Input> = $props();
|
||||
</script>
|
||||
|
||||
<Input
|
||||
bind:ref
|
||||
bind:value
|
||||
data-slot="sidebar-input"
|
||||
data-sidebar="input"
|
||||
class={cn('bg-background h-8 w-full shadow-none', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
27
src/shared/shadcn/ui/sidebar/sidebar-inset.svelte
Normal file
27
src/shared/shadcn/ui/sidebar/sidebar-inset.svelte
Normal file
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type WithElementRef,
|
||||
cn,
|
||||
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||
</script>
|
||||
|
||||
<main
|
||||
bind:this={ref}
|
||||
data-slot="sidebar-inset"
|
||||
class={cn(
|
||||
'bg-background relative flex w-full flex-1 flex-col',
|
||||
'md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ms-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ms-2',
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</main>
|
||||
46
src/shared/shadcn/ui/sidebar/sidebar-menu-action.svelte
Normal file
46
src/shared/shadcn/ui/sidebar/sidebar-menu-action.svelte
Normal file
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type WithElementRef,
|
||||
cn,
|
||||
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { HTMLButtonAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
showOnHover = false,
|
||||
children,
|
||||
child,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLButtonAttributes> & {
|
||||
child?: Snippet<[{ props: Record<string, unknown> }]>;
|
||||
showOnHover?: boolean;
|
||||
} = $props();
|
||||
|
||||
const mergedProps = $derived({
|
||||
class: cn(
|
||||
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute end-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden 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 md:after:hidden',
|
||||
'peer-data-[size=sm]/menu-button:top-1',
|
||||
'peer-data-[size=default]/menu-button:top-1.5',
|
||||
'peer-data-[size=lg]/menu-button:top-2.5',
|
||||
'group-data-[collapsible=icon]:hidden',
|
||||
showOnHover
|
||||
&& 'peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0',
|
||||
className,
|
||||
),
|
||||
'data-slot': 'sidebar-menu-action',
|
||||
'data-sidebar': 'menu-action',
|
||||
...restProps,
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<button bind:this={ref} {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
{/if}
|
||||
32
src/shared/shadcn/ui/sidebar/sidebar-menu-badge.svelte
Normal file
32
src/shared/shadcn/ui/sidebar/sidebar-menu-badge.svelte
Normal file
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type WithElementRef,
|
||||
cn,
|
||||
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="sidebar-menu-badge"
|
||||
data-sidebar="menu-badge"
|
||||
class={cn(
|
||||
'text-sidebar-foreground pointer-events-none absolute end-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none',
|
||||
'peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground',
|
||||
'peer-data-[size=sm]/menu-button:top-1',
|
||||
'peer-data-[size=default]/menu-button:top-1.5',
|
||||
'peer-data-[size=lg]/menu-button:top-2.5',
|
||||
'group-data-[collapsible=icon]:hidden',
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
114
src/shared/shadcn/ui/sidebar/sidebar-menu-button.svelte
Normal file
114
src/shared/shadcn/ui/sidebar/sidebar-menu-button.svelte
Normal file
@@ -0,0 +1,114 @@
|
||||
<script lang="ts" module>
|
||||
import {
|
||||
type VariantProps,
|
||||
tv,
|
||||
} from 'tailwind-variants';
|
||||
|
||||
export const sidebarMenuButtonVariants = tv({
|
||||
base:
|
||||
'peer/menu-button ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-start text-sm outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pe-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:font-medium [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
|
||||
outline:
|
||||
'bg-background hover:bg-sidebar-accent hover:text-sidebar-accent-foreground shadow-[0_0_0_1px_var(--sidebar-border)] hover:shadow-[0_0_0_1px_var(--sidebar-accent)]',
|
||||
},
|
||||
size: {
|
||||
default: 'h-8 text-sm',
|
||||
sm: 'h-7 text-xs',
|
||||
lg: 'h-12 text-sm group-data-[collapsible=icon]:p-0!',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
});
|
||||
|
||||
export type SidebarMenuButtonVariant = VariantProps<
|
||||
typeof sidebarMenuButtonVariants
|
||||
>['variant'];
|
||||
export type SidebarMenuButtonSize = VariantProps<typeof sidebarMenuButtonVariants>['size'];
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import * as Tooltip from '$shared/shadcn/ui/tooltip/index.js';
|
||||
import {
|
||||
type WithElementRef,
|
||||
type WithoutChildrenOrChild,
|
||||
cn,
|
||||
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import { mergeProps } from 'bits-ui';
|
||||
import type {
|
||||
ComponentProps,
|
||||
Snippet,
|
||||
} from 'svelte';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { useSidebar } from './context.svelte.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
child,
|
||||
variant = 'default',
|
||||
size = 'default',
|
||||
isActive = false,
|
||||
tooltipContent,
|
||||
tooltipContentProps,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLButtonElement>, HTMLButtonElement> & {
|
||||
isActive?: boolean;
|
||||
variant?: SidebarMenuButtonVariant;
|
||||
size?: SidebarMenuButtonSize;
|
||||
tooltipContent?: Snippet | string;
|
||||
tooltipContentProps?: WithoutChildrenOrChild<ComponentProps<typeof Tooltip.Content>>;
|
||||
child?: Snippet<[{ props: Record<string, unknown> }]>;
|
||||
} = $props();
|
||||
|
||||
const sidebar = useSidebar();
|
||||
|
||||
const buttonProps = $derived({
|
||||
class: cn(sidebarMenuButtonVariants({ variant, size }), className),
|
||||
'data-slot': 'sidebar-menu-button',
|
||||
'data-sidebar': 'menu-button',
|
||||
'data-size': size,
|
||||
'data-active': isActive,
|
||||
...restProps,
|
||||
});
|
||||
</script>
|
||||
|
||||
{#snippet Button({ props }: { props?: Record<string, unknown> })}
|
||||
{@const mergedProps = mergeProps(buttonProps, props)}
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<button bind:this={ref} {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#if !tooltipContent}
|
||||
{@render Button({})}
|
||||
{:else}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
{#snippet child({ props })}
|
||||
{@render Button({ props })}
|
||||
{/snippet}
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content
|
||||
side="right"
|
||||
align="center"
|
||||
hidden={sidebar.state !== 'collapsed' || sidebar.isMobile}
|
||||
{...tooltipContentProps}
|
||||
>
|
||||
{#if typeof tooltipContent === 'string'}
|
||||
{tooltipContent}
|
||||
{:else if tooltipContent}
|
||||
{@render tooltipContent()}
|
||||
{/if}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/if}
|
||||
24
src/shared/shadcn/ui/sidebar/sidebar-menu-item.svelte
Normal file
24
src/shared/shadcn/ui/sidebar/sidebar-menu-item.svelte
Normal file
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type WithElementRef,
|
||||
cn,
|
||||
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLLIElement>, HTMLLIElement> = $props();
|
||||
</script>
|
||||
|
||||
<li
|
||||
bind:this={ref}
|
||||
data-slot="sidebar-menu-item"
|
||||
data-sidebar="menu-item"
|
||||
class={cn('group/menu-item relative', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</li>
|
||||
39
src/shared/shadcn/ui/sidebar/sidebar-menu-skeleton.svelte
Normal file
39
src/shared/shadcn/ui/sidebar/sidebar-menu-skeleton.svelte
Normal file
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { Skeleton } from '$shared/shadcn/ui/skeleton/index.js';
|
||||
import {
|
||||
type WithElementRef,
|
||||
cn,
|
||||
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
showIcon = false,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> & {
|
||||
showIcon?: boolean;
|
||||
} = $props();
|
||||
|
||||
// Random width between 50% and 90%
|
||||
const width = `${Math.floor(Math.random() * 40) + 50}%`;
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="sidebar-menu-skeleton"
|
||||
data-sidebar="menu-skeleton"
|
||||
class={cn('flex h-8 items-center gap-2 rounded-md px-2', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{#if showIcon}
|
||||
<Skeleton class="size-4 rounded-md" data-sidebar="menu-skeleton-icon" />
|
||||
{/if}
|
||||
<Skeleton
|
||||
class="h-4 max-w-(--skeleton-width) flex-1"
|
||||
data-sidebar="menu-skeleton-text"
|
||||
style="--skeleton-width: {width};"
|
||||
/>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
46
src/shared/shadcn/ui/sidebar/sidebar-menu-sub-button.svelte
Normal file
46
src/shared/shadcn/ui/sidebar/sidebar-menu-sub-button.svelte
Normal file
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type WithElementRef,
|
||||
cn,
|
||||
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { HTMLAnchorAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
children,
|
||||
child,
|
||||
class: className,
|
||||
size = 'md',
|
||||
isActive = false,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAnchorAttributes> & {
|
||||
child?: Snippet<[{ props: Record<string, unknown> }]>;
|
||||
size?: 'sm' | 'md';
|
||||
isActive?: boolean;
|
||||
} = $props();
|
||||
|
||||
const mergedProps = $derived({
|
||||
class: cn(
|
||||
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
|
||||
'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
|
||||
size === 'sm' && 'text-xs',
|
||||
size === 'md' && 'text-sm',
|
||||
'group-data-[collapsible=icon]:hidden',
|
||||
className,
|
||||
),
|
||||
'data-slot': 'sidebar-menu-sub-button',
|
||||
'data-sidebar': 'menu-sub-button',
|
||||
'data-size': size,
|
||||
'data-active': isActive,
|
||||
...restProps,
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<a bind:this={ref} {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</a>
|
||||
{/if}
|
||||
24
src/shared/shadcn/ui/sidebar/sidebar-menu-sub-item.svelte
Normal file
24
src/shared/shadcn/ui/sidebar/sidebar-menu-sub-item.svelte
Normal file
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type WithElementRef,
|
||||
cn,
|
||||
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
children,
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLLIElement>> = $props();
|
||||
</script>
|
||||
|
||||
<li
|
||||
bind:this={ref}
|
||||
data-slot="sidebar-menu-sub-item"
|
||||
data-sidebar="menu-sub-item"
|
||||
class={cn('group/menu-sub-item relative', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</li>
|
||||
28
src/shared/shadcn/ui/sidebar/sidebar-menu-sub.svelte
Normal file
28
src/shared/shadcn/ui/sidebar/sidebar-menu-sub.svelte
Normal file
@@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type WithElementRef,
|
||||
cn,
|
||||
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLUListElement>> = $props();
|
||||
</script>
|
||||
|
||||
<ul
|
||||
bind:this={ref}
|
||||
data-slot="sidebar-menu-sub"
|
||||
data-sidebar="menu-sub"
|
||||
class={cn(
|
||||
'border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-s px-2.5 py-0.5',
|
||||
'group-data-[collapsible=icon]:hidden',
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</ul>
|
||||
24
src/shared/shadcn/ui/sidebar/sidebar-menu.svelte
Normal file
24
src/shared/shadcn/ui/sidebar/sidebar-menu.svelte
Normal file
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type WithElementRef,
|
||||
cn,
|
||||
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLUListElement>, HTMLUListElement> = $props();
|
||||
</script>
|
||||
|
||||
<ul
|
||||
bind:this={ref}
|
||||
data-slot="sidebar-menu"
|
||||
data-sidebar="menu"
|
||||
class={cn('flex w-full min-w-0 flex-col gap-1', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</ul>
|
||||
57
src/shared/shadcn/ui/sidebar/sidebar-provider.svelte
Normal file
57
src/shared/shadcn/ui/sidebar/sidebar-provider.svelte
Normal file
@@ -0,0 +1,57 @@
|
||||
<script lang="ts">
|
||||
import * as Tooltip from '$shared/shadcn/ui/tooltip/index.js';
|
||||
import {
|
||||
type WithElementRef,
|
||||
cn,
|
||||
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import {
|
||||
SIDEBAR_COOKIE_MAX_AGE,
|
||||
SIDEBAR_COOKIE_NAME,
|
||||
SIDEBAR_WIDTH,
|
||||
SIDEBAR_WIDTH_ICON,
|
||||
} from './constants.js';
|
||||
import { setSidebar } from './context.svelte.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
open = $bindable(true),
|
||||
onOpenChange = () => {},
|
||||
class: className,
|
||||
style,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
} = $props();
|
||||
|
||||
const sidebar = setSidebar({
|
||||
open: () => open,
|
||||
setOpen: (value: boolean) => {
|
||||
open = value;
|
||||
onOpenChange(value);
|
||||
|
||||
// This sets the cookie to keep the sidebar state.
|
||||
document.cookie =
|
||||
`${SIDEBAR_COOKIE_NAME}=${open}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={sidebar.handleShortcutKeydown} />
|
||||
|
||||
<Tooltip.Provider delayDuration={0}>
|
||||
<div
|
||||
data-slot="sidebar-wrapper"
|
||||
style="--sidebar-width: {SIDEBAR_WIDTH}; --sidebar-width-icon: {SIDEBAR_WIDTH_ICON}; {style}"
|
||||
class={cn(
|
||||
'group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full',
|
||||
className,
|
||||
)}
|
||||
bind:this={ref}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</Tooltip.Provider>
|
||||
39
src/shared/shadcn/ui/sidebar/sidebar-rail.svelte
Normal file
39
src/shared/shadcn/ui/sidebar/sidebar-rail.svelte
Normal file
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type WithElementRef,
|
||||
cn,
|
||||
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { useSidebar } from './context.svelte.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLButtonElement>, HTMLButtonElement> = $props();
|
||||
|
||||
const sidebar = useSidebar();
|
||||
</script>
|
||||
|
||||
<button
|
||||
bind:this={ref}
|
||||
data-sidebar="rail"
|
||||
data-slot="sidebar-rail"
|
||||
aria-label="Toggle Sidebar"
|
||||
tabIndex={-1}
|
||||
onclick={sidebar.toggle}
|
||||
title="Toggle Sidebar"
|
||||
class={cn(
|
||||
'hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-end-4 group-data-[side=right]:start-0 after:absolute after:inset-y-0 after:start-[calc(1/2*100%-1px)] after:w-[2px] sm:flex',
|
||||
'in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize',
|
||||
'[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
|
||||
'hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:start-full',
|
||||
'[[data-side=left][data-collapsible=offcanvas]_&]:-end-2',
|
||||
'[[data-side=right][data-collapsible=offcanvas]_&]:-start-2',
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
19
src/shared/shadcn/ui/sidebar/sidebar-separator.svelte
Normal file
19
src/shared/shadcn/ui/sidebar/sidebar-separator.svelte
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { Separator } from '$shared/shadcn/ui/separator/index.js';
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: ComponentProps<typeof Separator> = $props();
|
||||
</script>
|
||||
|
||||
<Separator
|
||||
bind:ref
|
||||
data-slot="sidebar-separator"
|
||||
data-sidebar="separator"
|
||||
class={cn('bg-sidebar-border', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
35
src/shared/shadcn/ui/sidebar/sidebar-trigger.svelte
Normal file
35
src/shared/shadcn/ui/sidebar/sidebar-trigger.svelte
Normal file
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$shared/shadcn/ui/button/index.js';
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import PanelLeftIcon from '@lucide/svelte/icons/panel-left';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
import { useSidebar } from './context.svelte.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
onclick,
|
||||
...restProps
|
||||
}: ComponentProps<typeof Button> & {
|
||||
onclick?: (e: MouseEvent) => void;
|
||||
} = $props();
|
||||
|
||||
const sidebar = useSidebar();
|
||||
</script>
|
||||
|
||||
<Button
|
||||
data-sidebar="trigger"
|
||||
data-slot="sidebar-trigger"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class={cn('size-7', className)}
|
||||
type="button"
|
||||
onclick={(e => {
|
||||
onclick?.(e);
|
||||
sidebar.toggle();
|
||||
})}
|
||||
{...restProps}
|
||||
>
|
||||
<PanelLeftIcon />
|
||||
<span class="sr-only">Toggle Sidebar</span>
|
||||
</Button>
|
||||
108
src/shared/shadcn/ui/sidebar/sidebar.svelte
Normal file
108
src/shared/shadcn/ui/sidebar/sidebar.svelte
Normal file
@@ -0,0 +1,108 @@
|
||||
<script lang="ts">
|
||||
import * as Sheet from '$shared/shadcn/ui/sheet/index.js';
|
||||
import {
|
||||
type WithElementRef,
|
||||
cn,
|
||||
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { SIDEBAR_WIDTH_MOBILE } from './constants.js';
|
||||
import { useSidebar } from './context.svelte.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
side = 'left',
|
||||
variant = 'sidebar',
|
||||
collapsible = 'offcanvas',
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||
side?: 'left' | 'right';
|
||||
variant?: 'sidebar' | 'floating' | 'inset';
|
||||
collapsible?: 'offcanvas' | 'icon' | 'none';
|
||||
} = $props();
|
||||
|
||||
const sidebar = useSidebar();
|
||||
</script>
|
||||
|
||||
{#if collapsible === 'none'}
|
||||
<div
|
||||
class={cn(
|
||||
'bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col',
|
||||
className,
|
||||
)}
|
||||
bind:this={ref}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{:else if sidebar.isMobile}
|
||||
<Sheet.Root
|
||||
bind:open={() => sidebar.openMobile, v => sidebar.setOpenMobile(v)}
|
||||
{...restProps}
|
||||
>
|
||||
<Sheet.Content
|
||||
data-sidebar="sidebar"
|
||||
data-slot="sidebar"
|
||||
data-mobile="true"
|
||||
class="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
|
||||
style="--sidebar-width: {SIDEBAR_WIDTH_MOBILE};"
|
||||
{side}
|
||||
>
|
||||
<Sheet.Header class="sr-only">
|
||||
<Sheet.Title>Sidebar</Sheet.Title>
|
||||
<Sheet.Description>Displays the mobile sidebar.</Sheet.Description>
|
||||
</Sheet.Header>
|
||||
<div class="flex h-full w-full flex-col">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</Sheet.Content>
|
||||
</Sheet.Root>
|
||||
{:else}
|
||||
<div
|
||||
bind:this={ref}
|
||||
class="text-sidebar-foreground group peer hidden md:block"
|
||||
data-state={sidebar.state}
|
||||
data-collapsible={sidebar.state === 'collapsed' ? collapsible : ''}
|
||||
data-variant={variant}
|
||||
data-side={side}
|
||||
data-slot="sidebar"
|
||||
>
|
||||
<!-- This is what handles the sidebar gap on desktop -->
|
||||
<div
|
||||
data-slot="sidebar-gap"
|
||||
class={cn(
|
||||
'relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear',
|
||||
'group-data-[collapsible=offcanvas]:w-0',
|
||||
'group-data-[side=right]:rotate-180',
|
||||
variant === 'floating' || variant === 'inset'
|
||||
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]'
|
||||
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)',
|
||||
)}
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
data-slot="sidebar-container"
|
||||
class={cn(
|
||||
'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex',
|
||||
side === 'left'
|
||||
? 'start-0 group-data-[collapsible=offcanvas]:start-[calc(var(--sidebar-width)*-1)]'
|
||||
: 'end-0 group-data-[collapsible=offcanvas]:end-[calc(var(--sidebar-width)*-1)]',
|
||||
// Adjust the padding for floating and inset variants.
|
||||
variant === 'floating' || variant === 'inset'
|
||||
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]'
|
||||
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-e group-data-[side=right]:border-s',
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
<div
|
||||
data-sidebar="sidebar"
|
||||
data-slot="sidebar-inner"
|
||||
class="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
7
src/shared/shadcn/ui/skeleton/index.ts
Normal file
7
src/shared/shadcn/ui/skeleton/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Root from './skeleton.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Skeleton,
|
||||
};
|
||||
22
src/shared/shadcn/ui/skeleton/skeleton.svelte
Normal file
22
src/shared/shadcn/ui/skeleton/skeleton.svelte
Normal file
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type WithElementRef,
|
||||
type WithoutChildren,
|
||||
cn,
|
||||
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="skeleton"
|
||||
class={cn('bg-accent animate-pulse rounded-md', className)}
|
||||
{...restProps}
|
||||
>
|
||||
</div>
|
||||
19
src/shared/shadcn/ui/tooltip/index.ts
Normal file
19
src/shared/shadcn/ui/tooltip/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import Content from './tooltip-content.svelte';
|
||||
import Portal from './tooltip-portal.svelte';
|
||||
import Provider from './tooltip-provider.svelte';
|
||||
import Trigger from './tooltip-trigger.svelte';
|
||||
import Root from './tooltip.svelte';
|
||||
|
||||
export {
|
||||
Content,
|
||||
Content as TooltipContent,
|
||||
Portal,
|
||||
Portal as TooltipPortal,
|
||||
Provider,
|
||||
Provider as TooltipProvider,
|
||||
Root,
|
||||
//
|
||||
Root as Tooltip,
|
||||
Trigger,
|
||||
Trigger as TooltipTrigger,
|
||||
};
|
||||
53
src/shared/shadcn/ui/tooltip/tooltip-content.svelte
Normal file
53
src/shared/shadcn/ui/tooltip/tooltip-content.svelte
Normal file
@@ -0,0 +1,53 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import type { WithoutChildrenOrChild } from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import { Tooltip as TooltipPrimitive } from 'bits-ui';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
import TooltipPortal from './tooltip-portal.svelte';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
sideOffset = 0,
|
||||
side = 'top',
|
||||
children,
|
||||
arrowClasses,
|
||||
portalProps,
|
||||
...restProps
|
||||
}: TooltipPrimitive.ContentProps & {
|
||||
arrowClasses?: string;
|
||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof TooltipPortal>>;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<TooltipPortal {...portalProps}>
|
||||
<TooltipPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="tooltip-content"
|
||||
{sideOffset}
|
||||
{side}
|
||||
class={cn(
|
||||
'bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-end-2 data-[side=right]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--bits-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance',
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
<TooltipPrimitive.Arrow>
|
||||
{#snippet child({ props })}
|
||||
<div
|
||||
class={cn(
|
||||
'bg-primary z-50 size-2.5 rotate-45 rounded-[2px]',
|
||||
'data-[side=top]:translate-x-1/2 data-[side=top]:translate-y-[calc(-50%_+_2px)]',
|
||||
'data-[side=bottom]:-translate-x-1/2 data-[side=bottom]:-translate-y-[calc(-50%_+_1px)]',
|
||||
'data-[side=right]:translate-x-[calc(50%_+_2px)] data-[side=right]:translate-y-1/2',
|
||||
'data-[side=left]:-translate-y-[calc(50%_-_3px)]',
|
||||
arrowClasses,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
</div>
|
||||
{/snippet}
|
||||
</TooltipPrimitive.Arrow>
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPortal>
|
||||
7
src/shared/shadcn/ui/tooltip/tooltip-portal.svelte
Normal file
7
src/shared/shadcn/ui/tooltip/tooltip-portal.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Tooltip as TooltipPrimitive } from 'bits-ui';
|
||||
|
||||
let { ...restProps }: TooltipPrimitive.PortalProps = $props();
|
||||
</script>
|
||||
|
||||
<TooltipPrimitive.Portal {...restProps} />
|
||||
7
src/shared/shadcn/ui/tooltip/tooltip-provider.svelte
Normal file
7
src/shared/shadcn/ui/tooltip/tooltip-provider.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Tooltip as TooltipPrimitive } from 'bits-ui';
|
||||
|
||||
let { ...restProps }: TooltipPrimitive.ProviderProps = $props();
|
||||
</script>
|
||||
|
||||
<TooltipPrimitive.Provider {...restProps} />
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user