Compare commits
16 Commits
c06aad1a8a
...
75ea5ab382
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75ea5ab382 | ||
|
|
f07b699926 | ||
|
|
b031e560af | ||
|
|
fbaf596fef | ||
|
|
1a2c44fb97 | ||
|
|
04602f0372 | ||
|
|
433fd2f7e6 | ||
|
|
87c4e04458 | ||
|
|
fb843c87af | ||
|
|
b2af3683bc | ||
|
|
90f11d8d16 | ||
|
|
a3f9bc12a0 | ||
|
|
6634f6df1e | ||
|
|
3f7ce63736 | ||
|
|
c665a579be | ||
|
|
ac7f094d13 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -35,6 +35,8 @@ vite.config.ts.timestamp-*
|
||||
|
||||
/docs
|
||||
AGENTS.md
|
||||
*.md
|
||||
!README.md
|
||||
|
||||
*storybook.log
|
||||
storybook-static
|
||||
|
||||
480
FIX_SUMMARY.md
480
FIX_SUMMARY.md
@@ -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
|
||||
@@ -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
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}
|
||||
<FontSampler font={font} bind:text={displayedFontsStore.text} />
|
||||
<div animate:flip={{ delay: 0, duration: 400, easing: quintOut }}>
|
||||
<FontSampler font={font} bind:text={displayedFontsStore.text} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
<FontApplicator id={font.id} name={font.name}>
|
||||
<ContentEditable bind:text={text} {...restProps} />
|
||||
</FontApplicator>
|
||||
<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}
|
||||
fontSize={fontSize}
|
||||
lineHeight={lineHeight}
|
||||
letterSpacing={letterSpacing}
|
||||
/>
|
||||
</FontApplicator>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -4,6 +4,7 @@ export {
|
||||
controlManager,
|
||||
DEFAULT_FONT_SIZE,
|
||||
DEFAULT_FONT_WEIGHT,
|
||||
DEFAULT_LETTER_SPACING,
|
||||
DEFAULT_LINE_HEIGHT,
|
||||
FONT_SIZE_STEP,
|
||||
FONT_WEIGHT_STEP,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export {
|
||||
DEFAULT_FONT_SIZE,
|
||||
DEFAULT_FONT_WEIGHT,
|
||||
DEFAULT_LETTER_SPACING,
|
||||
DEFAULT_LINE_HEIGHT,
|
||||
FONT_SIZE_STEP,
|
||||
FONT_WEIGHT_STEP,
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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' }),
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
50
src/shared/ui/IconButton/IconButton.svelte
Normal file
50
src/shared/ui/IconButton/IconButton.svelte
Normal 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>
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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)
|
||||
"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
import { unifiedFontStore } from './src/entities/Font/index.ts';
|
||||
console.log('Import successful:', !!unifiedFontStore);
|
||||
Reference in New Issue
Block a user