@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 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
@@ -7,6 +7,7 @@
7
7
 
8
8
  export * from './provider';
9
9
  export * from './navigation';
10
+ export * from './apps';
10
11
  export * from './page-renderer';
11
12
  export * from './edit-mode';
12
13
  export * from './theme-variant-resolver';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usequeek/theme-kit",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "description": "The framework a Queek storefront theme is built against.",
@@ -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
+ });