Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
<p><strong>High-quality image optimization with responsive, provider-first and SSR-friendly workflow for React/Next.js, Angular, and Svelte/SvelteKit.</strong></p>

<p>
<a href="https://www.npmjs.com/package/@desource/image"><img src="https://img.shields.io/npm/v/@desource/image?logo=npm" alt="@desource/image"></a>
<a href="https://www.npmjs.com/package/@desource/image-angular"><img src="https://img.shields.io/npm/v/@desource/image-angular?logo=angular&logoColor=white" alt="@desource/image-angular"></a>
<a href="https://www.npmjs.com/package/@desource/image-react"><img src="https://img.shields.io/npm/v/@desource/image-react?logo=react" alt="@desource/image-react"></a>
<a href="https://www.npmjs.com/package/@desource/image-svelte"><img src="https://img.shields.io/npm/v/@desource/image-svelte?logo=svelte" alt="@desource/image-svelte"></a>
<a href="https://www.npmjs.com/package/@desource/image"><img src="https://img.shields.io/npm/v/@desource/image?color=blue&logo=typescript" alt="@desource/image"></a>
<a href="https://www.npmjs.com/package/@desource/image-angular"><img src="https://img.shields.io/npm/v/@desource/image-angular?color=blue&logo=angular&logoColor=white" alt="@desource/image-angular"></a>
<a href="https://www.npmjs.com/package/@desource/image-react"><img src="https://img.shields.io/npm/v/@desource/image-react?color=blue&logo=react" alt="@desource/image-react"></a>
<a href="https://www.npmjs.com/package/@desource/image-svelte"><img src="https://img.shields.io/npm/v/@desource/image-svelte?color=blue&logo=svelte" alt="@desource/image-svelte"></a>
<br />
<a href="https://codecov.io/gh/DeSource-Labs/image"><img src="https://codecov.io/gh/DeSource-Labs/image/branch/main/graph/badge.svg" alt="Coverage"></a>
<a href="https://sonarcloud.io/summary/new_code?id=DeSource-Labs_image"><img src="https://sonarcloud.io/api/project_badges/measure?project=DeSource-Labs_image&metric=alert_status" alt="SonarCloud"></a>
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT license"></a>
Expand Down
59 changes: 59 additions & 0 deletions demo/src/lib/home/ProvidersSection.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@
<Gallery images={providerImages} />
</div>
<p class="gallery-hint">Drag to explore. Select a provider for setup, options, and examples.</p>
<a class="custom-guide" href={resolve('/providers/custom')}>
<span>
<strong>Build a custom provider</strong>
<small>Define typed options, URL mapping, source rules, and tests.</small>
</span>
<span aria-hidden="true">Read guide →</span>
</a>
<details class="provider-directory animated-details">
<summary>Browse all {providers.length} providers</summary>
<ul>
Expand Down Expand Up @@ -56,6 +63,52 @@
font-size: 0.85rem;
}

.custom-guide {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
margin-top: 28px;
padding: 20px 22px;
border: 1px solid rgba(191, 244, 139, 0.22);
border-radius: 12px;
color: #ecf5ff;
background: linear-gradient(110deg, rgba(191, 244, 139, 0.08), rgba(143, 184, 255, 0.04));
text-decoration: none;
transition:
border-color 160ms ease,
background 160ms ease,
transform 160ms ease;

> span:first-child {
display: grid;
gap: 6px;
}

strong {
font-size: 0.95rem;
}

small {
color: var(--muted);
font-size: 0.82rem;
line-height: 1.5;
}

> span:last-child {
flex-shrink: 0;
color: var(--lime);
font-size: 0.8rem;
font-weight: 750;
}

&:hover {
border-color: rgba(191, 244, 139, 0.42);
background: linear-gradient(110deg, rgba(191, 244, 139, 0.12), rgba(143, 184, 255, 0.07));
transform: translateY(-1px);
}
}

.provider-directory {
margin-top: 28px;
padding: 20px 0;
Expand Down Expand Up @@ -118,5 +171,11 @@
.providers {
padding-block: 100px;
}

.custom-guide {
align-items: flex-start;
flex-direction: column;
gap: 14px;
}
}
</style>
1 change: 1 addition & 0 deletions demo/src/routes/providers/[provider]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<details class="animated-details">
<summary>Choose a provider</summary>
<ul>
<li><a href={resolve('/providers/custom')}>Custom provider</a></li>
{#each providers as provider (provider.id)}
<li>
<a
Expand Down
157 changes: 157 additions & 0 deletions demo/src/routes/providers/custom/+page.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import type { PageServerLoad } from './$types';

const provider = `import {
appendQuery,
configureProvider,
defineProvider,
type ImageModifiers,
type ProviderOptionsOf
} from '@desource/image';

interface AcmeModifiers extends ImageModifiers {
sharpen?: number;
}

interface AcmeOptions {
baseURL?: string;
modifiers?: Partial<AcmeModifiers>;
}

const providerSetup = defineProvider<AcmeOptions>({
validateDomains: true,
getImage(src, { baseURL = 'https://images.acme.test/transform', modifiers }) {
return {
url: appendQuery(baseURL, {
src,
w: modifiers.width,
h: modifiers.height,
q: modifiers.quality,
fm: modifiers.format === 'jpeg' ? 'jpg' : modifiers.format,
fit: modifiers.fit,
sharpen: modifiers.sharpen
}),
isOptimized: true
};
}
});

export type AcmeProviderOptions = Partial<ProviderOptionsOf<typeof providerSetup>>;

export function acmeProvider(options: AcmeProviderOptions = {}) {
return configureProvider(providerSetup, options, 'acme');
}`;

const configuration = `import type { ImageConfig } from '@desource/image';
import { acmeProvider } from './acme-provider';

export const imageConfig = {
provider: 'acme',
domains: ['assets.example.com'],
providers: {
acme: acmeProvider({
baseURL: 'https://images.acme.test/transform',
modifiers: { quality: 75 }
})
}
} satisfies ImageConfig;`;

const examples = {
core: `import { createImage } from '@desource/image';
import { imageConfig } from './image.config';

const image = createImage(imageConfig);

export const heroURL = image('https://assets.example.com/photo.jpg', {
width: 960,
height: 640,
format: 'webp',
sharpen: 2
});`,
svelte: `<script lang="ts">
import { DsImage, setDsImageConfig } from '@desource/image-svelte';
import { imageConfig } from './image.config';

setDsImageConfig(imageConfig);
</script>

<DsImage
src="https://assets.example.com/photo.jpg"
alt="Mountain ridge at sunrise"
width={960}
height={640}
format="webp"
modifiers={{ sharpen: 2 }}
/>`,
react: `import { DsImage, DsImageProvider } from '@desource/image-react';
import { imageConfig } from './image.config';

export function HeroImage() {
return (
<DsImageProvider config={imageConfig}>
<DsImage
src="https://assets.example.com/photo.jpg"
alt="Mountain ridge at sunrise"
width={960}
height={640}
format="webp"
modifiers={{ sharpen: 2 }}
/>
</DsImageProvider>
);
}`,
angular: `import { Component } from '@angular/core';
import { DsImageComponent, provideDsImage } from '@desource/image-angular';
import { imageConfig } from './image.config';

@Component({
selector: 'app-hero-image',
imports: [DsImageComponent],
providers: [provideDsImage(imageConfig)],
template: \`
<ds-image
src="https://assets.example.com/photo.jpg"
alt="Mountain ridge at sunrise"
[width]="960"
[height]="640"
format="webp"
[modifiers]="{ sharpen: 2 }"
/>
\`
})
export class HeroImageComponent {}`
} as const;

const test = `import { describe, expect, it } from 'vitest';
import { createImage } from '@desource/image';
import { acmeProvider } from './acme-provider';

describe('acmeProvider', () => {
it('maps standard and custom modifiers', () => {
const image = createImage({
provider: 'acme',
domains: ['assets.example.com'],
providers: {
acme: acmeProvider({ baseURL: 'https://images.acme.test/transform' })
}
});

expect(
image('https://assets.example.com/photo.jpg', {
width: 640,
format: 'webp',
sharpen: 2
})
).toBe(
'https://images.acme.test/transform' +
'?src=https%3A%2F%2Fassets.example.com%2Fphoto.jpg&w=640&fm=webp&sharpen=2'
);
});
});`;

export const load: PageServerLoad = ({ url }) => ({
origin: url.origin,
provider,
configuration,
examples,
test
});
Loading