@fluentcommerce/ai-skills 0.2.0 → 0.3.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.

Potentially problematic release.


This version of @fluentcommerce/ai-skills might be problematic. Click here for more details.

Files changed (169) hide show
  1. package/README.md +866 -616
  2. package/bin/cli.mjs +2112 -1973
  3. package/content/cli/agents/fluent-cli/agent.json +149 -149
  4. package/content/cli/agents/fluent-cli.md +132 -132
  5. package/content/cli/skills/fluent-bootstrap/SKILL.md +214 -190
  6. package/content/cli/skills/fluent-cli-index/SKILL.md +1 -1
  7. package/content/cli/skills/fluent-cli-mcp-cicd/SKILL.md +117 -1
  8. package/content/cli/skills/fluent-cli-reference/SKILL.md +1040 -623
  9. package/content/cli/skills/fluent-cli-retailer/SKILL.md +27 -2
  10. package/content/cli/skills/fluent-cli-settings/SKILL.md +21 -1
  11. package/content/cli/skills/fluent-connect/SKILL.md +937 -886
  12. package/content/cli/skills/fluent-module-deploy/SKILL.md +181 -17
  13. package/content/cli/skills/fluent-profile/SKILL.md +73 -0
  14. package/content/cli/skills/fluent-workflow/SKILL.md +360 -310
  15. package/content/dev/agents/fluent-backend-dev/AGENT.md +58 -0
  16. package/content/dev/agents/fluent-backend-dev/agent.json +69 -0
  17. package/content/dev/agents/fluent-backend-dev.md +287 -0
  18. package/content/dev/agents/fluent-dev/AGENT.md +98 -76
  19. package/content/dev/agents/fluent-dev/agent.json +24 -2
  20. package/content/dev/agents/fluent-dev.md +194 -524
  21. package/content/dev/agents/fluent-frontend-dev/AGENT.md +63 -0
  22. package/content/dev/agents/fluent-frontend-dev/agent.json +52 -0
  23. package/content/dev/agents/fluent-frontend-dev.md +323 -0
  24. package/content/dev/skills/fluent-archive/SKILL.md +234 -0
  25. package/content/dev/skills/fluent-build/SKILL.md +312 -170
  26. package/content/dev/skills/fluent-connection-analysis/SKILL.md +422 -386
  27. package/content/dev/skills/fluent-custom-code/SKILL.md +15 -9
  28. package/content/dev/skills/fluent-data-module-scaffold/SKILL.md +731 -0
  29. package/content/dev/skills/fluent-e2e-test/SKILL.md +501 -394
  30. package/content/dev/skills/fluent-event-api/SKILL.md +962 -945
  31. package/content/dev/skills/fluent-feature-explain/SKILL.md +680 -603
  32. package/content/dev/skills/fluent-feature-plan/PLAN_TEMPLATE.md +40 -11
  33. package/content/dev/skills/fluent-feature-plan/SKILL.md +478 -221
  34. package/content/dev/skills/fluent-feature-status/SKILL.md +335 -0
  35. package/content/dev/skills/fluent-feedback/SKILL.md +221 -0
  36. package/content/dev/skills/fluent-implementation-map/SKILL.md +644 -0
  37. package/content/dev/skills/fluent-job-batch/SKILL.md +10 -0
  38. package/content/dev/skills/fluent-module-scaffold/SKILL.md +134 -3
  39. package/content/dev/skills/fluent-module-validate/SKILL.md +778 -775
  40. package/content/dev/skills/fluent-mystique-analyze/SKILL.md +817 -0
  41. package/content/dev/skills/fluent-mystique-builder/COMPONENT_TEMPLATE.md +81 -0
  42. package/content/dev/skills/fluent-mystique-builder/README.md +63 -0
  43. package/content/dev/skills/fluent-mystique-builder/SKILL.md +1294 -0
  44. package/content/dev/skills/fluent-mystique-builder/components/INDEX.md +92 -0
  45. package/content/dev/skills/fluent-mystique-builder/components/fc.accordion.md +48 -0
  46. package/content/dev/skills/fluent-mystique-builder/components/fc.action.field.fulfilmentpack.md +20 -0
  47. package/content/dev/skills/fluent-mystique-builder/components/fc.action.field.multiparcel.md +21 -0
  48. package/content/dev/skills/fluent-mystique-builder/components/fc.action.field.returnitems.md +21 -0
  49. package/content/dev/skills/fluent-mystique-builder/components/fc.action.field.wavepick.md +21 -0
  50. package/content/dev/skills/fluent-mystique-builder/components/fc.action.inline.md +24 -0
  51. package/content/dev/skills/fluent-mystique-builder/components/fc.activity.entity.md +25 -0
  52. package/content/dev/skills/fluent-mystique-builder/components/fc.analytics.viz.md +20 -0
  53. package/content/dev/skills/fluent-mystique-builder/components/fc.attribute.column.md +111 -0
  54. package/content/dev/skills/fluent-mystique-builder/components/fc.attribute.json.md +20 -0
  55. package/content/dev/skills/fluent-mystique-builder/components/fc.attribute.jsoneditor.md +54 -0
  56. package/content/dev/skills/fluent-mystique-builder/components/fc.attribute.locationId.md +51 -0
  57. package/content/dev/skills/fluent-mystique-builder/components/fc.attribute.retailerId.md +52 -0
  58. package/content/dev/skills/fluent-mystique-builder/components/fc.button.bar.md +57 -0
  59. package/content/dev/skills/fluent-mystique-builder/components/fc.button.print.download.md +53 -0
  60. package/content/dev/skills/fluent-mystique-builder/components/fc.button.print.inline.compatibility.md +60 -0
  61. package/content/dev/skills/fluent-mystique-builder/components/fc.button.print.inline.md +53 -0
  62. package/content/dev/skills/fluent-mystique-builder/components/fc.button.print.md +24 -0
  63. package/content/dev/skills/fluent-mystique-builder/components/fc.button.print.pick.md +61 -0
  64. package/content/dev/skills/fluent-mystique-builder/components/fc.buttons.add.reject.md +20 -0
  65. package/content/dev/skills/fluent-mystique-builder/components/fc.card.attribute.md +73 -0
  66. package/content/dev/skills/fluent-mystique-builder/components/fc.card.attributes.grid.md +40 -0
  67. package/content/dev/skills/fluent-mystique-builder/components/fc.card.image.md +37 -0
  68. package/content/dev/skills/fluent-mystique-builder/components/fc.card.map.point.md +24 -0
  69. package/content/dev/skills/fluent-mystique-builder/components/fc.card.multi.md +79 -0
  70. package/content/dev/skills/fluent-mystique-builder/components/fc.card.product.md +27 -0
  71. package/content/dev/skills/fluent-mystique-builder/components/fc.chart.area.md +34 -0
  72. package/content/dev/skills/fluent-mystique-builder/components/fc.chart.area.wrapper.feed.md +98 -0
  73. package/content/dev/skills/fluent-mystique-builder/components/fc.chart.bar.md +52 -0
  74. package/content/dev/skills/fluent-mystique-builder/components/fc.chart.bar.wrapper.source.md +104 -0
  75. package/content/dev/skills/fluent-mystique-builder/components/fc.chart.gauge.md +28 -0
  76. package/content/dev/skills/fluent-mystique-builder/components/fc.chart.gauge.wrapper.threshold.md +118 -0
  77. package/content/dev/skills/fluent-mystique-builder/components/fc.chart.line.md +32 -0
  78. package/content/dev/skills/fluent-mystique-builder/components/fc.conditional.md +62 -0
  79. package/content/dev/skills/fluent-mystique-builder/components/fc.dashboard.threshold.md +65 -0
  80. package/content/dev/skills/fluent-mystique-builder/components/fc.daterange.wrapper.forwarder.md +56 -0
  81. package/content/dev/skills/fluent-mystique-builder/components/fc.drawer.button.md +21 -0
  82. package/content/dev/skills/fluent-mystique-builder/components/fc.event.detail.md +20 -0
  83. package/content/dev/skills/fluent-mystique-builder/components/fc.events.search.md +21 -0
  84. package/content/dev/skills/fluent-mystique-builder/components/fc.field.daterange.md +83 -0
  85. package/content/dev/skills/fluent-mystique-builder/components/fc.field.filterComplex.md +106 -0
  86. package/content/dev/skills/fluent-mystique-builder/components/fc.field.intrange.md +82 -0
  87. package/content/dev/skills/fluent-mystique-builder/components/fc.field.multistring.md +50 -0
  88. package/content/dev/skills/fluent-mystique-builder/components/fc.filterPanel.md +53 -0
  89. package/content/dev/skills/fluent-mystique-builder/components/fc.json.editor.md +22 -0
  90. package/content/dev/skills/fluent-mystique-builder/components/fc.json.viewer.md +21 -0
  91. package/content/dev/skills/fluent-mystique-builder/components/fc.list.customAction.md +79 -0
  92. package/content/dev/skills/fluent-mystique-builder/components/fc.list.md +116 -0
  93. package/content/dev/skills/fluent-mystique-builder/components/fc.list.wrapper.bppmetrics.md +69 -0
  94. package/content/dev/skills/fluent-mystique-builder/components/fc.list.wrapper.feed.md +65 -0
  95. package/content/dev/skills/fluent-mystique-builder/components/fc.list.wrapper.source.md +64 -0
  96. package/content/dev/skills/fluent-mystique-builder/components/fc.modal.button.addItem.md +60 -0
  97. package/content/dev/skills/fluent-mystique-builder/components/fc.modal.button.md +21 -0
  98. package/content/dev/skills/fluent-mystique-builder/components/fc.mutation.inline.md +88 -0
  99. package/content/dev/skills/fluent-mystique-builder/components/fc.mystique.collapsible.attributes.md +83 -0
  100. package/content/dev/skills/fluent-mystique-builder/components/fc.mystique.collapsible.text.md +33 -0
  101. package/content/dev/skills/fluent-mystique-builder/components/fc.mystique.link.md +30 -0
  102. package/content/dev/skills/fluent-mystique-builder/components/fc.order.itemDetails.md +20 -0
  103. package/content/dev/skills/fluent-mystique-builder/components/fc.order.shipmentDetails.md +20 -0
  104. package/content/dev/skills/fluent-mystique-builder/components/fc.page.filter.select.md +87 -0
  105. package/content/dev/skills/fluent-mystique-builder/components/fc.page.md +64 -0
  106. package/content/dev/skills/fluent-mystique-builder/components/fc.page.refresh.md +48 -0
  107. package/content/dev/skills/fluent-mystique-builder/components/fc.page.section.column.md +71 -0
  108. package/content/dev/skills/fluent-mystique-builder/components/fc.page.section.header.md +61 -0
  109. package/content/dev/skills/fluent-mystique-builder/components/fc.page.section.md +59 -0
  110. package/content/dev/skills/fluent-mystique-builder/components/fc.page.wizard.md +45 -0
  111. package/content/dev/skills/fluent-mystique-builder/components/fc.page.wizard.summary.md +56 -0
  112. package/content/dev/skills/fluent-mystique-builder/components/fc.progress.circular.md +20 -0
  113. package/content/dev/skills/fluent-mystique-builder/components/fc.provider.graphql.md +71 -0
  114. package/content/dev/skills/fluent-mystique-builder/components/fc.quantity.list.md +87 -0
  115. package/content/dev/skills/fluent-mystique-builder/components/fc.repeater.md +56 -0
  116. package/content/dev/skills/fluent-mystique-builder/components/fc.reports.ipuipc.md +54 -0
  117. package/content/dev/skills/fluent-mystique-builder/components/fc.return.rowExpansion.md +19 -0
  118. package/content/dev/skills/fluent-mystique-builder/components/fc.scanner.barcode.md +21 -0
  119. package/content/dev/skills/fluent-mystique-builder/components/fc.scanner.barcodeFilter.md +72 -0
  120. package/content/dev/skills/fluent-mystique-builder/components/fc.scanner.camera.md +20 -0
  121. package/content/dev/skills/fluent-mystique-builder/components/fc.settingForm.md +64 -0
  122. package/content/dev/skills/fluent-mystique-builder/components/fc.sourcing.profile.drawer.button.md +19 -0
  123. package/content/dev/skills/fluent-mystique-builder/components/fc.sourcing.profile.modal.button.md +64 -0
  124. package/content/dev/skills/fluent-mystique-builder/components/fc.sourcing.strategy.modal.button.md +20 -0
  125. package/content/dev/skills/fluent-mystique-builder/components/fc.stepper.md +20 -0
  126. package/content/dev/skills/fluent-mystique-builder/components/fc.tab.content.md +56 -0
  127. package/content/dev/skills/fluent-mystique-builder/components/fc.tabs.card.md +64 -0
  128. package/content/dev/skills/fluent-mystique-builder/components/fc.tabs.md +69 -0
  129. package/content/dev/skills/fluent-mystique-builder/components/fc.tile.metric.md +73 -0
  130. package/content/dev/skills/fluent-mystique-builder/components/fc.workflow.provider.md +77 -0
  131. package/content/dev/skills/fluent-mystique-builder/validate-docs.ps1 +260 -0
  132. package/content/dev/skills/fluent-mystique-scaffold/SKILL.md +1830 -0
  133. package/content/dev/skills/fluent-mystique-validate/SKILL.md +646 -0
  134. package/content/dev/skills/fluent-pre-deploy-check/SKILL.md +1144 -1090
  135. package/content/dev/skills/fluent-retailer-config/SKILL.md +1162 -1120
  136. package/content/dev/skills/fluent-rollback/SKILL.md +387 -0
  137. package/content/dev/skills/fluent-rule-scaffold/SKILL.md +515 -394
  138. package/content/dev/skills/fluent-scope-decompose/SKILL.md +1123 -1021
  139. package/content/dev/skills/fluent-session-audit-export/SKILL.md +880 -632
  140. package/content/dev/skills/fluent-session-summary/SKILL.md +320 -195
  141. package/content/dev/skills/fluent-settings/SKILL.md +151 -2
  142. package/content/dev/skills/fluent-source-onboard/SKILL.md +23 -4
  143. package/content/dev/skills/fluent-sourcing/SKILL.md +14 -0
  144. package/content/dev/skills/fluent-system-monitoring/SKILL.md +771 -767
  145. package/content/dev/skills/fluent-test-data/SKILL.md +514 -513
  146. package/content/dev/skills/fluent-trace/SKILL.md +1169 -1143
  147. package/content/dev/skills/fluent-transition-api/SKILL.md +364 -346
  148. package/content/dev/skills/fluent-use-case-discover/SKILL.md +593 -471
  149. package/content/dev/skills/fluent-use-case-discover/SPEC_TEMPLATE.md +22 -1
  150. package/content/dev/skills/fluent-version-manage/SKILL.md +44 -3
  151. package/content/dev/skills/fluent-workflow-analyzer/SKILL.md +995 -959
  152. package/content/dev/skills/fluent-workflow-builder/SKILL.md +668 -326
  153. package/content/dev/skills/fluent-workflow-deploy/SKILL.md +480 -0
  154. package/content/dev/skills/fluent-workspace-tree/SKILL.md +281 -0
  155. package/content/mcp-extn/agents/fluent-mcp.md +133 -132
  156. package/content/mcp-extn/skills/fluent-mcp-tools/SKILL.md +812 -800
  157. package/content/mcp-official/agents/fluent-mcp-core.md +91 -91
  158. package/content/mcp-official/skills/fluent-mcp-core/SKILL.md +94 -94
  159. package/content/rfl/skills/fluent-rfl-assess/SKILL.md +172 -172
  160. package/docs/CAPABILITY_MAP.md +106 -73
  161. package/docs/DEPLOYMENT_PROMOTION_RUNBOOK.md +218 -0
  162. package/docs/DESIGN-implementation-map.md +698 -0
  163. package/docs/DEV_WORKFLOW.md +814 -802
  164. package/docs/FLOW_RUN.md +142 -142
  165. package/docs/GETTING_STARTED.md +427 -0
  166. package/docs/USE_CASES.md +906 -50
  167. package/metadata.json +184 -155
  168. package/package.json +7 -2
  169. package/docs/USE_CASES.pdf +0 -0
@@ -0,0 +1,1830 @@
1
+ ---
2
+ name: fluent-mystique-scaffold
3
+ description: Scaffold a new Mystique SDK component project with webpack, TypeScript, Storybook, Jest, component registration, and i18n — ready to develop and deploy. Triggers on "scaffold component", "new mystique project", "create sdk component", "mystique sdk setup", "new ui plugin".
4
+ user-invocable: true
5
+ allowed-tools: Bash, Read, Write, Edit, Glob, Grep
6
+ argument-hint: <project-name> [--components ComponentA,ComponentB] [--category content|layout|page|filter]
7
+ ---
8
+
9
+ # Mystique SDK Component Scaffolder
10
+
11
+ Generate a complete, buildable Mystique SDK component project from a project name and optional component list. The output is a directory under `accounts/<PROFILE>/SOURCE/` containing a React/TypeScript/webpack project that produces a `bundle.[contenthash].js` file loadable via the `plugins[]` array in any Mystique manifest.
12
+
13
+ A Mystique SDK project is a standalone React application that registers custom UI components with the Mystique ComponentRegistry. At runtime, the Fluent OMX host application loads the bundle from a URL, executes it, and the registered components become available for use in manifests alongside built-in `fc.*` components. The host application provides React, ReactDOM, Material-UI, and Mystique globals as webpack externals -- the bundle does NOT include these libraries.
14
+
15
+ ## Pre-Check: New Project or Extend Existing?
16
+
17
+ **ALWAYS run this decision tree before scaffolding. Do NOT skip.**
18
+
19
+ ```
20
+ User asks: "I need a UI component" / "Create a Mystique plugin" / "Build a custom component"
21
+ |
22
+ +-- 1. Discover existing SDK projects in workspace
23
+ | Search: accounts/<PROFILE>/SOURCE/*/package.json (look for mystique deps)
24
+ | Search: accounts/<PROFILE>/SOURCE/*/webpack.config.*
25
+ | Search: accounts/<PROFILE>/SOURCE/*/@types/mystique/
26
+ | List found projects with their registered components
27
+ |
28
+ +-- 2. Check deployed plugins in live manifests
29
+ | Look for plugins[] array entries in deployed manifests
30
+ | Cross-reference with local SOURCE/
31
+ |
32
+ +-- 3. Evaluate: does the new component fit an existing project?
33
+ | +-- YES -> Add component to existing project (just generate component files)
34
+ | +-- NO -> Genuinely new domain, proceed with full scaffolding
35
+ | +-- UNCLEAR -> Ask user
36
+ ```
37
+
38
+ ### Source Code Availability Check
39
+
40
+ Before extending an existing project, verify the source is accessible:
41
+
42
+ | Source State | Action |
43
+ |---|---|
44
+ | `accounts/<PROFILE>/SOURCE/<project>/` exists with `package.json` + `@types/mystique/` | Ready -- add component files directly |
45
+ | Plugin deployed but no source in `SOURCE/` | Ask user to clone repo into `SOURCE/` |
46
+ | Source exists but registered components unknown | Read `src/index.tsx` to discover existing registrations |
47
+
48
+ ## Planning Gate
49
+
50
+ ### Pre-flight: Plan Verification
51
+
52
+ Before proceeding, check for an existing approved plan:
53
+
54
+ 1. **Search** `accounts/<PROFILE>/features/*/status.json` for an entry with `plan: "APPROVED"` that covers this UI work, OR check `accounts/<PROFILE>/tasks/` for a task plan with `Status: APPROVED`
55
+ 2. **If multi-artifact work** (SDK component + manifest + workflows + settings): STOP. Invoke `/fluent-feature-plan` first -- this skill cannot be used for multi-artifact work without a feature plan
56
+ 3. **If approved plan found:** Skip to implementation, referencing the plan's UI sections
57
+ 4. **If standalone SDK project work with no plan:** Continue to the Planning Gate below to write a plan for this skill
58
+
59
+ **After the pre-check determines a NEW project is needed, write a plan.** Same pattern as `/fluent-module-scaffold`.
60
+
61
+ **Mystique-scaffold specific emphasis -- ensure these are covered:**
62
+
63
+ 1. **Component inventory** -- names, aliases, categories, props interface for each component
64
+ 2. **Data dependencies** -- what GraphQL entities/queries each component will need
65
+ 3. **Styling approach** -- Material-UI theme extension or custom CSS via Emotion
66
+ 4. **Integration points** -- which manifest pages will use these components
67
+ 5. **Testing strategy** -- unit tests, Storybook stories, Cypress E2E
68
+ 6. **Hosting strategy** -- where the production bundle will be deployed (Vercel, S3, CDN)
69
+
70
+ **Write the plan to:** `accounts/<PROFILE>/tasks/<YYYY-MM-DD>-mystique-scaffold-<slug>.md`. Set `Status: PENDING`.
71
+
72
+ Present the full plan content to the user and wait for approval before generating any files. On approval, update the file to `Status: APPROVED`. If the user says "just do it", proceed directly (still write the file for audit trail).
73
+
74
+ ## Handoff Protocol
75
+
76
+ ### Signals emitted by this skill
77
+
78
+ | Signal | Condition | Example |
79
+ |--------|-----------|---------|
80
+ | `-> READY: <path>` | Project directory scaffolded | `-> READY: accounts/SAGIRISH/SOURCE/mystique-plugin-curbside/` |
81
+ | `-> NEXT: /fluent-<skill>` | Ready for manifest wiring | `-> NEXT: /fluent-mystique-builder` |
82
+ | `-> BLOCKED: <reason>` | Cannot proceed | `-> BLOCKED: PLAN_REQUIRED -- Write a plan via /fluent-feature-plan` |
83
+ | `-> SKIP: <reason>` | Existing project covers the domain | `-> SKIP: Extend existing project mystique-plugin-hm. Add component files directly` |
84
+
85
+ ### Error codes
86
+
87
+ | Code | Condition | Recovery |
88
+ |------|-----------|----------|
89
+ | `PLAN_REQUIRED` | Scaffolding attempted without approved plan | Run `/fluent-feature-plan` first |
90
+ | `VALIDATION_FAILED` | Project name conflicts or invalid component names | Fix input parameters |
91
+
92
+ ## When to Use
93
+
94
+ - Creating a brand-new Mystique SDK component project from scratch
95
+ - Starting a greenfield UI plugin that needs webpack, TypeScript, Storybook, Jest, and component registration
96
+ - Bootstrapping a component project for a new feature requiring custom UI beyond built-in `fc.*` components
97
+ - Generating a project shell to be populated incrementally with new components
98
+
99
+ ## Ownership Boundary
100
+
101
+ This skill owns:
102
+
103
+ - Creating the complete SDK project directory structure
104
+ - Generating `package.json` with correct dependencies and scripts
105
+ - Generating `webpack.config.ts` with proper externals (React, ReactDOM, Material-UI, Mystique)
106
+ - Generating `tsconfig.json` with strict TypeScript configuration
107
+ - Generating `jest.config.js` with ts-jest, mocks, and coverage thresholds
108
+ - Generating `.babelrc` with React/TypeScript/Emotion presets
109
+ - Copying `@types/mystique/` type definitions (7 files)
110
+ - Generating `lib/` SDK initialization files (init.js, mystique.js, mystique-export.js, material-ui.js)
111
+ - Generating Storybook configuration (`.storybook/main.ts`, `preview.js`, `helpers.ts`)
112
+ - Generating component files with test and story for each `--components` entry
113
+ - Generating `src/index.tsx` with ComponentRegistry registrations
114
+ - Generating example manifest in `manifests/`
115
+ - Generating `src/index.html` for dev server
116
+ - Generating `.gitignore`, `.eslintrc.js`
117
+
118
+ This skill does **not** own:
119
+
120
+ - Creating or editing Mystique manifests --> `/fluent-mystique-builder`
121
+ - Validating manifest JSON --> `/fluent-mystique-validate`
122
+ - Analyzing existing manifests --> `/fluent-mystique-analyze`
123
+ - Deploying manifests as settings --> `/fluent-settings`
124
+ - Building/deploying Java modules --> `/fluent-build`, `/fluent-module-deploy`
125
+
126
+ ## Inputs
127
+
128
+ | Parameter | Required | Default | Description |
129
+ |-----------|----------|---------|-------------|
130
+ | `project-name` | Yes | -- | Project directory name (e.g., `mystique-plugin-curbside`). Used for directory name and package.json name. Lowercase alphanumeric with hyphens. |
131
+ | `--components` | No | -- | Comma-separated PascalCase component names to scaffold (e.g., `OrderTracker,StoreMap`). Each gets a `.tsx`, `.test.tsx`, `.stories.tsx`, and `index.ts`. |
132
+ | `--category` | No | `content` | Default component category for ComponentRegistry registration. Valid: `page`, `layout`, `content`, `filter`. |
133
+ | `--namespace` | No | Auto-detect from project name | Component alias prefix (e.g., `acme` produces aliases like `acme.content.order-tracker`). |
134
+ | `--profile` | No | Active profile | Fluent CLI profile for path resolution |
135
+
136
+ ## Pre-Check: Project Already Exists?
137
+
138
+ Before creating anything, check if the target directory already exists:
139
+
140
+ ```
141
+ accounts/<PROFILE>/SOURCE/<project-name>/
142
+ ```
143
+
144
+ If it exists, **abort** with message:
145
+ ```
146
+ Project directory already exists: accounts/<PROFILE>/SOURCE/<project-name>/
147
+ Use this skill with --components to add components to the existing project, or choose a different name.
148
+ ```
149
+
150
+ ## Namespace Detection
151
+
152
+ The namespace is used in ComponentRegistry aliases following the pattern `<namespace>.<category>.<kebab-name>`. Detection order:
153
+
154
+ ```
155
+ 1. If --namespace provided, use it directly
156
+
157
+ 2. Else derive from project-name:
158
+ mystique-plugin-curbside -> curbside
159
+ mystique-plugin-hm-custom -> hm
160
+ fluent-component-acme -> acme
161
+ Strip "mystique-plugin-" or "fluent-component-" prefix, take first word
162
+
163
+ 3. Fallback: "custom"
164
+ Warn user: "Could not derive namespace. Components will use 'custom.*' aliases."
165
+ ```
166
+
167
+ ## Generated Directory Structure
168
+
169
+ ```
170
+ accounts/<PROFILE>/SOURCE/<project-name>/
171
+ +-- package.json # Dependencies, scripts, browserslist
172
+ +-- tsconfig.json # TypeScript config (strict mode, ES6 target)
173
+ +-- webpack.config.ts # Bundle output with content hash, externals
174
+ +-- jest.config.js # Jest with ts-jest, coverage thresholds
175
+ +-- .babelrc # Babel presets for React/TypeScript/Emotion
176
+ +-- .eslintrc.js # ESLint config for TypeScript + React
177
+ +-- .gitignore
178
+ +-- sdk-version.json # SDK version tracking
179
+ +-- .storybook/
180
+ | +-- main.ts # Storybook webpack config with externals
181
+ | +-- preview.js # Decorators, viewport config
182
+ | +-- helpers.ts # Story template utility
183
+ | +-- Storyshots.test.ts # Snapshot testing for stories
184
+ +-- @types/
185
+ | +-- images.d.ts # Image import declarations
186
+ | +-- mystique/ # Mystique SDK type definitions
187
+ | +-- index.d.ts # Common types (QueryResponse, Connection, Edge)
188
+ | +-- manifest.d.ts # Full manifest schema types (MystiqueManifest, Route, Page, Section)
189
+ | +-- components.d.ts # Component prop interfaces (Card, List, Children, DynamicValue, Loading)
190
+ | +-- hooks.d.ts # Hook definitions (useAuth, useQuery, useSettings, useI18n, useData, useRest, useUserActions)
191
+ | +-- services.d.ts # Service types (MystiqueThemeColours)
192
+ | +-- registry.d.ts # Registry interfaces (ComponentRegistry, FieldRegistry, TemplateRegistry)
193
+ | +-- frame.d.ts # Modal/Drawer/Toast utilities (pushModal, pushDrawer, pushToast)
194
+ +-- i18n/
195
+ | +-- locales/
196
+ | +-- en/
197
+ | +-- translation.json # Initial i18n keys
198
+ +-- lib/
199
+ | +-- init.js # Global setup: React, ReactDOM, MaterialUI, Emotion on globalThis
200
+ | +-- material-ui.js # MaterialUI UMD global (bundled for Storybook/Jest)
201
+ | +-- mystique.js # Mystique global object (bundled for Storybook/Jest)
202
+ | +-- mystique-export.js # Named exports from globalThis.Mystique for import resolution
203
+ +-- src/
204
+ | +-- index.tsx # Entry point -- ComponentRegistry.register() calls
205
+ | +-- index.html # Dev server HTML with CDN React/ReactDOM
206
+ | +-- setup-tests.ts # Jest setup (Emotion matchers)
207
+ | +-- components/
208
+ | | +-- <ComponentName>/
209
+ | | +-- <ComponentName>.tsx # Component implementation
210
+ | | +-- <ComponentName>.test.tsx # Unit test
211
+ | | +-- <ComponentName>.stories.tsx # Storybook story
212
+ | | +-- index.ts # Re-export
213
+ | +-- utils/
214
+ | +-- GqlUtils.ts # GraphQL document-to-string utility
215
+ | +-- TestUtils.tsx # MaterialUI classname fix for test snapshots
216
+ +-- manifests/
217
+ | +-- fc.mystique.manifest.example.ts # Example full manifest
218
+ | +-- fc.mystique.manifest.fragment.example.ts # Example fragment manifest
219
+ +-- build/
220
+ | +-- GenerateVersionPlugin.ts # Webpack plugin: emits version.json with bundle path
221
+ | +-- GenerateManifestsPlugin.ts # Webpack plugin: compiles manifest .ts to .json in dist/
222
+ +-- cypress/ # (empty scaffold, ready for E2E tests)
223
+ | +-- cypress.json
224
+ +-- dist/ # (empty, webpack output directory)
225
+ ```
226
+
227
+ ## File Templates
228
+
229
+ ### package.json
230
+
231
+ ```json
232
+ {
233
+ "name": "${PROJECT_NAME}",
234
+ "version": "1.0.0",
235
+ "description": "Mystique SDK Plugin - ${PROJECT_TITLE}",
236
+ "main": "index.js",
237
+ "scripts": {
238
+ "start": "webpack serve --mode development --open --hot --port 3001",
239
+ "build": "webpack --mode production && jest ./build",
240
+ "bundle": "webpack",
241
+ "test": "jest --coverage --testPathIgnorePatterns ./build/**",
242
+ "test:bail": "jest --bail --testPathIgnorePatterns ./build/**",
243
+ "test:storybook": "start-storybook --smoke-test",
244
+ "check-types": "tsc",
245
+ "storybook": "start-storybook -p 6006 -s ./lib",
246
+ "storybook:build": "build-storybook -o dist/_storybook && cp ./lib/*.js ./dist/_storybook",
247
+ "lint": "eslint . --ext .ts,.tsx",
248
+ "lint:fix": "yarn run lint -- --fix"
249
+ },
250
+ "devDependencies": {
251
+ "@babel/core": "^7.13.8",
252
+ "@babel/plugin-proposal-class-properties": "^7.12.1",
253
+ "@babel/plugin-proposal-object-rest-spread": "^7.12.1",
254
+ "@babel/preset-env": "^7.13.9",
255
+ "@babel/preset-react": "^7.12.10",
256
+ "@babel/preset-typescript": "^7.12.7",
257
+ "@emotion/babel-preset-css-prop": "^11.10.0",
258
+ "@emotion/eslint-plugin": "^11.10.0",
259
+ "@emotion/jest": "^11.10.0",
260
+ "@emotion/react": "^11.10.0",
261
+ "@storybook/addon-actions": "6.5.12",
262
+ "@storybook/addon-essentials": "6.5.12",
263
+ "@storybook/addon-links": "6.5.12",
264
+ "@storybook/addon-storyshots": "6.5.12",
265
+ "@storybook/builder-webpack5": "6.5.12",
266
+ "@storybook/core-common": "6.5.12",
267
+ "@storybook/manager-webpack5": "6.5.12",
268
+ "@storybook/react": "6.5.12",
269
+ "@testing-library/dom": "^7.31.2",
270
+ "@testing-library/jest-dom": "^5.11.9",
271
+ "@testing-library/react": "^11.2.5",
272
+ "@testing-library/react-hooks": "^5.1.1",
273
+ "@testing-library/user-event": "^13.1.9",
274
+ "@types/jest": "^26.0.20",
275
+ "@types/lodash": "^4.14.167",
276
+ "@types/material-ui": "^0.21.8",
277
+ "@types/node": "^18.7.14",
278
+ "@types/react": "^17.0.0",
279
+ "@types/react-dom": "^17.0.0",
280
+ "@types/testing-library__jest-dom": "^5.14.0",
281
+ "@types/webpack": "^5.0.0",
282
+ "@types/webpack-dev-server": "^3.11.3",
283
+ "@typescript-eslint/eslint-plugin": "^4.28.0",
284
+ "@typescript-eslint/parser": "^4.28.0",
285
+ "babel-jest": "^26.6.3",
286
+ "babel-loader": "^8.2.2",
287
+ "copy-webpack-plugin": "^9.0.1",
288
+ "eslint": "^7.29.0",
289
+ "eslint-plugin-no-only-tests": "^2.6.0",
290
+ "eslint-plugin-react": "^7.24.0",
291
+ "eslint-plugin-react-hooks": "^4.2.0",
292
+ "graphql": "^15.4.0",
293
+ "graphql-tag": "^2.12.4",
294
+ "html-webpack-plugin": "^5.3.1",
295
+ "jest": "^26.6.3",
296
+ "jest-fetch-mock": "^3.0.3",
297
+ "jest-html-reporter": "^3.3.0",
298
+ "lodash": "^4.17.21",
299
+ "react-test-renderer": "^17.0.2",
300
+ "ts-jest": "^26.5.2",
301
+ "typescript": "^4.1.3",
302
+ "url-loader": "^4.1.1",
303
+ "webpack": "^5.74.0",
304
+ "webpack-cli": "^4.10.0",
305
+ "webpack-dev-server": "^4.10.0"
306
+ },
307
+ "dependencies": {
308
+ "@material-ui/core": "4.12.1",
309
+ "@material-ui/pickers": "^3.3.10",
310
+ "qs": "^6.10.1",
311
+ "react": "^17.0.1",
312
+ "react-dom": "^17.0.1"
313
+ },
314
+ "browserslist": {
315
+ "production": [">0.2%", "not dead", "not op_mini all"],
316
+ "development": ["last 1 chrome version", "last 1 firefox version", "last 1 safari version"]
317
+ }
318
+ }
319
+ ```
320
+
321
+ **Important notes about dependencies:**
322
+ - `react`, `react-dom`, and `@material-ui/core` are listed as `dependencies` but are treated as webpack **externals** -- they are NOT bundled into the output. They exist in `dependencies` for TypeScript type resolution and for Storybook/Jest test execution.
323
+ - The host Fluent OMX application provides React 17, ReactDOM 17, and Material-UI 4.12 at runtime via `window.React`, `window.ReactDOM`, and `window.MaterialUI` globals.
324
+ - Storybook version is pinned to 6.5.12 for compatibility with the Mystique SDK.
325
+
326
+ ### webpack.config.ts
327
+
328
+ This is the most critical file -- it controls how the bundle is produced and what is externalized.
329
+
330
+ ```typescript
331
+ const path = require('path');
332
+ import { Configuration } from 'webpack';
333
+ import HtmlWebpackPlugin from 'html-webpack-plugin';
334
+ const GenerateVersionPlugin = require('./build/GenerateVersionPlugin');
335
+ const GenerateManifestsPlugin = require('./build/GenerateManifestsPlugin');
336
+ const CopyWebpackPlugin = require('copy-webpack-plugin');
337
+
338
+ const config: Configuration = {
339
+
340
+ // Entry point -- all ComponentRegistry.register() calls happen here
341
+ entry: './src/index',
342
+
343
+ devServer: {
344
+ allowedHosts: 'all',
345
+ static: {
346
+ directory: path.resolve(__dirname, 'dist'),
347
+ watch: true,
348
+ },
349
+ headers: {
350
+ 'Access-Control-Allow-Origin': '*',
351
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
352
+ 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization',
353
+ },
354
+ },
355
+
356
+ // Output: bundle.[contenthash].js for cache busting
357
+ output: {
358
+ path: path.join(__dirname, '/dist'),
359
+ clean: true,
360
+ filename: 'bundle.[contenthash].js',
361
+ },
362
+
363
+ resolve: {
364
+ extensions: ['.ts', '.tsx', '.js'],
365
+ },
366
+
367
+ module: {
368
+ rules: [
369
+ {
370
+ test: /\.(ts|js)x?$/,
371
+ exclude: /node_modules/,
372
+ use: {
373
+ loader: 'babel-loader',
374
+ },
375
+ },
376
+ {
377
+ test: /\.(png|jp(e*)g|svg)$/,
378
+ use: [{
379
+ loader: 'url-loader',
380
+ options: {
381
+ limit: 8000,
382
+ name: 'images/[hash]-[name].[ext]',
383
+ },
384
+ }],
385
+ },
386
+ ],
387
+ },
388
+ plugins: [
389
+ new HtmlWebpackPlugin({
390
+ template: './src/index.html',
391
+ }),
392
+ new CopyWebpackPlugin({
393
+ patterns: [
394
+ {
395
+ from: 'i18n/locales',
396
+ to: path.join(__dirname, 'dist/i18n'),
397
+ },
398
+ ],
399
+ }),
400
+ new GenerateVersionPlugin(),
401
+ new GenerateManifestsPlugin(),
402
+ ],
403
+
404
+ // CRITICAL: externals configuration
405
+ // React, ReactDOM, Emotion, Material-UI, and Mystique are provided by the host app
406
+ // They must NOT be bundled -- the bundle uses window globals at runtime
407
+ externals: [{
408
+ 'react': 'React',
409
+ 'react-dom': 'ReactDOM',
410
+ '@emotion/react': 'emotionReact',
411
+ },
412
+ externalMaterialUI,
413
+ externalMystique,
414
+ ],
415
+ };
416
+
417
+ /** Re-route @material-ui/core imports to the global UMD import */
418
+ export function externalMaterialUI (_: any, module: any, callback: any): any {
419
+ if (module === '@material-ui/core') {
420
+ return callback(null, `window["MaterialUI"]`);
421
+ }
422
+ const isMaterialUIComponent = /^@material-ui\/core\/.+$/;
423
+ const match = isMaterialUIComponent.exec(module);
424
+ if (match !== null) {
425
+ const parts = module.split('/');
426
+ const component = parts[parts.length - 1];
427
+ if (component === 'styles') {
428
+ return callback(null, `window["MaterialUI"]`);
429
+ }
430
+ return callback(null, `window["MaterialUI"].${component}`);
431
+ }
432
+ callback();
433
+ }
434
+
435
+ /** Re-route mystique/* imports to the global Mystique object */
436
+ export function externalMystique (_: any, module: any, callback: any): any {
437
+ const isMystiqueComponent = /^mystique\/.+$/;
438
+ const match = isMystiqueComponent.exec(module);
439
+ if (match !== null) {
440
+ return callback(null, `window["Mystique"]`);
441
+ }
442
+ callback();
443
+ }
444
+
445
+ export default config;
446
+ ```
447
+
448
+ **Key points about the webpack config:**
449
+ - `CORS headers` on devServer are required so the Fluent OMX host app (running on a different port/domain) can load the bundle.
450
+ - `contenthash` in the filename enables cache busting in production.
451
+ - The `GenerateVersionPlugin` emits a `version.json` that maps the hashed bundle filename, enabling the host app to auto-discover the current bundle.
452
+ - The `GenerateManifestsPlugin` compiles `.ts` manifest files under `manifests/` into `.json` in `dist/manifests/`.
453
+ - The three external handlers ensure ZERO framework code ends up in the bundle. The host OMX app provides everything via `window` globals.
454
+
455
+ ### tsconfig.json
456
+
457
+ ```json
458
+ {
459
+ "compilerOptions": {
460
+ "target": "es6",
461
+ "outDir": "./dist/",
462
+ "strictNullChecks": true,
463
+ "moduleResolution": "node",
464
+ "resolveJsonModule": true,
465
+ "allowJs": true,
466
+ "noEmit": true,
467
+ "strict": true,
468
+ "esModuleInterop": true,
469
+ "jsx": "react-jsx",
470
+ "jsxImportSource": "@emotion/react",
471
+ "baseUrl": "./src",
472
+ "lib": ["es2015", "dom.iterable", "es2016.array.include", "es2017.object", "dom"],
473
+ "types": ["node", "jest", "@testing-library/jest-dom"],
474
+ "typeRoots": ["./node_modules/@types", "./@types/**/*"],
475
+ "module": "es6",
476
+ "removeComments": true,
477
+ "alwaysStrict": true,
478
+ "allowUnreachableCode": false,
479
+ "noImplicitAny": true,
480
+ "noImplicitThis": true,
481
+ "noUnusedLocals": true,
482
+ "noUnusedParameters": true,
483
+ "noImplicitReturns": true,
484
+ "noFallthroughCasesInSwitch": true,
485
+ "forceConsistentCasingInFileNames": true,
486
+ "importHelpers": true,
487
+ "skipLibCheck": true
488
+ },
489
+ "include": ["src/**/*", "manifests/**/*", "@types/**/*", "./@types"],
490
+ "exclude": ["node_modules", "./node_modules", "./node_modules/*", "./node_modules/@types/node/index.d.ts"]
491
+ }
492
+ ```
493
+
494
+ **Key: `jsxImportSource: "@emotion/react"`** enables Emotion CSS-in-JS props on JSX elements. The `typeRoots` includes `./@types/**/*` which is where the Mystique type definitions live.
495
+
496
+ ### jest.config.js
497
+
498
+ ```javascript
499
+ const path = require('path');
500
+
501
+ module.exports = {
502
+ restoreMocks: true,
503
+ roots: [
504
+ "<rootDir>"
505
+ ],
506
+ setupFilesAfterSetup: ['./src/setup-tests.ts'],
507
+ setupFiles: [
508
+ "./lib/material-ui.js",
509
+ "./lib/init.js",
510
+ "./lib/mystique.js",
511
+ ],
512
+ testMatch: [
513
+ "**/?(*.)+(test).+(ts|tsx)"
514
+ ],
515
+ coverageThreshold: {
516
+ global: {
517
+ statements: 80,
518
+ branches: 80,
519
+ functions: 80,
520
+ lines: 80
521
+ }
522
+ },
523
+ coveragePathIgnorePatterns: [
524
+ ".storybook/",
525
+ "src/index.tsx",
526
+ '.stories.'
527
+ ],
528
+ collectCoverageFrom: [
529
+ "src/**/*.{ts,tsx}"
530
+ ],
531
+ preset: 'ts-jest',
532
+ transform: {
533
+ '^.+\\.(ts|tsx)?$': 'ts-jest',
534
+ "^.+\\.(js|jsx)$": "babel-jest",
535
+ },
536
+ moduleNameMapper: {
537
+ "\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "identity-obj-proxy",
538
+ "\\.(css|less)$": "identity-obj-proxy",
539
+ "^./react": path.resolve(__dirname, 'node_modules/react'),
540
+ "^mystique": path.resolve(__dirname, 'lib/mystique-export.js')
541
+ },
542
+ reporters: [
543
+ "default",
544
+ ["./node_modules/jest-html-reporter", {
545
+ "pageTitle": "Test Report",
546
+ "outputPath": "./test-report/test-report.html"
547
+ }]
548
+ ],
549
+ };
550
+ ```
551
+
552
+ **Critical: `setupFiles`** loads `lib/material-ui.js`, `lib/init.js`, and `lib/mystique.js` before tests run. These files set up the global objects (`window.React`, `window.MaterialUI`, `window.Mystique`) that the webpack externals resolve to at runtime. The `moduleNameMapper` for `^mystique` redirects Mystique module imports to `lib/mystique-export.js` which provides named exports from the global Mystique object.
553
+
554
+ ### .babelrc
555
+
556
+ ```json
557
+ {
558
+ "presets": [
559
+ "@babel/preset-env",
560
+ "@babel/preset-typescript",
561
+ [
562
+ "@babel/preset-react",
563
+ {
564
+ "runtime": "automatic"
565
+ }
566
+ ],
567
+ "@emotion/babel-preset-css-prop"
568
+ ],
569
+ "plugins": ["@emotion", "@babel/proposal-class-properties", "@babel/proposal-object-rest-spread"]
570
+ }
571
+ ```
572
+
573
+ ### lib/init.js
574
+
575
+ Sets up global objects that the webpack externals resolve to. Required for both Jest tests and the dev server HTML page.
576
+
577
+ ```javascript
578
+ import fetchMock from 'jest-fetch-mock';
579
+ fetchMock.enableMocks();
580
+
581
+ import React from 'react';
582
+ globalThis.React = React;
583
+
584
+ import ReactDOM from 'react-dom';
585
+ globalThis.ReactDOM = ReactDOM;
586
+
587
+ import * as MaterialUI from '@material-ui/core';
588
+ globalThis.MaterialUI = MaterialUI;
589
+
590
+ import * as Emotion from '@emotion/react';
591
+ globalThis.emotionReact = Emotion;
592
+ ```
593
+
594
+ ### lib/mystique-export.js
595
+
596
+ Provides named exports from the global `Mystique` object. This file is what `moduleNameMapper` in Jest points `mystique/*` imports to, and what makes hook/component imports work in tests.
597
+
598
+ ```javascript
599
+ const m = globalThis.Mystique;
600
+
601
+ // registry
602
+ export const ComponentRegistry = m.ComponentRegistry;
603
+ export const FieldRegistry = m.FieldRegistry;
604
+
605
+ // frame
606
+ export const DataModal = m.DataModal;
607
+ export const pushModal = m.pushModal;
608
+ export const pushDrawer = m.pushDrawer;
609
+ export const pushToast = m.pushToast;
610
+
611
+ // hooks
612
+ export const useAuth = m.useAuth;
613
+ export const useI18n = m.useI18n;
614
+ export const useSettings = m.useSettings;
615
+ export const useQuery = m.useQuery;
616
+ export const useMutation = m.useMutation;
617
+ export const useRest = m.useRest;
618
+ export const getQuery = m.getQuery;
619
+ export const getRest = m.getRest;
620
+
621
+ // components
622
+ export const Card = m.Card;
623
+ export const CardContent = m.CardContent;
624
+ export const Children = m.Children;
625
+ export const DynamicValue = m.DynamicValue;
626
+ export const Loading = m.Loading;
627
+ export const QuantitySelectorComponent = m.QuantitySelectorComponent;
628
+ export const QuantityList = m.QuantityList;
629
+
630
+ // utils
631
+ export const decorateQueryResult = m.decorateQueryResult;
632
+ ```
633
+
634
+ ### lib/mystique.js and lib/material-ui.js
635
+
636
+ These are large bundled files from the Fluent Commerce SDK distribution. They set up `globalThis.Mystique` and `globalThis.MaterialUI` respectively. **Do NOT generate these files** -- they must be copied from the reference SDK at `OMX Mystique components/omx-component-sdk/omx-component-sdk/lib/`. If the reference SDK is not available locally, warn the user:
637
+
638
+ ```
639
+ WARNING: lib/mystique.js and lib/material-ui.js are required but cannot be auto-generated.
640
+ These files must be obtained from the Fluent Commerce OMX Component SDK distribution.
641
+ Copy them from: OMX Mystique components/omx-component-sdk/omx-component-sdk/lib/
642
+ Without these files, Jest tests and Storybook will fail (missing global Mystique/MaterialUI objects).
643
+ ```
644
+
645
+ ### src/index.tsx (Entry Point)
646
+
647
+ This is the entry point for the webpack bundle. All component registrations happen here.
648
+
649
+ ```tsx
650
+ import { ComponentRegistry } from 'mystique/registry/ComponentRegistry';
651
+ // Import each component
652
+ import { ${ComponentName} } from './components/${ComponentName}/${ComponentName}';
653
+
654
+ // Register with Mystique -- aliases are how the manifest references this component
655
+ // Convention: <namespace>.<category>.<kebab-name>
656
+ ComponentRegistry.register(
657
+ ['${NAMESPACE}.${CATEGORY}.${KEBAB_NAME}'],
658
+ ${ComponentName},
659
+ { category: '${CATEGORY}' }
660
+ );
661
+ ```
662
+
663
+ **Alias naming convention:** `<namespace>.<category>.<component-name>` where:
664
+ - `namespace` is the company/project prefix (e.g., `acme`, `hm`, `curbside`)
665
+ - `category` is one of `page`, `layout`, `content`, `filter`
666
+ - `component-name` is kebab-case (e.g., `order-tracker`, `store-map`)
667
+
668
+ Multiple aliases can be registered for the same component for backward compatibility.
669
+
670
+ ### src/index.html (Dev Server)
671
+
672
+ ```html
673
+ <!DOCTYPE html>
674
+ <html lang="en">
675
+ <head>
676
+ <meta charset="UTF-8">
677
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
678
+ <title>${PROJECT_TITLE} - Mystique Plugin</title>
679
+ <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
680
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
681
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
682
+ <style>
683
+ body { display:flex; flex-direction: column; align-items: center; justify-content: center; background-color:#efefef; font-family: 'Lato', sans-serif; }
684
+ #root { width: 600px; max-width:90%; padding: 4em 2em 2em 2em; }
685
+ </style>
686
+ </head>
687
+ <body>
688
+ <div id="root">
689
+ <p>Welcome to the ${PROJECT_TITLE} Mystique Plugin!</p>
690
+ <p>Add this plugin to your Fluent OMX app manifest:</p>
691
+ <pre>
692
+ {
693
+ "plugins": [
694
+ { "type": "url", "src": "http://localhost:3001" }
695
+ ]
696
+ }
697
+ </pre>
698
+ </div>
699
+ </body>
700
+ </html>
701
+ ```
702
+
703
+ ### src/setup-tests.ts
704
+
705
+ ```typescript
706
+ import { matchers } from '@emotion/jest';
707
+
708
+ expect.extend(matchers);
709
+ ```
710
+
711
+ ### src/utils/TestUtils.tsx
712
+
713
+ Provides consistent Material-UI classname generation for snapshot tests.
714
+
715
+ ```tsx
716
+ import { FC } from 'react';
717
+ import { StylesProvider, StylesOptions } from '@material-ui/styles';
718
+
719
+ const generateClassName: StylesOptions['generateClassName'] = (
720
+ rule,
721
+ sheet,
722
+ ): string => `${sheet!.options.classNamePrefix}-${rule.key}`;
723
+
724
+ /**
725
+ * Provides consistent classname generation for MaterialUI components to avoid inconsistent snapshots.
726
+ */
727
+ export const FixMuiClassnames: FC = ({ children }) => {
728
+ return (
729
+ <StylesProvider generateClassName={generateClassName}>
730
+ {children}
731
+ </StylesProvider>
732
+ );
733
+ };
734
+ ```
735
+
736
+ ### src/utils/GqlUtils.ts
737
+
738
+ ```typescript
739
+ import { DocumentNode } from 'graphql';
740
+ import { print } from 'graphql/language/printer';
741
+
742
+ /**
743
+ * Converts a GraphQL document node back to a query string.
744
+ * Useful for defining queries with gql`` template literals in manifests
745
+ * and converting them to strings for the manifest JSON output.
746
+ */
747
+ export const parse = (doc: DocumentNode) => print(doc);
748
+ ```
749
+
750
+ ## Component Template
751
+
752
+ For each component in `--components`, generate the following files:
753
+
754
+ ### Component File (`src/components/<Name>/<Name>.tsx`)
755
+
756
+ ```tsx
757
+ import React from 'react';
758
+ import { Card } from 'mystique/components/Card';
759
+ import { CardContent } from 'mystique/components/CardContent';
760
+ import { StdProps } from 'mystique/registry/ComponentRegistry';
761
+
762
+ /**
763
+ * Props for the ${Name} component.
764
+ * Extends StdProps to receive injected data from the page query via dataSource.
765
+ */
766
+ export interface ${Name}Props extends StdProps {
767
+ /**
768
+ * Component title displayed in the card header.
769
+ */
770
+ title?: string;
771
+ // TODO: Define component-specific props
772
+ }
773
+
774
+ /**
775
+ * ${Name} -- a custom Mystique component.
776
+ *
777
+ * Usage in manifest:
778
+ * { "component": "${NAMESPACE}.${CATEGORY}.${KEBAB_NAME}", "props": { "title": "My Title" } }
779
+ */
780
+ export const ${Name}: React.FC<${Name}Props> = ({ data, title = '${Name}', ...props }) => {
781
+ return (
782
+ <Card title={title}>
783
+ <CardContent>
784
+ <p data-testid="${KEBAB_NAME}-content">
785
+ {/* TODO: Implement component using data from props.data */}
786
+ {data ? JSON.stringify(data) : 'No data available'}
787
+ </p>
788
+ </CardContent>
789
+ </Card>
790
+ );
791
+ };
792
+ ```
793
+
794
+ ### Test File (`src/components/<Name>/<Name>.test.tsx`)
795
+
796
+ ```tsx
797
+ import { render, screen } from '@testing-library/react';
798
+ import { ${Name} } from './${Name}';
799
+
800
+ describe('${Name}', () => {
801
+ it('renders without crashing', () => {
802
+ render(<${Name} />);
803
+ expect(screen.getByTestId('${KEBAB_NAME}-content')).toBeInTheDocument();
804
+ });
805
+
806
+ it('displays title prop', () => {
807
+ const { container } = render(<${Name} title="Custom Title" />);
808
+ expect(container.textContent).toContain('Custom Title');
809
+ });
810
+
811
+ it('renders data when provided', () => {
812
+ const mockData = { id: '123', ref: 'TEST-001' };
813
+ render(<${Name} data={mockData} />);
814
+ expect(screen.getByTestId('${KEBAB_NAME}-content').textContent).toContain('TEST-001');
815
+ });
816
+ });
817
+ ```
818
+
819
+ ### Story File (`src/components/<Name>/<Name>.stories.tsx`)
820
+
821
+ ```tsx
822
+ import { ComponentMeta } from '@storybook/react';
823
+ import { storyTemplate } from '../../../.storybook/helpers';
824
+ import { ${Name} } from './${Name}';
825
+
826
+ export default {
827
+ title: 'Components/${Name}',
828
+ component: ${Name},
829
+ } as ComponentMeta<typeof ${Name}>;
830
+
831
+ const template = storyTemplate(${Name});
832
+
833
+ export const Default = template({
834
+ title: '${Name} Component',
835
+ });
836
+
837
+ export const WithData = template({
838
+ title: '${Name} with Data',
839
+ data: {
840
+ id: '1',
841
+ ref: 'EXAMPLE-001',
842
+ status: 'ACTIVE',
843
+ },
844
+ });
845
+ ```
846
+
847
+ ### Index File (`src/components/<Name>/index.ts`)
848
+
849
+ ```typescript
850
+ export { ${Name} } from './${Name}';
851
+ export type { ${Name}Props } from './${Name}';
852
+ ```
853
+
854
+ ## Storybook Configuration
855
+
856
+ ### .storybook/main.ts
857
+
858
+ ```typescript
859
+ import { StorybookConfig } from '@storybook/core-common';
860
+ import { externalMaterialUI, externalMystique } from '../webpack.config';
861
+
862
+ const config: StorybookConfig = {
863
+ webpackFinal: async (config: any) => {
864
+ config.externals = [{
865
+ 'react': 'React',
866
+ 'react-dom': 'ReactDOM',
867
+ },
868
+ externalMaterialUI,
869
+ externalMystique
870
+ ];
871
+ return config;
872
+ },
873
+ stories: [
874
+ '../src/**/*.stories.mdx',
875
+ '../src/**/*.stories.@(ts|tsx)'
876
+ ],
877
+ addons: [
878
+ '@storybook/addon-links',
879
+ '@storybook/addon-essentials'
880
+ ],
881
+ core: {
882
+ builder: 'webpack5'
883
+ }
884
+ };
885
+
886
+ module.exports = config;
887
+ ```
888
+
889
+ ### .storybook/preview.js
890
+
891
+ ```javascript
892
+ import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport';
893
+ import { FixMuiClassnames } from '../src/utils/TestUtils';
894
+
895
+ export const parameters = {
896
+ actions: { argTypesRegex: "^on[A-Z].*" },
897
+ viewport: {
898
+ defaultViewport: 'iphone5',
899
+ viewports: INITIAL_VIEWPORTS
900
+ },
901
+ };
902
+
903
+ export const decorators = [
904
+ (Story) => (
905
+ <FixMuiClassnames>
906
+ <Story />
907
+ </FixMuiClassnames>
908
+ )
909
+ ];
910
+ ```
911
+
912
+ ### .storybook/helpers.ts
913
+
914
+ ```typescript
915
+ import { ComponentStory, Story } from '@storybook/react';
916
+
917
+ export const storyTemplate = <P,>(Component: (props: P) => any) => (
918
+ props: P
919
+ ): Story<P> => {
920
+ const template: ComponentStory<typeof Component> = (args) => Component(args);
921
+ const story = template.bind({});
922
+ story.args = props;
923
+ return story;
924
+ };
925
+ ```
926
+
927
+ ## @types/mystique/ Type Definitions
928
+
929
+ The `@types/mystique/` directory contains 7 TypeScript declaration files that define the Mystique SDK API surface. These must be copied from the reference SDK or generated from the templates below.
930
+
931
+ | File | Purpose |
932
+ |------|---------|
933
+ | `index.d.ts` | Common types: `TranslatableString`, `QueryResponse<T>`, `Connection<T>`, `Edge<T>` |
934
+ | `manifest.d.ts` | Full manifest v2.0 schema: `MystiqueManifest`, `MystiqueSection`, `MystiquePage`, `MystiqueComponentInstance`, `MystiquePlugin`, `MystiqueManifestFragment`, `GQLQuery` |
935
+ | `components.d.ts` | Component prop interfaces: `Card`, `CardContent`, `Children`, `DynamicValue`, `Loading`, `List`, `QuantityList`, `QuantitySelectorComponent`, `DatePicker` |
936
+ | `hooks.d.ts` | Hook definitions: `useAuth`, `useEnv`, `useI18n`, `useSettings`, `useQuery`, `getQuery`, `useRest`, `getRest`, `useData`, `useUserActions`, `useUserActionForm`, `getApiDownload`, `getSettings` |
937
+ | `services.d.ts` | Service types: `MystiqueThemeColours` (color palette interface for theming) |
938
+ | `registry.d.ts` | Registry interfaces: `ComponentRegistry` (register/get components), `FieldRegistry` (register/get form fields), `TemplateRegistry` (register/render Handlebars helpers) |
939
+ | `frame.d.ts` | Frame utilities: `pushModal`, `pushDrawer`, `pushToast`, `DataModal`, `clearModals`, `clearDrawers` |
940
+
941
+ **These type definitions are the contract between your component code and the Mystique host application.** They provide full IntelliSense in the IDE for all Mystique hooks, components, and registries. The actual implementations are provided at runtime by the host app via `window.Mystique`.
942
+
943
+ ## React Hooks Usage Patterns
944
+
945
+ The Mystique SDK provides hooks via `import { useAuth, useQuery, ... } from 'mystique/hooks'`. All hooks are provided by the host app at runtime.
946
+
947
+ ### useAuth — Authentication and Context
948
+
949
+ ```typescript
950
+ import { useAuth } from 'mystique/hooks';
951
+
952
+ const MyComponent = () => {
953
+ const auth = useAuth();
954
+ // auth.username: string — current user's login name
955
+ // auth.firstName, auth.lastName: string
956
+ // auth.timezone: string — user's timezone (e.g., 'Australia/Sydney')
957
+ // auth.language: string — user's locale (e.g., 'en')
958
+ // auth.roles: Array<{ name: string }> — assigned roles
959
+ // auth.contextId: number — current retailer/location context ID
960
+ // auth.contextType: string — 'RETAILER' | 'LOCATION'
961
+ // auth.switchContext(contextId, contextType) — switch retailer/location
962
+ // auth.logout() — end session
963
+
964
+ const isAdmin = auth.roles.some(r => r.name === 'ADMIN');
965
+ return isAdmin ? <AdminPanel /> : <StandardView />;
966
+ };
967
+ ```
968
+
969
+ ### useQuery — GraphQL Data Fetching
970
+
971
+ ```typescript
972
+ import { useQuery } from 'mystique/hooks';
973
+
974
+ const OrderDetail = ({ orderId }: { orderId: string }) => {
975
+ const [result, { loading, error, refetch }] = useQuery(
976
+ `query OrderById($id: ID!) { orderById(id: $id) { id ref status type createdOn } }`,
977
+ { id: orderId }
978
+ );
979
+
980
+ if (loading) return <Loading />;
981
+ if (error) return <div>Error: {error.message}</div>;
982
+
983
+ const order = result?.orderById;
984
+ // Connection/pagination pattern:
985
+ // result.orders.edges[].node — array of entities
986
+ // result.orders.pageInfo.hasNextPage — boolean
987
+ // result.orders.pageInfo.endCursor — cursor for next page
988
+ return <div>{order.ref} — {order.status}</div>;
989
+ };
990
+ ```
991
+
992
+ ### useSettings — Hierarchical Settings Access
993
+
994
+ ```typescript
995
+ import { useSettings } from 'mystique/hooks';
996
+
997
+ const ConfiguredComponent = () => {
998
+ const settings = useSettings(['fc.myFeature.enabled', 'fc.myFeature.maxItems']);
999
+ // Settings cascade: Location > Retailer > Account > Global (most specific wins)
1000
+ // Each key returns: { status: 'loading' | 'error' | 'success', result?: { value: string } }
1001
+
1002
+ const featureEnabled = settings['fc.myFeature.enabled'];
1003
+ if (featureEnabled.status === 'loading') return <Loading />;
1004
+ const enabled = featureEnabled.result?.value === 'true';
1005
+ return enabled ? <FeatureUI /> : null;
1006
+ };
1007
+ ```
1008
+
1009
+ ### useI18n — Internationalization
1010
+
1011
+ ```typescript
1012
+ import { useI18n } from 'mystique/hooks';
1013
+
1014
+ const TranslatedComponent = () => {
1015
+ const { translate, translateOr } = useI18n();
1016
+
1017
+ // translate(key) — single key lookup, returns key if not found
1018
+ const title = translate('fc.om.orders.title');
1019
+
1020
+ // translateOr([key1, key2, ..., fallback]) — try keys in order, last is fallback
1021
+ const label = translateOr(['fc.custom.order.label', 'fc.om.orders.label', 'Orders']);
1022
+
1023
+ return <h1>{title}</h1>;
1024
+ };
1025
+ ```
1026
+
1027
+ ### useData — Page Query Data Access
1028
+
1029
+ ```typescript
1030
+ import { useData } from 'mystique/hooks';
1031
+
1032
+ const PageComponent = () => {
1033
+ const data = useData();
1034
+ // data — contains the response from the page-level GraphQL query
1035
+ // In a list page: data.orders.edges, data.orders.pageInfo
1036
+ // In a detail page: data.orderById.ref, data.orderById.status
1037
+ // Variables and filters are managed by the host app
1038
+ return <span>{data?.orderById?.ref}</span>;
1039
+ };
1040
+ ```
1041
+
1042
+ ### useEnv — Environment Details
1043
+
1044
+ ```typescript
1045
+ import { useEnv } from 'mystique/hooks';
1046
+
1047
+ const DebugInfo = () => {
1048
+ const env = useEnv();
1049
+ // env.accountName: string — Fluent account name
1050
+ // env.appName: string — 'oms', 'store', 'servicepoint'
1051
+ // env.apiEndpoint: string — API base URL
1052
+ return <span>Connected to {env.accountName}</span>;
1053
+ };
1054
+ ```
1055
+
1056
+ ## Component Composition Primitives
1057
+
1058
+ Three essential patterns for building reusable, manifest-configurable custom components.
1059
+
1060
+ ### DynamicValue — Manifest-Configurable Content
1061
+
1062
+ `DynamicValue` renders a single dynamic attribute (text, image, or component) from manifest config. Lets manifest configurers control what your component displays without code changes.
1063
+
1064
+ ```typescript
1065
+ import { DynamicValue } from 'mystique/components';
1066
+
1067
+ const OrderCard = ({ data, props }) => {
1068
+ // props.attributes comes from the manifest's attributes[] array
1069
+ return (
1070
+ <div>
1071
+ {props.attributes?.map((attr, i) => (
1072
+ <div key={i}>
1073
+ <label>{attr.label}</label>
1074
+ <DynamicValue attribute={attr} context={data} defaultValue={<span>-</span>} />
1075
+ </div>
1076
+ ))}
1077
+ </div>
1078
+ );
1079
+ };
1080
+ ```
1081
+
1082
+ ### Children — Manifest-Driven Composition
1083
+
1084
+ `Children` renders descendant components configured in the manifest. Lets configurers place OOTB or custom components inside your component via `descendants`.
1085
+
1086
+ ```typescript
1087
+ import { Children } from 'mystique/components';
1088
+
1089
+ const Panel = ({ data, children }) => {
1090
+ return (
1091
+ <div className="custom-panel">
1092
+ <Children /> {/* renders all descendants from manifest */}
1093
+ </div>
1094
+ );
1095
+ };
1096
+
1097
+ // Wrapper variant: transform data for all descendants
1098
+ const FilteredPanel = ({ data }) => {
1099
+ const filteredData = data.items.filter(i => i.status === 'ACTIVE');
1100
+ return (
1101
+ <div>
1102
+ <Children dataOverride={filteredData} />
1103
+ </div>
1104
+ );
1105
+ };
1106
+ ```
1107
+
1108
+ ### TemplateRegistry.render — Resolve Templates in Code
1109
+
1110
+ Use `TemplateRegistry.render()` to resolve Handlebars template strings inside component code.
1111
+
1112
+ ```typescript
1113
+ import { TemplateRegistry } from 'mystique/registry';
1114
+
1115
+ const CustomDisplay = ({ data, props }) => {
1116
+ // props.titleTemplate might be "Order {{ref}} — {{status}}" from manifest
1117
+ const resolvedTitle = TemplateRegistry.render(props.titleTemplate, data);
1118
+ return <h2>{resolvedTitle}</h2>;
1119
+ };
1120
+ ```
1121
+
1122
+ ### FieldRegistry.get — Reuse OOTB Form Fields
1123
+
1124
+ Compose existing OOTB field components inside custom composite fields:
1125
+
1126
+ ```typescript
1127
+ import { FieldRegistry } from 'mystique/registry';
1128
+
1129
+ const CompositeField = ({ onChange, value }) => {
1130
+ const NumberField = FieldRegistry.get('number');
1131
+ return (
1132
+ <div>
1133
+ <NumberField value={value.min} onChange={(v) => onChange({ ...value, min: v })} />
1134
+ <span> to </span>
1135
+ <NumberField value={value.max} onChange={(v) => onChange({ ...value, max: v })} />
1136
+ </div>
1137
+ );
1138
+ };
1139
+ ```
1140
+
1141
+ ### Form Field onChange — Avoid Infinite Loop
1142
+
1143
+ **CRITICAL:** When building form field components, `onChange` triggers a re-render. Never call `onChange` in the render body — wrap in `useEffect`:
1144
+
1145
+ ```typescript
1146
+ // WRONG — infinite re-render loop
1147
+ const BadField = ({ onChange, value }) => {
1148
+ onChange(value || 'default'); // called every render!
1149
+ return <input value={value} />;
1150
+ };
1151
+
1152
+ // CORRECT — useEffect prevents the loop
1153
+ const GoodField = ({ onChange, value }) => {
1154
+ useEffect(() => {
1155
+ if (!value) onChange('default');
1156
+ }, [value]); // only runs when value changes
1157
+
1158
+ return <input value={value} onChange={(e) => onChange(e.target.value)} />;
1159
+ };
1160
+ ```
1161
+
1162
+ ### User Action Attribute to Field Mapping
1163
+
1164
+ Workflow user actions define `eventAttributes` with a `type` property. The Mystique form engine maps each type to a FieldRegistry entry:
1165
+
1166
+ | Workflow `eventAttributes[].type` | FieldRegistry Entry | Input Type |
1167
+ |---|---|---|
1168
+ | `TEXT` / `STRING` | text | Single-line text input |
1169
+ | `TEXTAREA` | textarea | Multi-line text area |
1170
+ | `NUMBER` / `INTEGER` | number | Numeric input |
1171
+ | `BOOLEAN` | boolean | Toggle switch |
1172
+ | `DATE` | date | Date picker |
1173
+ | `DROPDOWN` | dropdown | Select from options |
1174
+ | Custom type name | Custom field (if registered) | Your custom component |
1175
+
1176
+ Register custom fields with `FieldRegistry.register('myCustomType', MyFieldComponent)`. The form engine auto-selects them when a user action has `type: "myCustomType"`.
1177
+
1178
+ ## Build & Development Lifecycle
1179
+
1180
+ ### Development Cycle
1181
+
1182
+ ```
1183
+ 1. Scaffold project
1184
+ /fluent-mystique-scaffold my-plugin --components OrderTracker,StoreMap
1185
+
1186
+ 2. Install dependencies
1187
+ cd accounts/<PROFILE>/SOURCE/my-plugin/
1188
+ yarn install
1189
+
1190
+ 3. Start dev server
1191
+ yarn start
1192
+ -> Serves on http://localhost:3001
1193
+ -> Outputs bundle.[hash].js with CORS headers
1194
+
1195
+ 4. Point manifest plugins[] to localhost
1196
+ In the Fluent OMX app manifest (via Settings or /fluent-mystique-builder):
1197
+ {
1198
+ "plugins": [
1199
+ { "type": "url", "src": "http://localhost:3001/" }
1200
+ ]
1201
+ }
1202
+
1203
+ 5. Test in Fluent OMX App
1204
+ -> Load the OMX app in browser
1205
+ -> The host app fetches version.json from localhost:3001
1206
+ -> Loads the hashed bundle
1207
+ -> Your components are now available in the manifest
1208
+
1209
+ 6. Write tests
1210
+ yarn test # Run Jest with coverage
1211
+ yarn storybook # Visual component testing on port 6006
1212
+
1213
+ 7. Iterate
1214
+ -> Webpack HMR reloads on save
1215
+ -> Refresh the OMX app to pick up changes
1216
+ ```
1217
+
1218
+ ### Production Deployment
1219
+
1220
+ ```
1221
+ 1. Build the bundle
1222
+ yarn build
1223
+ -> Produces dist/bundle.[contenthash].js
1224
+ -> Produces dist/version.json (maps bundle filename)
1225
+ -> Produces dist/manifests/*.json (compiled manifest examples)
1226
+ -> Runs build tests to verify output
1227
+
1228
+ 2. Deploy to hosting
1229
+ Fluent Commerce does NOT host custom component bundles.
1230
+ The bundle must be self-hosted. Options:
1231
+
1232
+ a) Vercel (recommended for simplicity):
1233
+ vercel deploy
1234
+ -> Returns URL like https://my-plugin.vercel.app/
1235
+
1236
+ b) AWS S3 + CloudFront:
1237
+ aws s3 sync dist/ s3://my-bucket/my-plugin/ --cache-control max-age=31536000
1238
+
1239
+ c) Any static hosting / CDN:
1240
+ Upload dist/ contents to your hosting provider
1241
+
1242
+ 3. Update manifest plugins[] with production URL
1243
+ {
1244
+ "plugins": [
1245
+ {
1246
+ "type": "url",
1247
+ "src": "https://my-plugin.vercel.app/",
1248
+ "name": "my-plugin"
1249
+ }
1250
+ ]
1251
+ }
1252
+
1253
+ NOTE: The trailing "/" in the src URL enables version.json auto-discovery.
1254
+ The host app fetches <src>/version.json, reads the bundle filename,
1255
+ then loads <src>/<bundle-filename>.
1256
+
1257
+ 4. Deploy manifest to Fluent environment
1258
+ Use /fluent-mystique-builder to update the manifest, then
1259
+ /fluent-settings to upload it as a Fluent Setting.
1260
+ ```
1261
+
1262
+ ### Key Deployment Facts
1263
+
1264
+ - **Fluent Commerce does NOT host custom component bundles.** The bundle must be self-hosted on Vercel, S3, CDN, or any static file server.
1265
+ - **The manifest `plugins[]` array loads bundles by URL at runtime.** The OMX host app fetches each plugin URL, executes the JavaScript, and the `ComponentRegistry.register()` calls make components available.
1266
+ - **Webpack externals ensure React, ReactDOM, and Material-UI are NOT bundled.** These are provided by the host app. Including them would cause version conflicts and dramatically increase bundle size.
1267
+ - **Bundle output uses `[contenthash]` for cache busting.** The `version.json` file maps the current hash, allowing the host app to discover the latest bundle without hardcoding the hash in the manifest.
1268
+ - **For local development, webpack dev server serves on port 3001** with CORS headers enabled so the host app on a different domain can load the bundle.
1269
+
1270
+ ### Vercel Deployment (Optional Convenience)
1271
+
1272
+ If using Vercel, generate a `vercel.json` in the project root:
1273
+
1274
+ ```json
1275
+ {
1276
+ "version": 2,
1277
+ "builds": [
1278
+ {
1279
+ "src": "dist/**",
1280
+ "use": "@vercel/static"
1281
+ }
1282
+ ],
1283
+ "routes": [
1284
+ {
1285
+ "src": "/(.*)",
1286
+ "dest": "/dist/$1"
1287
+ }
1288
+ ]
1289
+ }
1290
+ ```
1291
+
1292
+ Deploy with:
1293
+ ```bash
1294
+ yarn build
1295
+ vercel deploy --prod
1296
+ ```
1297
+
1298
+ The production URL is stable and can be used directly in the manifest `plugins[]` array.
1299
+
1300
+ ## Build Plugins
1301
+
1302
+ ### build/GenerateVersionPlugin.ts
1303
+
1304
+ Emits `version.json` alongside the bundle, mapping the hashed filename:
1305
+
1306
+ ```typescript
1307
+ import { Compiler, sources } from 'webpack';
1308
+
1309
+ class GenerateVersionPlugin {
1310
+ apply(compiler: Compiler) {
1311
+ compiler.hooks.afterCompile.tap('GenerateVersionPlugin', (compilation) => {
1312
+ Object.entries(compilation.assets).forEach(([pathname]) => {
1313
+ if (pathname.match(/bundle\.[a-z0-9]+\.js$/))
1314
+ compilation.assets['version.json'] =
1315
+ new sources.RawSource(JSON.stringify({
1316
+ bundle: pathname,
1317
+ version: process.env.GIT_TAG || Date.now().toString()
1318
+ }));
1319
+ });
1320
+ });
1321
+ }
1322
+ }
1323
+
1324
+ module.exports = GenerateVersionPlugin;
1325
+ ```
1326
+
1327
+ ### build/GenerateManifestsPlugin.ts
1328
+
1329
+ Compiles TypeScript manifest files in `manifests/` to JSON in `dist/manifests/`:
1330
+
1331
+ ```typescript
1332
+ import webpack from 'webpack';
1333
+ const fs = require('fs-extra');
1334
+
1335
+ class GenerateManifestsPlugin {
1336
+ apply(compiler: webpack.Compiler) {
1337
+ compiler.hooks.afterEmit.tap('GenerateManifestsPlugin', () => {
1338
+ fs.readdir('./manifests', (err: string, files: string[]) => {
1339
+ if (err) {
1340
+ console.log(err);
1341
+ } else {
1342
+ files.forEach((file: string) => {
1343
+ if (file.includes('fc.mystique.manifest') && !file.includes('.test.')) {
1344
+ const filenameNoExt = file.slice(0, -3);
1345
+ import(`../manifests/${filenameNoExt}`).then(manifest => {
1346
+ fs.outputFile(
1347
+ `dist/manifests/${filenameNoExt}.json`,
1348
+ JSON.stringify(manifest.manifest, null, 4)
1349
+ );
1350
+ });
1351
+ }
1352
+ });
1353
+ }
1354
+ });
1355
+ });
1356
+ }
1357
+ }
1358
+
1359
+ module.exports = GenerateManifestsPlugin;
1360
+ ```
1361
+
1362
+ ## .gitignore
1363
+
1364
+ ```gitignore
1365
+ # Dependencies
1366
+ node_modules/
1367
+
1368
+ # Build output
1369
+ dist/
1370
+
1371
+ # Test output
1372
+ test-report/
1373
+ coverage/
1374
+
1375
+ # IDE files
1376
+ .idea/
1377
+ *.iml
1378
+ .vscode/
1379
+
1380
+ # OS files
1381
+ .DS_Store
1382
+ Thumbs.db
1383
+
1384
+ # Logs
1385
+ *.log
1386
+ yarn-debug.log*
1387
+ yarn-error.log*
1388
+
1389
+ # Environment
1390
+ .env
1391
+ .env.local
1392
+ .env.production
1393
+
1394
+ # Storybook
1395
+ storybook-static/
1396
+ ```
1397
+
1398
+ ## Execution Flow
1399
+
1400
+ ```
1401
+ 1. VALIDATE inputs
1402
+ a. project-name: must match /^[a-z][a-z0-9-]*$/ (lowercase, hyphens allowed)
1403
+ b. component names (if --components): must match /^[A-Z][a-zA-Z0-9]*$/ (PascalCase)
1404
+ c. category (if --category): must be page|layout|content|filter
1405
+
1406
+ 2. PRE-CHECK: Project already exists?
1407
+ a. Check accounts/<PROFILE>/SOURCE/<project-name>/ exists
1408
+ b. If exists -> ABORT with guidance (or add components if --components specified)
1409
+
1410
+ 3. DETECT namespace
1411
+ a. If --namespace provided -> use it
1412
+ b. Else derive from project-name
1413
+ c. Fallback: "custom" with warning
1414
+
1415
+ 4. COMPUTE derived values
1416
+ a. PROJECT_TITLE = title-case of project-name with hyphens as spaces
1417
+ b. KEBAB_NAME = kebab-case of each component name
1418
+ c. CATEGORY = from --category or default "content"
1419
+ d. Alias = <namespace>.<category>.<kebab-name>
1420
+
1421
+ 5. CREATE directory structure
1422
+ a. Create all directories listed in the structure section
1423
+ b. Ensure all parent directories exist before writing files
1424
+
1425
+ 6. COPY @types/mystique/ from reference SDK
1426
+ a. Search for reference SDK: OMX Mystique components/omx-component-sdk/omx-component-sdk/@types/mystique/
1427
+ b. Copy all 7 .d.ts files
1428
+ c. Also copy @types/images.d.ts
1429
+
1430
+ 7. COPY lib/ files from reference SDK
1431
+ a. Copy mystique.js and material-ui.js (large bundled files, cannot be generated)
1432
+ b. Generate init.js, mystique-export.js from templates
1433
+
1434
+ 8. GENERATE project config files
1435
+ a. package.json with substituted project name and description
1436
+ b. webpack.config.ts
1437
+ c. tsconfig.json
1438
+ d. jest.config.js
1439
+ e. .babelrc
1440
+ f. .gitignore
1441
+ g. sdk-version.json
1442
+ h. .storybook/main.ts, preview.js, helpers.ts
1443
+
1444
+ 9. GENERATE source files
1445
+ a. src/index.tsx with ComponentRegistry.register() for each component
1446
+ b. src/index.html with project title
1447
+ c. src/setup-tests.ts
1448
+ d. src/utils/GqlUtils.ts, TestUtils.tsx
1449
+ e. manifests/fc.mystique.manifest.example.ts
1450
+ f. manifests/fc.mystique.manifest.fragment.example.ts
1451
+ g. build/GenerateVersionPlugin.ts, GenerateManifestsPlugin.ts
1452
+ h. i18n/locales/en/translation.json (empty object {})
1453
+
1454
+ 10. FOR EACH component in --components (if specified):
1455
+ a. Create src/components/<Name>/ directory
1456
+ b. Generate <Name>.tsx using component template
1457
+ c. Generate <Name>.test.tsx using test template
1458
+ d. Generate <Name>.stories.tsx using story template
1459
+ e. Generate index.ts re-export
1460
+ f. Add import and ComponentRegistry.register() to src/index.tsx
1461
+
1462
+ 11. VERIFY structure
1463
+ If yarn is available: yarn install && yarn check-types
1464
+ This confirms TypeScript compiles and dependencies resolve.
1465
+
1466
+ 12. REPORT generated files
1467
+ List all files created with their full paths.
1468
+ Print next steps:
1469
+ - "Project scaffolded at: accounts/<PROFILE>/SOURCE/<project-name>/"
1470
+ - "Install dependencies: cd accounts/<PROFILE>/SOURCE/<project-name> && yarn install"
1471
+ - "Start dev server: yarn start (serves on http://localhost:3001)"
1472
+ - "Run tests: yarn test"
1473
+ - "Run Storybook: yarn storybook (serves on http://localhost:6006)"
1474
+ - "Build for production: yarn build (outputs to dist/)"
1475
+ - "Next: Wire components into a manifest with /fluent-mystique-builder"
1476
+ ```
1477
+
1478
+ ## Integration with Other Skills
1479
+
1480
+ | Skill | Relationship |
1481
+ |-------|-------------|
1482
+ | `/fluent-mystique-builder` | After scaffolding, use this to create or edit manifests that reference the new components via their registered aliases. |
1483
+ | `/fluent-mystique-validate` | Validate manifests that reference the new components. Checks that component aliases are known and props are well-formed. |
1484
+ | `/fluent-mystique-analyze` | Analyze deployed manifests to understand existing component usage before adding new ones. |
1485
+ | `/fluent-settings` | Deploy the compiled manifest JSON as a Fluent Setting for production use. |
1486
+ | `/fluent-feature-plan` | Required for multi-artifact work (SDK + manifest + workflows). Produces the comprehensive plan. |
1487
+
1488
+ ### Post-Scaffold Workflow
1489
+
1490
+ ```
1491
+ /fluent-mystique-scaffold mystique-plugin-curbside --components CurbsideStatus,StoreMap --category content
1492
+ |
1493
+ v
1494
+ cd accounts/<PROFILE>/SOURCE/mystique-plugin-curbside/ && yarn install
1495
+ |
1496
+ v
1497
+ yarn start (develop and test locally on port 3001)
1498
+ |
1499
+ v
1500
+ /fluent-mystique-builder (create manifest pages using curbside.content.curbside-status, curbside.content.store-map)
1501
+ |
1502
+ v
1503
+ /fluent-mystique-validate (validate the manifest before deployment)
1504
+ |
1505
+ v
1506
+ yarn build (produce dist/bundle.[hash].js)
1507
+ |
1508
+ v
1509
+ vercel deploy --prod (deploy bundle to hosting)
1510
+ |
1511
+ v
1512
+ /fluent-settings (upload manifest JSON to Fluent environment as a Setting)
1513
+ ```
1514
+
1515
+ ## Edge Cases
1516
+
1517
+ ### No components specified
1518
+
1519
+ When `--components` is omitted:
1520
+ - `src/index.tsx` contains only a comment: `// Register your components here`
1521
+ - No component directories are created
1522
+ - The project is still buildable (produces a bundle with no registrations)
1523
+ - This is valid as a starting point -- add components incrementally
1524
+
1525
+ ### Project name conflicts
1526
+
1527
+ Check for any directory matching the project name:
1528
+
1529
+ ```bash
1530
+ ls accounts/<PROFILE>/SOURCE/ | grep -i "<project-name>"
1531
+ ```
1532
+
1533
+ If found, abort. Do NOT overwrite an existing project.
1534
+
1535
+ ### Reference SDK not available
1536
+
1537
+ If `OMX Mystique components/omx-component-sdk/` is not found in the workspace:
1538
+ - Generate `@types/mystique/` files from the embedded templates (all 7 files documented in the plan)
1539
+ - Warn about missing `lib/mystique.js` and `lib/material-ui.js`
1540
+ - The project will build but tests and Storybook will fail without these lib files
1541
+
1542
+ ### Windows path handling
1543
+
1544
+ - All webpack paths use forward slashes (Node.js standard)
1545
+ - Use `path.join()` and `path.resolve()` in webpack config, not hardcoded separators
1546
+ - Build scripts use `yarn` (cross-platform) rather than shell-specific commands
1547
+
1548
+ ### Duplicate component names
1549
+
1550
+ If `--components` contains duplicates, deduplicate silently and warn:
1551
+ ```
1552
+ Warning: Duplicate component name 'OrderTracker' removed from list.
1553
+ ```
1554
+
1555
+ ### Component name validation
1556
+
1557
+ Reject component names that:
1558
+ - Start with a lowercase letter or digit
1559
+ - Contain hyphens, underscores, or spaces (must be PascalCase)
1560
+ - Are empty or longer than 50 characters
1561
+ - Conflict with built-in Mystique components (e.g., `Card`, `List`, `Loading`)
1562
+
1563
+ ## React Hooks API Reference
1564
+
1565
+ The Mystique host app provides 6 hooks via `mystique/hooks/*`. Type declarations are generated in `@types/mystique/hooks.d.ts`. Here is the runtime API for each:
1566
+
1567
+ ### useAuth — User Session & Context
1568
+
1569
+ ```typescript
1570
+ import { useAuth } from 'mystique/hooks/useAuth';
1571
+
1572
+ const auth = useAuth();
1573
+
1574
+ // User info
1575
+ auth.user.username; // string
1576
+ auth.user.firstName; // string
1577
+ auth.user.timezone; // string (e.g., "Australia/Sydney")
1578
+ auth.user.language; // string (e.g., "en")
1579
+ auth.user.roles; // { role: { name: string } }[]
1580
+
1581
+ // Current context
1582
+ auth.context.current.contextId; // string — retailer or location ID
1583
+ auth.context.current.contextType; // "RETAILER" | "LOCATION"
1584
+ auth.context.current.details?.ref; // string — entity ref
1585
+
1586
+ // Actions
1587
+ auth.switchContext({ contextType: 'RETAILER', contextId });
1588
+ auth.logout();
1589
+ ```
1590
+
1591
+ **Use case**: Role-based rendering, context-aware queries, user info display.
1592
+
1593
+ ### useQuery — GraphQL Data Fetching
1594
+
1595
+ ```typescript
1596
+ import { useQuery } from 'mystique/hooks/useQuery';
1597
+
1598
+ const [result, { loading, error, refetch }] = useQuery<ResultType>(
1599
+ queryString,
1600
+ { variables }
1601
+ );
1602
+
1603
+ // result.data — typed query result (null while loading)
1604
+ // loading — boolean
1605
+ // error — Error object or null
1606
+ // refetch — () => void — re-execute query
1607
+ ```
1608
+
1609
+ Results follow the Relay connection pattern: `edges[].node`, `pageInfo { hasNextPage, endCursor }`.
1610
+
1611
+ **Prefer page query data over useQuery** — only use useQuery for follow-up queries not available in the page data context. Page queries are defined in the manifest `data` block and auto-executed.
1612
+
1613
+ ### useSettings — Dynamic Configuration
1614
+
1615
+ ```typescript
1616
+ import { useSettings } from 'mystique/hooks/useSettings';
1617
+
1618
+ const settings = useSettings({ key: 'my.setting.name' });
1619
+
1620
+ // Result shape:
1621
+ // settings.key.status — 'loading' | 'error' | (truthy on success)
1622
+ // settings.key.result?.value — the setting value (any type)
1623
+ ```
1624
+
1625
+ **Hierarchy**: Location → Retailer → Account → Global (cascades up). A Location-level setting overrides Retailer, which overrides Account.
1626
+
1627
+ ### useI18n — Internationalization
1628
+
1629
+ ```typescript
1630
+ import { useI18n } from 'mystique/hooks/useI18n';
1631
+
1632
+ const { translate, translateOr, languages, currentLanguage } = useI18n();
1633
+
1634
+ translate('fc.om.orders.title'); // single key lookup
1635
+ translateOr([ // fallback chain
1636
+ 'acme.errors.specificError',
1637
+ 'acme.errors.genericError',
1638
+ 'An error occurred' // last item = default
1639
+ ]);
1640
+ currentLanguage.label; // e.g., "English"
1641
+ languages; // { value, label }[]
1642
+ ```
1643
+
1644
+ Language dictionaries auto-load from the setting matching the user's language preference (e.g., `LANGUAGE_JA-JP`).
1645
+
1646
+ ### useData — Page Query Access
1647
+
1648
+ ```typescript
1649
+ import { useData } from 'mystique/hooks/useData';
1650
+
1651
+ const { data, variables, loading, error } = useData();
1652
+
1653
+ data.order; // page query result, scoped by component's dataSource
1654
+ variables.orderId; // query variables from manifest
1655
+ ```
1656
+
1657
+ **Use case**: Components that interact with page-level data (filters, list controls) without needing their own query.
1658
+
1659
+ ### useEnv — Environment Details
1660
+
1661
+ ```typescript
1662
+ import { useEnv } from 'mystique/hooks/useEnv';
1663
+
1664
+ const env = useEnv();
1665
+
1666
+ env.account; // Account name
1667
+ env.app; // App name: "oms", "store", "servicepoint", or custom
1668
+ env.api; // API endpoint URL
1669
+ ```
1670
+
1671
+ ## Advanced Component Patterns
1672
+
1673
+ ### DynamicValue — User-Configurable Display
1674
+
1675
+ `<DynamicValue />` renders manifest-configured attributes as text, image, or embedded component. This is the key to making components reusable across different manifest contexts:
1676
+
1677
+ ```typescript
1678
+ import { DynamicValue } from 'mystique/components/DynamicValue';
1679
+
1680
+ // GOOD — configurer controls what is displayed
1681
+ export const MyComponent: FC<ComponentProps> = ({ attribute, data }) => {
1682
+ return <DynamicValue attribute={attribute} context={data} defaultValue={<span>-</span>} />;
1683
+ };
1684
+
1685
+ // AVOID — hardcoded field access, not reusable
1686
+ export const MyComponent: FC<ComponentProps> = ({ data }) => {
1687
+ return <div>{data.order.ref}</div>;
1688
+ };
1689
+ ```
1690
+
1691
+ ### Children — Manifest-Driven Composition
1692
+
1693
+ `<Children />` renders whatever descendant components are configured in the manifest. This lets configurers compose UI without touching code:
1694
+
1695
+ ```typescript
1696
+ import { Children } from 'mystique/components/Children';
1697
+
1698
+ // Render descendants from manifest
1699
+ export const MyAccordion: FC = () => (
1700
+ <CollapsibleDiv>
1701
+ <Children />
1702
+ </CollapsibleDiv>
1703
+ );
1704
+
1705
+ // Override data context for all descendants
1706
+ export const MyDataWrapper: FC = ({ data }) => {
1707
+ const transformed = transformData(data);
1708
+ return (
1709
+ <div>
1710
+ <Children dataOverride={transformed} />
1711
+ </div>
1712
+ );
1713
+ };
1714
+ ```
1715
+
1716
+ ### TemplateRegistry.render — Resolve Manifest Templates in Code
1717
+
1718
+ When your component receives a template string from the manifest (e.g., `"{{order.ref}} - {{order.status}}"`), resolve it at runtime:
1719
+
1720
+ ```typescript
1721
+ import { TemplateRegistry } from 'mystique/registry/TemplateRegistry';
1722
+
1723
+ const resolved = TemplateRegistry.render('{{order.ref}} - {{order.status}}', data);
1724
+ ```
1725
+
1726
+ ### FieldRegistry.get — Compose Existing Fields
1727
+
1728
+ Reuse OOTB field components inside custom composite fields:
1729
+
1730
+ ```typescript
1731
+ import { FieldRegistry } from 'mystique/registry/FieldRegistry';
1732
+
1733
+ const QuantityField = FieldRegistry.get('number');
1734
+
1735
+ // Use in your component's render:
1736
+ <QuantityField name="qty" label="Quantity" value={qty} onChange={setQty} />
1737
+ ```
1738
+
1739
+ ### onChange + useEffect — Loop Prevention (CRITICAL)
1740
+
1741
+ Form field `onChange` MUST be wrapped in `useEffect`. Calling `onChange` directly in a handler causes infinite re-renders:
1742
+
1743
+ ```typescript
1744
+ // CORRECT — useEffect breaks the render cycle
1745
+ const [value, setValue] = useState('');
1746
+ useEffect(() => {
1747
+ onChange({ value });
1748
+ }, [value]);
1749
+
1750
+ // WRONG — causes infinite loop
1751
+ const handleChange = (v: string) => {
1752
+ setValue(v);
1753
+ onChange({ value: v }); // triggers parent re-render → this component re-renders → calls onChange again
1754
+ };
1755
+ ```
1756
+
1757
+ ### User Action Attribute ↔ Field Mapping
1758
+
1759
+ Workflow user actions define form attributes with a `type` field. Mystique auto-selects the matching FieldRegistry entry:
1760
+
1761
+ | Workflow Attribute Type | FieldRegistry Key | Component |
1762
+ |------------------------|-------------------|-----------|
1763
+ | `STRING` | `text` | Text input |
1764
+ | `INTEGER` | `number` | Number input |
1765
+ | `BOOLEAN` | `boolean` | Checkbox |
1766
+ | `JSON` | `json` | JSON editor |
1767
+ | `<custom>` | `<custom>` | Your registered field |
1768
+
1769
+ Register a custom field to handle custom types: `FieldRegistry.register(['stockReservation'], StockReservationField)`.
1770
+
1771
+ ### Material-UI Version Lock
1772
+
1773
+ The host OMX app ships **Material-UI v4.12.4**. This version is locked — do NOT use MUI v5/v6 APIs. Some newer patterns (certain keyframe syntax, `sx` prop, Emotion-based `styled()`) are unsupported.
1774
+
1775
+ Theme colors: primary `#00A9E0`, secondary (brand purple), plus `success`, `error`, `warning`, `info` from the theme palette. Access via `makeStyles`:
1776
+
1777
+ ```typescript
1778
+ const useStyles = makeStyles((theme) => ({
1779
+ root: { color: theme.palette.primary.main }, // #00A9E0
1780
+ error: { color: theme.palette.error.main },
1781
+ }));
1782
+ ```
1783
+
1784
+ Responsive breakpoints: `theme.breakpoints.down('sm')`, `theme.breakpoints.up('md')`.
1785
+
1786
+ ## Session Tracking
1787
+
1788
+ When invoked, log the following to the session tracking protocol (consumed by `/fluent-session-summary` and `/fluent-session-audit-export`):
1789
+
1790
+ **On entry:**
1791
+ ```json
1792
+ { "skill": "fluent-mystique-scaffold", "timestamp": "<ISO-8601>", "arguments": { "projectName": "<name>", "components": ["<list>"], "category": "<cat>" } }
1793
+ ```
1794
+
1795
+ **On exit:**
1796
+ ```json
1797
+ { "skill": "fluent-mystique-scaffold", "outcome": "<completed|failed|skipped>", "changesProduced": ["<seq-numbers>"], "toolCallsProduced": ["<seq-numbers>"], "nextRecommended": "/fluent-mystique-builder" }
1798
+ ```
1799
+
1800
+ ## Handoff
1801
+
1802
+ | Output Artifact | Consumed By | Path |
1803
+ |----------------|-------------|------|
1804
+ | SDK project with components | `/fluent-mystique-builder` (creates manifest referencing components) | `accounts/<PROFILE>/SOURCE/<project-name>/` |
1805
+ | Built bundle | Manifest `plugins[]` array (loaded at runtime by host app) | `accounts/<PROFILE>/SOURCE/<project-name>/dist/bundle.[hash].js` |
1806
+ | Compiled manifests | `/fluent-mystique-validate` (validates before deploy) | `accounts/<PROFILE>/SOURCE/<project-name>/dist/manifests/` |
1807
+
1808
+ ## Error Reporting
1809
+
1810
+ Errors from this skill follow the standard format:
1811
+
1812
+ | Field | Description |
1813
+ |-------|-------------|
1814
+ | `phase` | Skill phase where error occurred (e.g., `pre-check`, `validation`, `generation`, `verification`) |
1815
+ | `severity` | `CRITICAL` (blocks downstream), `HIGH` (needs fix), `MEDIUM` (advisory), `LOW` (informational) |
1816
+ | `message` | Human-readable error description |
1817
+ | `resolution` | Suggested fix or downstream skill to invoke |
1818
+
1819
+ ## Gotchas
1820
+
1821
+ - **`lib/mystique.js` is ~5MB** and `lib/material-ui.js` is ~1.2MB. These are bundled Fluent Commerce SDK files that cannot be generated -- they must be copied from the reference SDK distribution. Without them, Jest and Storybook will fail with `Cannot read properties of undefined (reading 'ComponentRegistry')`.
1822
+ - **Webpack externals are order-sensitive.** The three externals (`static map`, `externalMaterialUI`, `externalMystique`) must be in an array. If any handler catches a module it should not, the bundle will break at runtime.
1823
+ - **`@material-ui/core` subpath imports** (e.g., `@material-ui/core/Button`) are routed to `window.MaterialUI.Button` by the `externalMaterialUI` function. The `styles` subpath is special-cased to return the root `MaterialUI` object.
1824
+ - **`mystique/*` imports** (e.g., `mystique/hooks/useQuery`) all resolve to `window.Mystique` at runtime. The TypeScript type declarations in `@types/mystique/` provide compile-time safety, but the actual objects come from the host app global.
1825
+ - **React 17 is required.** The host Fluent OMX app provides React 17. Using React 18 APIs will fail at runtime even if they compile successfully.
1826
+ - **Content hash in filename** means the URL changes on every build. The `version.json` file is the stable discovery mechanism -- the manifest `plugins[].src` should point to the directory (with trailing `/`), not the specific bundle file.
1827
+ - **CORS headers are essential** for local development. Without `Access-Control-Allow-Origin: *` on the dev server, the host app cannot load the bundle from localhost:3001.
1828
+ - **Storybook 6.5.x** is the last version compatible with this SDK's webpack 5 setup. Do not upgrade to Storybook 7+ without verifying compatibility.
1829
+ - **The `@emotion/react` external** is mapped to `emotionReact` (not `Emotion`). This matches how `lib/init.js` sets `globalThis.emotionReact = Emotion`.
1830
+ - **Manifest `plugins[]` type field:** Use `{ "type": "url", "src": "..." }` for URL-based plugins (production bundles) or `{ "type": "setting", "setting": "..." }` for setting-based plugin references. Local development always uses the URL type.