juo 0.3.0-alpha.1 → 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.
- package/README.md +1 -1
- package/bin/dev.js +1 -0
- package/dist/commands/create.js +85 -39
- package/dist/commands/generate.d.ts +5 -5
- package/dist/commands/publish/index.d.ts +2 -2
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.js +1 -1
- package/dist/{packageGenerator-D81lWXTA.js → packageGenerator-BToocy-h.js} +3 -2
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
- package/templates/.storybook/preview-head.html.liquid +3 -5
- package/templates/block/preact/index.ts.liquid +65 -10
- package/templates/block/preact/{{block.clsName}}.tsx.liquid +100 -130
- package/templates/block/react/index.ts.liquid +64 -8
- package/templates/block/react/{{block.clsName}}.tsx.liquid +104 -53
- package/templates/configFiles/vite-env.d.ts.liquid +11 -1
- package/templates/css/styles.css +31 -35
- package/templates/dev/index.html.liquid +10 -1
- package/templates/dev/vite/juo-editor-plugin.ts.liquid +22 -1
- package/templates/dev/vite/mock-login.ts.liquid +83 -0
- package/templates/dev/vite/mock-services.ts.liquid +140 -0
- package/templates/dev/vite/theme-shell.ts.liquid +123 -2
- package/templates/dev/vite/workflowOverlay.ts.liquid +280 -0
- package/templates/stories/{{block.clsName}}.stories.tsx.liquid +6 -3
|
@@ -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">✓</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 }} +
|
|
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
|
-
|