juo 0.3.0-alpha.0 → 0.3.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +1 -1
  2. package/bin/dev.js +15 -4
  3. package/dist/commands/blocks/dev.d.ts +2 -2
  4. package/dist/commands/create.d.ts +4 -4
  5. package/dist/commands/create.js +85 -38
  6. package/dist/commands/generate.d.ts +5 -5
  7. package/dist/commands/publish/index.d.ts +2 -2
  8. package/dist/lib/index.d.ts +2 -0
  9. package/dist/lib/index.js +1 -1
  10. package/dist/{packageGenerator-Bx2FAq2Y.js → packageGenerator-BToocy-h.js} +6 -7
  11. package/oclif.manifest.json +1 -1
  12. package/package.json +1 -1
  13. package/templates/.storybook/preview-head.html.liquid +3 -5
  14. package/templates/block/preact/index.ts.liquid +65 -10
  15. package/templates/block/preact/{{block.clsName}}.tsx.liquid +100 -130
  16. package/templates/block/react/index.ts.liquid +64 -8
  17. package/templates/block/react/{{block.clsName}}.tsx.liquid +109 -61
  18. package/templates/configFiles/src/main.ts.liquid +5 -0
  19. package/templates/configFiles/vite-env.d.ts.liquid +11 -1
  20. package/templates/configFiles/vite.config.ts.liquid +1 -1
  21. package/templates/css/styles.css +31 -35
  22. package/templates/dev/index.html.liquid +18 -2
  23. package/templates/dev/vite/blocks-shim.ts.liquid +6 -0
  24. package/templates/dev/vite/juo-editor-plugin.ts.liquid +23 -2
  25. package/templates/dev/vite/mock-login.ts.liquid +83 -0
  26. package/templates/dev/vite/mock-services.ts.liquid +140 -0
  27. package/templates/dev/vite/theme-shell.ts.liquid +267 -0
  28. package/templates/dev/vite/workflowOverlay.ts.liquid +280 -0
  29. package/templates/stories/{{block.clsName}}.stories.tsx.liquid +6 -3
  30. package/templates/tailwind/postcss.config.js.liquid +6 -0
  31. package/templates/tailwind/src/tailwind.css.liquid +4 -2
  32. package/templates/tailwind/tailwind.config.cjs.liquid +15 -0
  33. package/templates/dev/src/main.ts.liquid +0 -136
  34. package/templates/tailwind/vite.config.ts.liquid +0 -35
@@ -0,0 +1,267 @@
1
+ // Dev-only theme shell. Lives outside the project's `src/` so the author's
2
+ // source tree contains only their blocks. Vite serves this file directly (see
3
+ // the <script> + import map injected in index.html); it never enters the built
4
+ // blocks bundle (whose entry is `src/blocks/register.ts`).
5
+ import "@juo/blocks/web-components/editor";
6
+ {% if tailwind %}import "../src/tailwind.css";
7
+ {% else %}import "../src/styles.css";
8
+ {% endif %}
9
+ import {
10
+ createBlockInstance,
11
+ createThemeState,
12
+ defineBlock,
13
+ effect,
14
+ injectContext,
15
+ isEditorPreview,
16
+ provideContext,
17
+ registerBlock,
18
+ serializeBlocks,
19
+ ThemeStateContext,
20
+ TranslationContext,
21
+ createTranslationService,
22
+ createOverlayService,
23
+ OverlayServiceContext,
24
+ untracked,
25
+ } from "@juo/blocks";
26
+ import { registerBlocks as registerProjectBlocks } from "../src/main";
27
+ import { provideMockServices } from "./mock-services";
28
+ // Native blocks shipped by @juo/blocks-extensions, loaded from CDN. Its bare
29
+ // "@juo/blocks" import is bridged to the local instance via the index.html
30
+ // import map (see vite/blocks-shim.ts).
31
+ // @ts-expect-error Remote, untyped module
32
+ import { blocks as nativeBlocks } from "https://cdn.juo.io/web/blocks/juo/{{ extensionsVersion }}/extensions.js";
33
+
34
+ function hexToOklch(hex: string): { l: number; h: number; c: number } {
35
+ const r = parseInt(hex.slice(1, 3), 16) / 255;
36
+ const g = parseInt(hex.slice(3, 5), 16) / 255;
37
+ const b = parseInt(hex.slice(5, 7), 16) / 255;
38
+ const linearize = (v: number) =>
39
+ v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
40
+ const lr = linearize(r);
41
+ const lg = linearize(g);
42
+ const lb = linearize(b);
43
+ const lm = Math.cbrt(0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb);
44
+ const mm = Math.cbrt(0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb);
45
+ const sm = Math.cbrt(0.0883024619 * lr + 0.2220049178 * lg + 0.6896926208 * lb);
46
+ const L = 0.2104542553 * lm + 0.793617785 * mm - 0.0040720468 * sm;
47
+ const a = 1.9779984951 * lm - 2.428592205 * mm + 0.4505937099 * sm;
48
+ const bOk = 0.0259040371 * lm + 0.7827717662 * mm - 0.808675766 * sm;
49
+ const c = Math.sqrt(a * a + bOk * bOk);
50
+ const h = Math.atan2(bOk, a) * (180 / Math.PI);
51
+ return { l: L * 100, h: h < 0 ? h + 360 : h, c };
52
+ }
53
+
54
+ const homePageBlock = defineBlock("HomePage", {
55
+ displayName: "Home page",
56
+ group: "page",
57
+ schema: {
58
+ type: "object",
59
+ properties: {
60
+ props: {
61
+ type: "object",
62
+ properties: {},
63
+ additionalProperties: false,
64
+ },
65
+ slots: {
66
+ type: "object",
67
+ properties: { main: {} },
68
+ additionalProperties: false,
69
+ },
70
+ },
71
+ required: ["props", "slots"],
72
+ additionalProperties: false,
73
+ },
74
+ initialValue: () => ({
75
+ props: {},
76
+ slots: {
77
+ main: [
78
+ createBlockInstance("Banner"),
79
+ createBlockInstance("Header"),
80
+ createBlockInstance("RenewalSummary"),
81
+ createBlockInstance("UpcomingOrderItems"),
82
+ createBlockInstance("Starter Block"),
83
+ createBlockInstance("CustomerSubscriptions"),
84
+ createBlockInstance("OrderList"),
85
+ ],
86
+ },
87
+ }),
88
+ renderer() {
89
+ const page = document.createElement("juo-page");
90
+ page.setAttribute("name", "HomePage");
91
+ const extensionRoot = document.createElement("juo-extension-root");
92
+ extensionRoot.setAttribute("name", "main");
93
+ page.appendChild(extensionRoot);
94
+ return page;
95
+ },
96
+ });
97
+
98
+ function registerBlocks() {
99
+ Object.values(nativeBlocks).forEach((block) => registerBlock(block));
100
+ registerProjectBlocks();
101
+ registerBlock(homePageBlock);
102
+ }
103
+
104
+ async function setupEditorMode(root: HTMLElement) {
105
+ const editorModule = await import("@juo/blocks/editor");
106
+ const editor = editorModule.createEditorBridge();
107
+ const routerService = {
108
+ push: () => {},
109
+ subscribe: () => () => {},
110
+ getPages: () => [{ path: "/", block: "HomePage", title: "Home", routeNames: [] }],
111
+ };
112
+ const themeState = createThemeState({
113
+ resolve: (surface, path) => editor.requestThemeState(surface, path),
114
+ upsertState: (surface, path, blocks) =>
115
+ editor.upsertThemeState(surface, path, blocks),
116
+ translations: {
117
+ resolveTranslations: (locale) =>
118
+ editor.requestTranslationOverrides(locale),
119
+ },
120
+ routerService,
121
+ });
122
+ provideContext(root, ThemeStateContext, themeState);
123
+ const overlayService = createOverlayService(themeState);
124
+ provideContext(root, OverlayServiceContext, overlayService);
125
+ await editorModule.setupEditorMode({ routerService, themeState, overlayService });
126
+ }
127
+
128
+ function setupViewMode(root: HTMLElement) {
129
+ const themeState = createThemeState({
130
+ resolve: async () => ({
131
+ blocks: serializeBlocks([createBlockInstance("HomePage")]),
132
+ }),
133
+ });
134
+ provideContext(root, ThemeStateContext, themeState);
135
+ provideContext(root, OverlayServiceContext, createOverlayService(themeState));
136
+ }
137
+
138
+ async function initializeTheme() {
139
+ const root = document.querySelector("juo-context-root") as HTMLElement;
140
+
141
+ registerBlocks();
142
+
143
+ if (isEditorPreview()) {
144
+ await setupEditorMode(root);
145
+ } else {
146
+ setupViewMode(root);
147
+ }
148
+
149
+ const themeState = injectContext(root, ThemeStateContext);
150
+
151
+ effect(() => {
152
+ const styles = themeState.globalStyles.value;
153
+ const el = root;
154
+
155
+ el.setAttribute("data-volume", styles.volume);
156
+ el.setAttribute("data-style", styles.style);
157
+ el.setAttribute("data-corners", styles.corners);
158
+ el.setAttribute("data-button-corners", styles.buttonCorners);
159
+ el.setAttribute("data-thumbnail-corners", styles.thumbnailCorners);
160
+
161
+ const bg = styles.colors.background;
162
+ if (bg) {
163
+ el.style.setProperty("background-color", bg);
164
+ } else {
165
+ el.style.removeProperty("background-color");
166
+ }
167
+
168
+ const colorizableKeys = ["tint", "accent", "secondary", "callout"] as const;
169
+
170
+ if (styles.theme === "custom") {
171
+ for (const key of colorizableKeys) {
172
+ el.setAttribute(`data-${key}`, "custom");
173
+ const hex = styles.colors[key];
174
+ if (hex) {
175
+ const { l, h, c } = hexToOklch(hex);
176
+ el.style.setProperty(`--${key}-l`, `${l}%`);
177
+ el.style.setProperty(`--${key}-hue`, String(h));
178
+ el.style.setProperty(`--${key}-chroma`, String(c / 0.045));
179
+ }
180
+ }
181
+ } else {
182
+ el.setAttribute("data-tint", styles.themeType ?? "neutral");
183
+ el.removeAttribute("data-callout");
184
+
185
+ const customColorKeys = ["accent", "secondary"] as const;
186
+ for (const key of customColorKeys) {
187
+ const hex = styles.colors[key];
188
+ if (hex) {
189
+ el.setAttribute(`data-${key}`, "custom");
190
+ const { l, h, c } = hexToOklch(hex);
191
+ el.style.setProperty(`--${key}-l`, `${l}%`);
192
+ el.style.setProperty(`--${key}-hue`, String(h));
193
+ el.style.setProperty(`--${key}-chroma`, String(c / 0.045));
194
+ } else {
195
+ el.removeAttribute(`data-${key}`);
196
+ el.style.removeProperty(`--${key}-l`);
197
+ el.style.removeProperty(`--${key}-hue`);
198
+ el.style.removeProperty(`--${key}-chroma`);
199
+ }
200
+ }
201
+
202
+ el.style.removeProperty("--tint-l");
203
+ el.style.removeProperty("--tint-hue");
204
+ el.style.removeProperty("--tint-chroma");
205
+ el.style.removeProperty("--callout-l");
206
+ el.style.removeProperty("--callout-hue");
207
+ el.style.removeProperty("--callout-chroma");
208
+ }
209
+
210
+ const semanticColorMap = [
211
+ { themeKey: "success", cssPrefix: "success" },
212
+ { themeKey: "error", cssPrefix: "error" },
213
+ { themeKey: "information", cssPrefix: "info" },
214
+ { themeKey: "warning", cssPrefix: "warning" },
215
+ ] as const;
216
+
217
+ for (const { themeKey, cssPrefix } of semanticColorMap) {
218
+ const hex = styles.colors[themeKey];
219
+ if (hex) {
220
+ el.setAttribute(`data-${cssPrefix}`, "custom");
221
+ const { l, h, c } = hexToOklch(hex);
222
+ el.style.setProperty(`--${cssPrefix}-l`, `${l}%`);
223
+ el.style.setProperty(`--${cssPrefix}-hue`, String(h));
224
+ el.style.setProperty(`--${cssPrefix}-chroma`, String(c / 0.045));
225
+ } else {
226
+ el.removeAttribute(`data-${cssPrefix}`);
227
+ el.style.removeProperty(`--${cssPrefix}-l`);
228
+ el.style.removeProperty(`--${cssPrefix}-hue`);
229
+ el.style.removeProperty(`--${cssPrefix}-chroma`);
230
+ }
231
+ }
232
+ });
233
+
234
+ await themeState.locales.setLocale("en");
235
+ provideContext(
236
+ root,
237
+ TranslationContext,
238
+ createTranslationService(themeState.locales, {
239
+ async loadDefaultTranslations() {
240
+ return {};
241
+ },
242
+ }),
243
+ );
244
+
245
+ // Provide every blocks service (backed by mock adapters) so authored blocks
246
+ // have their service dependencies available in the dev preview.
247
+ provideMockServices(root, themeState);
248
+
249
+ void themeState.resolve("page", "/");
250
+
251
+ effect(() => {
252
+ const block = themeState.page.blocks.value.at(0);
253
+ untracked(() => {
254
+ if (block == null) {
255
+ document.querySelector("#app")?.replaceChildren();
256
+ } else {
257
+ document.querySelector("#app")?.replaceChildren(block.definition.render(block));
258
+ }
259
+ });
260
+ });
261
+ }
262
+
263
+ if (document.readyState === "loading") {
264
+ document.addEventListener("DOMContentLoaded", () => void initializeTheme());
265
+ } else {
266
+ void initializeTheme();
267
+ }
@@ -0,0 +1,280 @@
1
+ /**
2
+ * Workflow Overlay — ported from the reference theme's `lib/workflowOverlay.ts`
3
+ * (itself ported from customer-portal's WorkflowOverlay.vue).
4
+ *
5
+ * Renders the workflow UI in a modal or sidebar container using lit-html +
6
+ * Preact signals effects (consistent with the theme/blocks approach).
7
+ *
8
+ * Behaviour:
9
+ * - Watches WorkflowService signals (state, currentStepInfo, isLoading)
10
+ * - Determines layout (modal/sidebar) from the flow root block props
11
+ * - Renders `<juo-workflow>` for step content
12
+ * - Shows loading spinner, completed checkmark, error message
13
+ * - Close button cancels the workflow
14
+ *
15
+ * Dev-only: lives in `vite/` and never enters the built blocks bundle.
16
+ */
17
+
18
+ import { html, render, nothing } from "lit-html";
19
+ import {
20
+ effect,
21
+ type WorkflowService,
22
+ type ThemeState,
23
+ } from "@juo/blocks";
24
+
25
+ // ============================================
26
+ // SVG icons (inlined from customer portal)
27
+ // ============================================
28
+
29
+ const backArrowSvg = html`<svg width="18" height="8" viewBox="0 0 18 8" fill="none" xmlns="http://www.w3.org/2000/svg">
30
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M4.67634 7.71985C4.30281 8.09338 3.69719 8.09338 3.32366 7.71985L0.280146 4.67634C-0.093382 4.3028 -0.093382 3.69719 0.280146 3.32366L3.32366 0.280148C3.69719 -0.0933826 4.30281 -0.0933826 4.67634 0.280148C5.04987 0.653678 5.04987 1.25929 4.67634 1.63282L3.26564 3.04351H18V4.95648H3.26564L4.67634 6.36718C5.04987 6.74071 5.04987 7.34632 4.67634 7.71985Z" fill="currentColor"/>
31
+ </svg>`;
32
+
33
+ const closeSvg = html`<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
34
+ <rect opacity="0.5" width="24" height="24" rx="12" fill="#D9D9D9"/>
35
+ <path d="M16 14.959C15.9904 14.8523 15.94 14.7533 15.8488 14.6619L13.185 11.9945L15.8486 9.32734C15.9316 9.24429 15.9778 9.14942 15.9874 9.04271C15.9969 8.93601 15.967 8.84796 15.8978 8.77857L15.2117 8.09163C15.1424 8.02225 15.0545 7.99233 14.9479 8.00189C14.8414 8.01146 14.7466 8.05776 14.6637 8.14081L12.0001 10.808L9.3363 8.14065C9.25336 8.0576 9.15861 8.0113 9.05205 8.00174C8.94549 7.99218 8.85756 8.02209 8.78827 8.09148L8.10225 8.77842C8.03296 8.84781 8.00308 8.93585 8.01263 9.04256C8.02218 9.14926 8.06842 9.24414 8.15136 9.32718L10.8151 11.9945L8.1512 14.6621C8.05997 14.7534 8.00958 14.8524 8.00003 14.9591C7.99878 15.0575 8.0328 15.1414 8.10209 15.2108L8.78811 15.8978C8.85741 15.9672 8.94119 16.0012 9.03946 16C9.14602 15.9904 9.24491 15.94 9.33614 15.8486L12.0001 13.1811L14.6639 15.8484C14.7551 15.9398 14.854 15.9903 14.9605 15.9998C15.0588 16.0011 15.1426 15.967 15.2119 15.8976L15.8979 15.2107C15.9672 15.1413 16.0012 15.0574 16 14.959Z" fill="#2E3234"/>
36
+ </svg>`;
37
+
38
+ // ============================================
39
+ // Shared styles (injected once)
40
+ // ============================================
41
+
42
+ const overlayStyles = `
43
+ @keyframes wf-spin {
44
+ to { transform: rotate(360deg); }
45
+ }
46
+ @keyframes wf-slide-in {
47
+ from { transform: translateX(100%); }
48
+ to { transform: translateX(0); }
49
+ }
50
+ @keyframes wf-fade-in {
51
+ from { opacity: 0; }
52
+ to { opacity: 1; }
53
+ }
54
+ @keyframes wf-scale-in {
55
+ from { opacity: 0; transform: scale(0.95); }
56
+ to { opacity: 1; transform: scale(1); }
57
+ }
58
+ `;
59
+
60
+ // ============================================
61
+ // Content templates
62
+ // ============================================
63
+
64
+ function loadingTemplate() {
65
+ return html`
66
+ <div style="display:flex;justify-content:center;align-items:center;min-height:200px">
67
+ <div style="width:32px;height:32px;border:3px solid #e5e7eb;border-top-color:#3b82f6;border-radius:50%;animation:wf-spin 1s linear infinite"></div>
68
+ </div>
69
+ `;
70
+ }
71
+
72
+ function stepTemplate() {
73
+ return html`<div><juo-workflow></juo-workflow></div>`;
74
+ }
75
+
76
+ function completedTemplate() {
77
+ return html`
78
+ <div style="text-align:center;padding:24px 0">
79
+ <div style="width:48px;height:48px;margin:0 auto 16px;background:#22c55e;color:white;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:24px">&#x2713;</div>
80
+ <h3 style="font-weight:600;font-size:18px;line-height:21px;text-align:center">Completed successfully!</h3>
81
+ </div>
82
+ `;
83
+ }
84
+
85
+ function errorTemplate(errorMessage: string | null) {
86
+ return html`
87
+ <div style="text-align:center;padding:24px 0">
88
+ <p style="color:#ef4444;font-size:14px;line-height:22px">${errorMessage}</p>
89
+ </div>
90
+ `;
91
+ }
92
+
93
+ function closeButtonTemplate(handleClose: () => void) {
94
+ return html`
95
+ <div style="display:flex;justify-content:center;gap:24px;padding:16px 16px 24px">
96
+ <button
97
+ style="display:inline-flex;justify-content:center;align-items:center;border-radius:2px;font-size:14px;padding:8px 20px;border:1px solid #d1d5db;background:white;cursor:pointer;font-family:inherit"
98
+ @click=${handleClose}
99
+ >Close</button>
100
+ </div>
101
+ `;
102
+ }
103
+
104
+ // ============================================
105
+ // Layout renderers
106
+ // ============================================
107
+
108
+ function renderSidebar(
109
+ contentTemplate: unknown,
110
+ terminalActions: unknown,
111
+ handleClose: () => void,
112
+ ) {
113
+ return html`
114
+ <style>${overlayStyles}</style>
115
+ <div style="position:fixed;inset:0;z-index:999999998;overflow:hidden">
116
+ <div style="width:100%;height:100%;overflow-x:hidden;overflow-y:auto">
117
+ <aside style="position:relative;min-height:100%;display:flex;flex-direction:column;margin-left:auto;width:520px;background:white;box-shadow:-7px 0 4px rgba(204,204,204,0.25);animation:wf-slide-in 0.3s ease-out">
118
+ <header style="position:sticky;top:0;padding:14px 24px;z-index:30;background:white;border-bottom:1px solid #d9d9d9">
119
+ <div style="display:flex">
120
+ <button style="display:block;max-width:80px;padding:8px 0;background:none;border:none;cursor:pointer" @click=${handleClose}>
121
+ ${backArrowSvg}
122
+ <span style="position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0">Close menu</span>
123
+ </button>
124
+ </div>
125
+ </header>
126
+ <div style="padding:14px 24px;flex-grow:1;display:flex;flex-direction:column">
127
+ ${contentTemplate}
128
+ </div>
129
+ ${terminalActions}
130
+ </aside>
131
+ </div>
132
+ </div>
133
+ `;
134
+ }
135
+
136
+ function renderModal(
137
+ contentTemplate: unknown,
138
+ terminalActions: unknown,
139
+ handleClose: () => void,
140
+ canAbort: boolean,
141
+ maxWidth: string,
142
+ ) {
143
+ return html`
144
+ <style>${overlayStyles}</style>
145
+ <div style="position:fixed;inset:0;z-index:999999999">
146
+ <div
147
+ style="position:fixed;inset:0;background:rgba(47,50,52,0.1);transition:opacity 0.3s ease-out;animation:wf-fade-in 0.3s ease-out"
148
+ @click=${handleClose}
149
+ ></div>
150
+ <div style="position:fixed;inset:0;z-index:10;overflow-y:auto">
151
+ <div style="display:flex;align-items:center;min-height:100%;justify-content:center;margin:auto;padding:0;text-align:center">
152
+ <div style="width:100%;overflow-y:auto;padding:16px 16px 20px;filter:drop-shadow(0 10px 8px rgb(0 0 0 / 0.04)) drop-shadow(0 4px 3px rgb(0 0 0 / 0.1))">
153
+ <div
154
+ style="position:relative;overflow:hidden;border-radius:14px;background:white;text-align:left;padding:16px 0;margin:32px auto;width:100%;max-width:${maxWidth};animation:wf-scale-in 0.3s ease-out"
155
+ @click=${(e: Event) => e.stopPropagation()}
156
+ >
157
+ ${canAbort
158
+ ? html`
159
+ <button
160
+ style="position:absolute;top:10px;right:10px;background:none;border:none;cursor:pointer;padding:0"
161
+ type="button"
162
+ @click=${handleClose}
163
+ >${closeSvg}</button>
164
+ `
165
+ : nothing}
166
+ <main style="padding:18px 32px 16px">
167
+ ${contentTemplate}
168
+ </main>
169
+ ${terminalActions}
170
+ </div>
171
+ </div>
172
+ </div>
173
+ </div>
174
+ </div>
175
+ `;
176
+ }
177
+
178
+ // ============================================
179
+ // Public API
180
+ // ============================================
181
+
182
+ /**
183
+ * Create and mount the workflow overlay.
184
+ * Appends a container element and reactively renders the overlay UI
185
+ * based on WorkflowService and ThemeState signals.
186
+ */
187
+ export function createWorkflowOverlay(
188
+ container: HTMLElement,
189
+ workflowService: WorkflowService,
190
+ themeState: ThemeState,
191
+ ) {
192
+ const overlayEl = document.createElement("div");
193
+ overlayEl.id = "workflow-overlay";
194
+ container.appendChild(overlayEl);
195
+
196
+ // Cache layout so it persists after overlay blocks are cleared (on completion/error).
197
+ // Without caching, the layout would revert to "modal" default when blocks are emptied,
198
+ // causing a visible jump from sidebar → modal on flow completion.
199
+ let cachedLayout: "modal" | "sidebar" = "modal";
200
+
201
+ effect(() => {
202
+ const state = workflowService.state.value;
203
+ const stepInfo = workflowService.currentStepInfo.value;
204
+ const loading = workflowService.isLoading.value;
205
+ const overlayBlocks = themeState.overlay.blocks.value;
206
+
207
+ // Extract props from the flow root block
208
+ const flowRootBlock = overlayBlocks?.[0] ?? null;
209
+ const canAbort = Boolean(flowRootBlock?.props?.canAbort ?? true);
210
+ const layout = String(flowRootBlock?.props?.layout ?? "modal") as
211
+ | "modal"
212
+ | "sidebar";
213
+ const size = String(flowRootBlock?.props?.size ?? "md") as
214
+ | "sm"
215
+ | "md"
216
+ | "lg"
217
+ | "xl";
218
+
219
+ // Update cached layout when block is available
220
+ if (flowRootBlock) {
221
+ cachedLayout = layout;
222
+ }
223
+
224
+ // Reset cached layout when workflow returns to idle
225
+ if (state.status === "idle") {
226
+ cachedLayout = "modal";
227
+ }
228
+
229
+ // Map workflow size to modal max-width
230
+ const maxWidth = size === "xl" ? "576px" : "512px";
231
+
232
+ // Visible when the workflow is running AND we know the layout (blocks are set),
233
+ // or when showing a final state (completed/error).
234
+ // Gating on flowRootBlock during loading/active prevents the overlay from appearing
235
+ // before the layout is determined, which would cause a flash of the wrong container.
236
+ const isVisible = (() => {
237
+ const status = state.status;
238
+ if (status === "completed" || status === "error") return true;
239
+ if (status === "active" || status === "loading") {
240
+ return flowRootBlock != null;
241
+ }
242
+ return false;
243
+ })();
244
+
245
+ if (!isVisible) {
246
+ render(nothing, overlayEl);
247
+ return;
248
+ }
249
+
250
+ const handleClose = () => workflowService.cancel();
251
+
252
+ // Determine content based on workflow state
253
+ const content = (() => {
254
+ if (loading && !stepInfo) return loadingTemplate();
255
+ if (stepInfo) return stepTemplate();
256
+ if (state.status === "completed") return completedTemplate();
257
+ if (state.status === "error") return errorTemplate(state.error);
258
+ return nothing;
259
+ })();
260
+
261
+ // Show close button for terminal states (completed/error)
262
+ const terminalActions =
263
+ state.status === "error" || state.status === "completed"
264
+ ? closeButtonTemplate(handleClose)
265
+ : nothing;
266
+
267
+ // Render the appropriate layout
268
+ if (cachedLayout === "sidebar") {
269
+ render(
270
+ renderSidebar(content, terminalActions, handleClose),
271
+ overlayEl,
272
+ );
273
+ } else {
274
+ render(
275
+ renderModal(content, terminalActions, handleClose, canAbort, maxWidth),
276
+ overlayEl,
277
+ );
278
+ }
279
+ });
280
+ }
@@ -1,6 +1,9 @@
1
1
  import type { Meta, StoryObj } from "@storybook/react-vite";
2
2
  import { {{ block.clsName }} } from "../src/blocks/{{block.slug}}/{{ block.clsName }}";
3
3
 
4
+ // The block's copy is rendered through <juo-text>, which resolves translations
5
+ // at runtime and falls back to the values passed via args when rendered outside
6
+ // a Blocks runtime (as in Storybook).
4
7
  const meta: Meta<typeof {{ block.clsName }}> = {
5
8
  title: "Blocks/{{ block.name }}",
6
9
  component: {{ block.clsName }},
@@ -15,10 +18,10 @@ type Story = StoryObj<typeof {{ block.clsName }}>;
15
18
 
16
19
  export const Default: Story = {
17
20
  args: {
18
- title: "{{ block.framework | capitalize }} + Typescript",
21
+ title: "{{ block.framework | capitalize }} + TypeScript",
19
22
  subtitle: "Starter Block",
23
+ buttonVariant: "solid",
24
+ accentColor: "",
20
25
  initialCount: 0,
21
26
  },
22
27
  };
23
-
24
-
@@ -0,0 +1,6 @@
1
+ import autoprefixer from "autoprefixer";
2
+ import tailwindcss from "tailwindcss";
3
+
4
+ export default {
5
+ plugins: [tailwindcss(), autoprefixer()],
6
+ };
@@ -1,4 +1,6 @@
1
- @import "tailwindcss";
2
1
  @import "@juo/customer-ui/design-tokens.css";
3
2
  @import "@juo/customer-ui/theme-modes.css";
4
- @import "@juo/customer-ui/tailwind-v4.css";
3
+
4
+ @tailwind base;
5
+ @tailwind components;
6
+ @tailwind utilities;
@@ -0,0 +1,15 @@
1
+ /* eslint-disable @typescript-eslint/no-require-imports */
2
+ /** @type {import('tailwindcss').Config} */
3
+ module.exports = {
4
+ presets: [{ theme: { extend: require("@juo/customer-ui/tailwind-v3").theme } }],
5
+ content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx,vue}"],
6
+ theme: {
7
+ extend: {
8
+ colors: {
9
+ primary: "var(--primary)",
10
+ muted: "var(--muted)",
11
+ },
12
+ },
13
+ },
14
+ plugins: [],
15
+ };