@usequeek/theme-kit 0.1.11 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -0
- package/apps.tsx +49 -0
- package/client.ts +1 -0
- package/package.json +1 -1
- package/tests/apps.test.tsx +33 -0
package/README.md
CHANGED
|
@@ -93,6 +93,35 @@ export default function ProductPage({ product, metafieldDefinitions }: ProductPa
|
|
|
93
93
|
- Dynamic sources (`$source` refs) resolve server-side; block data always
|
|
94
94
|
reaches the theme as literals — nothing to handle.
|
|
95
95
|
|
|
96
|
+
## App blocks slot
|
|
97
|
+
|
|
98
|
+
Product pages render merchant-placed app blocks through the slot — never app
|
|
99
|
+
code:
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
import { AppBlocks } from '@usequeek/theme-kit/apps';
|
|
103
|
+
|
|
104
|
+
export default function ProductPage({ product }: ProductPageProps) {
|
|
105
|
+
return <AppBlocks target="product" productId={product.slug} />;
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
- MUST: place `<AppBlocks target="product" productId={...} />` on the
|
|
110
|
+
product page. `target` is product-only in phase 1 — no other slot exists.
|
|
111
|
+
`productId` scopes availability reads and deep links to the shown product;
|
|
112
|
+
omit `vendorSlug` (the host resolves the store itself).
|
|
113
|
+
- The host registers its placement component once through `AppBlocksProvider`
|
|
114
|
+
(`renderBlocks`); the slot renders whatever the host registered, or nothing
|
|
115
|
+
when no provider is mounted — never an error, never a shifted layout.
|
|
116
|
+
- Empty and error states render nothing: a store with no placements ships no
|
|
117
|
+
markup and fires no request; a slow or dead app collapses to its fallback
|
|
118
|
+
and never breaks the page. No app HTML/JS ever reaches the theme — blocks
|
|
119
|
+
are Queek-rendered primitives (date/time/select) plus a deep-link CTA.
|
|
120
|
+
- Style only: blocks emit `core-app-block` with `__title`, `__description`,
|
|
121
|
+
`__box`, `__skeleton*`, `__fallback*`, `__field`, `__label`, `__input`,
|
|
122
|
+
`__slots`/`__slot`, `__empty`, `__cta`, `__retry` and `__image` hooks. No
|
|
123
|
+
kit CSS ships for these — style them in your theme.
|
|
124
|
+
|
|
96
125
|
## Attribution: Powered by Queek
|
|
97
126
|
|
|
98
127
|
Every storefront is a billboard — every footer variant of every theme renders
|
package/apps.tsx
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, type ComponentType, type JSX, type ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Where a theme renders installed app blocks. Themes import this module,
|
|
7
|
+
* never app code — which framework and which storefront serves the blocks
|
|
8
|
+
* is Queek's decision, and a theme written against this slot keeps working
|
|
9
|
+
* when that changes. Only the host's registration does.
|
|
10
|
+
*
|
|
11
|
+
* import { AppBlocks } from '@usequeek/theme-kit/apps';
|
|
12
|
+
* <AppBlocks target="product" productId={product.slug} />
|
|
13
|
+
*
|
|
14
|
+
* The slot is deliberately presentation-free: the host registers its real
|
|
15
|
+
* placement component (fetch, loading shell, Queek-rendered blocks) through
|
|
16
|
+
* `AppBlocksProvider`, and the slot renders whatever the host registered.
|
|
17
|
+
* No provider (or none yet on the server) renders nothing — never an error,
|
|
18
|
+
* never a shifted layout.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export type AppBlocksTarget = 'product';
|
|
22
|
+
|
|
23
|
+
export interface AppBlocksRendererProps {
|
|
24
|
+
target: AppBlocksTarget;
|
|
25
|
+
productId?: string;
|
|
26
|
+
vendorSlug?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type AppBlocksRenderer = ComponentType<AppBlocksRendererProps>;
|
|
30
|
+
|
|
31
|
+
const AppBlocksContext = createContext<AppBlocksRenderer | null>(null);
|
|
32
|
+
|
|
33
|
+
/** The host wraps the theme tree and registers its placement component. */
|
|
34
|
+
export function AppBlocksProvider({
|
|
35
|
+
renderBlocks,
|
|
36
|
+
children,
|
|
37
|
+
}: {
|
|
38
|
+
renderBlocks: AppBlocksRenderer;
|
|
39
|
+
children: ReactNode;
|
|
40
|
+
}): JSX.Element {
|
|
41
|
+
return <AppBlocksContext.Provider value={renderBlocks}>{children}</AppBlocksContext.Provider>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The theme-facing slot: renders the host's component, or nothing. */
|
|
45
|
+
export function AppBlocks({ target, productId, vendorSlug }: AppBlocksRendererProps): JSX.Element | null {
|
|
46
|
+
const RenderBlocks = useContext(AppBlocksContext);
|
|
47
|
+
if (RenderBlocks === null) return null;
|
|
48
|
+
return <RenderBlocks target={target} productId={productId} vendorSlug={vendorSlug} />;
|
|
49
|
+
}
|
package/client.ts
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import * as apps from '../apps';
|
|
5
|
+
import { AppBlocks, AppBlocksProvider } from '../apps';
|
|
6
|
+
|
|
7
|
+
describe('apps — the theme-facing app-block slot', () => {
|
|
8
|
+
it('is exactly the provider, the slot, and the shared types', () => {
|
|
9
|
+
expect(Object.keys(apps).sort()).toEqual(['AppBlocks', 'AppBlocksProvider']);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('renders nothing without a host registration (SSR-safe)', () => {
|
|
13
|
+
const html = renderToStaticMarkup(
|
|
14
|
+
React.createElement(AppBlocks, { target: 'product', productId: 'p1' }),
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
expect(html).toBe('');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('renders whatever the host registered, with the slot props', () => {
|
|
21
|
+
function HostBlocks({ target, productId }: { target: 'product'; productId?: string }) {
|
|
22
|
+
return <div data-host-blocks={`${target}:${productId ?? ''}`} />;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const html = renderToStaticMarkup(
|
|
26
|
+
<AppBlocksProvider renderBlocks={HostBlocks}>
|
|
27
|
+
<AppBlocks target="product" productId="p1" vendorSlug="glow" />
|
|
28
|
+
</AppBlocksProvider>,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
expect(html).toContain('data-host-blocks="product:p1"');
|
|
32
|
+
});
|
|
33
|
+
});
|