@usequeek/theme-kit 0.1.0

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.
Files changed (120) hide show
  1. package/README.md +37 -0
  2. package/api/collections.ts +93 -0
  3. package/api/orders.ts +23 -0
  4. package/apps/analytics-events.ts +32 -0
  5. package/apps/apps.css +613 -0
  6. package/apps/follow-card.tsx +192 -0
  7. package/apps/gtag.ts +19 -0
  8. package/apps/subscribe-form.tsx +124 -0
  9. package/client.ts +52 -0
  10. package/components/auth/auth-email-login-step.tsx +46 -0
  11. package/components/auth/auth-email-otp-step.tsx +54 -0
  12. package/components/auth/auth-email-otp-verify-step.tsx +82 -0
  13. package/components/auth/auth-email-register-step.tsx +59 -0
  14. package/components/auth/auth-flow.tsx +26 -0
  15. package/components/auth/auth-signup-step.tsx +53 -0
  16. package/components/blog/category-filter.tsx +92 -0
  17. package/components/blog/index.ts +12 -0
  18. package/components/blog/pagination.tsx +73 -0
  19. package/components/blog/post-categories.tsx +42 -0
  20. package/components/blog/post-meta.tsx +49 -0
  21. package/components/blog/related-posts.tsx +54 -0
  22. package/components/blog/share-buttons.tsx +89 -0
  23. package/components/cart/cart-panel-controller.tsx +67 -0
  24. package/components/cart/default-cart-shell.tsx +207 -0
  25. package/components/checkout/checkout-contact.tsx +84 -0
  26. package/components/checkout/default-checkout-shell.tsx +177 -0
  27. package/components/checkout/policy-links.tsx +98 -0
  28. package/components/image.tsx +149 -0
  29. package/components/markdown.tsx +54 -0
  30. package/components/product-media-frame.tsx +56 -0
  31. package/components/variant-picker.css +84 -0
  32. package/components/variant-picker.tsx +107 -0
  33. package/design-token-preview.tsx +81 -0
  34. package/edit-mode-active.tsx +324 -0
  35. package/edit-mode.tsx +80 -0
  36. package/hooks/use-address-autocomplete.ts +62 -0
  37. package/hooks/use-auth-flow.tsx +346 -0
  38. package/hooks/use-auth.ts +17 -0
  39. package/hooks/use-cart.ts +14 -0
  40. package/hooks/use-categories.ts +112 -0
  41. package/hooks/use-collection-params.ts +67 -0
  42. package/hooks/use-href.ts +15 -0
  43. package/hooks/use-posts.ts +75 -0
  44. package/hooks/use-product-detail.ts +69 -0
  45. package/hooks/use-product-questions.ts +77 -0
  46. package/hooks/use-product-sections.ts +165 -0
  47. package/hooks/use-products.ts +216 -0
  48. package/hooks/use-related-products.ts +56 -0
  49. package/hooks/use-resolve-coordinates.ts +48 -0
  50. package/hooks/use-reviews.ts +167 -0
  51. package/hooks/use-search.ts +55 -0
  52. package/hooks/use-shop-cart.ts +31 -0
  53. package/hooks/use-shop-params.ts +68 -0
  54. package/hooks/use-shop.ts +186 -0
  55. package/hooks/use-variant-selection.ts +98 -0
  56. package/index.ts +49 -0
  57. package/package.json +41 -0
  58. package/page-renderer.tsx +365 -0
  59. package/provider.tsx +91 -0
  60. package/sdk/client.ts +221 -0
  61. package/sdk/error-handler.ts +37 -0
  62. package/sdk/platform.ts +78 -0
  63. package/sdk/queek-client.ts +104 -0
  64. package/sdk/reauth-guard.ts +27 -0
  65. package/sdk/server-store-client.ts +165 -0
  66. package/shared-blocks/button.tsx +41 -0
  67. package/shared-blocks/callout.tsx +35 -0
  68. package/shared-blocks/content-default.tsx +23 -0
  69. package/shared-blocks/core-blocks.css +1792 -0
  70. package/shared-blocks/divider.tsx +17 -0
  71. package/shared-blocks/embed.tsx +40 -0
  72. package/shared-blocks/faq.tsx +44 -0
  73. package/shared-blocks/image.tsx +29 -0
  74. package/shared-blocks/index.ts +24 -0
  75. package/shared-blocks/manifest.ts +76 -0
  76. package/shared-blocks/product-qa.tsx +203 -0
  77. package/shared-blocks/quote.tsx +20 -0
  78. package/shared-blocks/reviews.tsx +436 -0
  79. package/shared-blocks/table.tsx +45 -0
  80. package/shared-blocks/video.tsx +60 -0
  81. package/stores/address-modal-store.ts +15 -0
  82. package/stores/auth-modal-store.ts +17 -0
  83. package/stores/cart-panel-store.ts +17 -0
  84. package/stores/cart-store.ts +172 -0
  85. package/stores/checkout-store.ts +167 -0
  86. package/stores/edit-preview-store.ts +61 -0
  87. package/stores/location-store.ts +41 -0
  88. package/stores/orders-sheet-store.ts +21 -0
  89. package/stores/policy-modal-store.ts +15 -0
  90. package/stores/product-modal-store.ts +18 -0
  91. package/stores/search-modal-store.ts +17 -0
  92. package/stores/toast-store.ts +46 -0
  93. package/stores/user-store.ts +88 -0
  94. package/theme-context.tsx +28 -0
  95. package/theme-variant-resolver.ts +44 -0
  96. package/types/block.ts +292 -0
  97. package/types/cart.ts +21 -0
  98. package/types/media.ts +55 -0
  99. package/types/menu.ts +27 -0
  100. package/types/order.ts +119 -0
  101. package/types/page.ts +86 -0
  102. package/types/product.ts +279 -0
  103. package/types/theme.ts +380 -0
  104. package/types/vendor.ts +243 -0
  105. package/utils/addon-selection.ts +31 -0
  106. package/utils/benefit-icon.tsx +145 -0
  107. package/utils/brand-palette.ts +64 -0
  108. package/utils/brand.ts +518 -0
  109. package/utils/contact-location.ts +44 -0
  110. package/utils/format.ts +64 -0
  111. package/utils/hero-content.ts +35 -0
  112. package/utils/image-srcset.ts +157 -0
  113. package/utils/markdown.tsx +237 -0
  114. package/utils/match-variant.ts +31 -0
  115. package/utils/menu-link.ts +41 -0
  116. package/utils/placeholder-resolver.ts +184 -0
  117. package/utils/product-media.ts +77 -0
  118. package/utils/product-normalizer.ts +323 -0
  119. package/utils/vendor-host.ts +173 -0
  120. package/utils/video-embed.ts +38 -0
@@ -0,0 +1,17 @@
1
+ 'use client';
2
+
3
+ import type { DividerBlockData } from '../types/block';
4
+
5
+ /**
6
+ * Framework-owned divider block. Themes style via `.core-block-divider`
7
+ * and the modifier classes in their theme.css.
8
+ */
9
+ export function CoreDividerBlock({ style }: DividerBlockData): JSX.Element {
10
+ const modifier = style === 'space' ? 'core-block-divider--space' : 'core-block-divider--line';
11
+
12
+ return (
13
+ <div className={`core-block-divider ${modifier}`} role="separator">
14
+ {style !== 'space' ? <hr className="core-block-divider__line" /> : null}
15
+ </div>
16
+ );
17
+ }
@@ -0,0 +1,40 @@
1
+ 'use client';
2
+
3
+ import type { EmbedBlockData } from '../types/block';
4
+
5
+ /**
6
+ * Framework-owned embed block. Renders an iframe for any external URL
7
+ * (Instagram, Spotify, Twitter, custom). Themes style via `.core-block-embed`.
8
+ */
9
+ export function CoreEmbedBlock({ url }: EmbedBlockData): JSX.Element {
10
+ if (!url) {
11
+ return (
12
+ <section className="core-block core-block-embed">
13
+ <div className="core-block-embed__wrap core-block-embed__wrap--empty">
14
+ <div className="core-block-embed__placeholder">
15
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
16
+ <polyline points="16 18 22 12 16 6" />
17
+ <polyline points="8 6 2 12 8 18" />
18
+ </svg>
19
+ <span>Paste an Instagram, Spotify, or iframe URL</span>
20
+ </div>
21
+ </div>
22
+ </section>
23
+ );
24
+ }
25
+
26
+ return (
27
+ <section className="core-block core-block-embed">
28
+ <div className="core-block-embed__wrap">
29
+ <iframe
30
+ className="core-block-embed__frame"
31
+ src={url}
32
+ title="Embedded content"
33
+ allowFullScreen
34
+ loading="lazy"
35
+ referrerPolicy="no-referrer-when-downgrade"
36
+ />
37
+ </div>
38
+ </section>
39
+ );
40
+ }
@@ -0,0 +1,44 @@
1
+ 'use client';
2
+
3
+ import type { FaqBlockData } from '../types/block';
4
+
5
+ /**
6
+ * Framework-owned FAQ block. Renders an accessible, SSR-friendly accordion
7
+ * using native <details>/<summary> — works with zero JS. Themes style via
8
+ * `.core-block-faq` + sub-elements.
9
+ */
10
+ export function CoreFaqBlock({ heading, items }: FaqBlockData): JSX.Element | null {
11
+ const safeItems = Array.isArray(items) ? items.filter((item) => item && item.question) : [];
12
+
13
+ if (safeItems.length === 0) return null;
14
+
15
+ return (
16
+ <section className="core-block core-block-faq">
17
+ {heading ? <h2 className="core-block-faq__heading">{heading}</h2> : null}
18
+ <div className="core-block-faq__list">
19
+ {safeItems.map((item, index) => (
20
+ <details key={index} className="core-block-faq__item">
21
+ <summary className="core-block-faq__question">
22
+ <span>{item.question}</span>
23
+ <svg
24
+ className="core-block-faq__chevron"
25
+ width="18"
26
+ height="18"
27
+ viewBox="0 0 24 24"
28
+ fill="none"
29
+ stroke="currentColor"
30
+ strokeWidth="2"
31
+ strokeLinecap="round"
32
+ strokeLinejoin="round"
33
+ aria-hidden="true"
34
+ >
35
+ <polyline points="6 9 12 15 18 9" />
36
+ </svg>
37
+ </summary>
38
+ <div className="core-block-faq__answer">{item.answer}</div>
39
+ </details>
40
+ ))}
41
+ </div>
42
+ </section>
43
+ );
44
+ }
@@ -0,0 +1,29 @@
1
+ 'use client';
2
+
3
+ import type { ImageBlockData } from '../types/block';
4
+ import { Image } from '../components/image';
5
+
6
+ /**
7
+ * Framework-owned image block. Wraps the core <Image> (with Smart Placeholder
8
+ * support) and an optional caption. Themes style via `.core-block-image`.
9
+ */
10
+ export function CoreImageBlock({ url, alt, caption }: ImageBlockData): JSX.Element {
11
+ return (
12
+ <section className="core-block core-block-image">
13
+ <figure className="core-block-image__figure">
14
+ <Image
15
+ src={url || undefined}
16
+ alt={alt ?? ''}
17
+ className="core-block-image__img"
18
+ placeholder={{
19
+ label: alt ?? caption ?? undefined,
20
+ width: 1200,
21
+ height: 800,
22
+ variant: 'photo',
23
+ }}
24
+ />
25
+ {caption ? <figcaption className="core-block-image__caption">{caption}</figcaption> : null}
26
+ </figure>
27
+ </section>
28
+ );
29
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Framework-owned blocks. Themes do NOT implement these — the React logic
3
+ * lives here, themes only style them via `.core-block-*` scoped selectors
4
+ * in their theme.css.
5
+ */
6
+
7
+ export { CoreDividerBlock } from './divider';
8
+ export { CoreCalloutBlock } from './callout';
9
+ export { CoreQuoteBlock } from './quote';
10
+ export { CoreEmbedBlock } from './embed';
11
+ export { CoreVideoBlock } from './video';
12
+ export { CoreTableBlock } from './table';
13
+ export { CoreButtonBlock } from './button';
14
+ export { CoreImageBlock } from './image';
15
+ export { CoreContentDefaultBlock } from './content-default';
16
+ export { CoreReviewsBlock } from './reviews';
17
+ export { CoreFaqBlock } from './faq';
18
+ export { CoreProductQaBlock } from './product-qa';
19
+
20
+ export {
21
+ FRAMEWORK_BLOCK_MANIFEST,
22
+ FRAMEWORK_OWNED_TYPES,
23
+ CONTENT_DEFAULT_IS_FRAMEWORK_OWNED,
24
+ } from './manifest';
@@ -0,0 +1,76 @@
1
+ import type { ThemeManifestVariant } from '../types/theme';
2
+
3
+ /**
4
+ * Framework-owned block manifest. These variants are available in EVERY
5
+ * theme's Section Library drawer — themes don't implement them and can't
6
+ * override their React logic, only their CSS styling via `.core-block-*`
7
+ * scoped selectors.
8
+ *
9
+ * Keep in sync with FRAMEWORK_OWNED_TYPES in page-renderer.tsx and the
10
+ * shared-blocks index export.
11
+ */
12
+ export const FRAMEWORK_BLOCK_MANIFEST: Record<string, ThemeManifestVariant[]> = {
13
+ divider: [
14
+ { id: 'default', label: 'Divider', default: true, purpose: 'Visual separator between sections' },
15
+ ],
16
+ callout: [
17
+ { id: 'default', label: 'Callout', default: true, purpose: 'Highlighted info/warn/success/note box for article emphasis' },
18
+ ],
19
+ quote: [
20
+ { id: 'default', label: 'Quote', default: true, purpose: 'Pull-quote with optional attribution' },
21
+ ],
22
+ embed: [
23
+ { id: 'default', label: 'Embed', default: true, purpose: 'Instagram, Spotify, Twitter, or any iframe URL' },
24
+ ],
25
+ video: [
26
+ { id: 'default', label: 'Video', default: true, purpose: 'Embed a YouTube, Vimeo, or self-hosted video' },
27
+ ],
28
+ table: [
29
+ { id: 'default', label: 'Table', default: true, purpose: 'Pricing, specs, or comparison tables' },
30
+ ],
31
+ button: [
32
+ { id: 'default', label: 'Button', default: true, purpose: 'Standalone call-to-action link' },
33
+ ],
34
+ image: [
35
+ { id: 'default', label: 'Image', default: true, purpose: 'A single standalone image with caption' },
36
+ ],
37
+ reviews: [
38
+ { id: 'grid', label: 'Grid', default: true, purpose: 'Verified customer reviews in a responsive grid' },
39
+ { id: 'carousel', label: 'Carousel', purpose: 'Verified customer reviews in a horizontal scroller' },
40
+ { id: 'wall', label: 'Wall', purpose: 'Masonry-style wall of verified customer reviews' },
41
+ { id: 'compact', label: 'Compact', purpose: 'Compact list with rating breakdown header' },
42
+ ],
43
+ faq: [
44
+ { id: 'default', label: 'FAQ', default: true, purpose: 'Accordion of question/answer pairs' },
45
+ ],
46
+ product_qa: [
47
+ { id: 'default', label: 'Product Q&A', default: true, purpose: 'Customer questions and vendor answers for this product' },
48
+ ],
49
+ };
50
+
51
+ /**
52
+ * Block types that are fully framework-owned (React logic + routing).
53
+ * Themes can style these via `.core-block-*` CSS but do NOT implement the
54
+ * React components. page-renderer.tsx routes these to core directly.
55
+ */
56
+ export const FRAMEWORK_OWNED_TYPES = new Set<string>([
57
+ 'divider',
58
+ 'callout',
59
+ 'quote',
60
+ 'embed',
61
+ 'video',
62
+ 'table',
63
+ 'button',
64
+ 'image',
65
+ 'reviews',
66
+ 'faq',
67
+ 'product_qa',
68
+ ]);
69
+
70
+ /**
71
+ * `content/default` is a special case — the default content variant is
72
+ * framework-owned (plain markdown rendering), but content variants with
73
+ * specific IDs (promo, brand-story, testimonials, marquee, features-columns,
74
+ * before-after) stay theme-owned. page-renderer.tsx handles this split.
75
+ */
76
+ export const CONTENT_DEFAULT_IS_FRAMEWORK_OWNED = true;
@@ -0,0 +1,203 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import type { ProductQaBlockData, ProductQuestionItem } from '../types/block';
5
+ import { Image } from '../components/image';
6
+ import { useProductQuestions } from '../hooks/use-product-questions';
7
+ import { useAuth } from '../hooks/use-auth';
8
+ import { useStorefront } from '../provider';
9
+ import { getQueekClient } from '../sdk/queek-client';
10
+
11
+ const MAX_QUESTION_LENGTH = 1000;
12
+
13
+ /**
14
+ * Framework-owned Product Q&A block. Renders answered customer questions
15
+ * fetched from `GET /client/store/products/{slug}/questions` and lets
16
+ * authenticated customers ask new ones via `POST` to the same path. Themes
17
+ * style via `.core-block-product-qa` and its `__` sub-elements.
18
+ */
19
+ export function CoreProductQaBlock(props: ProductQaBlockData): JSX.Element {
20
+ const { questions, isLoading } = useProductQuestions(props);
21
+ const title = props.title;
22
+ const hasQuestions = questions.length > 0;
23
+
24
+ return (
25
+ <section className="core-block core-block-product-qa" data-loading={isLoading ? 'true' : undefined}>
26
+ <header className="core-block-product-qa__header">
27
+ {title ? <h2 className="core-block-product-qa__title">{title}</h2> : null}
28
+ <AskQuestionPanel productSlug={props.product_slug} />
29
+ </header>
30
+
31
+ {!hasQuestions && !isLoading && (
32
+ <p className="core-block-product-qa__empty">No questions yet — be the first to ask.</p>
33
+ )}
34
+
35
+ {hasQuestions && (
36
+ <div className="core-block-product-qa__list">
37
+ {questions.map((question) => (
38
+ <QuestionCard key={question.id} question={question} />
39
+ ))}
40
+ </div>
41
+ )}
42
+ </section>
43
+ );
44
+ }
45
+
46
+ function AskQuestionPanel({ productSlug }: { productSlug: string }): JSX.Element {
47
+ const { vendor } = useStorefront();
48
+ const { user, openAuthModal } = useAuth();
49
+ const [open, setOpen] = useState(false);
50
+ const [question, setQuestion] = useState('');
51
+ const [submitting, setSubmitting] = useState(false);
52
+ const [error, setError] = useState<string | null>(null);
53
+ const [submitted, setSubmitted] = useState(false);
54
+
55
+ const handleOpen = (): void => {
56
+ if (!user) {
57
+ openAuthModal('login');
58
+ return;
59
+ }
60
+ setSubmitted(false);
61
+ setOpen(true);
62
+ };
63
+
64
+ const handleCancel = (): void => {
65
+ setOpen(false);
66
+ setError(null);
67
+ setQuestion('');
68
+ };
69
+
70
+ const handleSubmit = async (): Promise<void> => {
71
+ const trimmed = question.trim();
72
+ if (!trimmed) {
73
+ setError('Please write a question.');
74
+ return;
75
+ }
76
+
77
+ setSubmitting(true);
78
+ setError(null);
79
+ try {
80
+ const client = getQueekClient(vendor.slug ?? undefined);
81
+ await client.post(`/client/store/products/${productSlug}/questions`, { question: trimmed });
82
+ setOpen(false);
83
+ setQuestion('');
84
+ setSubmitted(true);
85
+ } catch {
86
+ setError('Could not submit your question. Please try again.');
87
+ } finally {
88
+ setSubmitting(false);
89
+ }
90
+ };
91
+
92
+ if (submitted) {
93
+ return (
94
+ <div className="core-block-product-qa__ask">
95
+ <p className="core-block-product-qa__thanks">
96
+ Thanks — we&apos;ll notify you when the vendor answers.
97
+ </p>
98
+ </div>
99
+ );
100
+ }
101
+
102
+ if (!open) {
103
+ return (
104
+ <div className="core-block-product-qa__ask">
105
+ <button type="button" className="core-block-product-qa__ask-btn" onClick={handleOpen}>
106
+ Ask a question
107
+ </button>
108
+ </div>
109
+ );
110
+ }
111
+
112
+ return (
113
+ <div className="core-block-product-qa__ask core-block-product-qa__ask--open">
114
+ <textarea
115
+ className="core-block-product-qa__textarea"
116
+ rows={3}
117
+ maxLength={MAX_QUESTION_LENGTH}
118
+ placeholder="What would you like to know about this product?"
119
+ value={question}
120
+ onChange={(e) => setQuestion(e.target.value)}
121
+ disabled={submitting}
122
+ />
123
+ {error ? <p className="core-block-product-qa__error">{error}</p> : null}
124
+ <div className="core-block-product-qa__ask-actions">
125
+ <button
126
+ type="button"
127
+ className="core-block-product-qa__cancel-btn"
128
+ onClick={handleCancel}
129
+ disabled={submitting}
130
+ >
131
+ Cancel
132
+ </button>
133
+ <button
134
+ type="button"
135
+ className="core-block-product-qa__submit-btn"
136
+ onClick={() => void handleSubmit()}
137
+ disabled={submitting || question.trim().length === 0}
138
+ >
139
+ {submitting ? 'Submitting…' : 'Submit question'}
140
+ </button>
141
+ </div>
142
+ </div>
143
+ );
144
+ }
145
+
146
+ function QuestionCard({ question }: { question: ProductQuestionItem }): JSX.Element {
147
+ return (
148
+ <article className="core-block-product-qa__card">
149
+ <header className="core-block-product-qa__card-head">
150
+ <div className="core-block-product-qa__avatar">
151
+ {question.user.avatar ? (
152
+ <Image
153
+ src={question.user.avatar}
154
+ alt={question.user.name}
155
+ className="core-block-product-qa__avatar-img"
156
+ />
157
+ ) : (
158
+ <span className="core-block-product-qa__avatar-initials" aria-hidden="true">
159
+ {initialsFor(question.user.name)}
160
+ </span>
161
+ )}
162
+ </div>
163
+ <div className="core-block-product-qa__card-meta">
164
+ <p className="core-block-product-qa__author">{question.user.name}</p>
165
+ <p className="core-block-product-qa__date">{formatDate(question.created_at)}</p>
166
+ </div>
167
+ </header>
168
+
169
+ <p className="core-block-product-qa__question">{question.question}</p>
170
+
171
+ {question.answer ? (
172
+ <div className="core-block-product-qa__answer">
173
+ <p className="core-block-product-qa__answer-label">
174
+ Answered{question.answered_at ? ` · ${formatDate(question.answered_at)}` : ''}
175
+ </p>
176
+ <p className="core-block-product-qa__answer-body">{question.answer}</p>
177
+ </div>
178
+ ) : null}
179
+ </article>
180
+ );
181
+ }
182
+
183
+ function initialsFor(name: string): string {
184
+ return name
185
+ .split(/\s+/)
186
+ .map((part) => part.charAt(0))
187
+ .filter(Boolean)
188
+ .slice(0, 2)
189
+ .join('')
190
+ .toUpperCase();
191
+ }
192
+
193
+ function formatDate(iso: string): string {
194
+ try {
195
+ return new Date(iso).toLocaleDateString(undefined, {
196
+ month: 'short',
197
+ day: 'numeric',
198
+ year: 'numeric',
199
+ });
200
+ } catch {
201
+ return '';
202
+ }
203
+ }
@@ -0,0 +1,20 @@
1
+ 'use client';
2
+
3
+ import type { QuoteBlockData } from '../types/block';
4
+
5
+ /**
6
+ * Framework-owned pull-quote block. Renders a blockquote with an optional
7
+ * attribution line. Themes style via `.core-block-quote` scoped selectors.
8
+ */
9
+ export function CoreQuoteBlock({ markdown, text, cite }: QuoteBlockData): JSX.Element {
10
+ const body = (markdown ?? text ?? '').trim();
11
+
12
+ return (
13
+ <section className="core-block core-block-quote">
14
+ <blockquote className="core-block-quote__inner">
15
+ <p className="core-block-quote__text">{body}</p>
16
+ {cite ? <cite className="core-block-quote__cite">{cite}</cite> : null}
17
+ </blockquote>
18
+ </section>
19
+ );
20
+ }