create-ollie-shop 1.1.0 → 1.3.1

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.
@@ -1,5 +1,5 @@
1
1
 
2
- > create-ollie-shop@1.1.0 build /home/runner/work/ollie-shop/ollie-shop/packages/create-ollie-shop
2
+ > create-ollie-shop@1.3.1 build /home/runner/work/ollie-shop/ollie-shop/packages/create-ollie-shop
3
3
  > tsup
4
4
 
5
5
  CLI Building entry: src/index.ts
@@ -9,5 +9,5 @@
9
9
  CLI Target: es2022
10
10
  CLI Cleaning output folder
11
11
  ESM Build start
12
- ESM dist/index.js 5.83 KB
13
- ESM ⚡️ Build success in 209ms
12
+ ESM dist/index.js 7.76 KB
13
+ ESM ⚡️ Build success in 176ms
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # create-ollie-shop
2
2
 
3
+ ## 1.3.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 7c76974: Remove two orphaned files from `template/.claude/` that no longer have any consumer: `build-component.mjs` (one-shot esbuild wrapper used by the old `capability.studio_harness` skill) and `registry.json` (loading-order manifest for the old `.claude/skills/*.md` + `.claude/agents/*.md` files removed earlier). Both referenced content that lives in the standardized `@ollie-shop/skills` package now.
8
+
9
+ ## 1.3.0
10
+
11
+ ### Minor Changes
12
+
13
+ - bca53ae: Install Ollie agent skills into the project on scaffolding. After copying the template, `create-ollie-shop` downloads the latest release tag of the public skill mirror at `github.com/ollie-shop/skills`, extracts `skills/ollie-shop/` into the new project's `.claude/skills/ollie-shop/` directory, and stamps `.claude/skills/ollie-shop/.version` with the installed version. The install path matches the default location of `npx skills add ollie-shop/skills` so customers see the same `.claude/skills/ollie-shop/` regardless of which onboarding path they choose. The stale `template/.claude/skills/` and `template/.claude/agents/` are removed in favor of the live download.
14
+
15
+ ## 1.2.0
16
+
17
+ ### Minor Changes
18
+
19
+ - bac681e: Scaffold an agentic Studio workflow into new projects: an `ollie_checkout_studio` agent plus `capability.design_reference` and `capability.studio_harness` skills, a server-less build helper (`.claude/build-component.mjs`), and a `chrome-devtools` MCP config. This lets an agent ingest a visual reference (a live site, Figma, or screenshot), build the component independently with esbuild, and inject its source into the store's real checkout via the hosted studio harness at `admin.ollie.shop/studio-harness.html` (same-site, so no cookie handling) — iterating from screenshots, then deploying.
20
+
3
21
  ## 1.1.0
4
22
 
5
23
  ### Minor Changes
package/dist/index.js CHANGED
@@ -10,13 +10,21 @@ import { z } from "zod";
10
10
  // src/create-project.ts
11
11
  import { exec } from "child_process";
12
12
  import fs from "fs/promises";
13
+ import os from "os";
13
14
  import path from "path";
15
+ import { Readable } from "stream";
16
+ import { pipeline } from "stream/promises";
14
17
  import { fileURLToPath } from "url";
15
18
  import { promisify } from "util";
16
19
  import fsExtra from "fs-extra";
17
20
  import slugify from "slugify";
21
+ import * as tar from "tar";
18
22
  var execAsync = promisify(exec);
19
23
  var __dirname = path.dirname(fileURLToPath(import.meta.url));
24
+ var SKILLS_MIRROR_OWNER = "ollie-shop";
25
+ var SKILLS_MIRROR_REPO = "skills";
26
+ var SKILLS_API_BASE = `https://api.github.com/repos/${SKILLS_MIRROR_OWNER}/${SKILLS_MIRROR_REPO}`;
27
+ var SKILLS_TARBALL_BASE = `https://codeload.github.com/${SKILLS_MIRROR_OWNER}/${SKILLS_MIRROR_REPO}/tar.gz`;
20
28
  async function copyWithTemplate(from, to, variables) {
21
29
  const dirname = path.dirname(to);
22
30
  await fsExtra.ensureDir(dirname);
@@ -66,6 +74,48 @@ async function copyTemplateDir(templateDir, targetDir, variables) {
66
74
  }
67
75
  }
68
76
  }
77
+ async function fetchLatestSkillsTag() {
78
+ const res = await fetch(`${SKILLS_API_BASE}/tags`);
79
+ if (!res.ok) {
80
+ throw new Error(
81
+ `Failed to list tags from ${SKILLS_MIRROR_OWNER}/${SKILLS_MIRROR_REPO}: ${res.status} ${res.statusText}`
82
+ );
83
+ }
84
+ const tags = await res.json();
85
+ if (tags.length === 0) {
86
+ throw new Error(
87
+ `No release tags published on ${SKILLS_MIRROR_OWNER}/${SKILLS_MIRROR_REPO} yet.`
88
+ );
89
+ }
90
+ return tags[0].name;
91
+ }
92
+ async function installSkills(projectPath) {
93
+ const tag = await fetchLatestSkillsTag();
94
+ const version = tag.startsWith("v") ? tag.slice(1) : tag;
95
+ const tarballUrl = `${SKILLS_TARBALL_BASE}/refs/tags/${tag}`;
96
+ const res = await fetch(tarballUrl);
97
+ if (!res.ok || !res.body) {
98
+ throw new Error(
99
+ `Failed to download ${tarballUrl}: ${res.status} ${res.statusText}`
100
+ );
101
+ }
102
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ollie-skills-"));
103
+ try {
104
+ await pipeline(
105
+ Readable.fromWeb(res.body),
106
+ tar.x({ cwd: tempDir })
107
+ );
108
+ const entries = await fs.readdir(tempDir);
109
+ const archiveRoot = path.join(tempDir, entries[0]);
110
+ const skillSrc = path.join(archiveRoot, "skills", "ollie-shop");
111
+ const skillDest = path.join(projectPath, ".claude", "skills", "ollie-shop");
112
+ await fsExtra.copy(skillSrc, skillDest);
113
+ await fs.writeFile(path.join(skillDest, ".version"), `${version}
114
+ `);
115
+ } finally {
116
+ await fs.rm(tempDir, { recursive: true, force: true });
117
+ }
118
+ }
69
119
  async function createProject(projectPath, options) {
70
120
  const { storeId, versionId } = options;
71
121
  const projectName = slugify(path.basename(projectPath), { lower: true });
@@ -89,6 +139,8 @@ async function createProject(projectPath, options) {
89
139
  ollieConfig.versionId = versionId;
90
140
  await fs.writeFile(ollieConfigPath, JSON.stringify(ollieConfig, null, 2));
91
141
  }
142
+ console.log("\u{1F9E0} Installing Ollie skills...");
143
+ await installSkills(projectPath);
92
144
  console.log("\u{1F527} Initializing git repository...");
93
145
  try {
94
146
  await execAsync("git init", { cwd: projectPath });
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "chrome-devtools": {
4
+ "command": "npx",
5
+ "args": ["-y", "chrome-devtools-mcp@latest"]
6
+ }
7
+ }
8
+ }
@@ -8,9 +8,10 @@
8
8
  },
9
9
  "devDependencies": {
10
10
  "@ollie-shop/cli": "latest",
11
- "@ollie-shop/sdk": "latest",
11
+ "@ollie-shop/sdk": "^1",
12
12
  "@types/node": "^22.14.0",
13
13
  "@types/react": "^19.2.7",
14
+ "esbuild": "^0.25.0",
14
15
  "react": "^19.0.0",
15
16
  "typescript": "^5.7.3"
16
17
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-ollie-shop",
3
- "version": "1.1.0",
3
+ "version": "1.3.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -10,6 +10,7 @@
10
10
  "devDependencies": {
11
11
  "@types/fs-extra": "^11.0.4",
12
12
  "@types/node": "^22.14.0",
13
+ "@types/tar": "^6.1.13",
13
14
  "tsup": "^8.4.0",
14
15
  "typescript": "^5.7.3",
15
16
  "vitest": "^3.0.4"
@@ -18,6 +19,7 @@
18
19
  "fs-extra": "^11.3.0",
19
20
  "meow": "^13.2.0",
20
21
  "slugify": "^1.6.6",
22
+ "tar": "^7.4.3",
21
23
  "zod": "^3.24.2"
22
24
  },
23
25
  "publishConfig": {
@@ -1,15 +1,24 @@
1
1
  import { exec } from "node:child_process";
2
2
  import fs from "node:fs/promises";
3
+ import os from "node:os";
3
4
  import path from "node:path";
5
+ import { Readable } from "node:stream";
6
+ import { pipeline } from "node:stream/promises";
4
7
  import { fileURLToPath } from "node:url";
5
8
  import { promisify } from "node:util";
6
9
  import fsExtra from "fs-extra";
7
10
  import slugify from "slugify";
11
+ import * as tar from "tar";
8
12
 
9
13
  const execAsync = promisify(exec);
10
14
 
11
15
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
16
 
17
+ const SKILLS_MIRROR_OWNER = "ollie-shop";
18
+ const SKILLS_MIRROR_REPO = "skills";
19
+ const SKILLS_API_BASE = `https://api.github.com/repos/${SKILLS_MIRROR_OWNER}/${SKILLS_MIRROR_REPO}`;
20
+ const SKILLS_TARBALL_BASE = `https://codeload.github.com/${SKILLS_MIRROR_OWNER}/${SKILLS_MIRROR_REPO}/tar.gz`;
21
+
13
22
  interface CreateProjectOptions {
14
23
  storeId: string;
15
24
  versionId?: string;
@@ -101,6 +110,68 @@ async function copyTemplateDir(
101
110
  }
102
111
  }
103
112
 
113
+ /**
114
+ * Ask the public mirror at github.com/ollie-shop/skills for its newest tag.
115
+ * Tags are created by the sync workflow on each release (vX.Y.Z); the GitHub
116
+ * API returns them ordered by creation date descending, so the first entry is
117
+ * the latest release.
118
+ */
119
+ async function fetchLatestSkillsTag(): Promise<string> {
120
+ const res = await fetch(`${SKILLS_API_BASE}/tags`);
121
+ if (!res.ok) {
122
+ throw new Error(
123
+ `Failed to list tags from ${SKILLS_MIRROR_OWNER}/${SKILLS_MIRROR_REPO}: ${res.status} ${res.statusText}`,
124
+ );
125
+ }
126
+ const tags = (await res.json()) as Array<{ name: string }>;
127
+ if (tags.length === 0) {
128
+ throw new Error(
129
+ `No release tags published on ${SKILLS_MIRROR_OWNER}/${SKILLS_MIRROR_REPO} yet.`,
130
+ );
131
+ }
132
+ return tags[0].name;
133
+ }
134
+
135
+ /**
136
+ * Download the ollie-shop skill from the public mirror and install it into
137
+ * the new project's `.claude/skills/ollie-shop/` directory (the default
138
+ * install location the vercel-labs/skills CLI uses). The installed version
139
+ * is recorded in `.claude/skills/ollie-shop/.version` so future updates can
140
+ * detect drift.
141
+ */
142
+ async function installSkills(projectPath: string): Promise<void> {
143
+ const tag = await fetchLatestSkillsTag();
144
+ const version = tag.startsWith("v") ? tag.slice(1) : tag;
145
+
146
+ const tarballUrl = `${SKILLS_TARBALL_BASE}/refs/tags/${tag}`;
147
+ const res = await fetch(tarballUrl);
148
+ if (!res.ok || !res.body) {
149
+ throw new Error(
150
+ `Failed to download ${tarballUrl}: ${res.status} ${res.statusText}`,
151
+ );
152
+ }
153
+
154
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ollie-skills-"));
155
+ try {
156
+ // Extract the full tarball; the archive wraps everything in a single
157
+ // top-level directory like `ollie-shop-skills-<sha>/`.
158
+ await pipeline(
159
+ Readable.fromWeb(res.body as never),
160
+ tar.x({ cwd: tempDir }),
161
+ );
162
+
163
+ const entries = await fs.readdir(tempDir);
164
+ const archiveRoot = path.join(tempDir, entries[0]);
165
+ const skillSrc = path.join(archiveRoot, "skills", "ollie-shop");
166
+ const skillDest = path.join(projectPath, ".claude", "skills", "ollie-shop");
167
+
168
+ await fsExtra.copy(skillSrc, skillDest);
169
+ await fs.writeFile(path.join(skillDest, ".version"), `${version}\n`);
170
+ } finally {
171
+ await fs.rm(tempDir, { recursive: true, force: true });
172
+ }
173
+ }
174
+
104
175
  /**
105
176
  * Create a new Ollie Shop project
106
177
  */
@@ -144,6 +215,11 @@ export async function createProject(
144
215
  await fs.writeFile(ollieConfigPath, JSON.stringify(ollieConfig, null, 2));
145
216
  }
146
217
 
218
+ // Install Ollie skills from the public mirror into .claude/skills/ollie-shop/
219
+ // (the default location vercel-labs/skills CLI uses for `npx skills add`)
220
+ console.log("🧠 Installing Ollie skills...");
221
+ await installSkills(projectPath);
222
+
147
223
  // Initialize git
148
224
  console.log("🔧 Initializing git repository...");
149
225
  try {
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "chrome-devtools": {
4
+ "command": "npx",
5
+ "args": ["-y", "chrome-devtools-mcp@latest"]
6
+ }
7
+ }
8
+ }
@@ -8,9 +8,10 @@
8
8
  },
9
9
  "devDependencies": {
10
10
  "@ollie-shop/cli": "latest",
11
- "@ollie-shop/sdk": "latest",
11
+ "@ollie-shop/sdk": "^1",
12
12
  "@types/node": "^22.14.0",
13
13
  "@types/react": "^19.2.7",
14
+ "esbuild": "^0.25.0",
14
15
  "react": "^19.0.0",
15
16
  "typescript": "^5.7.3"
16
17
  }
@@ -1,18 +0,0 @@
1
- ---
2
- name: ollie_custom_components_frontend
3
- description: "Use this agent when the user asks to create, build, or implement a React UI component for Ollie Checkout — for example: 'create a coupon input component', 'build this component from the screenshot', or 'implement this Figma design as a component'."
4
- model: sonnet
5
- scope: component
6
- ---
7
-
8
- Before doing anything else, use the Read tool to load your skill files in this exact order:
9
-
10
- 1. `.claude/skills/core.identity.md`
11
- 2. `.claude/skills/core.rules.md`
12
- 3. `.claude/skills/core.constraints.md`
13
- 4. `.claude/skills/agent.components.md`
14
- 5. `.claude/skills/capability.react.md`
15
- 6. `.claude/skills/capability.sdk.md`
16
- 7. `.claude/skills/capability.component_architecture.md`
17
-
18
- After reading all files, follow the instructions defined in `agent.components.md` to assist the user.
@@ -1,22 +0,0 @@
1
- {
2
- "version": "1.0.0",
3
- "_maintenance": "Ollie Checkout Skill Runtime Registry. Defines the deterministic loading order for all skills. To add a new skill: create the .md file in skills/, add it here in the correct section, and reference it in the agent file.",
4
- "_skillDescriptions": {
5
- "core.identity": "Agent persona, priorities and communication style",
6
- "core.rules": "Agent behavior rules and component code rules",
7
- "core.constraints": "SDK usage rules, domain isolation, and hard limits for components",
8
- "agent.components": "4-step workflow for component creation (inputs, questions, output, quality)",
9
- "capability.react": "React/TypeScript standards: functional components, hooks, naming, prop patterns",
10
- "capability.sdk": "Full @ollie-shop/sdk reference: hooks, types, action types, integration pattern",
11
- "capability.component_architecture": "Folder structure, CSS naming convention, image handling, README template"
12
- },
13
- "core": ["core.identity", "core.rules", "core.constraints"],
14
- "agents": {
15
- "components": [
16
- "agent.components",
17
- "capability.react",
18
- "capability.sdk",
19
- "capability.component_architecture"
20
- ]
21
- }
22
- }
@@ -1,72 +0,0 @@
1
- ---
2
- name: agent.components
3
- type: agent
4
- version: 1.0.0
5
- description: >
6
- Workflow for creating React UI components for Ollie Checkout. Defines the 4-step
7
- process: required inputs → clarifying questions → generate files → run lint.
8
- scope: component
9
- ---
10
-
11
- # Agent: Components
12
-
13
- ## Mission
14
-
15
- Help a designer build self-contained React UI components for the Ollie Checkout.
16
-
17
- ## Step 1 — Required Inputs (ask before coding)
18
-
19
- Before writing any code, ask the user for:
20
-
21
- 1. **Component screenshot/print** — the exact UI to implement.
22
- 2. **A `.txt` file with CSS references extracted from Figma** — spacing, typography, colors, radii.
23
- 3. **The component name** — used as the folder name and CSS class prefix.
24
-
25
- If any of these are missing, ask for them first.
26
-
27
- ## Step 2 — Clarifying Questions (ask after receiving inputs)
28
-
29
- Ask about what you cannot infer from the screenshot alone. Group related questions together to avoid back-and-forth. Typical topics:
30
-
31
- - **States & interactions**: Which states exist (loading, error, success, empty, disabled)? Are there hover/focus effects or animations?
32
- - **Responsiveness**: Are there layout changes at specific breakpoints, or does it scale fluidly?
33
- - **Icons**: Will the designer provide SVGs, or should you suggest an icon library?
34
-
35
- Skip questions that are clearly answered by the screenshot or Figma file.
36
-
37
- ## Step 3 — Generate Output
38
-
39
- Produce the full folder structure:
40
-
41
- ```
42
- ComponentName/
43
- index.tsx
44
- ComponentName.module.css
45
- README.md
46
- components/
47
- SubComponentName/
48
- index.tsx
49
- SubComponentName.module.css
50
- ```
51
-
52
- See `capability.component_architecture` for structure rules and CSS naming.
53
- See `capability.react` for React and TypeScript rules.
54
- See `capability.sdk` for SDK integration patterns and how to document integration points.
55
-
56
- ## Step 4 — Quality
57
-
58
- After generating all files, run `pnpm lint` to fix formatting issues. Only mention it to the designer if there were significant fixes or issues that need attention. Do not let linting block the designer's progress.
59
-
60
- ## Final Checklist
61
-
62
- Before answering, verify:
63
-
64
- - [ ] Screenshot/print, Figma CSS `.txt`, and component name were provided.
65
- - [ ] Clarifying questions were asked about states, behaviors, responsiveness, and icons.
66
- - [ ] Full folder structure generated (`index.tsx`, `styles.module.css`, README, subcomponents).
67
- - [ ] Every `.tsx` file includes `import React from 'react';`.
68
- - [ ] All CSS classes are in camelCase and correctly prefixed.
69
- - [ ] Images referenced via dedicated `<img>` subcomponents.
70
- - [ ] No direct use of `window` / `document` / `navigator`.
71
- - [ ] README is in English with the defined structure, including SDK integration notes.
72
- - [ ] `pnpm lint` was run.
@@ -1,102 +0,0 @@
1
- ---
2
- name: capability.component_architecture
3
- type: capability
4
- version: 1.0.0
5
- description: >
6
- Rules for folder structure, file naming, CSS class naming, image handling,
7
- README format, and accessibility guidelines.
8
- loads_with:
9
- - agent.components
10
- ---
11
-
12
- # Capability: Component Architecture
13
-
14
- ## Folder Structure
15
-
16
- ```
17
- ComponentName/
18
- index.tsx ← parent component (named export)
19
- ComponentName.module.css ← parent styles
20
- README.md ← English docs
21
- components/
22
- SubComponentName/
23
- index.tsx
24
- SubComponentName.module.css
25
- components/ ← only if SubComponentName has nested children
26
- NestedSubComponent/
27
- index.tsx
28
- NestedSubComponent.module.css
29
- ```
30
-
31
- - Parent file: always `index.tsx`, never `ComponentName.tsx`.
32
- - Always create subcomponents — keep parent JSX readable and maintainable.
33
- - Each subcomponent has its own `styles.module.css`.
34
-
35
- ## CSS Class Naming
36
-
37
- All classes must be **camelCase** and **prefixed**:
38
-
39
- - Each component uses its **own name** as the prefix → `freeShippingTopBannerRoot`, `messageRotatorRoot`
40
-
41
- ### Figma `.txt` References
42
-
43
- - Use Figma values as source of truth for spacing, typography, colors, radii.
44
- - Rename all Figma class names to match the prefix convention above.
45
- - If values conflict, prefer the ones that match the screenshot.
46
-
47
- ## Styling Rules
48
-
49
- - Use CSS Modules (`styles.module.css`) for all styles.
50
- - Prefer HTML + CSS over JS-driven styling.
51
- - Use flex, grid, gap, padding, margin, border, etc.
52
- - Implement responsiveness with CSS media queries, not `isMobile` props.
53
-
54
- ## Images
55
-
56
- If the component uses images, create a dedicated image subcomponent:
57
-
58
- ```tsx
59
- // ComponentName/components/BrandLogoImage/index.tsx
60
- import React from 'react';
61
- import styles from './BrandLogoImage.module.css';
62
-
63
- export type BrandLogoImageProps = { src: string; alt: string };
64
-
65
- export function BrandLogoImage({ src, alt }: BrandLogoImageProps) {
66
- return <img className={styles.brandLogoImageRoot} src={src} alt={alt} />;
67
- }
68
- ```
69
-
70
- - Use plain `<img>`. Do not assume Next.js `<Image />`.
71
- - Pass `src` as a prop so integrators can wire real assets.
72
-
73
- ## README Structure
74
-
75
- ```md
76
- # ComponentName
77
-
78
- ## Overview
79
- Short description of what the component is and where it fits in the checkout flow.
80
-
81
- ## Props
82
- List each prop, its type, and what it controls visually.
83
-
84
- ## States / Behaviors
85
- Describe the main visual states (default, hover, focus, error, success, disabled, loading).
86
-
87
- ## Usage
88
- Minimal example with fake data and handlers.
89
-
90
- ## Notes for Developers
91
- Which props map to which SDK hooks/actions. See `capability.sdk` for the SDK reference.
92
- ```
93
-
94
- ## Accessibility
95
-
96
- Good to have, not a blocker:
97
-
98
- - `<button>` for clickable actions.
99
- - `<label>` with `htmlFor` linked to inputs.
100
- - `<ul>/<li>` for lists.
101
- - `aria-invalid`, `aria-describedby` for form fields when natural.
102
- - Basic keyboard support for complex interactive elements.
@@ -1,40 +0,0 @@
1
- ---
2
- name: capability.react
3
- type: capability
4
- version: 1.0.0
5
- description: >
6
- React and TypeScript standards for generated components. Covers functional components,
7
- named exports, hook usage, and prop naming conventions.
8
- loads_with:
9
- - agent.components
10
- ---
11
-
12
- # Capability: React
13
-
14
- ## Rules
15
-
16
- - **Functional components only.** No class components.
17
- - **TypeScript required.** All files are `.tsx` with explicit types.
18
- - **Named exports only.** No default exports for components.
19
- - **Every `.tsx` file must start with:** `import React from 'react';`
20
- - This includes `index.tsx` and all subcomponent files.
21
-
22
- ## Internal State
23
-
24
- - `useState` is allowed for UI-only state (focused, open, hovered, animations).
25
- - `useEffect` is allowed only for visual concerns (focus management, animations).
26
- - No `useEffect` for data fetching or business logic.
27
-
28
- ## Naming
29
-
30
- - Components: PascalCase (e.g., `CouponInput`)
31
- - Props interface: `<ComponentName>Props` (e.g., `CouponInputProps`)
32
- - Handler props: `on<Action>` (e.g., `onApply`, `onChange`)
33
- - Boolean props: `is<State>` or `has<Feature>` (e.g., `isLoading`, `isDisabled`)
34
-
35
- ## Props Patterns
36
-
37
- - Inputs/fields: `value`, `onChange`, optional `placeholder`, `label`, `helperText`, `errorMessage`, `isDisabled`
38
- - Actions: `onApplyCoupon`, `onRemoveItem`, `onToggle` — callbacks only, parent decides behavior
39
- - Async UI: `isLoading`, `errorMessage`, `successMessage` — toggle visuals only
40
- - Lists: `items` (array), `onSelectItem`, `onRemoveItem`