create-skybridge 0.0.0-dev.7914624 → 0.0.0-dev.793efe5

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 (53) hide show
  1. package/dist/index.js +107 -17
  2. package/package.json +2 -2
  3. package/templates/blank/README.md +2 -1
  4. package/templates/blank/node_modules/.bin/alpic +2 -2
  5. package/templates/blank/node_modules/.bin/tsx +2 -2
  6. package/templates/blank/node_modules/.bin/vite +2 -2
  7. package/templates/blank/package.json +8 -7
  8. package/templates/blank/src/index.ts +3 -0
  9. package/templates/blank/src/server.ts +7 -11
  10. package/templates/blank/vite.config.ts +7 -1
  11. package/templates/demo/README.md +3 -1
  12. package/templates/demo/evals/start.eval.ts +11 -0
  13. package/templates/demo/node_modules/.bin/alpic +2 -2
  14. package/templates/demo/node_modules/.bin/tsx +2 -2
  15. package/templates/demo/node_modules/.bin/vite +2 -2
  16. package/templates/demo/node_modules/.bin/vitest +21 -0
  17. package/templates/demo/package.json +19 -13
  18. package/templates/demo/src/index.ts +3 -0
  19. package/templates/demo/src/server.ts +86 -88
  20. package/templates/demo/src/views/onboarding.tsx +3 -3
  21. package/templates/demo/tsconfig.evals.json +11 -0
  22. package/templates/demo/vite.config.ts +9 -3
  23. package/templates/ecom/.env.template +5 -0
  24. package/templates/ecom/README.md +2 -1
  25. package/templates/ecom/node_modules/.bin/alpic +2 -2
  26. package/templates/ecom/node_modules/.bin/ladle +2 -2
  27. package/templates/ecom/node_modules/.bin/vite +2 -2
  28. package/templates/ecom/package.json +3 -2
  29. package/templates/ecom/src/catalog/index.ts +19 -0
  30. package/templates/ecom/src/catalog/mock.ts +134 -0
  31. package/templates/ecom/src/catalog/shopify.ts +264 -0
  32. package/templates/ecom/src/components/chip.css.ts +15 -8
  33. package/templates/ecom/src/components/chip.stories.tsx +2 -1
  34. package/templates/ecom/src/components/chip.tsx +16 -8
  35. package/templates/ecom/src/components/image-gallery.css.ts +2 -2
  36. package/templates/ecom/src/components/image-gallery.tsx +5 -4
  37. package/templates/ecom/src/components/product-card.css.ts +9 -5
  38. package/templates/ecom/src/components/product-card.tsx +5 -4
  39. package/templates/ecom/src/components/variant-picker.stories.tsx +9 -6
  40. package/templates/ecom/src/components/variant-picker.tsx +17 -29
  41. package/templates/ecom/src/components/view-frame.tsx +4 -4
  42. package/templates/ecom/src/i18n.ts +2 -1
  43. package/templates/ecom/src/index.ts +3 -0
  44. package/templates/ecom/src/lib/variants.ts +100 -29
  45. package/templates/ecom/src/server.ts +17 -21
  46. package/templates/ecom/src/tools/render-carousel.ts +11 -92
  47. package/templates/ecom/src/tools/search-products.ts +40 -26
  48. package/templates/ecom/src/types.ts +77 -3
  49. package/templates/ecom/src/views/carousel/detail/detail.css.ts +25 -10
  50. package/templates/ecom/src/views/carousel/detail/detail.stories.tsx +33 -14
  51. package/templates/ecom/src/views/carousel/detail/index.tsx +83 -44
  52. package/templates/ecom/src/views/carousel/index.tsx +3 -4
  53. package/templates/ecom/vite.config.ts +1 -1
@@ -7,11 +7,21 @@ const TWO_COLUMN_MIN_WIDTH = "560px";
7
7
 
8
8
  export const detail = style({
9
9
  containerType: "inline-size",
10
+ maxHeight: "100dvh",
11
+ overflowY: "auto",
10
12
  padding: primitives.space.s,
11
13
  // Clear the notch / home indicator when the page bleeds to the screen edge.
12
14
  paddingBottom: `calc(${primitives.space.s} + env(safe-area-inset-bottom))`,
13
15
  });
14
16
 
17
+ // Product / variant id, idiomatically top-right. @todo: reposition (drop the
18
+ // right align, or move it in the JSX) or remove it.
19
+ export const reference = style({
20
+ textAlign: "right",
21
+ color: colors.content.subtle,
22
+ marginBottom: primitives.space["3xs"],
23
+ });
24
+
15
25
  // Single column by default; two columns (gallery | info) once the pane is wide.
16
26
  export const grid = style({
17
27
  display: "flex",
@@ -55,26 +65,31 @@ export const title = style({ color: colors.content.intense });
55
65
 
56
66
  export const price = style({ color: colors.content.intense });
57
67
 
58
- export const oos = style({ color: colors.common.error });
59
-
60
- export const specs = style({
68
+ // Product facts as a simple list: one line per fact, "label: value" (or just
69
+ // the value when unlabeled). marginTop separates it from the section heading.
70
+ // @todo: reshape freely (a table, grouped sections, inline chips…)
71
+ export const specList = style({
61
72
  display: "flex",
62
73
  flexDirection: "column",
63
- gap: primitives.space["4xs"],
74
+ gap: primitives.space["3xs"],
75
+ marginTop: primitives.space["3xs"],
76
+ marginBottom: 0,
64
77
  });
65
78
 
79
+ // One fact on a line: "label:" then value.
66
80
  export const specRow = style({
67
81
  display: "flex",
68
- justifyContent: "space-between",
69
- gap: primitives.space.s,
70
- borderBottom: `${primitives.stroke.thin} solid ${colors.border.thin}`,
71
- paddingBottom: primitives.space["4xs"],
82
+ gap: primitives.space["4xs"],
83
+ });
84
+
85
+ export const specLabel = style({
86
+ margin: 0,
87
+ color: colors.content.subtle,
72
88
  });
73
89
 
74
- export const specName = style({ color: colors.content.subtle });
75
90
  export const specValue = style({
91
+ margin: 0,
76
92
  color: colors.content.intense,
77
- textAlign: "right",
78
93
  });
79
94
 
80
95
  // Primary CTA. @todo: tune to your brand; add a secondary action beside it
@@ -1,4 +1,4 @@
1
- import type { Product } from "../../../tools/render-carousel.js";
1
+ import type { Product } from "../../../types.js";
2
2
  import { DetailView } from "./index";
3
3
 
4
4
  function shot(fill: string) {
@@ -29,25 +29,42 @@ function galleryMedia(primaryFill: string): string[] {
29
29
  const DESCRIPTION =
30
30
  "A relaxed-fit jacket in water-repellent cotton.\n\nDropped shoulders, a two-way zip, and ribbed cuffs. Fully lined, with two zip pockets at the front and one inside.";
31
31
 
32
- // Two axes with a sparse variant set: {black,M}, {black,L}, {sand,M} exist —
33
- // so choosing "Sand" disables "L".
32
+ // One story, every picker behavior:
33
+ // - Hole in the matrix: {sand,L} does not exist, so under Sand, L is
34
+ // hard-disabled; switching to Sand while L is selected snaps Size to M.
35
+ // - Sold out: {black,L} renders struck but selectable; the CTA locks as
36
+ // "Out of stock".
37
+ // - Axis not applicable: the indigo colorway is one-size (no `size` key), so
38
+ // choosing "Indigo" hides the Size row; indigo resolves and stays buyable.
34
39
  function jacket(
35
40
  id: string,
36
41
  color: string,
37
42
  colorLabel: string,
38
- size: string,
43
+ size: string | null,
44
+ outOfStock = false,
39
45
  ): Product["variants"][number] {
46
+ const fill =
47
+ color === "sand" ? "d8c7a8" : color === "indigo" ? "3f4a8a" : "2b2b2b";
40
48
  return {
41
49
  id,
42
- selection: { color, size },
50
+ selection: size == null ? { color } : { color, size },
43
51
  title: `Field jacket — ${colorLabel}`,
44
52
  description: DESCRIPTION,
45
- price: { amount: color === "sand" ? 249 : 229, currency: "EUR" },
46
- media: galleryMedia(color === "sand" ? "d8c7a8" : "2b2b2b"),
53
+ price: { amount: color === "black" ? 229 : 249, currency: "EUR" },
54
+ media: galleryMedia(fill),
47
55
  url: "https://example.com/jacket",
48
- attributes: [
49
- { name: "Material", value: "Water-repellent cotton" },
50
- { name: "Fit", value: "Relaxed" },
56
+ outOfStock,
57
+ specs: [
58
+ { label: "Material", value: "Water-repellent cotton" },
59
+ { label: "Lining", value: "Recycled polyester" },
60
+ { label: "Fit", value: "Relaxed" },
61
+ { label: "Closure", value: "Two-way zip" },
62
+ { label: "Pockets", value: "Two zip front, one inside" },
63
+ { label: "Collar", value: "Stand-up" },
64
+ { label: "Cuffs", value: "Ribbed" },
65
+ { label: "Weight", value: "780 g" },
66
+ { label: "Origin", value: "Made in Portugal" },
67
+ { value: "Machine washable" }, // label-less fact (renders value only)
51
68
  ],
52
69
  };
53
70
  }
@@ -61,6 +78,7 @@ const PRODUCT: Product = {
61
78
  values: [
62
79
  { id: "black", label: "Black", media: shot("2b2b2b") },
63
80
  { id: "sand", label: "Sand", media: shot("d8c7a8") },
81
+ { id: "indigo", label: "Indigo", media: shot("3f4a8a") },
64
82
  ],
65
83
  },
66
84
  {
@@ -74,14 +92,15 @@ const PRODUCT: Product = {
74
92
  ],
75
93
  variants: [
76
94
  jacket("fj-black-m", "black", "Black", "m"),
77
- jacket("fj-black-l", "black", "Black", "l"),
95
+ jacket("fj-black-l", "black", "Black", "l", true),
78
96
  jacket("fj-sand-m", "sand", "Sand", "m"),
97
+ jacket("fj-indigo", "indigo", "Indigo", null),
79
98
  ],
80
99
  card: {
81
100
  title: "Field jacket",
82
101
  price: { amount: 229, currency: "EUR" },
83
102
  media: [shot("2b2b2b")],
84
- attributes: [],
103
+ specs: [],
85
104
  },
86
105
  };
87
106
 
@@ -100,14 +119,14 @@ const SIMPLE: Product = {
100
119
  price: { amount: 39, currency: "EUR" },
101
120
  media: [shot("e1e1e1")],
102
121
  url: "https://example.com/tote",
103
- attributes: [{ name: "Material", value: "Canvas" }],
122
+ specs: [{ label: "Material", value: "Canvas" }],
104
123
  },
105
124
  ],
106
125
  card: {
107
126
  title: "Canvas tote",
108
127
  price: { amount: 39, currency: "EUR" },
109
128
  media: [shot("e1e1e1")],
110
- attributes: [],
129
+ specs: [],
111
130
  },
112
131
  };
113
132
 
@@ -12,7 +12,7 @@ import {
12
12
  resolveVariant,
13
13
  type Selection,
14
14
  } from "../../../lib/variants.js";
15
- import type { Product } from "../../../tools/render-carousel.js";
15
+ import type { Product, Variant } from "../../../types.js";
16
16
  import * as styles from "./detail.css";
17
17
 
18
18
  // Price to show: the resolved variant's price, else the range across variants
@@ -54,31 +54,42 @@ function priceText(
54
54
  // scoped to the visible selection.
55
55
  function grounding(
56
56
  product: Product,
57
+ variant: Variant | undefined,
57
58
  title: string,
58
59
  price: string,
59
60
  selection: Selection,
60
- outOfStock: boolean,
61
+ unpurchasable: boolean,
61
62
  labels: Labels,
62
63
  ): string {
63
64
  const parts = [`The user is viewing "${title}" (product id: ${product.id}).`];
64
65
  const chosen: string[] = [];
65
66
  for (const option of product.options) {
66
- let label = "not selected";
67
67
  const valueId = selection[option.id];
68
+ let label: string | undefined;
68
69
  for (const value of option.values) {
69
70
  if (value.id === valueId) {
70
71
  label = value.label;
71
72
  break;
72
73
  }
73
74
  }
75
+ if (label === undefined) {
76
+ // Axis skipped by the shown variant vs simply not picked yet.
77
+ label =
78
+ variant != null && variant.selection[option.id] == null
79
+ ? "not applicable"
80
+ : "not selected";
81
+ }
74
82
  chosen.push(`${option.label}: ${label}`);
75
83
  }
76
84
  if (chosen.length > 0) {
77
85
  parts.push(`Selected — ${chosen.join(", ")}.`);
78
86
  }
79
87
  parts.push(`Price: ${price}.`);
80
- if (outOfStock) {
81
- parts.push(labels.outOfStock);
88
+ if (unpurchasable) {
89
+ // Sold-out real variant vs a combination that does not exist.
90
+ parts.push(
91
+ variant != null ? labels.outOfStock : labels.combinationUnavailable,
92
+ );
82
93
  }
83
94
  return parts.join(" ");
84
95
  }
@@ -98,19 +109,27 @@ export function DetailView({ product }: { product: Product }) {
98
109
  initialSelection(product),
99
110
  );
100
111
 
101
- const variant = resolveVariant(product, selection);
102
- // Fall back to the card for display fields until a variant resolves.
103
- const displayTitle = variant?.title ?? product.card.title;
104
- const description = variant?.description ?? product.card.description;
105
- const media = variant?.media.length ? variant.media : product.card.media;
106
- const attributes = variant?.attributes ?? product.card.attributes;
107
- const outOfStock = variant?.outOfStock ?? product.card.outOfStock ?? false;
108
- const url = variant?.url ?? product.card.url;
112
+ // The exact variant for the selection; card fields fill in when none
113
+ // resolves (the applyChoice edge case, or a product with no variants).
114
+ const shown = resolveVariant(product, selection);
115
+ const displayTitle = shown?.title ?? product.card.title;
116
+ const description = shown?.description ?? product.card.description;
117
+ const media = shown?.media.length ? shown.media : product.card.media;
118
+ const specs = shown?.specs ?? product.card.specs;
119
+ // Sold out, or no variant resolved.
120
+ const unpurchasable = shown ? (shown.outOfStock ?? false) : true;
121
+ const url = shown?.url ?? product.card.url;
122
+ // The shown item's id: the resolved variant's SKU, else the product id.
123
+ const reference = shown?.id ?? product.id;
124
+ // @todo: read any custom Meta fields you added the same way (shown first,
125
+ // then card), e.g. `const rating = shown?.rating ?? product.card.rating;`,
126
+ // then render them in the agreed spot (rating by the title, discount by the
127
+ // price, badges as chips…).
109
128
 
110
- const price = priceText(product, variant?.price, locale, labels);
111
- // Buy CTA is enabled only once a variant resolves and carries a real link
129
+ const price = priceText(product, shown?.price, locale, labels);
130
+ // Buy CTA is enabled only for an exact, in-stock variant with a real link
112
131
  // (an empty url would leave the button enabled but dead).
113
- const canBuy = variant != null && Boolean(url);
132
+ const canBuy = shown != null && Boolean(url) && !unpurchasable;
114
133
 
115
134
  // Point the host's fullscreen "Open in app" affordance at the selected
116
135
  // variant's page. Apps-SDK only; MCP Apps hosts reject, so ignore that.
@@ -125,13 +144,21 @@ export function DetailView({ product }: { product: Product }) {
125
144
  className={styles.detail}
126
145
  data-llm={grounding(
127
146
  product,
147
+ shown,
128
148
  displayTitle,
129
149
  price,
130
150
  selection,
131
- outOfStock,
151
+ unpurchasable,
132
152
  labels,
133
153
  )}
134
154
  >
155
+ {/* Product / variant reference, idiomatically top-right. @todo: move it
156
+ (e.g. into the info column under the title) or drop it; restyle in
157
+ detail.css.ts (`reference`). */}
158
+ <p className={cx(text({ style: "bodyS" }), styles.reference)}>
159
+ {labels.reference} {reference}
160
+ </p>
161
+
135
162
  <div className={styles.grid}>
136
163
  <div className={styles.galleryCell}>
137
164
  {media.length > 0 ? (
@@ -159,12 +186,6 @@ export function DetailView({ product }: { product: Product }) {
159
186
  {price}
160
187
  </p>
161
188
 
162
- {outOfStock ? (
163
- <p className={cx(text({ style: "bodyS" }), styles.oos)}>
164
- {labels.outOfStock}
165
- </p>
166
- ) : null}
167
-
168
189
  <VariantPicker
169
190
  product={product}
170
191
  selection={selection}
@@ -173,39 +194,57 @@ export function DetailView({ product }: { product: Product }) {
173
194
 
174
195
  {description ? <ExpandableText>{description}</ExpandableText> : null}
175
196
 
197
+ {/* Sold-out and unresolved selections stay composable; the CTA locks
198
+ and its label names the cause. */}
176
199
  <button
177
200
  type="button"
178
201
  className={cx(styles.cta, sprinkles({ mt: "3xs" }))}
179
202
  disabled={!canBuy}
180
203
  onClick={canBuy && url ? () => openExternal(url) : undefined}
181
204
  >
182
- {variant ? labels.viewOnSite : labels.selectOptions}
205
+ {shown
206
+ ? unpurchasable
207
+ ? labels.outOfStock
208
+ : labels.viewOnSite
209
+ : labels.combinationUnavailable}
183
210
  </button>
184
211
 
185
- {/* Specs rendered as a simple table, after the CTA. This is the visual
186
- treatment only — the full spec is already in view state for the
187
- model (see the carousel orchestrator), so hiding the table never
188
- hides specs from the assistant. @todo: group/collapse, restyle, or
189
- drop the table entirely. */}
190
- {attributes.length > 0 ? (
191
- <section className={styles.specs}>
212
+ {/* Product facts as a simple list, after the CTA: one "label: value"
213
+ line per fact (label-less facts show the value alone). This is the
214
+ visual treatment only; the full spec is already in view state for
215
+ the model (see the carousel orchestrator), so restyling or dropping
216
+ it never hides facts from the assistant. @todo: pick the shape that
217
+ best presents your specs: this list (default), a two-column table,
218
+ grouped sections, inline chips, etc. */}
219
+ {specs.length > 0 ? (
220
+ <section>
192
221
  <h2 className={text({ style: "labelM", weight: "medium" })}>
193
222
  {labels.specifications}
194
223
  </h2>
195
- {attributes.map((attribute) => (
196
- <div key={attribute.name} className={styles.specRow}>
197
- <span
198
- className={cx(text({ style: "bodyS" }), styles.specName)}
199
- >
200
- {attribute.name}
201
- </span>
202
- <span
203
- className={cx(text({ style: "bodyS" }), styles.specValue)}
224
+ <dl className={styles.specList}>
225
+ {specs.map((spec) => (
226
+ <div
227
+ key={`${spec.label ?? ""}:${spec.value}`}
228
+ className={styles.specRow}
204
229
  >
205
- {attribute.value}
206
- </span>
207
- </div>
208
- ))}
230
+ {spec.label ? (
231
+ <dt
232
+ className={cx(
233
+ text({ style: "bodyS" }),
234
+ styles.specLabel,
235
+ )}
236
+ >
237
+ {spec.label}:
238
+ </dt>
239
+ ) : null}
240
+ <dd
241
+ className={cx(text({ style: "bodyS" }), styles.specValue)}
242
+ >
243
+ {spec.value}
244
+ </dd>
245
+ </div>
246
+ ))}
247
+ </dl>
209
248
  </section>
210
249
  ) : null}
211
250
  </div>
@@ -20,8 +20,7 @@ import { sprinkles } from "../../design/tokens";
20
20
  import { useToolInfo } from "../../helpers.js";
21
21
  import { useLabels } from "../../i18n";
22
22
  import { formatPrice } from "../../lib/format";
23
- import type { Product } from "../../tools/render-carousel.js";
24
- import type { Attribute, Price } from "../../types.js";
23
+ import type { Price, Product, Spec } from "../../types.js";
25
24
  import { DetailView } from "./detail";
26
25
 
27
26
  const SKELETON_COUNT = 4;
@@ -43,7 +42,7 @@ type VariantSpec = {
43
42
  selection: Record<string, string>; // option label -> chosen value label
44
43
  price?: Price;
45
44
  available: boolean;
46
- attributes: Attribute[];
45
+ specs: Spec[];
47
46
  };
48
47
  type ProductSpec = { id: string; title: string; variants: VariantSpec[] };
49
48
  type ViewState = {
@@ -78,7 +77,7 @@ function buildProductSpec(product: Product): ProductSpec {
78
77
  selection,
79
78
  price: variant.price,
80
79
  available: !variant.outOfStock,
81
- attributes: variant.attributes,
80
+ specs: variant.specs,
82
81
  });
83
82
  }
84
83
  return { id: product.id, title: product.card.title, variants };
@@ -1,6 +1,6 @@
1
+ import { skybridge } from "@skybridge/vite-plugin";
1
2
  import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin";
2
3
  import react from "@vitejs/plugin-react";
3
- import { skybridge } from "skybridge/vite";
4
4
  import { defineConfig, type PluginOption } from "vite";
5
5
 
6
6
  export default defineConfig({