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 /docs
AGENTS.md AGENTS.md
*.md
!README.md
*storybook.log *storybook.log
storybook-static 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" "https://plugins.dprint.dev/g-plane/markup_fmt-v0.25.3.wasm"
], ],
"typescript": { "typescript": {
"lineWidth": 100, "lineWidth": 120,
"indentWidth": 4, "indentWidth": 4,
"useTabs": false, "useTabs": false,
"semiColons": "prefer", "semiColons": "prefer",
@@ -41,7 +41,7 @@
"lineWidth": 100 "lineWidth": 100
}, },
"markup": { "markup": {
"printWidth": 100, "printWidth": 120,
"indentWidth": 4, "indentWidth": 4,
"useTabs": false, "useTabs": false,
"quotes": "double", "quotes": "double",

View File

@@ -24,11 +24,9 @@ describe('Font Normalization', () => {
subsets: ['latin', 'latin-ext'], subsets: ['latin', 'latin-ext'],
files: { files: {
regular: 'https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKOzY.woff2', regular: 'https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKOzY.woff2',
'700': '700': 'https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1Mu72xWUlvAx05IsDqlA.woff2',
'https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1Mu72xWUlvAx05IsDqlA.woff2',
italic: 'https://fonts.gstatic.com/s/roboto/v30/KFOkCnqEu92Fr1Mu51xIIzI.woff2', italic: 'https://fonts.gstatic.com/s/roboto/v30/KFOkCnqEu92Fr1Mu51xIIzI.woff2',
'700italic': '700italic': 'https://fonts.gstatic.com/s/roboto/v30/KFOjCnqEu92Fr1Mu51TzBic6CsQ.woff2',
'https://fonts.gstatic.com/s/roboto/v30/KFOjCnqEu92Fr1Mu51TzBic6CsQ.woff2',
}, },
version: 'v30', version: 'v30',
lastModified: '2022-01-01', 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-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-col gap-1 transition-all duration-150 ease-out">
<div class="flex flex-row gap-1"> <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} {font.provider}
</Badge> </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} {font.category}
</Badge> </Badge>
</div> </div>

View File

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

View File

@@ -6,9 +6,15 @@
import { import {
FontApplicator, FontApplicator,
type UnifiedFont, type UnifiedFont,
selectedFontsStore,
} from '$entities/Font'; } from '$entities/Font';
import { controlManager } from '$features/SetupFont'; 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 { interface Props {
/** /**
@@ -33,18 +39,43 @@ let {
...restProps ...restProps
}: Props = $props(); }: 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> </script>
<div <div
class=" class="
w-full rounded-xl w-full h-full rounded-xl
bg-white p-6 border border-slate-200 bg-white border border-slate-200
shadow-sm dark:border-slate-800 dark:bg-slate-950 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}> <FontApplicator id={font.id} name={font.name}>
<ContentEditable bind:text={text} {...restProps} /> <ContentEditable
bind:text={text}
{...restProps}
fontSize={fontSize}
lineHeight={lineHeight}
letterSpacing={letterSpacing}
/>
</FontApplicator> </FontApplicator>
</div> </div>
</div>

View File

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

View File

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

View File

@@ -43,6 +43,10 @@ export class TypographyControlManager {
get height() { get height() {
return this.#controls.get('line_height')?.instance.value; 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 * Font size constants
*/ */
export const DEFAULT_FONT_SIZE = 16; export const DEFAULT_FONT_SIZE = 48;
export const MIN_FONT_SIZE = 8; export const MIN_FONT_SIZE = 8;
export const MAX_FONT_SIZE = 100; export const MAX_FONT_SIZE = 100;
export const FONT_SIZE_STEP = 1; export const FONT_SIZE_STEP = 1;

View File

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

View File

@@ -32,7 +32,7 @@ $effect(() => {
</script> </script>
<!-- Font List --> <!-- 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} {#key isEmptyScreen}
<div <div
class={cn( class={cn(
@@ -53,7 +53,7 @@ $effect(() => {
</div> </div>
{/key} {/key}
<div class="my-2 mx-10"> <div class="my-6">
<ComparisonSlider /> <ComparisonSlider />
</div> </div>

View File

@@ -56,6 +56,5 @@ export const api = {
body: JSON.stringify(body), body: JSON.stringify(body),
}), }),
delete: <T>(url: string, options?: RequestInit) => delete: <T>(url: string, options?: RequestInit) => request<T>(url, { ...options, method: 'DELETE' }),
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. * 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 charIndex - Index of the character in the line
* @param lineData - The line data object
* @param sliderPos - Current slider position (0-100) * @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) * @returns Object containing proximity (0-1) and isPast (boolean)
*/ */
function getCharState( function getCharState(
lineIndex: number,
charIndex: number, charIndex: number,
lineData: LineData,
sliderPos: number, 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 if (!charElement) {
// 1. Find the left edge of the centered line return { proximity: 0, isPast: false };
const lineStartOffset = (containerWidth - lineData.width) / 2; }
// 2. Find the character's center relative to the line // Get the actual bounding box of the character
const charRelativePercent = (charIndex + 0.5) / lineData.text.length; const charRect = charElement.getBoundingClientRect();
const charPixelPos = lineStartOffset + (charRelativePercent * lineData.width); const containerRect = container.getBoundingClientRect();
// 3. Convert back to global percentage (0-100) // Calculate character center relative to container
const charGlobalPercent = (charPixelPos / containerWidth) * 100; const charCenter = charRect.left + (charRect.width / 2) - containerRect.left;
const charGlobalPercent = (charCenter / containerWidth) * 100;
const distance = Math.abs(sliderPos - charGlobalPercent); const distance = Math.abs(sliderPos - charGlobalPercent);
const range = 5;
// Proximity range: +/- 15% around the slider
const range = 15;
const proximity = Math.max(0, 1 - distance / range); const proximity = Math.max(0, 1 - distance / range);
const isPast = sliderPos > charGlobalPercent; const isPast = sliderPos > charGlobalPercent;
return { proximity, isPast }; 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', '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: { variants: {
variant: { variant: {
default: default: 'bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent',
'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',
secondary:
'bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent',
destructive: 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', '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', outline: 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',

View File

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

View File

@@ -59,8 +59,7 @@ const hasSelection = $derived(selectedCount > 0);
class={buttonVariants({ class={buttonVariants({
variant: 'ghost', variant: 'ghost',
size: 'sm', size: 'sm',
class: class: 'flex-1 justify-between gap-2 hover:bg-transparent focus-visible:ring-1 focus-visible:ring-ring',
'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> <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 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 <Checkbox
id={property.id} id={property.id}
bind:checked={property.selected} bind:checked={property.selected}

View File

@@ -24,6 +24,7 @@ import {
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 type { ChangeEventHandler } from 'svelte/elements'; import type { ChangeEventHandler } from 'svelte/elements';
import IconButton from '../IconButton/IconButton.svelte';
interface ComboControlProps { interface ComboControlProps {
/** /**
@@ -79,34 +80,16 @@ const handleSliderChange = (newValue: number) => {
<TooltipRoot> <TooltipRoot>
<ButtonGroupRoot class="bg-transparent border-none shadow-none"> <ButtonGroupRoot class="bg-transparent border-none shadow-none">
<TooltipTrigger class="flex items-center"> <TooltipTrigger class="flex items-center">
<Button <IconButton
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"
onclick={control.decrease} onclick={control.decrease}
disabled={control.isAtMin} disabled={control.isAtMin}
aria-label={decreaseLabel} aria-label={decreaseLabel}
rotation="counterclockwise"
> >
<MinusIcon {#snippet icon({ className })}
class=" <MinusIcon class={className} />
size-4 transition-all duration-200 {/snippet}
stroke-slate-600/50 </IconButton>
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>
<PopoverRoot> <PopoverRoot>
<PopoverTrigger> <PopoverTrigger>
{#snippet child({ props })} {#snippet child({ props })}
@@ -144,33 +127,16 @@ const handleSliderChange = (newValue: number) => {
</PopoverContent> </PopoverContent>
</PopoverRoot> </PopoverRoot>
<Button <IconButton
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"
aria-label={increaseLabel} aria-label={increaseLabel}
onclick={control.increase} onclick={control.increase}
disabled={control.isAtMax} disabled={control.isAtMax}
rotation="clockwise"
> >
<PlusIcon {#snippet icon({ className })}
class=" <PlusIcon class={className} />
size-4 transition-all duration-200 {/snippet}
stroke-slate-600/50 </IconButton>
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>
</TooltipTrigger> </TooltipTrigger>
</ButtonGroupRoot> </ButtonGroupRoot>
{#if controlLabel} {#if controlLabel}

View File

@@ -32,8 +32,7 @@ const { Story } = defineMeta({
parameters: { parameters: {
docs: { docs: {
description: { description: {
component: component: 'Animated styled wrapper for content that can be expanded and collapsed.',
'Animated styled wrapper for content that can be expanded and collapsed.',
}, },
story: { inline: false }, // Render stories in iframe for state isolation 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 }]>; children: Snippet<[{ item: T; index: number; isVisible: boolean; proximity: number }]>;
} }
let { items, itemHeight = 80, overscan = 5, class: className, onVisibleItemsChange, children }: let { items, itemHeight = 80, overscan = 5, class: className, onVisibleItemsChange, children }: Props = $props();
Props = $props();
const virtualizer = createVirtualizer(() => ({ const virtualizer = createVirtualizer(() => ({
count: items.length, count: items.length,

View File

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

View File

@@ -68,6 +68,8 @@ const charComparison = createCharacterComparison(
() => sizeControl.value, () => sizeControl.value,
); );
let lineElements = $state<(HTMLElement | undefined)[]>([]);
/** Physics-based spring for smooth handle movement */ /** Physics-based spring for smooth handle movement */
const sliderSpring = new Spring(50, { const sliderSpring = new Spring(50, {
stiffness: 0.2, // Balanced for responsiveness stiffness: 0.2, // Balanced for responsiveness
@@ -136,14 +138,15 @@ $effect(() => {
}); });
</script> </script>
{#snippet renderLine(line: LineData, lineIndex: number)} {#snippet renderLine(line: LineData, index: number)}
<div <div
bind:this={lineElements[index]}
class="relative flex w-full justify-center items-center whitespace-nowrap" class="relative flex w-full justify-center items-center whitespace-nowrap"
style:height={`${heightControl.value}em`} style:height={`${heightControl.value}em`}
style:line-height={`${heightControl.value}em`} style:line-height={`${heightControl.value}em`}
> >
{#each line.text.split('') as char, charIndex} {#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 Single Character Span
- Font Family switches based on `isPast` - Font Family switches based on `isPast`
@@ -177,29 +180,25 @@ $effect(() => {
aria-label="Font comparison slider" aria-label="Font comparison slider"
onpointerdown={startDragging} onpointerdown={startDragging}
class=" class="
group relative w-full py-16 px-6 sm:py-24 sm:px-12 overflow-hidden group relative w-full py-16 px-0 sm:py-24 sm:px-0 overflow-hidden
bg-indigo-50 rounded-[2.5rem] border border-slate-100 shadow-2xl rounded-[2.5rem]
select-none touch-none cursor-ew-resize min-h-100 flex flex-col justify-center 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 }} 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 --> <!-- Text Rendering Container -->
<div <div
class=" class="
relative flex flex-col items-center gap-4 relative flex flex-col items-center gap-4
text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold leading-[1.15] text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold leading-[1.15]
z-10 pointer-events-none text-center z-10 pointer-events-none text-center
drop-shadow-[0_3px_6px_rgba(255,255,255,0.9)]
" "
style:perspective="1000px" 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-weight={weight}
style:font-size={`${size}px`} style:font-size={`${size}px`}
style:transform=" style:transform="
scale({1 + proximity * 0.2}) scale({1 + proximity * 0.3})
translateY({-proximity * 12}px) translateY({-proximity * 12}px)
rotateY({proximity * 25 * (isPast ? -1 : 1)}deg) rotateY({proximity * 25 * (isPast ? -1 : 1)}deg)
" "

View File

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

View File

@@ -17,40 +17,54 @@ interface Props {
} }
let { sliderPos, isDragging }: Props = $props(); let { sliderPos, isDragging }: Props = $props();
</script> </script>
<div <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}%" 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 <div
class={cn( 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', 'relative h-full rounded-sm transition-all duration-500',
isDragging ? 'via-indigo-500/12' : '', '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> </div>
<!-- Vertical divider line --> <!-- We use part of lucide cursor svg icon as a handle -->
<div <svg
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={cn(
class:shadow-[0_0_20px_rgba(99,102,241,0.7)]={isDragging} '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> <path d="M17 2h-1a4 4 0 0 0-4 4v6" />
<path d="M7 2h1a4 4 0 0 1 4 4v6" />
<!-- Top knob --> </svg>
<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>
</div> </div>

View File

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