Compare commits
4 Commits
cfaff46d59
...
8a93c7b545
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a93c7b545 | |||
| 0004b81e40 | |||
| fb1d2765d0 | |||
| 12e8bc0a89 |
@@ -41,7 +41,7 @@ jobs:
|
|||||||
run: yarn lint
|
run: yarn lint
|
||||||
|
|
||||||
- name: Type Check
|
- name: Type Check
|
||||||
run: yarn check:shadcn-excluded
|
run: yarn check
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
needs: build # Only runs if tests/lint pass
|
needs: build # Only runs if tests/lint pass
|
||||||
|
|||||||
@@ -4,12 +4,11 @@
|
|||||||
|
|
||||||
This provides:
|
This provides:
|
||||||
- ResponsiveManager context for breakpoint tracking
|
- ResponsiveManager context for breakpoint tracking
|
||||||
- TooltipProvider for shadcn Tooltip components
|
- TooltipProvider for tooltip components
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createResponsiveManager } from '$shared/lib';
|
import { createResponsiveManager } from '$shared/lib';
|
||||||
import type { ResponsiveManager } from '$shared/lib';
|
import type { ResponsiveManager } from '$shared/lib';
|
||||||
import { Provider as TooltipProvider } from '$shared/shadcn/ui/tooltip';
|
|
||||||
import { setContext } from 'svelte';
|
import { setContext } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -24,6 +23,4 @@ $effect(() => responsiveManager.init());
|
|||||||
setContext<ResponsiveManager>('responsive', responsiveManager);
|
setContext<ResponsiveManager>('responsive', responsiveManager);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<TooltipProvider delayDuration={200} skipDelayDuration={300}>
|
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</TooltipProvider>
|
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ A modern font exploration and comparison tool for browsing fonts from Google Fon
|
|||||||
- **Side-by-Side Comparison**: Compare up to 4 fonts simultaneously with customizable text, size, and typography settings
|
- **Side-by-Side Comparison**: Compare up to 4 fonts simultaneously with customizable text, size, and typography settings
|
||||||
- **Advanced Filtering**: Filter by category, provider, character subsets, and weight
|
- **Advanced Filtering**: Filter by category, provider, character subsets, and weight
|
||||||
- **Virtual Scrolling**: Fast, smooth browsing of thousands of fonts
|
- **Virtual Scrolling**: Fast, smooth browsing of thousands of fonts
|
||||||
- **Responsive UI**: Beautiful interface built with shadcn components and Tailwind CSS
|
- **Responsive UI**: Beautiful interface built with Tailwind CSS
|
||||||
- **Type-Safe**: Full TypeScript coverage with strict mode enabled
|
- **Type-Safe**: Full TypeScript coverage with strict mode enabled
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
- **Framework**: Svelte 5 with reactive primitives (runes)
|
- **Framework**: Svelte 5 with reactive primitives (runes)
|
||||||
- **Styling**: Tailwind CSS v4
|
- **Styling**: Tailwind CSS v4
|
||||||
- **Components**: shadcn-svelte (via bits-ui)
|
- **Components**: Bits UI primitives
|
||||||
- **State Management**: TanStack Query for async data
|
- **State Management**: TanStack Query for async data
|
||||||
- **Architecture**: Feature-Sliced Design (FSD)
|
- **Architecture**: Feature-Sliced Design (FSD)
|
||||||
- **Quality**: oxlint (linting), dprint (formatting), lefthook (git hooks)
|
- **Quality**: oxlint (linting), dprint (formatting), lefthook (git hooks)
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://shadcn-svelte.com/schema.json",
|
|
||||||
"tailwind": {
|
|
||||||
"css": "src/app.css",
|
|
||||||
"baseColor": "zinc"
|
|
||||||
},
|
|
||||||
"aliases": {
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
+6
-1
@@ -31,7 +31,12 @@
|
|||||||
"importDeclaration.forceMultiLine": "whenMultiple",
|
"importDeclaration.forceMultiLine": "whenMultiple",
|
||||||
"importDeclaration.forceSingleLine": false,
|
"importDeclaration.forceSingleLine": false,
|
||||||
"exportDeclaration.forceMultiLine": "whenMultiple",
|
"exportDeclaration.forceMultiLine": "whenMultiple",
|
||||||
"exportDeclaration.forceSingleLine": false
|
"exportDeclaration.forceSingleLine": false,
|
||||||
|
"ifStatement.useBraces": "always",
|
||||||
|
"whileStatement.useBraces": "always",
|
||||||
|
"forStatement.useBraces": "always",
|
||||||
|
"forInStatement.useBraces": "always",
|
||||||
|
"forOfStatement.useBraces": "always"
|
||||||
},
|
},
|
||||||
"json": {
|
"json": {
|
||||||
"indentWidth": 2,
|
"indentWidth": 2,
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@ pre-push:
|
|||||||
run: yarn tsc --noEmit
|
run: yarn tsc --noEmit
|
||||||
|
|
||||||
svelte-check:
|
svelte-check:
|
||||||
run: yarn check:shadcn-excluded --threshold warning
|
run: yarn check --threshold warning
|
||||||
|
|
||||||
format-check:
|
format-check:
|
||||||
glob: "*.{ts,js,svelte,json,md}"
|
glob: "*.{ts,js,svelte,json,md}"
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
"prepare": "svelte-check --tsconfig ./tsconfig.json || echo ''",
|
"prepare": "svelte-check --tsconfig ./tsconfig.json || echo ''",
|
||||||
"check": "svelte-check",
|
"check": "svelte-check",
|
||||||
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
"check:shadcn-excluded": "svelte-check --no-tsconfig --ignore \"src/shared/shadcn\"",
|
|
||||||
"lint": "oxlint",
|
"lint": "oxlint",
|
||||||
"format": "dprint fmt",
|
"format": "dprint fmt",
|
||||||
"format:check": "dprint check",
|
"format:check": "dprint check",
|
||||||
|
|||||||
@@ -14,12 +14,10 @@
|
|||||||
*
|
*
|
||||||
* - Footer area (currently empty, reserved for future use)
|
* - Footer area (currently empty, reserved for future use)
|
||||||
*/
|
*/
|
||||||
import { BreadcrumbHeader } from '$entities/Breadcrumb';
|
|
||||||
import { themeManager } from '$features/ChangeAppTheme';
|
import { themeManager } from '$features/ChangeAppTheme';
|
||||||
import GD from '$shared/assets/GD.svg';
|
import GD from '$shared/assets/GD.svg';
|
||||||
import { ResponsiveProvider } from '$shared/lib';
|
import { ResponsiveProvider } from '$shared/lib';
|
||||||
import { Provider as TooltipProvider } from '$shared/shadcn/ui/tooltip';
|
import clsx from 'clsx';
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
|
||||||
import {
|
import {
|
||||||
type Snippet,
|
type Snippet,
|
||||||
onDestroy,
|
onDestroy,
|
||||||
@@ -80,16 +78,14 @@ onDestroy(() => themeManager.destroy());
|
|||||||
<ResponsiveProvider>
|
<ResponsiveProvider>
|
||||||
<div
|
<div
|
||||||
id="app-root"
|
id="app-root"
|
||||||
class={cn(
|
class={clsx(
|
||||||
'min-h-screen w-auto flex flex-col bg-surface dark:bg-dark-bg',
|
'min-h-screen w-auto flex flex-col bg-surface dark:bg-dark-bg',
|
||||||
theme === 'dark' ? 'dark' : '',
|
theme === 'dark' ? 'dark' : '',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<TooltipProvider>
|
|
||||||
{#if fontsReady}
|
{#if fontsReady}
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
{/if}
|
{/if}
|
||||||
</TooltipProvider>
|
|
||||||
<footer></footer>
|
<footer></footer>
|
||||||
</div>
|
</div>
|
||||||
</ResponsiveProvider>
|
</ResponsiveProvider>
|
||||||
|
|||||||
@@ -105,13 +105,17 @@ class ScrollBreadcrumbsStore {
|
|||||||
* (fires as soon as any part of element crosses viewport edge).
|
* (fires as soon as any part of element crosses viewport edge).
|
||||||
*/
|
*/
|
||||||
#initObserver(): void {
|
#initObserver(): void {
|
||||||
if (this.#observer) return;
|
if (this.#observer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.#observer = new IntersectionObserver(
|
this.#observer = new IntersectionObserver(
|
||||||
entries => {
|
entries => {
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const item = this.#items.find(i => i.element === entry.target);
|
const item = this.#items.find(i => i.element === entry.target);
|
||||||
if (!item) continue;
|
if (!item) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!entry.isIntersecting && this.#isScrollingDown) {
|
if (!entry.isIntersecting && this.#isScrollingDown) {
|
||||||
// Element exited viewport while scrolling DOWN - add to breadcrumbs
|
// Element exited viewport while scrolling DOWN - add to breadcrumbs
|
||||||
@@ -199,7 +203,9 @@ class ScrollBreadcrumbsStore {
|
|||||||
* @param offset - Scroll offset in pixels (for sticky headers)
|
* @param offset - Scroll offset in pixels (for sticky headers)
|
||||||
*/
|
*/
|
||||||
add(item: BreadcrumbItem, offset = 0): void {
|
add(item: BreadcrumbItem, offset = 0): void {
|
||||||
if (this.#items.find(i => i.index === item.index)) return;
|
if (this.#items.find(i => i.index === item.index)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.#scrollOffset = offset;
|
this.#scrollOffset = offset;
|
||||||
this.#items.push(item);
|
this.#items.push(item);
|
||||||
@@ -216,7 +222,9 @@ class ScrollBreadcrumbsStore {
|
|||||||
*/
|
*/
|
||||||
remove(index: number): void {
|
remove(index: number): void {
|
||||||
const item = this.#items.find(i => i.index === index);
|
const item = this.#items.find(i => i.index === index);
|
||||||
if (!item) return;
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.#observer?.unobserve(item.element);
|
this.#observer?.unobserve(item.element);
|
||||||
this.#items = this.#items.filter(i => i.index !== index);
|
this.#items = this.#items.filter(i => i.index !== index);
|
||||||
@@ -237,7 +245,9 @@ class ScrollBreadcrumbsStore {
|
|||||||
*/
|
*/
|
||||||
scrollTo(index: number, container: HTMLElement | Window = window): void {
|
scrollTo(index: number, container: HTMLElement | Window = window): void {
|
||||||
const item = this.#items.find(i => i.index === index);
|
const item = this.#items.find(i => i.index === index);
|
||||||
if (!item) return;
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const rect = item.element.getBoundingClientRect();
|
const rect = item.element.getBoundingClientRect();
|
||||||
const scrollTop = container === window ? window.scrollY : (container as HTMLElement).scrollTop;
|
const scrollTop = container === window ? window.scrollY : (container as HTMLElement).scrollTop;
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ class MockIntersectionObserver implements IntersectionObserver {
|
|||||||
|
|
||||||
constructor(callback: IntersectionObserverCallback, options?: IntersectionObserverInit) {
|
constructor(callback: IntersectionObserverCallback, options?: IntersectionObserverInit) {
|
||||||
this.callbacks.push(callback);
|
this.callbacks.push(callback);
|
||||||
if (options?.rootMargin) this.rootMargin = options.rootMargin;
|
if (options?.rootMargin) {
|
||||||
|
this.rootMargin = options.rootMargin;
|
||||||
|
}
|
||||||
if (options?.threshold) {
|
if (options?.threshold) {
|
||||||
this.thresholds = Array.isArray(options.threshold) ? options.threshold : [options.threshold];
|
this.thresholds = Array.isArray(options.threshold) ? options.threshold : [options.threshold];
|
||||||
}
|
}
|
||||||
@@ -120,7 +122,9 @@ describe('ScrollBreadcrumbsStore', () => {
|
|||||||
(event: string, listener: EventListenerOrEventListenerObject) => {
|
(event: string, listener: EventListenerOrEventListenerObject) => {
|
||||||
if (event === 'scroll') {
|
if (event === 'scroll') {
|
||||||
const index = scrollListeners.indexOf(listener as () => void);
|
const index = scrollListeners.indexOf(listener as () => void);
|
||||||
if (index > -1) scrollListeners.splice(index, 1);
|
if (index > -1) {
|
||||||
|
scrollListeners.splice(index, 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -197,7 +197,9 @@ export async function fetchProxyFontById(
|
|||||||
* @returns Promise resolving to an array of fonts
|
* @returns Promise resolving to an array of fonts
|
||||||
*/
|
*/
|
||||||
export async function fetchFontsByIds(ids: string[]): Promise<UnifiedFont[]> {
|
export async function fetchFontsByIds(ids: string[]): Promise<UnifiedFont[]> {
|
||||||
if (ids.length === 0) return [];
|
if (ids.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const queryString = ids.join(',');
|
const queryString = ids.join(',');
|
||||||
const url = `${PROXY_API_URL}/batch?ids=${queryString}`;
|
const url = `${PROXY_API_URL}/batch?ids=${queryString}`;
|
||||||
|
|||||||
@@ -529,8 +529,12 @@ export function createMockStore<T>(config: {
|
|||||||
* Returns semantic status string
|
* Returns semantic status string
|
||||||
*/
|
*/
|
||||||
get status() {
|
get status() {
|
||||||
if (isLoading) return 'pending';
|
if (isLoading) {
|
||||||
if (isError) return 'error';
|
return 'pending';
|
||||||
|
}
|
||||||
|
if (isError) {
|
||||||
|
return 'error';
|
||||||
|
}
|
||||||
return 'success';
|
return 'success';
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -93,12 +93,16 @@ export function createFontRowSizeResolver(options: FontRowSizeResolverOptions):
|
|||||||
return function resolveRowHeight(rowIndex: number): number {
|
return function resolveRowHeight(rowIndex: number): number {
|
||||||
const fonts = options.getFonts();
|
const fonts = options.getFonts();
|
||||||
const font = fonts[rowIndex];
|
const font = fonts[rowIndex];
|
||||||
if (!font) return options.fallbackHeight;
|
if (!font) {
|
||||||
|
return options.fallbackHeight;
|
||||||
|
}
|
||||||
|
|
||||||
const containerWidth = options.getContainerWidth();
|
const containerWidth = options.getContainerWidth();
|
||||||
const previewText = options.getPreviewText();
|
const previewText = options.getPreviewText();
|
||||||
|
|
||||||
if (containerWidth <= 0 || !previewText) return options.fallbackHeight;
|
if (containerWidth <= 0 || !previewText) {
|
||||||
|
return options.fallbackHeight;
|
||||||
|
}
|
||||||
|
|
||||||
const weight = options.getWeight();
|
const weight = options.getWeight();
|
||||||
// generateFontKey: '{id}@{weight}' for static fonts, '{id}@vf' for variable fonts.
|
// generateFontKey: '{id}@{weight}' for static fonts, '{id}@vf' for variable fonts.
|
||||||
@@ -107,7 +111,9 @@ export function createFontRowSizeResolver(options: FontRowSizeResolverOptions):
|
|||||||
// Reading via getStatus() allows the caller to pass appliedFontsManager.statuses.get(),
|
// Reading via getStatus() allows the caller to pass appliedFontsManager.statuses.get(),
|
||||||
// which creates a Svelte 5 reactive dependency when called inside $derived.by.
|
// which creates a Svelte 5 reactive dependency when called inside $derived.by.
|
||||||
const status = options.getStatus(fontKey);
|
const status = options.getStatus(fontKey);
|
||||||
if (status !== 'loaded') return options.fallbackHeight;
|
if (status !== 'loaded') {
|
||||||
|
return options.fallbackHeight;
|
||||||
|
}
|
||||||
|
|
||||||
const fontSizePx = options.getFontSizePx();
|
const fontSizePx = options.getFontSizePx();
|
||||||
const lineHeightPx = options.getLineHeightPx();
|
const lineHeightPx = options.getLineHeightPx();
|
||||||
@@ -116,7 +122,9 @@ export function createFontRowSizeResolver(options: FontRowSizeResolverOptions):
|
|||||||
|
|
||||||
const cacheKey = `${fontCssString}|${previewText}|${contentWidth}|${lineHeightPx}`;
|
const cacheKey = `${fontCssString}|${previewText}|${contentWidth}|${lineHeightPx}`;
|
||||||
const cached = cache.get(cacheKey);
|
const cached = cache.get(cacheKey);
|
||||||
if (cached !== undefined) return cached;
|
if (cached !== undefined) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
const { totalHeight } = engine.layout(previewText, fontCssString, contentWidth, lineHeightPx);
|
const { totalHeight } = engine.layout(previewText, fontCssString, contentWidth, lineHeightPx);
|
||||||
const result = totalHeight + options.chromeHeight;
|
const result = totalHeight + options.chromeHeight;
|
||||||
|
|||||||
@@ -246,12 +246,16 @@ export class AppliedFontsManager {
|
|||||||
);
|
);
|
||||||
|
|
||||||
for (const result of results) {
|
for (const result of results) {
|
||||||
if (result.ok) continue;
|
if (result.ok) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const { key, config, reason } = result;
|
const { key, config, reason } = result;
|
||||||
const isAbort = reason instanceof FontFetchError
|
const isAbort = reason instanceof FontFetchError
|
||||||
&& reason.cause instanceof Error
|
&& reason.cause instanceof Error
|
||||||
&& reason.cause.name === 'AbortError';
|
&& reason.cause.name === 'AbortError';
|
||||||
if (isAbort) continue;
|
if (isAbort) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (reason instanceof FontFetchError) {
|
if (reason instanceof FontFetchError) {
|
||||||
console.error(`Font fetch failed: ${config.name}`, reason);
|
console.error(`Font fetch failed: ${config.name}`, reason);
|
||||||
}
|
}
|
||||||
@@ -293,7 +297,9 @@ export class AppliedFontsManager {
|
|||||||
|
|
||||||
// Remove FontFace from document to free memory
|
// Remove FontFace from document to free memory
|
||||||
const font = this.#loadedFonts.get(key);
|
const font = this.#loadedFonts.get(key);
|
||||||
if (font) document.fonts.delete(font);
|
if (font) {
|
||||||
|
document.fonts.delete(font);
|
||||||
|
}
|
||||||
|
|
||||||
// Evict from cache and cleanup URL mapping
|
// Evict from cache and cleanup URL mapping
|
||||||
const url = this.#urlByKey.get(key);
|
const url = this.#urlByKey.get(key);
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ import type { UnifiedFont } from '../../model/types';
|
|||||||
* Standalone function to avoid 'this' issues during construction.
|
* Standalone function to avoid 'this' issues during construction.
|
||||||
*/
|
*/
|
||||||
async function fetchAndSeed(ids: string[]): Promise<UnifiedFont[]> {
|
async function fetchAndSeed(ids: string[]): Promise<UnifiedFont[]> {
|
||||||
if (ids.length === 0) return [];
|
if (ids.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
let response: UnifiedFont[];
|
let response: UnifiedFont[];
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -166,7 +166,9 @@ export class FontStore {
|
|||||||
const data = this.#qc.getQueryData<InfiniteData<ProxyFontsResponse, PageParam>>(
|
const data = this.#qc.getQueryData<InfiniteData<ProxyFontsResponse, PageParam>>(
|
||||||
this.buildQueryKey(this.#params),
|
this.buildQueryKey(this.#params),
|
||||||
);
|
);
|
||||||
if (!data) return undefined;
|
if (!data) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
return data.pages.flatMap(p => p.fonts);
|
return data.pages.flatMap(p => p.fonts);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,9 +338,15 @@ export class FontStore {
|
|||||||
throw new FontNetworkError(cause);
|
throw new FontNetworkError(cause);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response) throw new FontResponseError('response', response);
|
if (!response) {
|
||||||
if (!response.fonts) throw new FontResponseError('response.fonts', response.fonts);
|
throw new FontResponseError('response', response);
|
||||||
if (!Array.isArray(response.fonts)) throw new FontResponseError('response.fonts', response.fonts);
|
}
|
||||||
|
if (!response.fonts) {
|
||||||
|
throw new FontResponseError('response.fonts', response.fonts);
|
||||||
|
}
|
||||||
|
if (!Array.isArray(response.fonts)) {
|
||||||
|
throw new FontResponseError('response.fonts', response.fonts);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fonts: response.fonts,
|
fonts: response.fonts,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
- Adds smooth transition when font appears
|
- Adds smooth transition when font appears
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
import clsx from 'clsx';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import { prefersReducedMotion } from 'svelte/motion';
|
import { prefersReducedMotion } from 'svelte/motion';
|
||||||
import {
|
import {
|
||||||
@@ -64,7 +64,7 @@ const transitionClasses = $derived(
|
|||||||
style:font-family={shouldReveal
|
style:font-family={shouldReveal
|
||||||
? `'${font.name}'`
|
? `'${font.name}'`
|
||||||
: 'system-ui, -apple-system, sans-serif'}
|
: 'system-ui, -apple-system, sans-serif'}
|
||||||
class={cn(
|
class={clsx(
|
||||||
transitionClasses,
|
transitionClasses,
|
||||||
// If reduced motion is on, we skip the transform/blur entirely
|
// If reduced motion is on, we skip the transform/blur entirely
|
||||||
!shouldReveal
|
!shouldReveal
|
||||||
|
|||||||
@@ -65,7 +65,9 @@ function handleInternalVisibleChange(items: UnifiedFont[]) {
|
|||||||
$effect(() => {
|
$effect(() => {
|
||||||
const configs: FontLoadRequestConfig[] = visibleFonts.flatMap(item => {
|
const configs: FontLoadRequestConfig[] = visibleFonts.flatMap(item => {
|
||||||
const url = getFontUrl(item, weight);
|
const url = getFontUrl(item, weight);
|
||||||
if (!url) return [];
|
if (!url) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
return [{ id: item.id, name: item.name, weight, url, isVariable: item.features?.isVariable }];
|
return [{ id: item.id, name: item.name, weight, url, isVariable: item.features?.isVariable }];
|
||||||
});
|
});
|
||||||
if (configs.length > 0) {
|
if (configs.length > 0) {
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { fontStore } from '$entities/Font';
|
import { fontStore } from '$entities/Font';
|
||||||
import type { ResponsiveManager } from '$shared/lib';
|
import type { ResponsiveManager } from '$shared/lib';
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
|
||||||
import { Button } from '$shared/ui';
|
import { Button } from '$shared/ui';
|
||||||
import { Label } from '$shared/ui';
|
import { Label } from '$shared/ui';
|
||||||
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
|
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
|
||||||
|
import clsx from 'clsx';
|
||||||
import {
|
import {
|
||||||
getContext,
|
getContext,
|
||||||
untrack,
|
untrack,
|
||||||
@@ -45,7 +45,7 @@ function handleReset() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class={cn(
|
class={clsx(
|
||||||
'flex flex-col md:flex-row justify-between items-start md:items-center',
|
'flex flex-col md:flex-row justify-between items-start md:items-center',
|
||||||
'gap-1 md:gap-6',
|
'gap-1 md:gap-6',
|
||||||
'pt-6 mt-6 md:pt-8 md:mt-8',
|
'pt-6 mt-6 md:pt-8 md:mt-8',
|
||||||
@@ -77,7 +77,7 @@ function handleReset() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size={isMobileOrTabletPortrait ? 'xs' : 'sm'}
|
size={isMobileOrTabletPortrait ? 'xs' : 'sm'}
|
||||||
onclick={handleReset}
|
onclick={handleReset}
|
||||||
class={cn('group font-mono tracking-widest text-neutral-400', isMobileOrTabletPortrait && 'px-0')}
|
class={clsx('group font-mono tracking-widest text-neutral-400', isMobileOrTabletPortrait && 'px-0')}
|
||||||
iconPosition="left"
|
iconPosition="left"
|
||||||
>
|
>
|
||||||
{#snippet icon()}
|
{#snippet icon()}
|
||||||
|
|||||||
@@ -128,7 +128,9 @@ export class TypographySettingsManager {
|
|||||||
// This handles the "Multiplier" logic specifically for the Font Size Control
|
// This handles the "Multiplier" logic specifically for the Font Size Control
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const ctrl = this.#controls.get('font_size')?.instance;
|
const ctrl = this.#controls.get('font_size')?.instance;
|
||||||
if (!ctrl) return;
|
if (!ctrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// If the user moves the slider/clicks buttons in the UI:
|
// If the user moves the slider/clicks buttons in the UI:
|
||||||
// We update the baseSize (User Intent)
|
// We update the baseSize (User Intent)
|
||||||
@@ -147,10 +149,18 @@ export class TypographySettingsManager {
|
|||||||
* Gets initial value for a control from storage or defaults
|
* Gets initial value for a control from storage or defaults
|
||||||
*/
|
*/
|
||||||
#getInitialValue(id: string, saved: TypographySettings): number {
|
#getInitialValue(id: string, saved: TypographySettings): number {
|
||||||
if (id === 'font_size') return saved.fontSize * this.#multiplier;
|
if (id === 'font_size') {
|
||||||
if (id === 'font_weight') return saved.fontWeight;
|
return saved.fontSize * this.#multiplier;
|
||||||
if (id === 'line_height') return saved.lineHeight;
|
}
|
||||||
if (id === 'letter_spacing') return saved.letterSpacing;
|
if (id === 'font_weight') {
|
||||||
|
return saved.fontWeight;
|
||||||
|
}
|
||||||
|
if (id === 'line_height') {
|
||||||
|
return saved.lineHeight;
|
||||||
|
}
|
||||||
|
if (id === 'letter_spacing') {
|
||||||
|
return saved.letterSpacing;
|
||||||
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +175,9 @@ export class TypographySettingsManager {
|
|||||||
* Updates the multiplier and recalculates dependent control values
|
* Updates the multiplier and recalculates dependent control values
|
||||||
*/
|
*/
|
||||||
set multiplier(value: number) {
|
set multiplier(value: number) {
|
||||||
if (this.#multiplier === value) return;
|
if (this.#multiplier === value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.#multiplier = value;
|
this.#multiplier = value;
|
||||||
|
|
||||||
// When multiplier changes, we must update the Font Size Control's display value
|
// When multiplier changes, we must update the Font Size Control's display value
|
||||||
@@ -192,7 +204,9 @@ export class TypographySettingsManager {
|
|||||||
set baseSize(val: number) {
|
set baseSize(val: number) {
|
||||||
this.#baseSize = val;
|
this.#baseSize = val;
|
||||||
const ctrl = this.#controls.get('font_size')?.instance;
|
const ctrl = this.#controls.get('font_size')?.instance;
|
||||||
if (ctrl) ctrl.value = val * this.#multiplier;
|
if (ctrl) {
|
||||||
|
ctrl.value = val * this.#multiplier;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -268,9 +282,15 @@ export class TypographySettingsManager {
|
|||||||
// Map storage key to control id
|
// Map storage key to control id
|
||||||
const key = c.id.replace('_', '') as keyof TypographySettings;
|
const key = c.id.replace('_', '') as keyof TypographySettings;
|
||||||
// Simplified for brevity, you'd map these properly:
|
// Simplified for brevity, you'd map these properly:
|
||||||
if (c.id === 'font_weight') c.instance.value = defaults.fontWeight;
|
if (c.id === 'font_weight') {
|
||||||
if (c.id === 'line_height') c.instance.value = defaults.lineHeight;
|
c.instance.value = defaults.fontWeight;
|
||||||
if (c.id === 'letter_spacing') c.instance.value = defaults.letterSpacing;
|
}
|
||||||
|
if (c.id === 'line_height') {
|
||||||
|
c.instance.value = defaults.lineHeight;
|
||||||
|
}
|
||||||
|
if (c.id === 'letter_spacing') {
|
||||||
|
c.instance.value = defaults.letterSpacing;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
MULTIPLIER_S,
|
MULTIPLIER_S,
|
||||||
} from '$entities/Font';
|
} from '$entities/Font';
|
||||||
import type { ResponsiveManager } from '$shared/lib';
|
import type { ResponsiveManager } from '$shared/lib';
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
|
||||||
import {
|
import {
|
||||||
ComboControl,
|
ComboControl,
|
||||||
ControlGroup,
|
ControlGroup,
|
||||||
@@ -21,6 +20,7 @@ import {
|
|||||||
import Settings2Icon from '@lucide/svelte/icons/settings-2';
|
import Settings2Icon from '@lucide/svelte/icons/settings-2';
|
||||||
import XIcon from '@lucide/svelte/icons/x';
|
import XIcon from '@lucide/svelte/icons/x';
|
||||||
import { Popover } from 'bits-ui';
|
import { Popover } from 'bits-ui';
|
||||||
|
import clsx from 'clsx';
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import { cubicOut } from 'svelte/easing';
|
import { cubicOut } from 'svelte/easing';
|
||||||
import { fly } from 'svelte/transition';
|
import { fly } from 'svelte/transition';
|
||||||
@@ -48,7 +48,9 @@ let isOpen = $state(false);
|
|||||||
* Sets the common font size multiplier based on the current responsive state.
|
* Sets the common font size multiplier based on the current responsive state.
|
||||||
*/
|
*/
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!responsive) return;
|
if (!responsive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case responsive.isMobile:
|
case responsive.isMobile:
|
||||||
typographySettingsStore.multiplier = MULTIPLIER_S;
|
typographySettingsStore.multiplier = MULTIPLIER_S;
|
||||||
@@ -72,7 +74,7 @@ $effect(() => {
|
|||||||
{#snippet child({ props })}
|
{#snippet child({ props })}
|
||||||
<button
|
<button
|
||||||
{...props}
|
{...props}
|
||||||
class={cn(
|
class={clsx(
|
||||||
'inline-flex items-center justify-center',
|
'inline-flex items-center justify-center',
|
||||||
'size-8 p-0',
|
'size-8 p-0',
|
||||||
'border border-transparent rounded-none',
|
'border border-transparent rounded-none',
|
||||||
@@ -93,7 +95,7 @@ $effect(() => {
|
|||||||
side="top"
|
side="top"
|
||||||
align="start"
|
align="start"
|
||||||
sideOffset={8}
|
sideOffset={8}
|
||||||
class={cn(
|
class={clsx(
|
||||||
'z-50 w-72',
|
'z-50 w-72',
|
||||||
'bg-surface dark:bg-dark-card',
|
'bg-surface dark:bg-dark-card',
|
||||||
'border border-subtle',
|
'border border-subtle',
|
||||||
@@ -147,11 +149,11 @@ $effect(() => {
|
|||||||
</Popover.Root>
|
</Popover.Root>
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<div
|
||||||
class={cn('w-full md:w-auto', className)}
|
class={clsx('w-full md:w-auto', className)}
|
||||||
transition:fly={{ y: 100, duration: 200, easing: cubicOut }}
|
transition:fly={{ y: 100, duration: 200, easing: cubicOut }}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class={cn(
|
class={clsx(
|
||||||
'flex items-center gap-1 md:gap-2 p-1.5 md:p-2',
|
'flex items-center gap-1 md:gap-2 p-1.5 md:p-2',
|
||||||
'bg-surface/95 dark:bg-dark-bg/95 backdrop-blur-xl',
|
'bg-surface/95 dark:bg-dark-bg/95 backdrop-blur-xl',
|
||||||
'border border-subtle',
|
'border border-subtle',
|
||||||
|
|||||||
+15
-5
@@ -171,7 +171,9 @@ export class CharacterComparisonEngine {
|
|||||||
|
|
||||||
for (let sIdx = start.segmentIndex; sIdx <= end.segmentIndex; sIdx++) {
|
for (let sIdx = start.segmentIndex; sIdx <= end.segmentIndex; sIdx++) {
|
||||||
const segmentText = this.#preparedA!.segments[sIdx];
|
const segmentText = this.#preparedA!.segments[sIdx];
|
||||||
if (segmentText === undefined) continue;
|
if (segmentText === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// PERFORMANCE: Reuse segmenter results if possible, but for now just optimize the loop
|
// PERFORMANCE: Reuse segmenter results if possible, but for now just optimize the loop
|
||||||
const graphemes = Array.from(this.#segmenter.segment(segmentText), s => s.segment);
|
const graphemes = Array.from(this.#segmenter.segment(segmentText), s => s.segment);
|
||||||
@@ -242,7 +244,9 @@ export class CharacterComparisonEngine {
|
|||||||
const line = this.#lastResult.lines[lineIndex];
|
const line = this.#lastResult.lines[lineIndex];
|
||||||
const char = line.chars[charIndex];
|
const char = line.chars[charIndex];
|
||||||
|
|
||||||
if (!char) return { proximity: 0, isPast: false };
|
if (!char) {
|
||||||
|
return { proximity: 0, isPast: false };
|
||||||
|
}
|
||||||
|
|
||||||
// Center the comparison on the unified width
|
// Center the comparison on the unified width
|
||||||
// In the UI, lines are centered. So we need to calculate the global X.
|
// In the UI, lines are centered. So we need to calculate the global X.
|
||||||
@@ -279,9 +283,15 @@ export class CharacterComparisonEngine {
|
|||||||
|
|
||||||
unified.breakableFitAdvances = intA.breakableFitAdvances.map((advA: number[] | null, i: number) => {
|
unified.breakableFitAdvances = intA.breakableFitAdvances.map((advA: number[] | null, i: number) => {
|
||||||
const advB = intB.breakableFitAdvances[i];
|
const advB = intB.breakableFitAdvances[i];
|
||||||
if (!advA && !advB) return null;
|
if (!advA && !advB) {
|
||||||
if (!advA) return advB;
|
return null;
|
||||||
if (!advB) return advA;
|
}
|
||||||
|
if (!advA) {
|
||||||
|
return advB;
|
||||||
|
}
|
||||||
|
if (!advB) {
|
||||||
|
return advA;
|
||||||
|
}
|
||||||
|
|
||||||
return advA.map((w: number, j: number) => Math.max(w, advB[j]));
|
return advA.map((w: number, j: number) => Math.max(w, advB[j]));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -127,7 +127,9 @@ export class TextLayoutEngine {
|
|||||||
// Both cursors are grapheme-level: start is inclusive, end is exclusive.
|
// Both cursors are grapheme-level: start is inclusive, end is exclusive.
|
||||||
for (let sIdx = start.segmentIndex; sIdx <= end.segmentIndex; sIdx++) {
|
for (let sIdx = start.segmentIndex; sIdx <= end.segmentIndex; sIdx++) {
|
||||||
const segmentText = prepared.segments[sIdx];
|
const segmentText = prepared.segments[sIdx];
|
||||||
if (segmentText === undefined) continue;
|
if (segmentText === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const graphemes = Array.from(this.#segmenter.segment(segmentText), s => s.segment);
|
const graphemes = Array.from(this.#segmenter.segment(segmentText), s => s.segment);
|
||||||
const advances = breakableFitAdvances[sIdx];
|
const advances = breakableFitAdvances[sIdx];
|
||||||
|
|||||||
@@ -150,7 +150,9 @@ export function createResponsiveManager(customBreakpoints?: Partial<Breakpoints>
|
|||||||
* @returns Cleanup function to remove listeners
|
* @returns Cleanup function to remove listeners
|
||||||
*/
|
*/
|
||||||
function init() {
|
function init() {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
width = window.innerWidth;
|
width = window.innerWidth;
|
||||||
|
|||||||
@@ -175,7 +175,9 @@ export function createVirtualizer<T>(
|
|||||||
const { count, data } = options;
|
const { count, data } = options;
|
||||||
// Implicit dependency
|
// Implicit dependency
|
||||||
const v = _version;
|
const v = _version;
|
||||||
if (count === 0 || containerHeight === 0 || !data) return [];
|
if (count === 0 || containerHeight === 0 || !data) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const overscan = options.overscan ?? 5;
|
const overscan = options.overscan ?? 5;
|
||||||
|
|
||||||
@@ -264,7 +266,9 @@ export function createVirtualizer<T>(
|
|||||||
containerHeight = window.innerHeight;
|
containerHeight = window.innerHeight;
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
if (rafId !== null) return;
|
if (rafId !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
rafId = requestAnimationFrame(() => {
|
rafId = requestAnimationFrame(() => {
|
||||||
// Get current position of element relative to viewport
|
// Get current position of element relative to viewport
|
||||||
@@ -323,7 +327,9 @@ export function createVirtualizer<T>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver(([entry]) => {
|
const resizeObserver = new ResizeObserver(([entry]) => {
|
||||||
if (entry) containerHeight = entry.contentRect.height;
|
if (entry) {
|
||||||
|
containerHeight = entry.contentRect.height;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
node.addEventListener('scroll', handleScroll, { passive: true });
|
node.addEventListener('scroll', handleScroll, { passive: true });
|
||||||
@@ -423,7 +429,9 @@ export function createVirtualizer<T>(
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
function scrollToIndex(index: number, align: 'start' | 'center' | 'end' | 'auto' = 'auto') {
|
function scrollToIndex(index: number, align: 'start' | 'center' | 'end' | 'auto' = 'auto') {
|
||||||
if (!elementRef || index < 0 || index >= options.count) return;
|
if (!elementRef || index < 0 || index >= options.count) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const itemStart = offsets[index];
|
const itemStart = offsets[index];
|
||||||
const itemSize = measuredSizes[index] ?? options.estimateSize(index);
|
const itemSize = measuredSizes[index] ?? options.estimateSize(index);
|
||||||
@@ -431,16 +439,24 @@ export function createVirtualizer<T>(
|
|||||||
const { useWindowScroll } = optionsGetter();
|
const { useWindowScroll } = optionsGetter();
|
||||||
|
|
||||||
if (useWindowScroll) {
|
if (useWindowScroll) {
|
||||||
if (align === 'center') target = itemStart - window.innerHeight / 2 + itemSize / 2;
|
if (align === 'center') {
|
||||||
if (align === 'end') target = itemStart - window.innerHeight + itemSize;
|
target = itemStart - window.innerHeight / 2 + itemSize / 2;
|
||||||
|
}
|
||||||
|
if (align === 'end') {
|
||||||
|
target = itemStart - window.innerHeight + itemSize;
|
||||||
|
}
|
||||||
|
|
||||||
// Add container offset to target to get absolute document position
|
// Add container offset to target to get absolute document position
|
||||||
const absoluteTarget = target + elementOffsetTop;
|
const absoluteTarget = target + elementOffsetTop;
|
||||||
|
|
||||||
window.scrollTo({ top: absoluteTarget, behavior: 'smooth' });
|
window.scrollTo({ top: absoluteTarget, behavior: 'smooth' });
|
||||||
} else {
|
} else {
|
||||||
if (align === 'center') target = itemStart - containerHeight / 2 + itemSize / 2;
|
if (align === 'center') {
|
||||||
if (align === 'end') target = itemStart - containerHeight + itemSize;
|
target = itemStart - containerHeight / 2 + itemSize / 2;
|
||||||
|
}
|
||||||
|
if (align === 'end') {
|
||||||
|
target = itemStart - containerHeight + itemSize;
|
||||||
|
}
|
||||||
|
|
||||||
elementRef.scrollTo({ top: target, behavior: 'smooth' });
|
elementRef.scrollTo({ top: target, behavior: 'smooth' });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
correctly via the HTML element's class attribute.
|
correctly via the HTML element's class attribute.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
import clsx from 'clsx';
|
||||||
import type {
|
import type {
|
||||||
Component,
|
Component,
|
||||||
Snippet,
|
Snippet,
|
||||||
@@ -32,7 +32,7 @@ let { icon: Icon, class: className, attrs = {} }: Props = $props();
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if Icon}
|
{#if Icon}
|
||||||
{@const __iconClass__ = cn('size-4', className)}
|
{@const __iconClass__ = clsx('size-4', className)}
|
||||||
<!-- Render icon component dynamically with class prop -->
|
<!-- Render icon component dynamically with class prop -->
|
||||||
<Icon
|
<Icon
|
||||||
class={__iconClass__}
|
class={__iconClass__}
|
||||||
|
|||||||
@@ -4,13 +4,11 @@
|
|||||||
|
|
||||||
Provides:
|
Provides:
|
||||||
- responsive: ResponsiveManager context for breakpoint tracking
|
- responsive: ResponsiveManager context for breakpoint tracking
|
||||||
- tooltip: Tooltip.Provider context for shadcn Tooltip components
|
|
||||||
- Additional Radix UI providers can be added here as needed
|
- Additional Radix UI providers can be added here as needed
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createResponsiveManager } from '$shared/lib';
|
import { createResponsiveManager } from '$shared/lib';
|
||||||
import type { ResponsiveManager } from '$shared/lib';
|
import type { ResponsiveManager } from '$shared/lib';
|
||||||
import { Provider as TooltipProvider } from '$shared/shadcn/ui/tooltip';
|
|
||||||
import { setContext } from 'svelte';
|
import { setContext } from 'svelte';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
@@ -60,10 +58,5 @@ setContext<ResponsiveManager>('responsive', responsiveManager);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="storybook-providers" style:width="100%" style:height="100%">
|
<div class="storybook-providers" style:width="100%" style:height="100%">
|
||||||
<TooltipProvider
|
|
||||||
delayDuration={tooltipDelayDuration}
|
|
||||||
skipDelayDuration={tooltipSkipDelayDuration}
|
|
||||||
>
|
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ export function smoothScroll(node: HTMLAnchorElement) {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const hash = node.getAttribute('href');
|
const hash = node.getAttribute('href');
|
||||||
if (!hash || hash === '#') return;
|
if (!hash || hash === '#') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const targetElement = document.querySelector(hash);
|
const targetElement = document.querySelector(hash);
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ export function throttle<T extends (...args: any[]) => any>(
|
|||||||
fn(...args);
|
fn(...args);
|
||||||
} else {
|
} else {
|
||||||
// Schedule for end of wait period (trailing edge)
|
// Schedule for end of wait period (trailing edge)
|
||||||
if (timeoutId) clearTimeout(timeoutId);
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
timeoutId = setTimeout(() => {
|
timeoutId = setTimeout(() => {
|
||||||
lastCall = Date.now();
|
lastCall = Date.now();
|
||||||
fn(...args);
|
fn(...args);
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
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`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import Close from './popover-close.svelte';
|
|
||||||
import Content from './popover-content.svelte';
|
|
||||||
import Portal from './popover-portal.svelte';
|
|
||||||
import Trigger from './popover-trigger.svelte';
|
|
||||||
import Root from './popover.svelte';
|
|
||||||
|
|
||||||
export {
|
|
||||||
Close,
|
|
||||||
Close as PopoverClose,
|
|
||||||
Content,
|
|
||||||
Content as PopoverContent,
|
|
||||||
Portal,
|
|
||||||
Portal as PopoverPortal,
|
|
||||||
Root,
|
|
||||||
//
|
|
||||||
Root as Popover,
|
|
||||||
Trigger,
|
|
||||||
Trigger as PopoverTrigger,
|
|
||||||
};
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { Popover as PopoverPrimitive } from 'bits-ui';
|
|
||||||
|
|
||||||
let { ref = $bindable(null), ...restProps }: PopoverPrimitive.CloseProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<PopoverPrimitive.Close bind:ref data-slot="popover-close" {...restProps} />
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {
|
|
||||||
type WithoutChildrenOrChild,
|
|
||||||
cn,
|
|
||||||
} from '$shared/shadcn/utils/shadcn-utils.js';
|
|
||||||
import { Popover as PopoverPrimitive } from 'bits-ui';
|
|
||||||
import type { ComponentProps } from 'svelte';
|
|
||||||
import PopoverPortal from './popover-portal.svelte';
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
sideOffset = 4,
|
|
||||||
align = 'center',
|
|
||||||
portalProps,
|
|
||||||
...restProps
|
|
||||||
}: PopoverPrimitive.ContentProps & {
|
|
||||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof PopoverPortal>>;
|
|
||||||
} = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<PopoverPortal {...portalProps}>
|
|
||||||
<PopoverPrimitive.Content
|
|
||||||
bind:ref
|
|
||||||
data-slot="popover-content"
|
|
||||||
{sideOffset}
|
|
||||||
{align}
|
|
||||||
class={cn(
|
|
||||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-end-2 data-[side=right]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--bits-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...restProps}
|
|
||||||
/>
|
|
||||||
</PopoverPortal>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { Popover as PopoverPrimitive } from 'bits-ui';
|
|
||||||
|
|
||||||
let { ...restProps }: PopoverPrimitive.PortalProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<PopoverPrimitive.Portal {...restProps} />
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils.js';
|
|
||||||
import { Popover as PopoverPrimitive } from 'bits-ui';
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
...restProps
|
|
||||||
}: PopoverPrimitive.TriggerProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<PopoverPrimitive.Trigger
|
|
||||||
bind:ref
|
|
||||||
data-slot="popover-trigger"
|
|
||||||
class={cn('', className)}
|
|
||||||
{...restProps}
|
|
||||||
/>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { Popover as PopoverPrimitive } from 'bits-ui';
|
|
||||||
|
|
||||||
let { open = $bindable(false), ...restProps }: PopoverPrimitive.RootProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<PopoverPrimitive.Root bind:open {...restProps} />
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
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,
|
|
||||||
};
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { Tooltip as TooltipPrimitive } from 'bits-ui';
|
|
||||||
|
|
||||||
let { ...restProps }: TooltipPrimitive.PortalProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<TooltipPrimitive.Portal {...restProps} />
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { Tooltip as TooltipPrimitive } from 'bits-ui';
|
|
||||||
|
|
||||||
let { ...restProps }: TooltipPrimitive.ProviderProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<TooltipPrimitive.Provider {...restProps} />
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { Tooltip as TooltipPrimitive } from 'bits-ui';
|
|
||||||
|
|
||||||
let { ref = $bindable(null), ...restProps }: TooltipPrimitive.TriggerProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<TooltipPrimitive.Trigger bind:ref data-slot="tooltip-trigger" {...restProps} />
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { Tooltip as TooltipPrimitive } from 'bits-ui';
|
|
||||||
|
|
||||||
let { open = $bindable(false), ...restProps }: TooltipPrimitive.RootProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<TooltipPrimitive.Root bind:open {...restProps} />
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import {
|
|
||||||
type ClassValue,
|
|
||||||
clsx,
|
|
||||||
} from 'clsx';
|
|
||||||
import { twMerge } from 'tailwind-merge';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility function to merge Tailwind CSS classes
|
|
||||||
* Combines clsx for conditional classes and tailwind-merge to handle conflicts
|
|
||||||
*/
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
|
||||||
return twMerge(clsx(inputs));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type utility to add a ref property to HTML element attributes
|
|
||||||
* Used in shadcn-svelte components to support element references
|
|
||||||
* @template T - The attributes type (e.g., HTMLAttributes<HTMLDivElement>)
|
|
||||||
* @template E - The element type (e.g., HTMLDivElement)
|
|
||||||
*/
|
|
||||||
export type WithElementRef<T, E extends HTMLElement = HTMLElement> = T & {
|
|
||||||
/**
|
|
||||||
* Reference to the DOM element
|
|
||||||
*/
|
|
||||||
ref?: E | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type utility to remove 'children' and 'child' properties from a type
|
|
||||||
* Used in shadcn-svelte components that use Snippets instead of children
|
|
||||||
* @template T - The type to remove children from
|
|
||||||
*/
|
|
||||||
export type WithoutChildren<T> = Omit<T, 'children'>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type utility to remove 'children' and 'child' properties from a type
|
|
||||||
* Used in shadcn-svelte components that use Snippets instead of children
|
|
||||||
* @template T - The type to remove children and child from
|
|
||||||
*/
|
|
||||||
export type WithoutChildrenOrChild<T> = Omit<T, 'children' | 'child'>;
|
|
||||||
@@ -3,11 +3,11 @@
|
|||||||
Pill badge with border and optional status dot.
|
Pill badge with border and optional status dot.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
|
||||||
import {
|
import {
|
||||||
type LabelSize,
|
type LabelSize,
|
||||||
labelSizeConfig,
|
labelSizeConfig,
|
||||||
} from '$shared/ui/Label/config';
|
} from '$shared/ui/Label/config';
|
||||||
|
import clsx from 'clsx';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import type { HTMLAttributes } from 'svelte/elements';
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ let {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class={cn(
|
class={clsx(
|
||||||
'inline-flex items-center gap-1 px-2 py-0.5 border rounded-full',
|
'inline-flex items-center gap-1 px-2 py-0.5 border rounded-full',
|
||||||
'font-mono uppercase tracking-wide',
|
'font-mono uppercase tracking-wide',
|
||||||
labelSizeConfig[size],
|
labelSizeConfig[size],
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
design-system button. Uppercase, zero border-radius, Space Grotesk.
|
design-system button. Uppercase, zero border-radius, Space Grotesk.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
import clsx from 'clsx';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import type { HTMLButtonAttributes } from 'svelte/elements';
|
import type { HTMLButtonAttributes } from 'svelte/elements';
|
||||||
import type {
|
import type {
|
||||||
@@ -71,7 +71,7 @@ let {
|
|||||||
const isIconOnly = $derived(!!icon && !children);
|
const isIconOnly = $derived(!!icon && !children);
|
||||||
|
|
||||||
const variantStyles: Record<ButtonVariant, string> = {
|
const variantStyles: Record<ButtonVariant, string> = {
|
||||||
primary: cn(
|
primary: clsx(
|
||||||
'bg-swiss-red text-white',
|
'bg-swiss-red text-white',
|
||||||
'hover:bg-swiss-red/90',
|
'hover:bg-swiss-red/90',
|
||||||
'active:bg-swiss-red/80',
|
'active:bg-swiss-red/80',
|
||||||
@@ -87,7 +87,7 @@ const variantStyles: Record<ButtonVariant, string> = {
|
|||||||
'disabled:cursor-not-allowed',
|
'disabled:cursor-not-allowed',
|
||||||
'disabled:transform-none',
|
'disabled:transform-none',
|
||||||
),
|
),
|
||||||
secondary: cn(
|
secondary: clsx(
|
||||||
'bg-surface dark:bg-paper',
|
'bg-surface dark:bg-paper',
|
||||||
'text-swiss-black dark:text-neutral-200',
|
'text-swiss-black dark:text-neutral-200',
|
||||||
'border border-black/10 dark:border-white/10',
|
'border border-black/10 dark:border-white/10',
|
||||||
@@ -98,7 +98,7 @@ const variantStyles: Record<ButtonVariant, string> = {
|
|||||||
'disabled:text-neutral-400 dark:disabled:text-neutral-600',
|
'disabled:text-neutral-400 dark:disabled:text-neutral-600',
|
||||||
'disabled:cursor-not-allowed',
|
'disabled:cursor-not-allowed',
|
||||||
),
|
),
|
||||||
outline: cn(
|
outline: clsx(
|
||||||
'bg-transparent',
|
'bg-transparent',
|
||||||
'text-swiss-black dark:text-neutral-200',
|
'text-swiss-black dark:text-neutral-200',
|
||||||
'border border-black/20 dark:border-white/20',
|
'border border-black/20 dark:border-white/20',
|
||||||
@@ -109,7 +109,7 @@ const variantStyles: Record<ButtonVariant, string> = {
|
|||||||
'disabled:text-neutral-400 dark:disabled:text-neutral-600',
|
'disabled:text-neutral-400 dark:disabled:text-neutral-600',
|
||||||
'disabled:cursor-not-allowed',
|
'disabled:cursor-not-allowed',
|
||||||
),
|
),
|
||||||
ghost: cn(
|
ghost: clsx(
|
||||||
'bg-transparent',
|
'bg-transparent',
|
||||||
'text-secondary',
|
'text-secondary',
|
||||||
'border border-transparent',
|
'border border-transparent',
|
||||||
@@ -119,7 +119,7 @@ const variantStyles: Record<ButtonVariant, string> = {
|
|||||||
'disabled:text-neutral-400 dark:disabled:text-neutral-600',
|
'disabled:text-neutral-400 dark:disabled:text-neutral-600',
|
||||||
'disabled:cursor-not-allowed',
|
'disabled:cursor-not-allowed',
|
||||||
),
|
),
|
||||||
icon: cn(
|
icon: clsx(
|
||||||
'bg-surface dark:bg-dark-bg',
|
'bg-surface dark:bg-dark-bg',
|
||||||
'text-secondary',
|
'text-secondary',
|
||||||
'border border-transparent',
|
'border border-transparent',
|
||||||
@@ -130,8 +130,8 @@ const variantStyles: Record<ButtonVariant, string> = {
|
|||||||
'disabled:text-neutral-400 dark:disabled:text-neutral-600',
|
'disabled:text-neutral-400 dark:disabled:text-neutral-600',
|
||||||
'disabled:cursor-not-allowed',
|
'disabled:cursor-not-allowed',
|
||||||
),
|
),
|
||||||
tertiary: cn(
|
tertiary: clsx(
|
||||||
// Font override — must come after base in cn() to win via tailwind-merge
|
// Font override — must come after base in clsx() to win via tailwind-merge
|
||||||
'font-secondary font-medium normal-case tracking-normal',
|
'font-secondary font-medium normal-case tracking-normal',
|
||||||
// Inactive state
|
// Inactive state
|
||||||
'bg-transparent',
|
'bg-transparent',
|
||||||
@@ -175,7 +175,7 @@ const activeStyles: Partial<Record<ButtonVariant, string>> = {
|
|||||||
icon: 'bg-paper dark:bg-paper text-brand border-subtle',
|
icon: 'bg-paper dark:bg-paper text-brand border-subtle',
|
||||||
};
|
};
|
||||||
|
|
||||||
const classes = $derived(cn(
|
const classes = $derived(clsx(
|
||||||
// Base
|
// Base
|
||||||
'inline-flex items-center justify-center',
|
'inline-flex items-center justify-center',
|
||||||
'font-primary font-bold tracking-tight uppercase',
|
'font-primary font-bold tracking-tight uppercase',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
Use for segmented controls, view toggles, or any mutually exclusive button set.
|
Use for segmented controls, view toggles, or any mutually exclusive button set.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
import clsx from 'clsx';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import type { HTMLAttributes } from 'svelte/elements';
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ let { children, class: className, ...rest }: Props = $props();
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class={cn(
|
class={clsx(
|
||||||
'flex items-center gap-1 p-1',
|
'flex items-center gap-1 p-1',
|
||||||
'bg-surface dark:bg-dark-bg',
|
'bg-surface dark:bg-dark-bg',
|
||||||
'border border-subtle',
|
'border border-subtle',
|
||||||
|
|||||||
@@ -5,16 +5,12 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { TypographyControl } from '$shared/lib';
|
import type { TypographyControl } from '$shared/lib';
|
||||||
import {
|
|
||||||
Content as PopoverContent,
|
|
||||||
Root as PopoverRoot,
|
|
||||||
Trigger as PopoverTrigger,
|
|
||||||
} from '$shared/shadcn/ui/popover';
|
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
|
||||||
import { Slider } from '$shared/ui';
|
import { Slider } from '$shared/ui';
|
||||||
import { Button } from '$shared/ui/Button';
|
import { Button } from '$shared/ui/Button';
|
||||||
import MinusIcon from '@lucide/svelte/icons/minus';
|
import MinusIcon from '@lucide/svelte/icons/minus';
|
||||||
import PlusIcon from '@lucide/svelte/icons/plus';
|
import PlusIcon from '@lucide/svelte/icons/plus';
|
||||||
|
import { Popover } from 'bits-ui';
|
||||||
|
import clsx from 'clsx';
|
||||||
import TechText from '../TechText/TechText.svelte';
|
import TechText from '../TechText/TechText.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -66,7 +62,9 @@ let open = $state(false);
|
|||||||
// Smart value formatting matching the Figma design
|
// Smart value formatting matching the Figma design
|
||||||
const formattedValue = $derived(() => {
|
const formattedValue = $derived(() => {
|
||||||
const v = control.value;
|
const v = control.value;
|
||||||
if (Number.isInteger(v)) return String(v);
|
if (Number.isInteger(v)) {
|
||||||
|
return String(v);
|
||||||
|
}
|
||||||
return control.step < 0.1 ? v.toFixed(2) : v.toFixed(1);
|
return control.step < 0.1 ? v.toFixed(2) : v.toFixed(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -80,7 +78,7 @@ const displayLabel = $derived(label ?? controlLabel ?? '');
|
|||||||
-->
|
-->
|
||||||
{#if reduced}
|
{#if reduced}
|
||||||
<div
|
<div
|
||||||
class={cn(
|
class={clsx(
|
||||||
'flex gap-4 items-end w-full',
|
'flex gap-4 items-end w-full',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
@@ -100,7 +98,7 @@ const displayLabel = $derived(label ?? controlLabel ?? '');
|
|||||||
|
|
||||||
<!-- ── FULL MODE ──────────────────────────────────────────────────────────────── -->
|
<!-- ── FULL MODE ──────────────────────────────────────────────────────────────── -->
|
||||||
{:else}
|
{:else}
|
||||||
<div class={cn('flex items-center px-1 relative', className)}>
|
<div class={clsx('flex items-center px-1 relative', className)}>
|
||||||
<!-- Decrease button -->
|
<!-- Decrease button -->
|
||||||
<Button
|
<Button
|
||||||
variant="icon"
|
variant="icon"
|
||||||
@@ -116,12 +114,12 @@ const displayLabel = $derived(label ?? controlLabel ?? '');
|
|||||||
|
|
||||||
<!-- Trigger -->
|
<!-- Trigger -->
|
||||||
<div class="relative mx-1">
|
<div class="relative mx-1">
|
||||||
<PopoverRoot bind:open>
|
<Popover.Root bind:open>
|
||||||
<PopoverTrigger>
|
<Popover.Trigger>
|
||||||
{#snippet child({ props })}
|
{#snippet child({ props })}
|
||||||
<button
|
<button
|
||||||
{...props}
|
{...props}
|
||||||
class={cn(
|
class={clsx(
|
||||||
'flex flex-col items-center justify-center w-14 py-1',
|
'flex flex-col items-center justify-center w-14 py-1',
|
||||||
'select-none rounded-none transition-all duration-150',
|
'select-none rounded-none transition-all duration-150',
|
||||||
'border border-transparent',
|
'border border-transparent',
|
||||||
@@ -151,10 +149,10 @@ const displayLabel = $derived(label ?? controlLabel ?? '');
|
|||||||
</TechText>
|
</TechText>
|
||||||
</button>
|
</button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</PopoverTrigger>
|
</Popover.Trigger>
|
||||||
|
|
||||||
<!-- Vertical slider popover -->
|
<!-- Vertical slider popover -->
|
||||||
<PopoverContent
|
<Popover.Content
|
||||||
class="w-auto py-4 px-3 h-64 flex items-center justify-center rounded-none border border-subtle shadow-sm bg-paper dark:bg-dark-card"
|
class="w-auto py-4 px-3 h-64 flex items-center justify-center rounded-none border border-subtle shadow-sm bg-paper dark:bg-dark-card"
|
||||||
align="center"
|
align="center"
|
||||||
side="top"
|
side="top"
|
||||||
@@ -167,8 +165,8 @@ const displayLabel = $derived(label ?? controlLabel ?? '');
|
|||||||
step={control.step}
|
step={control.step}
|
||||||
orientation="vertical"
|
orientation="vertical"
|
||||||
/>
|
/>
|
||||||
</PopoverContent>
|
</Popover.Content>
|
||||||
</PopoverRoot>
|
</Popover.Root>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Increase button -->
|
<!-- Increase button -->
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
Labeled container for form controls
|
Labeled container for form controls
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
import clsx from 'clsx';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -24,7 +24,7 @@ interface Props {
|
|||||||
const { label, children, class: className }: Props = $props();
|
const { label, children, class: className }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={cn('flex flex-col gap-3 py-6 border-b border-subtle last:border-0', className)}>
|
<div class={clsx('flex flex-col gap-3 py-6 border-b border-subtle last:border-0', className)}>
|
||||||
<div class="flex justify-between items-center text-xs font-primary font-bold tracking-tight text-neutral-900 dark:text-neutral-100 uppercase leading-none">
|
<div class="flex justify-between items-center text-xs font-primary font-bold tracking-tight text-neutral-900 dark:text-neutral-100 uppercase leading-none">
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
1px separator line, horizontal or vertical.
|
1px separator line, horizontal or vertical.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/**
|
/**
|
||||||
@@ -24,7 +24,7 @@ let {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class={cn(
|
class={clsx(
|
||||||
'bg-black/10 dark:bg-white/10',
|
'bg-black/10 dark:bg-white/10',
|
||||||
orientation === 'horizontal' ? 'w-full h-px' : 'w-px h-full',
|
orientation === 'horizontal' ? 'w-full h-px' : 'w-px h-full',
|
||||||
className,
|
className,
|
||||||
|
|||||||
@@ -4,11 +4,11 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Filter } from '$shared/lib';
|
import type { Filter } from '$shared/lib';
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
|
||||||
import { Button } from '$shared/ui';
|
import { Button } from '$shared/ui';
|
||||||
import { Label } from '$shared/ui';
|
import { Label } from '$shared/ui';
|
||||||
import ChevronUpIcon from '@lucide/svelte/icons/chevron-up';
|
import ChevronUpIcon from '@lucide/svelte/icons/chevron-up';
|
||||||
import EllipsisIcon from '@lucide/svelte/icons/ellipsis';
|
import EllipsisIcon from '@lucide/svelte/icons/ellipsis';
|
||||||
|
import clsx from 'clsx';
|
||||||
import { cubicOut } from 'svelte/easing';
|
import { cubicOut } from 'svelte/easing';
|
||||||
import {
|
import {
|
||||||
draw,
|
draw,
|
||||||
@@ -68,7 +68,7 @@ $effect(() => {
|
|||||||
</svg>
|
</svg>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
<div class={cn('flex flex-col', className)}>
|
<div class={clsx('flex flex-col', className)}>
|
||||||
<Label
|
<Label
|
||||||
variant="default"
|
variant="default"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
Provides classes for styling footnotes
|
Provides classes for styling footnotes
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
import clsx from 'clsx';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -26,14 +26,14 @@ const { children, class: className, render }: Props = $props();
|
|||||||
|
|
||||||
{#if render}
|
{#if render}
|
||||||
{@render render({
|
{@render render({
|
||||||
class: cn(
|
class: clsx(
|
||||||
'font-mono text-3xs sm:text-2xs lowercase tracking-wider-mono text-text-soft',
|
'font-mono text-3xs sm:text-2xs lowercase tracking-wider-mono text-text-soft',
|
||||||
className,
|
className,
|
||||||
),
|
),
|
||||||
})}
|
})}
|
||||||
{:else if children}
|
{:else if children}
|
||||||
<span
|
<span
|
||||||
class={cn(
|
class={clsx(
|
||||||
'font-mono text-3xs sm:text-2xs lowercase tracking-wider-mono text-text-soft',
|
'font-mono text-3xs sm:text-2xs lowercase tracking-wider-mono text-text-soft',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
design-system input. Zero border-radius, Space Grotesk, precise states.
|
design-system input. Zero border-radius, Space Grotesk, precise states.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
|
||||||
import XIcon from '@lucide/svelte/icons/x';
|
import XIcon from '@lucide/svelte/icons/x';
|
||||||
|
import clsx from 'clsx';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import { cubicOut } from 'svelte/easing';
|
import { cubicOut } from 'svelte/easing';
|
||||||
import type { HTMLInputAttributes } from 'svelte/elements';
|
import type { HTMLInputAttributes } from 'svelte/elements';
|
||||||
@@ -90,7 +90,7 @@ const hasRightSlot = $derived(!!rightIcon || showClearButton);
|
|||||||
const cfg = $derived(inputSizeConfig[size]);
|
const cfg = $derived(inputSizeConfig[size]);
|
||||||
const styles = $derived(inputVariantConfig[variant]);
|
const styles = $derived(inputVariantConfig[variant]);
|
||||||
|
|
||||||
const inputClasses = $derived(cn(
|
const inputClasses = $derived(clsx(
|
||||||
'font-primary rounded-none outline-none transition-all duration-200',
|
'font-primary rounded-none outline-none transition-all duration-200',
|
||||||
'text-neutral-900 dark:text-neutral-100',
|
'text-neutral-900 dark:text-neutral-100',
|
||||||
'placeholder:text-neutral-400 dark:placeholder:text-neutral-600',
|
'placeholder:text-neutral-400 dark:placeholder:text-neutral-600',
|
||||||
@@ -107,8 +107,8 @@ const inputClasses = $derived(cn(
|
|||||||
));
|
));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={cn('flex flex-col gap-1', fullWidth && 'w-full')}>
|
<div class={clsx('flex flex-col gap-1', fullWidth && 'w-full')}>
|
||||||
<div class={cn('relative group', fullWidth && 'w-full')}>
|
<div class={clsx('relative group', fullWidth && 'w-full')}>
|
||||||
<!-- Left icon slot -->
|
<!-- Left icon slot -->
|
||||||
{#if leftIcon}
|
{#if leftIcon}
|
||||||
<div class="absolute left-3 top-1/2 -translate-y-1/2 text-neutral-400 dark:text-neutral-600 pointer-events-none z-10 flex items-center">
|
<div class="absolute left-3 top-1/2 -translate-y-1/2 text-neutral-400 dark:text-neutral-600 pointer-events-none z-10 flex items-center">
|
||||||
@@ -147,7 +147,7 @@ const inputClasses = $derived(cn(
|
|||||||
<!-- Helper / error text -->
|
<!-- Helper / error text -->
|
||||||
{#if helperText}
|
{#if helperText}
|
||||||
<span
|
<span
|
||||||
class={cn(
|
class={clsx(
|
||||||
'text-2xs font-mono tracking-wide px-1',
|
'text-2xs font-mono tracking-wide px-1',
|
||||||
error ? 'text-brand ' : 'text-secondary',
|
error ? 'text-brand ' : 'text-secondary',
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
Inline monospace label. The base primitive for all micrographic text.
|
Inline monospace label. The base primitive for all micrographic text.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
import clsx from 'clsx';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import {
|
import {
|
||||||
type LabelFont,
|
type LabelFont,
|
||||||
@@ -72,7 +72,7 @@ let {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class={cn(
|
class={clsx(
|
||||||
'font-mono tracking-widest leading-none',
|
'font-mono tracking-widest leading-none',
|
||||||
'inline-flex items-center gap-1.5',
|
'inline-flex items-center gap-1.5',
|
||||||
font === 'primary' && 'font-primary tracking-tight',
|
font === 'primary' && 'font-primary tracking-tight',
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
Project logo with apropriate styles
|
Project logo with apropriate styles
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
|
||||||
import { Badge } from '$shared/ui';
|
import { Badge } from '$shared/ui';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/**
|
/**
|
||||||
@@ -18,7 +18,7 @@ const { class: className }: Props = $props();
|
|||||||
const title = 'GLYPHDIFF';
|
const title = 'GLYPHDIFF';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={cn('flex items-center gap-2 md:gap-3 select-none', className)}>
|
<div class={clsx('flex items-center gap-2 md:gap-3 select-none', className)}>
|
||||||
<h1 class="font-logo font-extrabold text-base md:text-xl tracking-tight text-swiss-black dark:text-neutral-200">
|
<h1 class="font-logo font-extrabold text-base md:text-xl tracking-tight text-swiss-black dark:text-neutral-200">
|
||||||
{title}
|
{title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { PerspectiveManager } from '$shared/lib';
|
import type { PerspectiveManager } from '$shared/lib';
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
import clsx from 'clsx';
|
||||||
import { type Snippet } from 'svelte';
|
import { type Snippet } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -61,7 +61,9 @@ const style = $derived.by(() => {
|
|||||||
|
|
||||||
// Calculate horizontal constraints based on region
|
// Calculate horizontal constraints based on region
|
||||||
const regionStyleStr = $derived(() => {
|
const regionStyleStr = $derived(() => {
|
||||||
if (region === 'full') return '';
|
if (region === 'full') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
const side = region === 'left' ? 'left' : 'right';
|
const side = region === 'left' ? 'left' : 'right';
|
||||||
return `position: absolute; ${side}: 0; width: ${regionWidth}%; top: 0; bottom: 0;`;
|
return `position: absolute; ${side}: 0; width: ${regionWidth}%; top: 0; bottom: 0;`;
|
||||||
});
|
});
|
||||||
@@ -71,7 +73,7 @@ const isVisible = $derived(manager.isFront);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class={cn('will-change-transform', className)}
|
class={clsx('will-change-transform', className)}
|
||||||
style:transform-style="preserve-3d"
|
style:transform-style="preserve-3d"
|
||||||
style:transform={style?.transform}
|
style:transform={style?.transform}
|
||||||
style:filter={style?.filter}
|
style:filter={style?.filter}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
Numbered section heading with optional subtitle and pulse dot.
|
Numbered section heading with optional subtitle and pulse dot.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
|
||||||
import { Label } from '$shared/ui';
|
import { Label } from '$shared/ui';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/**
|
/**
|
||||||
@@ -41,7 +41,7 @@ let {
|
|||||||
const indexStr = $derived(String(index).padStart(2, '0'));
|
const indexStr = $derived(String(index).padStart(2, '0'));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={cn('flex items-center gap-3 md:gap-4 mb-2', className)}>
|
<div class={clsx('flex items-center gap-3 md:gap-4 mb-2', className)}>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
{#if pulse}
|
{#if pulse}
|
||||||
<span class="w-1.5 h-1.5 bg-brand rounded-full animate-pulse"></span>
|
<span class="w-1.5 h-1.5 bg-brand rounded-full animate-pulse"></span>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
A horizontal separator line used to visually separate sections within a page.
|
A horizontal separator line used to visually separate sections within a page.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/**
|
/**
|
||||||
@@ -15,4 +15,4 @@ interface Props {
|
|||||||
const { class: className = '' }: Props = $props();
|
const { class: className = '' }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={cn('w-full h-px bg-swiss-black/5 dark:bg-white/10 my-8 md:my-12', className)}></div>
|
<div class={clsx('w-full h-px bg-swiss-black/5 dark:bg-white/10 my-8 md:my-12', className)}></div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ResponsiveManager } from '$shared/lib';
|
import type { ResponsiveManager } from '$shared/lib';
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
import clsx from 'clsx';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import { cubicOut } from 'svelte/easing';
|
import { cubicOut } from 'svelte/easing';
|
||||||
@@ -79,7 +79,7 @@ function close() {
|
|||||||
The inner div stays w-80 so Sidebar layout never reflows mid-animation.
|
The inner div stays w-80 so Sidebar layout never reflows mid-animation.
|
||||||
-->
|
-->
|
||||||
<div
|
<div
|
||||||
class={cn(
|
class={clsx(
|
||||||
'shrink-0 z-30 h-full relative',
|
'shrink-0 z-30 h-full relative',
|
||||||
'overflow-hidden',
|
'overflow-hidden',
|
||||||
'will-change-[width]',
|
'will-change-[width]',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
Generic loading placeholder with shimmer animation.
|
Generic loading placeholder with shimmer animation.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
import clsx from 'clsx';
|
||||||
import type { HTMLAttributes } from 'svelte/elements';
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
interface Props extends HTMLAttributes<HTMLDivElement> {
|
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||||
@@ -18,7 +18,7 @@ let { class: className, animate = true, ...rest }: Props = $props();
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class={cn(
|
class={clsx(
|
||||||
'rounded-md bg-background-subtle/50 backdrop-blur-sm',
|
'rounded-md bg-background-subtle/50 backdrop-blur-sm',
|
||||||
animate && 'animate-pulse',
|
animate && 'animate-pulse',
|
||||||
className,
|
className,
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
A single key:value pair in Space Mono. Optional trailing divider.
|
A single key:value pair in Space Mono. Optional trailing divider.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
|
||||||
import { Label } from '$shared/ui';
|
import { Label } from '$shared/ui';
|
||||||
|
import clsx from 'clsx';
|
||||||
import type { ComponentProps } from 'svelte';
|
import type { ComponentProps } from 'svelte';
|
||||||
|
|
||||||
interface Props extends Pick<ComponentProps<typeof Label>, 'variant'> {
|
interface Props extends Pick<ComponentProps<typeof Label>, 'variant'> {
|
||||||
@@ -36,7 +36,7 @@ let {
|
|||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={cn('flex items-center gap-1', className)}>
|
<div class={clsx('flex items-center gap-1', className)}>
|
||||||
<Label variant="muted" size="xs">{label}:</Label>
|
<Label variant="muted" size="xs">{label}:</Label>
|
||||||
<Label {variant} size="xs" bold>{value}</Label>
|
<Label {variant} size="xs" bold>{value}</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
Renders multiple Stat components in a row with auto-separators.
|
Renders multiple Stat components in a row with auto-separators.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
|
||||||
import { Stat } from '$shared/ui';
|
import { Stat } from '$shared/ui';
|
||||||
|
import clsx from 'clsx';
|
||||||
import type { ComponentProps } from 'svelte';
|
import type { ComponentProps } from 'svelte';
|
||||||
|
|
||||||
interface StatItem extends Partial<Pick<ComponentProps<typeof Stat>, 'variant'>> {
|
interface StatItem extends Partial<Pick<ComponentProps<typeof Stat>, 'variant'>> {
|
||||||
@@ -26,7 +26,7 @@ interface Props {
|
|||||||
let { stats, class: className }: Props = $props();
|
let { stats, class: className }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={cn('flex items-center gap-4', className)}>
|
<div class={clsx('flex items-center gap-4', className)}>
|
||||||
{#each stats as stat, i}
|
{#each stats as stat, i}
|
||||||
<Stat
|
<Stat
|
||||||
label={stat.label}
|
label={stat.label}
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
Monospace <code> element for technical values, measurements, identifiers.
|
Monospace <code> element for technical values, measurements, identifiers.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
|
||||||
import {
|
import {
|
||||||
type LabelSize,
|
type LabelSize,
|
||||||
type LabelVariant,
|
type LabelVariant,
|
||||||
labelSizeConfig,
|
labelSizeConfig,
|
||||||
labelVariantConfig,
|
labelVariantConfig,
|
||||||
} from '$shared/ui/Label/config';
|
} from '$shared/ui/Label/config';
|
||||||
|
import clsx from 'clsx';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -42,7 +42,7 @@ let {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<code
|
<code
|
||||||
class={cn(
|
class={clsx(
|
||||||
'font-mono tracking-tight tabular-nums',
|
'font-mono tracking-tight tabular-nums',
|
||||||
labelSizeConfig[size],
|
labelSizeConfig[size],
|
||||||
labelVariantConfig[variant],
|
labelVariantConfig[variant],
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<script lang="ts" generics="T">
|
<script lang="ts" generics="T">
|
||||||
import { createVirtualizer } from '$shared/lib';
|
import { createVirtualizer } from '$shared/lib';
|
||||||
import { throttle } from '$shared/lib/utils';
|
import { throttle } from '$shared/lib/utils';
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
import clsx from 'clsx';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import type { HTMLAttributes } from 'svelte/elements';
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
@@ -136,7 +136,9 @@ const estimatedTotalSize = $derived.by(() => {
|
|||||||
|
|
||||||
// Add estimated size for unloaded rows
|
// Add estimated size for unloaded rows
|
||||||
const unloadedRows = totalRows - rowCount;
|
const unloadedRows = totalRows - rowCount;
|
||||||
if (unloadedRows <= 0) return loadedSize;
|
if (unloadedRows <= 0) {
|
||||||
|
return loadedSize;
|
||||||
|
}
|
||||||
|
|
||||||
// Estimate the size of unloaded rows
|
// Estimate the size of unloaded rows
|
||||||
const estimateFn = typeof itemHeight === 'function'
|
const estimateFn = typeof itemHeight === 'function'
|
||||||
@@ -293,13 +295,13 @@ $effect(() => {
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
{#if useWindowScroll}
|
{#if useWindowScroll}
|
||||||
<div class={cn('relative w-full', className)} bind:this={viewportRef} {...rest}>
|
<div class={clsx('relative w-full', className)} bind:this={viewportRef} {...rest}>
|
||||||
{@render content()}
|
{@render content()}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<div
|
||||||
bind:this={viewportRef}
|
bind:this={viewportRef}
|
||||||
class={cn(
|
class={clsx(
|
||||||
'relative overflow-y-auto overflow-x-hidden',
|
'relative overflow-y-auto overflow-x-hidden',
|
||||||
'rounded-md bg-background',
|
'rounded-md bg-background',
|
||||||
'w-full',
|
'w-full',
|
||||||
|
|||||||
@@ -95,16 +95,22 @@ export class ComparisonStore {
|
|||||||
// Effect 1: Sync batch results → fontA / fontB
|
// Effect 1: Sync batch results → fontA / fontB
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const fonts = this.#batchStore.fonts;
|
const fonts = this.#batchStore.fonts;
|
||||||
if (fonts.length === 0) return;
|
if (fonts.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { fontAId: aId, fontBId: bId } = storage.value;
|
const { fontAId: aId, fontBId: bId } = storage.value;
|
||||||
if (aId) {
|
if (aId) {
|
||||||
const fa = fonts.find(f => f.id === aId);
|
const fa = fonts.find(f => f.id === aId);
|
||||||
if (fa) this.#fontA = fa;
|
if (fa) {
|
||||||
|
this.#fontA = fa;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (bId) {
|
if (bId) {
|
||||||
const fb = fonts.find(f => f.id === bId);
|
const fb = fonts.find(f => f.id === bId);
|
||||||
if (fb) this.#fontB = fb;
|
if (fb) {
|
||||||
|
this.#fontB = fb;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -114,7 +120,9 @@ export class ComparisonStore {
|
|||||||
const fb = this.#fontB;
|
const fb = this.#fontB;
|
||||||
const weight = typographySettingsStore.weight;
|
const weight = typographySettingsStore.weight;
|
||||||
|
|
||||||
if (!fa || !fb) return;
|
if (!fa || !fb) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const configs: FontLoadRequestConfig[] = [];
|
const configs: FontLoadRequestConfig[] = [];
|
||||||
[fa, fb].forEach(f => {
|
[fa, fb].forEach(f => {
|
||||||
@@ -138,7 +146,9 @@ export class ComparisonStore {
|
|||||||
|
|
||||||
// Effect 3: Set default fonts when storage is empty
|
// Effect 3: Set default fonts when storage is empty
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (this.#fontA && this.#fontB) return;
|
if (this.#fontA && this.#fontB) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const fonts = fontStore.fonts;
|
const fonts = fontStore.fonts;
|
||||||
if (fonts.length >= 2) {
|
if (fonts.length >= 2) {
|
||||||
@@ -156,11 +166,19 @@ export class ComparisonStore {
|
|||||||
const fa = this.#fontA;
|
const fa = this.#fontA;
|
||||||
const fb = this.#fontB;
|
const fb = this.#fontB;
|
||||||
const w = typographySettingsStore.weight;
|
const w = typographySettingsStore.weight;
|
||||||
if (fa) appliedFontsManager.pin(fa.id, w, fa.features?.isVariable);
|
if (fa) {
|
||||||
if (fb) appliedFontsManager.pin(fb.id, w, fb.features?.isVariable);
|
appliedFontsManager.pin(fa.id, w, fa.features?.isVariable);
|
||||||
|
}
|
||||||
|
if (fb) {
|
||||||
|
appliedFontsManager.pin(fb.id, w, fb.features?.isVariable);
|
||||||
|
}
|
||||||
return () => {
|
return () => {
|
||||||
if (fa) appliedFontsManager.unpin(fa.id, w, fa.features?.isVariable);
|
if (fa) {
|
||||||
if (fb) appliedFontsManager.unpin(fb.id, w, fb.features?.isVariable);
|
appliedFontsManager.unpin(fa.id, w, fa.features?.isVariable);
|
||||||
|
}
|
||||||
|
if (fb) {
|
||||||
|
appliedFontsManager.unpin(fb.id, w, fb.features?.isVariable);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -183,7 +201,9 @@ export class ComparisonStore {
|
|||||||
const fontAName = this.#fontA?.name;
|
const fontAName = this.#fontA?.name;
|
||||||
const fontBName = this.#fontB?.name;
|
const fontBName = this.#fontB?.name;
|
||||||
|
|
||||||
if (!fontAName || !fontBName) return;
|
if (!fontAName || !fontBName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const fontAString = `${weight} ${size}px "${fontAName}"`;
|
const fontAString = `${weight} ${size}px "${fontAName}"`;
|
||||||
const fontBString = `${weight} ${size}px "${fontBName}"`;
|
const fontBString = `${weight} ${size}px "${fontBName}"`;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { typographySettingsStore } from '$features/SetupFont';
|
import { typographySettingsStore } from '$features/SetupFont';
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
import clsx from 'clsx';
|
||||||
import { comparisonStore } from '../../model';
|
import { comparisonStore } from '../../model';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -35,7 +35,9 @@ const displayChar = $derived(char === ' ' ? '\u00A0' : char);
|
|||||||
const targetFont = $derived(isPast ? fontA?.name ?? '' : fontB?.name ?? '');
|
const targetFont = $derived(isPast ? fontA?.name ?? '' : fontB?.name ?? '');
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!targetFont || slotFonts[slot] === targetFont) return;
|
if (!targetFont || slotFonts[slot] === targetFont) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const next = slot === 0 ? 1 : 0;
|
const next = slot === 0 ? 1 : 0;
|
||||||
slotFonts[next] = targetFont;
|
slotFonts[next] = targetFont;
|
||||||
slot = next;
|
slot = next;
|
||||||
@@ -50,7 +52,7 @@ $effect(() => {
|
|||||||
>
|
>
|
||||||
{#each [0, 1] as s (s)}
|
{#each [0, 1] as s (s)}
|
||||||
<span
|
<span
|
||||||
class={cn(
|
class={clsx(
|
||||||
'char-inner',
|
'char-inner',
|
||||||
'transition-colors duration-300',
|
'transition-colors duration-300',
|
||||||
isPast
|
isPast
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ThemeSwitch } from '$features/ChangeAppTheme';
|
import { ThemeSwitch } from '$features/ChangeAppTheme';
|
||||||
import type { ResponsiveManager } from '$shared/lib';
|
import type { ResponsiveManager } from '$shared/lib';
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Divider,
|
Divider,
|
||||||
@@ -17,6 +16,7 @@ import {
|
|||||||
} from '$shared/ui';
|
} from '$shared/ui';
|
||||||
import PanelLeftClose from '@lucide/svelte/icons/panel-left-close';
|
import PanelLeftClose from '@lucide/svelte/icons/panel-left-close';
|
||||||
import PanelLeftOpen from '@lucide/svelte/icons/panel-left-open';
|
import PanelLeftOpen from '@lucide/svelte/icons/panel-left-open';
|
||||||
|
import clsx from 'clsx';
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import { comparisonStore } from '../../model';
|
import { comparisonStore } from '../../model';
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ const fontBName = $derived(comparisonStore.fontB?.name ?? '');
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header
|
<header
|
||||||
class={cn(
|
class={clsx(
|
||||||
'flex items-center justify-between',
|
'flex items-center justify-between',
|
||||||
'px-4 md:px-8 py-4 md:py-6',
|
'px-4 md:px-8 py-4 md:py-6',
|
||||||
'h-16 md:h-20 z-20',
|
'h-16 md:h-20 z-20',
|
||||||
|
|||||||
@@ -5,12 +5,12 @@
|
|||||||
Content (font list, controls) is injected via snippets.
|
Content (font list, controls) is injected via snippets.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
|
||||||
import {
|
import {
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
Label,
|
Label,
|
||||||
ToggleButton,
|
ToggleButton,
|
||||||
} from '$shared/ui';
|
} from '$shared/ui';
|
||||||
|
import clsx from 'clsx';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import {
|
import {
|
||||||
type Side,
|
type Side,
|
||||||
@@ -40,7 +40,7 @@ let {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class={cn(
|
class={clsx(
|
||||||
'flex flex-col h-full',
|
'flex flex-col h-full',
|
||||||
'w-80',
|
'w-80',
|
||||||
'bg-surface dark:bg-dark-bg',
|
'bg-surface dark:bg-dark-bg',
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
CharacterComparisonEngine,
|
CharacterComparisonEngine,
|
||||||
} from '$shared/lib/helpers/CharacterComparisonEngine/CharacterComparisonEngine.svelte';
|
} from '$shared/lib/helpers/CharacterComparisonEngine/CharacterComparisonEngine.svelte';
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
|
||||||
import { Loader } from '$shared/ui';
|
import { Loader } from '$shared/ui';
|
||||||
|
import clsx from 'clsx';
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import { Spring } from 'svelte/motion';
|
import { Spring } from 'svelte/motion';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
@@ -65,7 +65,9 @@ const sliderSpring = new Spring(50, {
|
|||||||
const sliderPos = $derived(sliderSpring.current);
|
const sliderPos = $derived(sliderSpring.current);
|
||||||
|
|
||||||
function handleMove(e: PointerEvent) {
|
function handleMove(e: PointerEvent) {
|
||||||
if (!isDragging || !container) return;
|
if (!isDragging || !container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const rect = container.getBoundingClientRect();
|
const rect = container.getBoundingClientRect();
|
||||||
const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
|
const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
|
||||||
const percentage = (x / rect.width) * 100;
|
const percentage = (x / rect.width) * 100;
|
||||||
@@ -87,7 +89,9 @@ $effect(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!responsive) return;
|
if (!responsive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case responsive.isMobile:
|
case responsive.isMobile:
|
||||||
typography.multiplier = 0.5;
|
typography.multiplier = 0.5;
|
||||||
@@ -143,7 +147,9 @@ $effect(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
if (container && fontA && fontB) {
|
if (container && fontA && fontB) {
|
||||||
const width = container.offsetWidth;
|
const width = container.offsetWidth;
|
||||||
@@ -180,10 +186,10 @@ const scaleClass = $derived(
|
|||||||
Outer flex container — fills parent.
|
Outer flex container — fills parent.
|
||||||
The paper div inside scales down when the sidebar opens on desktop.
|
The paper div inside scales down when the sidebar opens on desktop.
|
||||||
-->
|
-->
|
||||||
<div class={cn('flex-1 relative flex items-center justify-center p-0 overflow-hidden bg-surface dark:bg-dark-bg', className)}>
|
<div class={clsx('flex-1 relative flex items-center justify-center p-0 overflow-hidden bg-surface dark:bg-dark-bg', className)}>
|
||||||
<!-- Paper surface -->
|
<!-- Paper surface -->
|
||||||
<div
|
<div
|
||||||
class={cn(
|
class={clsx(
|
||||||
'w-full h-full flex flex-col items-center justify-center relative',
|
'w-full h-full flex flex-col items-center justify-center relative',
|
||||||
'bg-paper dark:bg-dark-card',
|
'bg-paper dark:bg-dark-card',
|
||||||
'shadow-2xl shadow-black/5 dark:shadow-black/20',
|
'shadow-2xl shadow-black/5 dark:shadow-black/20',
|
||||||
@@ -247,7 +253,7 @@ const scaleClass = $derived(
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TypographyMenu
|
<TypographyMenu
|
||||||
class={cn(
|
class={clsx(
|
||||||
'absolute bottom-4 sm:bottom-5 right-4 sm:left-1/2 sm:right-[unset] sm:-translate-x-1/2 z-50',
|
'absolute bottom-4 sm:bottom-5 right-4 sm:left-1/2 sm:right-[unset] sm:-translate-x-1/2 z-50',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
1px red vertical rule with square handles at top and bottom.
|
1px red vertical rule with square handles at top and bottom.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
import clsx from 'clsx';
|
||||||
import { cubicOut } from 'svelte/easing';
|
import { cubicOut } from 'svelte/easing';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ let { sliderPos, isDragging }: Props = $props();
|
|||||||
>
|
>
|
||||||
<!-- Top handle -->
|
<!-- Top handle -->
|
||||||
<div
|
<div
|
||||||
class={cn(
|
class={clsx(
|
||||||
'w-5 h-5 md:w-6 md:h-6',
|
'w-5 h-5 md:w-6 md:h-6',
|
||||||
'-ml-2.5 md:-ml-3',
|
'-ml-2.5 md:-ml-3',
|
||||||
'mt-2 md:mt-4',
|
'mt-2 md:mt-4',
|
||||||
@@ -47,7 +47,7 @@ let { sliderPos, isDragging }: Props = $props();
|
|||||||
|
|
||||||
<!-- Bottom handle -->
|
<!-- Bottom handle -->
|
||||||
<div
|
<div
|
||||||
class={cn(
|
class={clsx(
|
||||||
'w-5 h-5 md:w-6 md:h-6',
|
'w-5 h-5 md:w-6 md:h-6',
|
||||||
'-ml-2.5 md:-ml-3',
|
'-ml-2.5 md:-ml-3',
|
||||||
'mb-2 md:mb-4',
|
'mb-2 md:mb-4',
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { NavigationWrapper } from '$entities/Breadcrumb';
|
import { NavigationWrapper } from '$entities/Breadcrumb';
|
||||||
import type { ResponsiveManager } from '$shared/lib';
|
import type { ResponsiveManager } from '$shared/lib';
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
|
||||||
import { Section } from '$shared/ui';
|
import { Section } from '$shared/ui';
|
||||||
|
import clsx from 'clsx';
|
||||||
import {
|
import {
|
||||||
getContext,
|
getContext,
|
||||||
untrack,
|
untrack,
|
||||||
@@ -38,7 +38,7 @@ $effect(() => {
|
|||||||
headerAction={registerAction}
|
headerAction={registerAction}
|
||||||
>
|
>
|
||||||
{#snippet content({ className })}
|
{#snippet content({ className })}
|
||||||
<div class={cn(className, !responsive.isDesktopLarge && 'col-start-0 col-span-2')}>
|
<div class={clsx(className, !responsive.isDesktopLarge && 'col-start-0 col-span-2')}>
|
||||||
<FontSearch bind:showFilters={isExpanded} />
|
<FontSearch bind:showFilters={isExpanded} />
|
||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|||||||
@@ -46,7 +46,9 @@ let isAboveMiddle = $state(false);
|
|||||||
let containerWidth = $state(0);
|
let containerWidth = $state(0);
|
||||||
|
|
||||||
const checkPosition = throttle(() => {
|
const checkPosition = throttle(() => {
|
||||||
if (!wrapper) return;
|
if (!wrapper) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const rect = wrapper.getBoundingClientRect();
|
const rect = wrapper.getBoundingClientRect();
|
||||||
const viewportMiddle = innerHeight / 2;
|
const viewportMiddle = innerHeight / 2;
|
||||||
|
|||||||
@@ -6,11 +6,11 @@
|
|||||||
import { NavigationWrapper } from '$entities/Breadcrumb';
|
import { NavigationWrapper } from '$entities/Breadcrumb';
|
||||||
import { fontStore } from '$entities/Font';
|
import { fontStore } from '$entities/Font';
|
||||||
import type { ResponsiveManager } from '$shared/lib';
|
import type { ResponsiveManager } from '$shared/lib';
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
|
||||||
import {
|
import {
|
||||||
Label,
|
Label,
|
||||||
Section,
|
Section,
|
||||||
} from '$shared/ui';
|
} from '$shared/ui';
|
||||||
|
import clsx from 'clsx';
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import { layoutManager } from '../../model';
|
import { layoutManager } from '../../model';
|
||||||
import LayoutSwitch from '../LayoutSwitch/LayoutSwitch.svelte';
|
import LayoutSwitch from '../LayoutSwitch/LayoutSwitch.svelte';
|
||||||
@@ -50,7 +50,7 @@ const responsive = getContext<ResponsiveManager>('responsive');
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
{#snippet content({ className })}
|
{#snippet content({ className })}
|
||||||
<div class={cn(className, !responsive.isDesktopLarge && 'col-start-0 col-span-2')}>
|
<div class={clsx(className, !responsive.isDesktopLarge && 'col-start-0 col-span-2')}>
|
||||||
<SampleList />
|
<SampleList />
|
||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|||||||
@@ -43,7 +43,6 @@
|
|||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules",
|
"node_modules",
|
||||||
"dist",
|
"dist",
|
||||||
"src/shared/shadcn/**/*",
|
|
||||||
"node_modules/**/*"
|
"node_modules/**/*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ export default defineConfig({
|
|||||||
'dist',
|
'dist',
|
||||||
'e2e',
|
'e2e',
|
||||||
'.storybook',
|
'.storybook',
|
||||||
'src/shared/shadcn/**/*',
|
|
||||||
],
|
],
|
||||||
restoreMocks: true,
|
restoreMocks: true,
|
||||||
coverage: {
|
coverage: {
|
||||||
@@ -38,7 +37,6 @@ export default defineConfig({
|
|||||||
'**/*.spec.ts',
|
'**/*.spec.ts',
|
||||||
'**/*.d.ts',
|
'**/*.d.ts',
|
||||||
'**/*.stories.svelte',
|
'**/*.stories.svelte',
|
||||||
'src/shared/shadcn/**/*',
|
|
||||||
'vitest.config.ts',
|
'vitest.config.ts',
|
||||||
],
|
],
|
||||||
thresholds: {
|
thresholds: {
|
||||||
|
|||||||
Reference in New Issue
Block a user