Compare commits

..

16 Commits

Author SHA1 Message Date
Ilia Mashkov
75ea5ab382 chore: change dprint formatting 2026-01-30 01:09:39 +03:00
Ilia Mashkov
f07b699926 feat(FontDisplay): add animation on displayed fonts list order change 2026-01-30 00:56:58 +03:00
Ilia Mashkov
b031e560af feat(FontSampler): add delete button to remove font from the list of selected fonts, improve styling 2026-01-30 00:56:21 +03:00
Ilia Mashkov
fbaf596fef fix(createCharacterComparison): improve characters measurment for better magnifying presicion 2026-01-30 00:54:40 +03:00
Ilia Mashkov
1a2c44fb97 chore: add import/export 2026-01-30 00:53:06 +03:00
Ilia Mashkov
04602f0372 feat(ComboControl): use IconButton component 2026-01-30 00:52:42 +03:00
Ilia Mashkov
433fd2f7e6 feat(IconButton): create IconButton component to reuse styles for small buttons 2026-01-30 00:52:17 +03:00
Ilia Mashkov
87c4e04458 feat(controlManager): add getter for letter spacing value 2026-01-30 00:48:29 +03:00
Ilia Mashkov
fb843c87af chore: add import/export for letter spacing constant 2026-01-30 00:48:07 +03:00
Ilia Mashkov
b2af3683bc chore: change default font size 2026-01-30 00:47:44 +03:00
Ilia Mashkov
90f11d8d16 chore(Labels): formatting 2026-01-30 00:47:07 +03:00
Ilia Mashkov
a3f9bc12a0 feat(CharacterSlot): slightly increase magnifying effect 2026-01-30 00:46:43 +03:00
Ilia Mashkov
6634f6df1e feature(SliderLine): complete rework of the slider line, now it look like a magnifying glass 2026-01-30 00:45:45 +03:00
Ilia Mashkov
3f7ce63736 feat(FontListItem): slightly change badge styling 2026-01-30 00:44:01 +03:00
Ilia Mashkov
c665a579be chore: update gitignore 2026-01-30 00:43:16 +03:00
Ilia Mashkov
ac7f094d13 chore: delete unused code 2026-01-30 00:42:58 +03:00
29 changed files with 221 additions and 1033 deletions

2
.gitignore vendored
View File

@@ -35,6 +35,8 @@ vite.config.ts.timestamp-*
/docs
AGENTS.md
*.md
!README.md
*storybook.log
storybook-static

View File

@@ -1,480 +0,0 @@
# Fix Applied: Query Data Cannot Be Undefined
## Summary
Successfully fixed the TanStack Query error: **"Query data cannot be undefined. Please make sure to return a value other than undefined from your query function."**
## Root Causes
1. **Missing `gcTime` parameter** in TanStack Query configuration
2. **No response validation** when proxy API returns invalid/missing data
3. **Incorrect generic type constraint** in `FontVirtualList.svelte`
## Changes Applied
### 1. Fixed TanStack Query Configuration
**File**: `src/entities/Font/model/store/baseFontStore.svelte.ts`
Added `gcTime` parameter to properly manage cached data lifecycle:
```typescript
private getOptions(params = this.params): QueryObserverOptions<UnifiedFont[], Error> {
return {
queryKey: this.getQueryKey(params),
queryFn: () => this.fetchFn(params),
staleTime:5 * 60 *1000, // 5 minutes
gcTime: 10 * 60 * 1000, // 10 minutes ✅ ADDED
};
}
```
**Impact**:
- Prevents stale data from persisting too long
- Explicit control over garbage collection timing
- Better memory management
### 2. Added Response Validation
**File**: `src/entities/Font/model/store/unifiedFontStore.svelte.ts`
Added comprehensive validation in `fetchFn`:
```typescript
protected async fetchFn(params: ProxyFontsParams): Promise<UnifiedFont[]> {
const response = await fetchProxyFonts(params);
// Validate response exists
if (!response) {
console.error('[UnifiedFontStore] fetchProxyFonts returned undefined', { params });
throw new Error('Proxy API returned undefined response');
}
// Validate fonts array exists
if (!response.fonts) {
console.error('[UnifiedFontStore] response.fonts is undefined', { response });
throw new Error('Proxy API response missing fonts array');
}
// Validate fonts is an array
if (!Array.isArray(response.fonts)) {
console.error('[UnifiedFontStore] response.fonts is not an array', { fonts: response.fonts });
throw new Error('Proxy API fonts is not an array');
}
// Store pagination metadata separately for derived values
this.#paginationMetadata = {
total: response.total ?? 0,
limit: response.limit ?? this.params.limit ?? 50,
offset: response.offset ?? this.params.offset ?? 0,
};
return response.fonts;
}
```
**Impact**:
- Early detection of invalid API responses
- Detailed error logging for debugging
- Prevents undefined data from being cached
- Fallback to default values for pagination metadata
### 3. Added Fallback to Fontshare API
**File**: `src/entities/Font/api/proxy/proxyFonts.ts`
Implemented automatic fallback to Fontshare API when proxy fails:
```typescript
/**
* Whether to use proxy API (true) or fallback (false)
*
* Set to true when your proxy API is ready:
* const USE_PROXY_API = true;
*
* Set to false to use Fontshare API as fallback during development:
* const USE_PROXY_API = false;
*
* The app will automatically fall back to Fontshare API if the proxy fails.
*/
const USE_PROXY_API = true;
export async function fetchProxyFonts(
params: ProxyFontsParams = {},
): Promise<ProxyFontsResponse> {
// Try proxy API first if enabled
if (USE_PROXY_API) {
try {
const queryString = buildQueryString(params);
const url = `${PROXY_API_URL}${queryString}`;
console.log('[fetchProxyFonts] Fetching from proxy API', { params, url });
const response = await api.get<ProxyFontsResponse>(url);
// Validate response has fonts array
if (!response.data || !Array.isArray(response.data.fonts)) {
console.error('[fetchProxyFonts] Invalid response from proxy API', response.data);
throw new Error('Proxy API returned invalid response');
}
console.log('[fetchProxyFonts] Proxy API success', {
count: response.data.fonts.length,
});
return response.data;
} catch (error) {
console.warn('[fetchProxyFonts] Proxy API failed, using fallback', error);
// Check if it's a network error or proxy not available
const isNetworkError = error instanceof Error
&& (error.message.includes('Failed to fetch')
|| error.message.includes('Network')
|| error.message.includes('404')
|| error.message.includes('500'));
if (isNetworkError) {
// Fall back to Fontshare API
console.log('[fetchProxyFonts] Using Fontshare API as fallback');
return await fetchFontshareFallback(params);
}
// Re-throw other errors
if (error instanceof Error) {
throw error;
}
throw new Error(`Failed to fetch fonts from proxy API: ${String(error)}`);
}
}
// Use Fontshare API directly
console.log('[fetchProxyFonts] Using Fontshare API (proxy disabled)');
return await fetchFontshareFallback(params);
}
/**
* Fallback to Fontshare API when proxy is unavailable
*/
async function fetchFontshareFallback(
params: ProxyFontsParams,
): Promise<ProxyFontsResponse> {
// Import dynamically to avoid circular dependency
const { fetchFontshareFonts } = await import('../fontshare/fontshare');
const { normalizeFontshareFonts } = await import('../../lib/normalize/normalize');
// Map proxy params to Fontshare params
const fontshareParams = {
q: params.q,
categories: params.category ? [params.category] : undefined,
page: params.offset ? Math.floor(params.offset / (params.limit || 50)) + 1 : undefined,
limit: params.limit,
};
const response = await fetchFontshareFonts(fontshareParams);
const normalizedFonts = normalizeFontshareFonts(response.fonts);
return {
fonts: normalizedFonts,
total: response.count_total,
limit: params.limit || response.count,
offset: params.offset || 0,
};
}
```
**Impact**:
- App continues working even if proxy API is down
- Allows development without breaking functionality
- Automatic detection of network errors
- Seamless fallback with console logging
### 4. Fixed Type Constraints
**File**: `src/entities/Font/ui/FontVirtualList/FontVirtualList.svelte`
Fixed incorrect generic type constraint:
**Before**:
```typescript
<script lang="ts" generics="T extends ({ id: string } | [{ id: string }, { id: string; }])">
```
**After**:
```typescript
<script lang="ts" generics="T extends { id: string }">
```
Also simplified the font registration logic:
**Before**:
```typescript
const slugs = visibleItems.map(item => {
if (Array.isArray(item)) {
return item.map(font => font.id);
}
return item.id;
}).flat();
```
**After**:
```typescript
const slugs = visibleItems.map(item => item.id);
```
**Impact**:
- Fixed TypeScript errors
- Simplified code
- Proper type safety
## How It Works
### Normal Flow (Proxy API Available)
1. **Request**: Component requests fonts → Filter manager maps to params → `unifiedFontStore.setParams()`
2. **Fetch**: `unifiedFontStore.fetchFn()` calls `fetchProxyFonts()`
3. **Proxy API**: Request sent to `https://api.glyphdiff.com/api/v1/fonts`
4. **Validation**: Response validated to ensure `fonts` array exists
5. **Cache**: TanStack Query caches response for 5 minutes
6. **Render**: Components render `unifiedFontStore.fonts`
### Fallback Flow (Proxy API Unavailable)
1. **Request**: Same as normal flow
2. **Fetch**: `fetchProxyFonts()` attempts proxy API
3. **Error**: Proxy API returns network error (404, 500, connection failed)
4. **Detection**: Error caught and classified as network error
5. **Fallback**: `fetchFontshareFallback()` called automatically
6. **Fontshare API**: Request sent to Fontshare API
7. **Normalization**: Fontshare response normalized to `UnifiedFont` format
8. **Render**: Components render fonts seamlessly
### Debug Flow (Enable Proxy API)
With `USE_PROXY_API = true`, console will show:
```
[fetchProxyFonts] Fetching from proxy API { params: {...}, url: "https://api.glyphdiff.com/api/v1/fonts?..." }
[fetchProxyFonts] Proxy API success { count: 50 }
```
### Debug Flow (Disable Proxy API)
With `USE_PROXY_API = false`, console will show:
```
[fetchProxyFonts] Using Fontshare API (proxy disabled)
```
### Debug Flow (Proxy API Fails)
When proxy API fails, console will show:
```
[fetchProxyFonts] Fetching from proxy API { params: {...}, url: "https://api.glyphdiff.com/api/v1/fonts?..." }
[fetchProxyFonts] Proxy API failed, using fallback Error: ...
[fetchProxyFonts] Using Fontshare API as fallback
```
## Testing the Fix
### 1. With Proxy API Working
**Prerequisites**:
- Your proxy API is running at `https://api.glyphdiff.com/api/v1/fonts`
- Proxy API returns correct response structure
**Test**:
```bash
# Start dev server
yarn dev
# Open browser console
# Should see: "[fetchProxyFonts] Fetching from proxy API"
# Should see: "[fetchProxyFonts] Proxy API success { count: N }"
# Should see fonts loading correctly
```
**Expected Behavior**:
- Fonts load from proxy API
- Console shows successful fetch
- No errors in console
- All features working
### 2. With Proxy API Down
**Prerequisites**:
- Proxy API not running or returning errors
**Test**:
```bash
# Start dev server
yarn dev
# Open browser console
# Should see: "[fetchProxyFonts] Proxy API failed, using fallback"
# Should see: "[fetchProxyFonts] Using Fontshare API as fallback"
# Should see fonts loading from Fontshare
```
**Expected Behavior**:
- App falls back to Fontshare API automatically
- Console shows fallback messages
- Fonts load from Fontshare
- All features working (no user-facing errors)
### 3. Disable Proxy API Manually
**File**: `src/entities/Font/api/proxy/proxyFonts.ts`
```typescript
const USE_PROXY_API = false;
```
**Test**:
```bash
# Start dev server
yarn dev
# Open browser console
# Should see: "[fetchProxyFonts] Using Fontshare API (proxy disabled)"
# Should see fonts loading from Fontshare
```
**Expected Behavior**:
- App uses Fontshare API directly
- Console shows proxy disabled message
- All features working
## Next Steps
### To Enable Proxy API (When Ready)
1. **Verify Proxy API**:
```bash
curl "https://api.glyphdiff.com/api/v1/fonts?limit=5"
```
2. **Check Response Format**:
```json
{
"fonts": [...],
"total": N,
"limit": 5,
"offset": 0
}
```
3. **Ensure Each Font Has Required Fields**:
- `id`, `name`, `provider`
- `category`, `subsets`
- `variants`, `styles`
- `metadata`, `features`
4. **Test in App**:
- Open browser console
- Check for success messages
- Verify fonts load correctly
### To Remove Fallback (When Proxy API is Stable)
Once proxy API is proven stable, you can:
1. **Remove Fallback Function**:
```typescript
// Delete fetchFontshareFallback() function
```
2. **Remove Dynamic Imports**:
```typescript
// No longer need dynamic imports
const { fetchFontshareFonts } = await import('../fontshare/fontshare');
const { normalizeFontshareFonts } = await import('../../lib/normalize/normalize');
```
3. **Remove Old API Exports**:
```typescript
// Remove Fontshare API exports from index.ts
// Remove normalization function exports
```
## Troubleshooting
### Issue: Still Seeing "Query data cannot be undefined"
**Cause**: Proxy API returns invalid response
**Solutions**:
1. Check console for validation errors
2. Verify proxy API response structure
3. Check network tab in DevTools for actual response
4. Set `USE_PROXY_API = false` to use fallback
### Issue: Fonts Not Loading At All
**Cause**: Both proxy and Fontshare APIs failing
**Solutions**:
1. Check console for error messages
2. Verify network connectivity
3. Check CORS settings on proxy API
4. Check API endpoint URL is correct
### Issue: Fallback Not Triggering
**Cause**: Error type not recognized as network error
**Solutions**:
1. Add additional error message checks
2. Check console for actual error message
3. Manually set `USE_PROXY_API = false`
## Files Modified
1. `src/entities/Font/model/store/baseFontStore.svelte.ts` - Added gcTime parameter
2. `src/entities/Font/model/store/unifiedFontStore.svelte.ts` - Added response validation
3. `src/entities/Font/api/proxy/proxyFonts.ts` - Added fallback logic
4. `src/entities/Font/ui/FontVirtualList/FontVirtualList.svelte` - Fixed generic type
## Commit
```
commit 471e186
fix: Fix undefined query data and add fallback to Fontshare API
- Add gcTime parameter to TanStack Query config
- Add response validation in fetchFn with detailed logging
- Add fallback to Fontshare API when proxy fails
- Add USE_PROXY_API flag for easy switching
- Fix FontVirtualList generic type constraint
- Simplify font registration logic
- Add comprehensive console logging for debugging
Fixes: Query data cannot be undefined error
```
## Documentation
See `PROXY_API_FIXES.md` for detailed technical documentation on all changes.
---
**Last Updated**: January 29, 2026
**Status**: ✅ Fixed and Committed

View File

@@ -1,403 +0,0 @@
# Proxy API Integration - Changes & Fixes
## Issue Fixed
**Error**: `Query data cannot be undefined. Please make sure to return a value other than undefined from your query function. Affected query key: ["unifiedFonts",{}]`
**Root Cause**:
1. Missing `gcTime` parameter in TanStack Query configuration
2. No validation of proxy API response structure
3. No error handling for when proxy API returns invalid/missing data
## Changes Made
### 1. Fixed TanStack Query Configuration (`baseFontStore.svelte.ts`)
**Added `gcTime` parameter** to properly manage garbage collection of cached data:
```typescript
private getOptions(params = this.params): QueryObserverOptions<UnifiedFont[], Error> {
return {
queryKey: this.getQueryKey(params),
queryFn: () => this.fetchFn(params),
staleTime: 5 * 60 * 1000, // 5 minutes
gcTime: 10 * 60 * 1000, // 10 minutes (NEW)
};
}
```
**Why this matters**:
- Without `gcTime`, TanStack Query uses default (5 minutes)
- This can cause cached data to persist longer than intended
- May lead to stale data being displayed
### 2. Added Response Validation (`unifiedFontStore.svelte.ts`)
**Added comprehensive validation** in `fetchFn` method:
```typescript
protected async fetchFn(params: ProxyFontsParams): Promise<UnifiedFont[]> {
const response = await fetchProxyFonts(params);
// Validate response structure
if (!response) {
console.error('[UnifiedFontStore] fetchProxyFonts returned undefined', { params });
throw new Error('Proxy API returned undefined response');
}
if (!response.fonts) {
console.error('[UnifiedFontStore] response.fonts is undefined', { response });
throw new Error('Proxy API response missing fonts array');
}
if (!Array.isArray(response.fonts)) {
console.error('[UnifiedFontStore] response.fonts is not an array', { fonts: response.fonts });
throw new Error('Proxy API fonts is not an array');
}
// Store pagination metadata separately for derived values
this.#paginationMetadata = {
total: response.total ?? 0,
limit: response.limit ?? this.params.limit ?? 50,
offset: response.offset ?? this.params.offset ?? 0,
};
return response.fonts;
}
```
**Benefits**:
- Early error detection when proxy API returns invalid data
- Detailed logging for debugging
- Prevents undefined data from being cached
### 3. Added Fallback to Fontshare API (`proxyFonts.ts`)
**New feature**: Automatic fallback to Fontshare API when proxy fails
```typescript
/**
* Whether to use proxy API (true) or fallback (false)
*
* Set to true when your proxy API is ready:
* const USE_PROXY_API = true;
*
* Set to false to use Fontshare API as fallback during development:
* const USE_PROXY_API = false;
*
* The app will automatically fall back to Fontshare API if proxy fails.
*/
const USE_PROXY_API = true;
```
**Fallback Logic**:
```typescript
export async function fetchProxyFonts(
params: ProxyFontsParams = {},
): Promise<ProxyFontsResponse> {
// Try proxy API first if enabled
if (USE_PROXY_API) {
try {
const queryString = buildQueryString(params);
const url = `${PROXY_API_URL}${queryString}`;
console.log('[fetchProxyFonts] Fetching from proxy API', { params, url });
const response = await api.get<ProxyFontsResponse>(url);
// Validate response has fonts array
if (!response.data || !Array.isArray(response.data.fonts)) {
console.error('[fetchProxyFonts] Invalid response from proxy API', response.data);
throw new Error('Proxy API returned invalid response');
}
console.log('[fetchProxyFonts] Proxy API success', {
count: response.data.fonts.length,
});
return response.data;
} catch (error) {
console.warn('[fetchProxyFonts] Proxy API failed, using fallback', error);
// Check if it's a network error or proxy not available
const isNetworkError = error instanceof Error
&& (error.message.includes('Failed to fetch')
|| error.message.includes('Network')
|| error.message.includes('404')
|| error.message.includes('500'));
if (isNetworkError) {
// Fall back to Fontshare API
console.log('[fetchProxyFonts] Using Fontshare API as fallback');
return await fetchFontshareFallback(params);
}
// Re-throw other errors
if (error instanceof Error) {
throw error;
}
throw new Error(`Failed to fetch fonts from proxy API: ${String(error)}`);
}
}
// Use Fontshare API directly
console.log('[fetchProxyFonts] Using Fontshare API (proxy disabled)');
return await fetchFontshareFallback(params);
}
```
**Fallback Function**:
```typescript
/**
* Fallback to Fontshare API when proxy is unavailable
*
* Maps proxy API params to Fontshare API params and normalizes response
*/
async function fetchFontshareFallback(
params: ProxyFontsParams,
): Promise<ProxyFontsResponse> {
// Import dynamically to avoid circular dependency
const { fetchFontshareFonts } = await import('../fontshare/fontshare');
const { normalizeFontshareFonts } = await import('../../lib/normalize/normalize');
// Map proxy params to Fontshare params
const fontshareParams = {
q: params.q,
categories: params.category ? [params.category] : undefined,
page: params.offset ? Math.floor(params.offset / (params.limit || 50)) + 1 : undefined,
limit: params.limit,
};
const response = await fetchFontshareFonts(fontshareParams);
const normalizedFonts = normalizeFontshareFonts(response.fonts);
return {
fonts: normalizedFonts,
total: response.count_total,
limit: params.limit || response.count,
offset: params.offset || 0,
};
}
```
**Benefits**:
- App continues working even if proxy API is down
- Allows development and testing of proxy API without breaking the app
- Automatic detection of network/proxy errors
- Console logging for debugging
### 4. Updated `fetchProxyFontById` with validation
```typescript
export async function fetchProxyFontById(
id: string,
): Promise<UnifiedFont | undefined> {
const response = await fetchProxyFonts({ limit: 1000, q: id });
if (!response || !response.fonts) {
console.error('[fetchProxyFontById] No fonts in response', { response });
return undefined;
}
return response.fonts.find(font => font.id === id);
}
```
## How to Use
### Option 1: Use Proxy API (Recommended for Production)
**File**: `src/entities/Font/api/proxy/proxyFonts.ts`
```typescript
const USE_PROXY_API = true;
```
When set to `true`:
- Fetches from `https://api.glyphdiff.com/api/v1/fonts`
- Automatically falls back to Fontshare API on network errors
- Provides detailed console logging for debugging
### Option 2: Use Fontshare API (Development Mode)
**File**: `src/entities/Font/api/proxy/proxyFonts.ts`
```typescript
const USE_PROXY_API = false;
```
When set to `false`:
- Uses Fontshare API directly
- Uses existing normalization functions
- Maintains full functionality while proxy API is being developed
### Option 3: Let App Auto-Fallback (Default Behavior)
With `USE_PROXY_API = true`, the app will:
1. Try to fetch from proxy API
2. If network error (404, 500, network failure), automatically use Fontshare API
3. Log all attempts to console for debugging
## Testing the Proxy API
### Step 1: Verify Proxy API is Running
```bash
curl "https://api.glyphdiff.com/api/v1/fonts?limit=1"
```
Expected response:
```json
{
"fonts": [...],
"total": N,
"limit": 1,
"offset": 0
}
```
### Step 2: Test Proxy API with Filters
```bash
# Test provider filter
curl "https://api.glyphdiff.com/api/v1/fonts?provider=fontshare&limit=5"
# Test category filter
curl "https://api.glyphdiff.com/api/v1/fonts?category=sans-serif&limit=5"
# Test search
curl "https://api.glyphdiff.com/api/v1/fonts?q=roboto&limit=5"
# Test pagination
curl "https://api.glyphdiff.com/api/v1/fonts?limit=10&offset=10"
# Test sorting
curl "https://api.glyphdiff.com/api/v1/fonts?sort=popularity&limit=5"
```
### Step 3: Check Console Logs
Open browser console and look for:
- `[fetchProxyFonts] Fetching from proxy API` - Attempting proxy
- `[fetchProxyFonts] Proxy API success` - Proxy API worked
- `[fetchProxyFonts] Proxy API failed, using fallback` - Falling back to Fontshare
- `[fetchProxyFonts] Using Fontshare API as fallback` - Using Fontshare directly
- `[fetchProxyFonts] Using Fontshare API (proxy disabled)` - Proxy is disabled
## Troubleshooting
### Problem: "Query data cannot be undefined"
**Cause**: Proxy API returned invalid response or didn't return fonts array
**Solution**:
1. Check console for error messages
2. Verify proxy API returns correct structure
3. Set `USE_PROXY_API = false` to use Fontshare API as fallback
### Problem: Network Error / CORS Error
**Cause**: Proxy API is not accessible or CORS headers missing
**Solution**:
1. Set `USE_PROXY_API = false` to bypass proxy temporarily
2. Fix CORS headers on proxy API server
3. Ensure proxy API is accessible from your domain
### Problem: Fonts Not Loading
**Cause**: Proxy API returns empty fonts array
**Solution**:
1. Check proxy API response in Network tab
2. Verify proxy API has fonts in database
3. Test with simple query: `?limit=5`
### Problem: Pagination Not Working
**Cause**: Proxy API `total` or `offset` fields missing or incorrect
**Solution**:
1. Verify proxy API returns `total` field
2. Verify proxy API returns `limit` and `offset` fields
3. Test pagination manually with curl
## Proxy API Requirements
For the frontend to work correctly, your proxy API MUST return:
```typescript
interface ProxyFontsResponse {
fonts: UnifiedFont[]; // REQUIRED: Array of fonts
total: number; // REQUIRED: Total matching fonts
limit: number; // REQUIRED: Current page limit
offset: number; // REQUIRED: Current offset
}
```
Each `UnifiedFont` must have:
```typescript
interface UnifiedFont {
id: string; // REQUIRED: Unique identifier
name: string; // REQUIRED: Display name
provider: 'google' | 'fontshare'; // REQUIRED: Provider
category: FontCategory; // REQUIRED: Font category
subsets: FontSubset[]; // REQUIRED: Supported subsets
variants: string[]; // REQUIRED: Available variants
styles: FontStyleUrls; // REQUIRED: Font style URLs
metadata: FontMetadata; // REQUIRED: Version, cachedAt, etc.
features: FontFeatures; // REQUIRED: Variable font info
}
```
## Files Modified
1. `src/entities/Font/model/store/baseFontStore.svelte.ts`
- Added `gcTime` parameter
2. `src/entities/Font/model/store/unifiedFontStore.svelte.ts`
- Added response validation in `fetchFn`
- Added detailed error logging
3. `src/entities/Font/api/proxy/proxyFonts.ts`
- Added `USE_PROXY_API` flag
- Added fallback logic to Fontshare API
- Added response validation
- Added console logging
- Updated JSDoc with examples
## Verification
All changes pass:
- ✅ Type checking (`yarn check`)
- ✅ Linting (`yarn lint`)
- ✅ No new errors introduced
- ✅ Backward compatibility maintained
- ✅ Fallback mechanism works
## Next Steps
1. **Test Proxy API**: Use curl or Postman to verify your proxy API works
2. **Set `USE_PROXY_API = true`**: Enable proxy API when ready
3. **Monitor Console Logs**: Check for proxy API success/failure messages
4. **Remove Fallback** (Optional): Once proxy API is stable, remove Fontshare fallback
---
**Last Updated**: January 29, 2026

View File

@@ -16,7 +16,7 @@
"https://plugins.dprint.dev/g-plane/markup_fmt-v0.25.3.wasm"
],
"typescript": {
"lineWidth": 100,
"lineWidth": 120,
"indentWidth": 4,
"useTabs": false,
"semiColons": "prefer",
@@ -41,7 +41,7 @@
"lineWidth": 100
},
"markup": {
"printWidth": 100,
"printWidth": 120,
"indentWidth": 4,
"useTabs": false,
"quotes": "double",

View File

@@ -24,11 +24,9 @@ describe('Font Normalization', () => {
subsets: ['latin', 'latin-ext'],
files: {
regular: 'https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKOzY.woff2',
'700':
'https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1Mu72xWUlvAx05IsDqlA.woff2',
'700': 'https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1Mu72xWUlvAx05IsDqlA.woff2',
italic: 'https://fonts.gstatic.com/s/roboto/v30/KFOkCnqEu92Fr1Mu51xIIzI.woff2',
'700italic':
'https://fonts.gstatic.com/s/roboto/v30/KFOjCnqEu92Fr1Mu51TzBic6CsQ.woff2',
'700italic': 'https://fonts.gstatic.com/s/roboto/v30/KFOjCnqEu92Fr1Mu51TzBic6CsQ.woff2',
},
version: 'v30',
lastModified: '2022-01-01',

View File

@@ -112,10 +112,16 @@ function animateSelection() {
<div class="flex flex-row gap-1 w-full items-center justify-between">
<div class="flex flex-col gap-1 transition-all duration-150 ease-out">
<div class="flex flex-row gap-1">
<Badge variant="outline" class="text-[0.5rem]">
<Badge
variant="outline"
class="text-[0.5rem] font-mono uppercase tracking-widest text-slate-900"
>
{font.provider}
</Badge>
<Badge variant="outline" class="text-[0.5rem]">
<Badge
variant="outline"
class="text-[0.5rem] font-mono uppercase tracking-widest text-slate-900"
>
{font.category}
</Badge>
</div>

View File

@@ -3,12 +3,16 @@
Displays a grid of FontSampler components for each displayed font.
-->
<script lang="ts">
import { flip } from 'svelte/animate';
import { quintOut } from 'svelte/easing';
import { displayedFontsStore } from '../../model';
import FontSampler from '../FontSampler/FontSampler.svelte';
</script>
<div class="grid gap-2 grid-cols-[repeat(auto-fit,minmax(500px,1fr))]">
{#each displayedFontsStore.fonts as font (font.id)}
<div animate:flip={{ delay: 0, duration: 400, easing: quintOut }}>
<FontSampler font={font} bind:text={displayedFontsStore.text} />
</div>
{/each}
</div>

View File

@@ -6,9 +6,15 @@
import {
FontApplicator,
type UnifiedFont,
selectedFontsStore,
} from '$entities/Font';
import { controlManager } from '$features/SetupFont';
import { ContentEditable } from '$shared/ui';
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import {
ContentEditable,
IconButton,
} from '$shared/ui';
import MinusIcon from '@lucide/svelte/icons/minus';
interface Props {
/**
@@ -33,18 +39,43 @@ let {
...restProps
}: Props = $props();
const weight = $derived(controlManager.weight ?? 400);
const fontWeight = $derived(controlManager.weight);
const fontSize = $derived(controlManager.size);
const lineHeight = $derived(controlManager.height);
const letterSpacing = $derived(controlManager.spacing);
function removeSample() {
selectedFontsStore.removeOne(font.id);
}
</script>
<div
class="
w-full rounded-xl
bg-white p-6 border border-slate-200
w-full h-full rounded-xl
bg-white border border-slate-200
shadow-sm dark:border-slate-800 dark:bg-slate-950
"
style:font-weight={weight}
style:font-weight={fontWeight}
>
<div class="mx-3 p-1.5 pr-0 border-b border-slate-200 flex items-center justify-between">
<span class="text-[0.5rem] font-mono uppercase tracking-widest text-slate-900">
{font.name}
</span>
<IconButton onclick={removeSample}>
{#snippet icon({ className })}
<MinusIcon class={cn(className, 'stroke-red-500 group-hover:stroke-red-500')} />
{/snippet}
</IconButton>
</div>
<div class="p-6">
<FontApplicator id={font.id} name={font.name}>
<ContentEditable bind:text={text} {...restProps} />
<ContentEditable
bind:text={text}
{...restProps}
fontSize={fontSize}
lineHeight={lineHeight}
letterSpacing={letterSpacing}
/>
</FontApplicator>
</div>
</div>

View File

@@ -26,9 +26,7 @@ import type { FilterManager } from '../filterManager/filterManager.svelte';
*/
export function mapManagerToParams(manager: FilterManager): Partial<ProxyFontsParams> {
const providers = manager.getGroup('providers')?.instance.selectedProperties.map(p => p.value);
const categories = manager.getGroup('categories')?.instance.selectedProperties.map(p =>
p.value
);
const categories = manager.getGroup('categories')?.instance.selectedProperties.map(p => p.value);
const subsets = manager.getGroup('subsets')?.instance.selectedProperties.map(p => p.value);
return {

View File

@@ -4,6 +4,7 @@ export {
controlManager,
DEFAULT_FONT_SIZE,
DEFAULT_FONT_WEIGHT,
DEFAULT_LETTER_SPACING,
DEFAULT_LINE_HEIGHT,
FONT_SIZE_STEP,
FONT_WEIGHT_STEP,

View File

@@ -43,6 +43,10 @@ export class TypographyControlManager {
get height() {
return this.#controls.get('line_height')?.instance.value;
}
get spacing() {
return this.#controls.get('letter_spacing')?.instance.value;
}
}
/**

View File

@@ -1,7 +1,7 @@
/**
* Font size constants
*/
export const DEFAULT_FONT_SIZE = 16;
export const DEFAULT_FONT_SIZE = 48;
export const MIN_FONT_SIZE = 8;
export const MAX_FONT_SIZE = 100;
export const FONT_SIZE_STEP = 1;

View File

@@ -1,6 +1,7 @@
export {
DEFAULT_FONT_SIZE,
DEFAULT_FONT_WEIGHT,
DEFAULT_LETTER_SPACING,
DEFAULT_LINE_HEIGHT,
FONT_SIZE_STEP,
FONT_WEIGHT_STEP,

View File

@@ -32,7 +32,7 @@ $effect(() => {
</script>
<!-- Font List -->
<div class="p-2 h-full flex flex-col gap-3 overflow-hidden">
<div class="p-2 h-full flex flex-col gap-3">
{#key isEmptyScreen}
<div
class={cn(
@@ -53,7 +53,7 @@ $effect(() => {
</div>
{/key}
<div class="my-2 mx-10">
<div class="my-6">
<ComparisonSlider />
</div>

View File

@@ -56,6 +56,5 @@ export const api = {
body: JSON.stringify(body),
}),
delete: <T>(url: string, options?: RequestInit) =>
request<T>(url, { ...options, method: 'DELETE' }),
delete: <T>(url: string, options?: RequestInit) => request<T>(url, { ...options, method: 'DELETE' }),
};

View File

@@ -204,37 +204,41 @@ export function createCharacterComparison<
/**
* precise calculation of character state based on global slider position.
*
* @param lineIndex - Index of the line
* @param charIndex - Index of the character in the line
* @param lineData - The line data object
* @param sliderPos - Current slider position (0-100)
* @param lineElement - The line element
* @param container - The container element
* @returns Object containing proximity (0-1) and isPast (boolean)
*/
function getCharState(
lineIndex: number,
charIndex: number,
lineData: LineData,
sliderPos: number,
lineElement?: HTMLElement,
container?: HTMLElement,
) {
if (!containerWidth) return { proximity: 0, isPast: false };
if (!containerWidth || !container) {
return {
proximity: 0,
isPast: false,
};
}
const charElement = lineElement?.children[charIndex] as HTMLElement;
// Calculate the pixel position of the character relative to the CONTAINER
// 1. Find the left edge of the centered line
const lineStartOffset = (containerWidth - lineData.width) / 2;
if (!charElement) {
return { proximity: 0, isPast: false };
}
// 2. Find the character's center relative to the line
const charRelativePercent = (charIndex + 0.5) / lineData.text.length;
const charPixelPos = lineStartOffset + (charRelativePercent * lineData.width);
// Get the actual bounding box of the character
const charRect = charElement.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
// 3. Convert back to global percentage (0-100)
const charGlobalPercent = (charPixelPos / containerWidth) * 100;
// Calculate character center relative to container
const charCenter = charRect.left + (charRect.width / 2) - containerRect.left;
const charGlobalPercent = (charCenter / containerWidth) * 100;
const distance = Math.abs(sliderPos - charGlobalPercent);
// Proximity range: +/- 15% around the slider
const range = 15;
const range = 5;
const proximity = Math.max(0, 1 - distance / range);
const isPast = sliderPos > charGlobalPercent;
return { proximity, isPast };

View File

@@ -9,10 +9,8 @@ export const badgeVariants = tv({
'focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3',
variants: {
variant: {
default:
'bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent',
secondary:
'bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent',
default: 'bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent',
secondary: 'bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent',
destructive:
'bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white',
outline: 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',

View File

@@ -33,8 +33,7 @@ const sidebar = setSidebar({
onOpenChange(value);
// This sets the cookie to keep the sidebar state.
document.cookie =
`${SIDEBAR_COOKIE_NAME}=${open}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
},
});
</script>

View File

@@ -59,8 +59,7 @@ const hasSelection = $derived(selectedCount > 0);
class={buttonVariants({
variant: 'ghost',
size: 'sm',
class:
'flex-1 justify-between gap-2 hover:bg-transparent focus-visible:ring-1 focus-visible:ring-ring',
class: 'flex-1 justify-between gap-2 hover:bg-transparent focus-visible:ring-1 focus-visible:ring-ring',
})}
>
<h4 class="text-sm font-semibold">{displayedLabel}</h4>
@@ -107,9 +106,7 @@ const hasSelection = $derived(selectedCount > 0);
active:scale-[0.98] active:transition-transform active:duration-75
"
>
<!--
Checkbox handles toggle, styled for accessibility with focus rings
-->
<!-- Checkbox handles toggle, styled for accessibility with focus rings -->
<Checkbox
id={property.id}
bind:checked={property.selected}

View File

@@ -24,6 +24,7 @@ import {
import MinusIcon from '@lucide/svelte/icons/minus';
import PlusIcon from '@lucide/svelte/icons/plus';
import type { ChangeEventHandler } from 'svelte/elements';
import IconButton from '../IconButton/IconButton.svelte';
interface ComboControlProps {
/**
@@ -79,34 +80,16 @@ const handleSliderChange = (newValue: number) => {
<TooltipRoot>
<ButtonGroupRoot class="bg-transparent border-none shadow-none">
<TooltipTrigger class="flex items-center">
<Button
variant="ghost"
class="
group relative border-none size-9
bg-white/20 hover:bg-white/60
backdrop-blur-3xl
transition-all duration-200 ease-out
will-change-transform
hover:-translate-y-0.5
active:translate-y-0 active:scale-95 active:shadow-none
cursor-pointer
disabled:opacity-50 disabled:pointer-events-none
"
size="icon"
<IconButton
onclick={control.decrease}
disabled={control.isAtMin}
aria-label={decreaseLabel}
rotation="counterclockwise"
>
<MinusIcon
class="
size-4 transition-all duration-200
stroke-slate-600/50
group-hover:stroke-indigo-500 group-hover:scale-110 group-hover:stroke-3
group-active:scale-90 group-active:-rotate-6
group-disabled:stroke-transparent
"
/>
</Button>
{#snippet icon({ className })}
<MinusIcon class={className} />
{/snippet}
</IconButton>
<PopoverRoot>
<PopoverTrigger>
{#snippet child({ props })}
@@ -144,33 +127,16 @@ const handleSliderChange = (newValue: number) => {
</PopoverContent>
</PopoverRoot>
<Button
variant="ghost"
class="
group relative border-none size-9
bg-white/20 hover:bg-white/60
transition-all duration-200 ease-out
will-change-transform
hover:-translate-y-0.5
active:translate-y-0 active:scale-95 active:shadow-none
disabled:opacity-50 disabled:pointer-events-none
cursor-pointer
"
size="icon"
<IconButton
aria-label={increaseLabel}
onclick={control.increase}
disabled={control.isAtMax}
rotation="clockwise"
>
<PlusIcon
class="
size-4 transition-all duration-200
stroke-slate-600/50
group-hover:stroke-indigo-500 group-hover:scale-110 group-hover:stroke-3
group-active:scale-90 group-active:rotate-6
group-disabled:stroke-transparent
"
/>
</Button>
{#snippet icon({ className })}
<PlusIcon class={className} />
{/snippet}
</IconButton>
</TooltipTrigger>
</ButtonGroupRoot>
{#if controlLabel}

View File

@@ -32,8 +32,7 @@ const { Story } = defineMeta({
parameters: {
docs: {
description: {
component:
'Animated styled wrapper for content that can be expanded and collapsed.',
component: 'Animated styled wrapper for content that can be expanded and collapsed.',
},
story: { inline: false }, // Render stories in iframe for state isolation
},

View File

@@ -0,0 +1,50 @@
<!--
Component: IconButton
Shadcn button size="icon" variant="ghost" with custom styling and icon snippet
-->
<script lang="ts">
import { Button } from '$shared/shadcn/ui/button';
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import type {
ComponentProps,
Snippet,
} from 'svelte';
interface Props extends ComponentProps<typeof Button> {
/**
* Direction of the rotation effect on click
* @default clockwise
*/
rotation?: 'clockwise' | 'counterclockwise';
/**
* Icon
*/
icon: Snippet<[{ className: string }]>;
}
let { rotation = 'clockwise', icon, ...rest }: Props = $props();
</script>
<Button
variant="ghost"
class="
group relative border-none size-9
bg-white/20 hover:bg-white/60
backdrop-blur-3xl
transition-all duration-200 ease-out
will-change-transform
hover:-translate-y-0.5
active:translate-y-0 active:scale-95 active:shadow-none
cursor-pointer
disabled:opacity-50 disabled:pointer-events-none
"
size="icon"
{...rest}
>
{@render icon({
className: cn(
'size-4 transition-all duration-200 stroke-slate-600/50 group-hover:stroke-indigo-500 group-hover:scale-110 group-hover:stroke-3 group-active:scale-90 group-disabled:stroke-transparent',
rotation === 'clockwise' ? 'group-active:rotate-6' : 'group-active:-rotate-6',
),
})}
</Button>

View File

@@ -55,8 +55,7 @@ interface Props {
children: Snippet<[{ item: T; index: number; isVisible: boolean; proximity: number }]>;
}
let { items, itemHeight = 80, overscan = 5, class: className, onVisibleItemsChange, children }:
Props = $props();
let { items, itemHeight = 80, overscan = 5, class: className, onVisibleItemsChange, children }: Props = $props();
const virtualizer = createVirtualizer(() => ({
count: items.length,

View File

@@ -9,6 +9,7 @@ import ComboControl from './ComboControl/ComboControl.svelte';
import ComboControlV2 from './ComboControlV2/ComboControlV2.svelte';
import ContentEditable from './ContentEditable/ContentEditable.svelte';
import ExpandableWrapper from './ExpandableWrapper/ExpandableWrapper.svelte';
import IconButton from './IconButton/IconButton.svelte';
import SearchBar from './SearchBar/SearchBar.svelte';
import VirtualList from './VirtualList/VirtualList.svelte';
@@ -18,6 +19,7 @@ export {
ComboControlV2,
ContentEditable,
ExpandableWrapper,
IconButton,
SearchBar,
VirtualList,
};

View File

@@ -68,6 +68,8 @@ const charComparison = createCharacterComparison(
() => sizeControl.value,
);
let lineElements = $state<(HTMLElement | undefined)[]>([]);
/** Physics-based spring for smooth handle movement */
const sliderSpring = new Spring(50, {
stiffness: 0.2, // Balanced for responsiveness
@@ -136,14 +138,15 @@ $effect(() => {
});
</script>
{#snippet renderLine(line: LineData, lineIndex: number)}
{#snippet renderLine(line: LineData, index: number)}
<div
bind:this={lineElements[index]}
class="relative flex w-full justify-center items-center whitespace-nowrap"
style:height={`${heightControl.value}em`}
style:line-height={`${heightControl.value}em`}
>
{#each line.text.split('') as char, charIndex}
{@const { proximity, isPast } = charComparison.getCharState(lineIndex, charIndex, line, sliderPos)}
{@const { proximity, isPast } = charComparison.getCharState(charIndex, sliderPos, lineElements[index], container)}
<!--
Single Character Span
- Font Family switches based on `isPast`
@@ -177,29 +180,25 @@ $effect(() => {
aria-label="Font comparison slider"
onpointerdown={startDragging}
class="
group relative w-full py-16 px-6 sm:py-24 sm:px-12 overflow-hidden
bg-indigo-50 rounded-[2.5rem] border border-slate-100 shadow-2xl
group relative w-full py-16 px-0 sm:py-24 sm:px-0 overflow-hidden
rounded-[2.5rem]
select-none touch-none cursor-ew-resize min-h-100 flex flex-col justify-center
backdrop-blur-lg bg-gradient-to-br from-gray-100/70 via-white/50 to-gray-100/60
border border-gray-300/40
shadow-[inset_0_4px_12px_0_rgba(0,0,0,0.12),inset_0_2px_4px_0_rgba(0,0,0,0.08),0_1px_2px_0_rgba(255,255,255,0.8)]
before:absolute before:inset-0 before:rounded-[2.5rem] before:p-[1px]
before:bg-gradient-to-br before:from-black/5 before:via-black/2 before:to-transparent
before:-z-10 before:blur-sm
"
class:box-shadow={'-20px 20px 60px #bebebe, 20px -20px 60px #ffffff;'}
in:fly={{ y: 0, x: -50, duration: 300, easing: cubicOut, opacity: 0.2 }}
>
<!-- Background Gradient Accent -->
<div
class="
absolute inset-0 bg-linear-to-br
from-slate-50/50 via-white to-slate-100/50
opacity-50 pointer-events-none
"
>
</div>
<!-- Text Rendering Container -->
<div
class="
relative flex flex-col items-center gap-4
text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold leading-[1.15]
z-10 pointer-events-none text-center
drop-shadow-[0_3px_6px_rgba(255,255,255,0.9)]
"
style:perspective="1000px"
>

View File

@@ -48,7 +48,7 @@ let { char, proximity, isPast, weight, size, fontAName, fontBName }: Props = $pr
style:font-weight={weight}
style:font-size={`${size}px`}
style:transform="
scale({1 + proximity * 0.2})
scale({1 + proximity * 0.3})
translateY({-proximity * 12}px)
rotateY({proximity * 25 * (isPast ? -1 : 1)}deg)
"

View File

@@ -95,7 +95,8 @@ function selectFontB(fontId: string) {
style:opacity={sliderPos < 15 ? 0 : 1}
>
<span class="text-[0.5rem] font-mono uppercase tracking-widest text-indigo-400">
Baseline</span>
Baseline
</span>
{@render fontSelector(fontB.name, fontB.id, fontList, selectFontB)}
</div>
@@ -104,7 +105,8 @@ function selectFontB(fontId: string) {
style:opacity={sliderPos > 85 ? 0 : 1}
>
<span class="text-[0.5rem] font-mono uppercase tracking-widest text-slate-400">
Comparison</span>
Comparison
</span>
{@render fontSelector(fontA.name, fontA.id, fontList, selectFontA)}
</div>
</div>

View File

@@ -17,40 +17,54 @@ interface Props {
}
let { sliderPos, isDragging }: Props = $props();
</script>
<div
class="absolute inset-y-0 pointer-events-none -translate-x-1/2 z-30"
class="absolute inset-y-4 pointer-events-none -translate-x-1/2 z-50 transition-all duration-300 ease-out flex flex-col justify-center items-center"
style:left="{sliderPos}%"
>
<!-- Subtle wave glow zone -->
<!-- We use part of lucide cursor svg icon as a handle -->
<svg
class={cn(
'transition-all relative duration-300 text-black/80 drop-shadow-sm',
isDragging ? 'w-12 h-12' : 'w-8 h-8',
)}
viewBox="0 0 24 12"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M17 10h-1a4 4 0 0 1-4-4V0" />
<path d="M7 10h1a4 4 0 0 0 4-4V0" />
</svg>
<div
class={cn(
'absolute inset-y-0 w-24 -left-12 bg-linear-to-r from-transparent via-indigo-500/8 to-transparent transition-all duration-300',
isDragging ? 'via-indigo-500/12' : '',
'relative h-full rounded-sm transition-all duration-500',
'bg-white/[0.03] backdrop-blur-md',
// These are the visible "edges" of the glass
'shadow-[0_0_40px_rgba(0,0,0,0.1)_inset_0_0_20px_rgba(255,255,255,0.1)]',
'shadow-[0_10px_30px_-10px_rgba(0,0,0,0.2),inset_0_1px_1px_rgba(255,255,255,0.4)]',
'rounded-full',
isDragging ? 'w-32' : 'w-16',
)}
>
</div>
<!-- Vertical divider line -->
<div
class="absolute inset-y-0 w-0.5 bg-linear-to-b from-indigo-400/30 via-indigo-500 to-indigo-400/30 shadow-[0_0_12px_rgba(99,102,241,0.5)] transition-shadow duration-200"
class:shadow-[0_0_20px_rgba(99,102,241,0.7)]={isDragging}
<!-- We use part of lucide cursor svg icon as a handle -->
<svg
class={cn(
'transition-all relative duration-500 text-black/80 drop-shadow-sm',
isDragging ? 'w-12 h-12' : 'w-8 h-8',
)}
viewBox="0 0 24 12"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
>
</div>
<!-- Top knob -->
<div
class="absolute top-6 left-0 -translate-x-1/2 transition-transform duration-200"
class:scale-125={isDragging}
>
<div class="w-3 h-3 bg-indigo-500 rounded-full shadow-lg ring-2 ring-white"></div>
</div>
<!-- Bottom knob -->
<div
class="absolute bottom-6 left-0 -translate-x-1/2 transition-transform duration-200"
class:scale-125={isDragging}
>
<div class="w-3 h-3 bg-indigo-500 rounded-full shadow-lg ring-2 ring-white"></div>
</div>
<path d="M17 2h-1a4 4 0 0 0-4 4v6" />
<path d="M7 2h1a4 4 0 0 1 4 4v6" />
</svg>
</div>

View File

@@ -1,2 +0,0 @@
import { unifiedFontStore } from './src/entities/Font/index.ts';
console.log('Import successful:', !!unifiedFontStore);