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
|
/docs
|
||||||
AGENTS.md
|
AGENTS.md
|
||||||
|
*.md
|
||||||
|
!README.md
|
||||||
|
|
||||||
*storybook.log
|
*storybook.log
|
||||||
storybook-static
|
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"
|
"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",
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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' }),
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
},
|
},
|
||||||
|
|||||||
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 }]>;
|
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,
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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)
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
import { unifiedFontStore } from './src/entities/Font/index.ts';
|
|
||||||
console.log('Import successful:', !!unifiedFontStore);
|
|
||||||
Reference in New Issue
Block a user