Skip to content

Commit 908d2cc

Browse files
committed
docs: add custom adapter guide with Preact and Lit examples
1 parent d28da51 commit 908d2cc

File tree

2 files changed

+217
-1
lines changed

2 files changed

+217
-1
lines changed

docs/frameworks/custom.mdx

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
---
2+
title: Build Your Own Adapter
3+
description: Integrate Logo Soup with any framework using the core engine's subscribe/getSnapshot API.
4+
---
5+
6+
Every first-party adapter follows the same pattern. If your framework isn't listed, you can build an adapter in 20-40 lines.
7+
8+
## The Pattern
9+
10+
```ts
11+
import { createLogoSoup } from "@sanity-labs/logo-soup";
12+
13+
// 1. Create an engine instance
14+
const engine = createLogoSoup();
15+
16+
// 2. Subscribe — push snapshots into your framework's reactivity
17+
const unsubscribe = engine.subscribe(() => {
18+
const snapshot = engine.getSnapshot();
19+
// yourFramework.setState(snapshot)
20+
});
21+
22+
// 3. Process — call when options change
23+
engine.process({ logos: ["a.svg", "b.svg"], baseSize: 48 });
24+
25+
// 4. Destroy — call on teardown
26+
unsubscribe();
27+
engine.destroy();
28+
```
29+
30+
## Example: Preact 10.x
31+
32+
Preact exposes `useSyncExternalStore` via `preact/compat` — the same API React uses. The adapter is nearly identical to the React one.
33+
34+
```tsx
35+
// use-logo-soup.ts
36+
import { useRef, useCallback, useEffect } from "preact/hooks";
37+
import { useSyncExternalStore } from "preact/compat";
38+
import { createLogoSoup } from "@sanity-labs/logo-soup";
39+
import type { ProcessOptions, LogoSoupState } from "@sanity-labs/logo-soup";
40+
41+
const SERVER_SNAPSHOT: LogoSoupState = {
42+
status: "idle",
43+
normalizedLogos: [],
44+
error: null,
45+
};
46+
47+
export function useLogoSoup(options: ProcessOptions) {
48+
const engineRef = useRef(createLogoSoup());
49+
const engine = engineRef.current;
50+
51+
const subscribe = useCallback(
52+
(cb: () => void) => engine.subscribe(cb),
53+
[engine],
54+
);
55+
const getSnapshot = useCallback(() => engine.getSnapshot(), [engine]);
56+
57+
const state = useSyncExternalStore(
58+
subscribe,
59+
getSnapshot,
60+
() => SERVER_SNAPSHOT,
61+
);
62+
63+
useEffect(() => {
64+
engine.process(options);
65+
}, [engine, options.logos, options.baseSize, options.scaleFactor]);
66+
67+
useEffect(() => () => engine.destroy(), [engine]);
68+
69+
return state;
70+
}
71+
```
72+
73+
```tsx
74+
// usage
75+
import { useLogoSoup } from "./use-logo-soup";
76+
import { getVisualCenterTransform } from "@sanity-labs/logo-soup";
77+
78+
function LogoStrip() {
79+
const { status, normalizedLogos } = useLogoSoup({
80+
logos: ["/logos/acme.svg", "/logos/globex.svg"],
81+
});
82+
83+
if (status !== "ready") return null;
84+
85+
return (
86+
<div style={{ textAlign: "center" }}>
87+
{normalizedLogos.map((logo) => (
88+
<img
89+
key={logo.src}
90+
src={logo.src}
91+
alt={logo.alt}
92+
width={logo.normalizedWidth}
93+
height={logo.normalizedHeight}
94+
style={{
95+
transform: getVisualCenterTransform(logo, "visual-center-y"),
96+
}}
97+
/>
98+
))}
99+
</div>
100+
);
101+
}
102+
```
103+
104+
## Example: Lit 3.x
105+
106+
Lit uses `ReactiveController` to encapsulate reusable logic that hooks into a component's update cycle. The controller subscribes to the engine and calls `host.requestUpdate()` when state changes.
107+
108+
```ts
109+
// logo-soup-controller.ts
110+
import { type ReactiveController, type ReactiveControllerHost } from "lit";
111+
import { createLogoSoup } from "@sanity-labs/logo-soup";
112+
import type { ProcessOptions, LogoSoupState } from "@sanity-labs/logo-soup";
113+
114+
export class LogoSoupController implements ReactiveController {
115+
private engine = createLogoSoup();
116+
private unsubscribe: (() => void) | null = null;
117+
118+
state: LogoSoupState = this.engine.getSnapshot();
119+
120+
constructor(private host: ReactiveControllerHost) {
121+
host.addController(this);
122+
}
123+
124+
hostConnected() {
125+
this.unsubscribe = this.engine.subscribe(() => {
126+
this.state = this.engine.getSnapshot();
127+
this.host.requestUpdate();
128+
});
129+
}
130+
131+
hostDisconnected() {
132+
this.unsubscribe?.();
133+
this.engine.destroy();
134+
}
135+
136+
process(options: ProcessOptions) {
137+
this.engine.process(options);
138+
}
139+
}
140+
```
141+
142+
```ts
143+
// usage
144+
import { LitElement, html } from "lit";
145+
import { customElement, property } from "lit/decorators.js";
146+
import { getVisualCenterTransform } from "@sanity-labs/logo-soup";
147+
import { LogoSoupController } from "./logo-soup-controller";
148+
149+
@customElement("logo-strip")
150+
export class LogoStrip extends LitElement {
151+
private soup = new LogoSoupController(this);
152+
153+
@property({ type: Array }) logos: string[] = [];
154+
@property({ type: Number }) baseSize = 48;
155+
156+
updated(changed: Map<string, unknown>) {
157+
if (changed.has("logos") || changed.has("baseSize")) {
158+
this.soup.process({ logos: this.logos, baseSize: this.baseSize });
159+
}
160+
}
161+
162+
render() {
163+
if (this.soup.state.status !== "ready") return html``;
164+
165+
return html`
166+
<div style="text-align: center">
167+
${this.soup.state.normalizedLogos.map(
168+
(logo) => html`
169+
<img
170+
src=${logo.src}
171+
alt=${logo.alt}
172+
width=${logo.normalizedWidth}
173+
height=${logo.normalizedHeight}
174+
style="display:inline-block;margin:0 14px;transform:${getVisualCenterTransform(
175+
logo,
176+
"visual-center-y",
177+
) ?? "none"}"
178+
/>
179+
`,
180+
)}
181+
</div>
182+
`;
183+
}
184+
}
185+
```
186+
187+
## Checklist
188+
189+
| Concern | What to do |
190+
| ------------- | ------------------------------------------------------------------------------------------- |
191+
| **Create** | Call `createLogoSoup()` once per component instance |
192+
| **Subscribe** | Push `engine.getSnapshot()` into your reactive state on each notification |
193+
| **Process** | Call `engine.process(options)` when inputs change |
194+
| **Cleanup** | Call both `unsubscribe()` and `engine.destroy()` on teardown |
195+
| **Stability** | Store the engine in a ref/field — don't recreate it on every render |
196+
| **SSR** | The engine needs `<canvas>`, so guard behind a client-side check if your framework does SSR |
197+
198+
## How Our First-Party Adapters Map
199+
200+
| Framework | Reactive primitive | Subscribe mechanism | Cleanup |
201+
| --------- | ---------------------- | -------------------------------------------------- | ------------------------- |
202+
| React | `useSyncExternalStore` | Engine's `subscribe`/`getSnapshot` directly | `useEffect` return |
203+
| Vue | `shallowRef` | `engine.subscribe()``ref.value = snapshot` | `onScopeDispose` |
204+
| Svelte | `createSubscriber` | Getter calls `subscribe()` before reading | `$effect` teardown |
205+
| Solid | `from()` | Producer function `(set) => engine.subscribe(...)` | `onCleanup` |
206+
| Angular | `signal()` | `engine.subscribe()``_state.set(snapshot)` | `DestroyRef.onDestroy` |
207+
| jQuery | `$.data()` | `engine.subscribe()` → re-render DOM | `$el.logoSoup('destroy')` |
208+
209+
Each adapter is 30-80 lines. The source is at [`src/react`](https://github.com/sanity-labs/logo-soup/tree/main/src/react), [`src/vue`](https://github.com/sanity-labs/logo-soup/tree/main/src/vue), [`src/svelte`](https://github.com/sanity-labs/logo-soup/tree/main/src/svelte), [`src/solid`](https://github.com/sanity-labs/logo-soup/tree/main/src/solid), [`src/angular`](https://github.com/sanity-labs/logo-soup/tree/main/src/angular), and [`src/jquery`](https://github.com/sanity-labs/logo-soup/tree/main/src/jquery).
210+
211+
<Tip>
212+
Built an adapter for a framework we don't support? [Let us
213+
know](https://github.com/sanity-labs/logo-soup/issues) — we'll link to it from
214+
the docs.
215+
</Tip>

docs/mint.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@
5757
"frameworks/solid",
5858
"frameworks/angular",
5959
"frameworks/jquery",
60-
"frameworks/vanilla"
60+
"frameworks/vanilla",
61+
"frameworks/custom"
6162
]
6263
},
6364
{

0 commit comments

Comments
 (0)