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
@@ -13,7 +13,17 @@ declare namespace React {
13
13
  React.HTMLAttributes<HTMLElementTagNameMap[K]> &
14
14
  Partial<Omit<HTMLElementTagNameMap[K], keyof HTMLElement>>;
15
15
  };
16
- interface IntrinsicElements extends _JuoElements {}
16
+ interface IntrinsicElements extends _JuoElements {
17
+ // `juo-text` (from @juo/blocks) also accepts a `prop` (the translation
18
+ // key it renders) and an optional `readonly` flag. Use `class` (not
19
+ // `className`): React doesn't alias `className` to the `class` attribute
20
+ // on custom elements, so `className` would render as a stray `classname`.
21
+ "juo-text": _JuoElements["juo-text"] & {
22
+ prop?: string;
23
+ readonly?: boolean;
24
+ class?: string;
25
+ };
26
+ }
17
27
  }
18
28
  }
19
29
  {% endif %}
@@ -23,7 +23,7 @@ export default defineConfig({
23
23
  },
24
24
  build: {
25
25
  lib: {
26
- entry: "src/blocks/register.ts",
26
+ entry: "src/main.ts",
27
27
  formats: ["es"],
28
28
  fileName: "index",
29
29
  },
@@ -1,59 +1,55 @@
1
1
  @import "@juo/customer-ui/design-tokens.css";
2
2
  @import "@juo/customer-ui/theme-modes.css";
3
3
 
4
- .container {
5
- font-weight: 540;
4
+ /*
5
+ * Styling for the starter block (non-Tailwind projects).
6
+ *
7
+ * Everything below comes from the Customer UI design tokens imported above:
8
+ * the `--space-*` spacing scale, the `--font-size-*` type scale, the
9
+ * `--radius-*` radius scale, and the `--tint-*` (neutral) / `--accent-*`
10
+ * (brand) color palettes. Prefer these tokens over hardcoded values so the
11
+ * block follows the merchant's theme automatically.
12
+ */
13
+
14
+ .starter-block {
6
15
  display: flex;
7
- gap: 2rem;
8
- justify-content: space-between;
9
16
  align-items: center;
10
- padding: 2.5rem;
11
- border-radius: 0.5rem;
12
- border: 1px solid #9ca3af;
13
- background-color: #fafafa;
17
+ justify-content: space-between;
18
+ gap: var(--space-32);
19
+ padding: var(--space-36);
14
20
  }
15
21
 
16
- .content {
22
+ .starter-block__info {
17
23
  display: flex;
18
24
  align-items: center;
19
- gap: 0.875rem;
25
+ gap: var(--space-12);
20
26
  }
21
27
 
22
- .icon-container {
28
+ .starter-block__icons {
23
29
  display: flex;
24
30
  align-items: center;
25
- gap: 0.5rem;
31
+ gap: var(--space-8);
32
+ /* Defaults to a neutral color; overridden inline by the color control. */
33
+ color: var(--tint-900);
26
34
  }
27
35
 
28
- .framework-icon {
29
- width: 2.25rem;
30
- height: 2.25rem;
31
- fill: currentColor;
32
- color: black;
36
+ .starter-block__icon {
37
+ width: var(--space-36);
38
+ height: var(--space-36);
33
39
  }
34
40
 
35
- .typescript-icon {
36
- width: 2.5rem;
37
- height: 2.5rem;
38
- fill: currentColor;
39
- color: black;
40
- }
41
-
42
- .text-container {
41
+ .starter-block__text {
43
42
  display: flex;
44
43
  flex-direction: column;
45
- align-self: center;
46
44
  }
47
45
 
48
- .subtitle {
49
- font-size: 12px;
50
- line-height: 1rem;
51
- color: #696969;
46
+ .starter-block__subtitle {
47
+ font-size: var(--font-size-12);
48
+ color: var(--tint-500);
52
49
  }
53
50
 
54
- .title {
55
- font-size: 15px;
56
- line-height: 1;
57
- color: #111;
51
+ .starter-block__title {
52
+ font-size: var(--font-size-16);
53
+ font-weight: 500;
54
+ color: var(--tint-900);
58
55
  }
59
-
@@ -4,11 +4,27 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Juo Blocks Dev</title>
7
+ <link rel="preconnect" href="https://rsms.me/" />
8
+ <link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
9
+ <style>
10
+ body {
11
+ font-family: InterVariable, Inter, sans-serif;
12
+ -webkit-font-smoothing: antialiased;
13
+ -moz-osx-font-smoothing: grayscale;
14
+ }
15
+ </style>
16
+ <script type="importmap">
17
+ {
18
+ "imports": {
19
+ "@juo/blocks": "/vite/blocks-shim.ts"
20
+ }
21
+ }
22
+ </script>
7
23
  </head>
8
24
  <body>
9
25
  <juo-context-root>
10
- <div id="app"></div>
26
+ <div id="app" style="padding: 20px; max-width: 1080px; margin: 0 auto;"></div>
11
27
  </juo-context-root>
12
- <script type="module" src="/src/main.ts"></script>
28
+ <script type="module" src="/vite/theme-shell.ts"></script>
13
29
  </body>
14
30
  </html>
@@ -0,0 +1,6 @@
1
+ // Re-exports the locally installed @juo/blocks. The CDN-hosted
2
+ // @juo/blocks-extensions bundle externalizes "@juo/blocks", so the index.html
3
+ // import map points that bare specifier at this shim — ensuring the native
4
+ // blocks register into the SAME @juo/blocks instance the theme shell uses
5
+ // (Vite dedupes both to one pre-bundled module). Dev-harness only.
6
+ export * from "@juo/blocks";
@@ -3,9 +3,28 @@ import type { Plugin } from "vite";
3
3
  const EDITOR_EMBED_URL =
4
4
  "https://cdn.juo.io/web/blocks/juo/editor/{{ editorVersion }}/juo-editor.embed.js";
5
5
 
6
+ const DEVTOOLS_EMBED_URL =
7
+ "https://cdn.juo.io/web/blocks/juo/devtools/{{ devtoolsVersion }}/blocks-devtools.embed.js";
8
+
6
9
  export default function juo(): Plugin {
7
10
  return {
8
11
  name: "juo-editor",
12
+
13
+ transformIndexHtml(_html, ctx) {
14
+ // Skip the editor page itself — devtools aren't needed there.
15
+ if (ctx.path === "/editor" || ctx.path.startsWith("/editor?")) return;
16
+ return {
17
+ html: _html,
18
+ tags: [
19
+ {
20
+ tag: "script",
21
+ attrs: { type: "module", src: DEVTOOLS_EMBED_URL },
22
+ injectTo: "head-prepend",
23
+ },
24
+ ],
25
+ };
26
+ },
27
+
9
28
  configureServer(server) {
10
29
  server.middlewares.use("/editor", (_req, res) => {
11
30
  res.writeHead(200, { "Content-Type": "text/html" });
@@ -15,8 +34,10 @@ export default function juo(): Plugin {
15
34
  <meta charset="UTF-8" />
16
35
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
17
36
  <title>Juo Blocks Editor</title>
37
+ <link rel="preconnect" href="https://rsms.me/" />
38
+ <link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
18
39
  <style>
19
- html, body { margin: 0; padding: 0; height: 100%; overflow: hidden; }
40
+ html, body { margin: 0; padding: 0; height: 100%; overflow: hidden; font-family: InterVariable, Inter, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
20
41
  juo-editor { display: block; height: 100vh; }
21
42
  </style>
22
43
  </head>
@@ -35,7 +56,7 @@ export default function juo(): Plugin {
35
56
  server.config.logger.info(
36
57
  `\n juo editor ${base}/editor` +
37
58
  `\n editor view mode ${base}/` +
38
- `\n blocks bundle ${base}/src/blocks/register.ts\n`,
59
+ `\n blocks bundle ${base}/src/main.ts\n`,
39
60
  );
40
61
  });
41
62
  },
@@ -0,0 +1,83 @@
1
+ // Dev-only mock login service. Ported from the reference theme's
2
+ // `lib/loginService.ts`. Core's `createLoginService` requires real deps
3
+ // (API adapter, router, shop, error formatter), so the dev preview hand-writes
4
+ // a mock that implements the `LoginServiceContext` shape directly.
5
+ import { ContextType, LoginServiceContext, signal } from "@juo/blocks";
6
+
7
+ export function createMockLoginService(): ContextType<typeof LoginServiceContext> {
8
+ const loggedIn = signal<boolean>(false);
9
+ const expectedCode = signal<string | null>(null);
10
+
11
+ const service: ContextType<typeof LoginServiceContext> = {
12
+ passwordless: {
13
+ email: {
14
+ async start(_) {
15
+ expectedCode.value = String(
16
+ Math.floor(100000 + Math.random() * 900000)
17
+ );
18
+ window.alert(`Use code ${expectedCode.value} to log in`);
19
+
20
+ return { _tag: "Success", data: undefined };
21
+ },
22
+
23
+ async verify(_, code) {
24
+ if (expectedCode.value != null && code === expectedCode.value) {
25
+ loggedIn.value = true;
26
+ return { _tag: "Success", data: undefined };
27
+ }
28
+
29
+ return {
30
+ _tag: "Failure",
31
+ error: {
32
+ title: "Invalid code",
33
+ message: "The provided code is invalid",
34
+ },
35
+ };
36
+ },
37
+ },
38
+ },
39
+
40
+ social: {
41
+ async start() {
42
+ return {
43
+ _tag: "Failure",
44
+ error: {
45
+ title: "Not implemented",
46
+ message: "This functionality is not available in test mode",
47
+ },
48
+ };
49
+ },
50
+
51
+ async verify() {
52
+ return {
53
+ _tag: "Failure",
54
+ error: {
55
+ title: "Not implemented",
56
+ message: "This functionality is not available in test mode",
57
+ },
58
+ };
59
+ },
60
+ },
61
+
62
+ async handleQueryParams() {
63
+ return { _tag: "Success", data: undefined };
64
+ },
65
+
66
+ async logout() {
67
+ loggedIn.value = false;
68
+ return { _tag: "Success", data: undefined };
69
+ },
70
+
71
+ isAuthenticated: loggedIn,
72
+
73
+ async getToken() {
74
+ return null;
75
+ },
76
+
77
+ async getAuthHeaders(): Promise<Record<string, string>> {
78
+ return {};
79
+ },
80
+ };
81
+
82
+ return service;
83
+ }
@@ -0,0 +1,140 @@
1
+ // Dev-only mock services. Provides every blocks service backed by its mock
2
+ // adapter so any block you author (or scaffold) has its service dependencies
3
+ // available in the dev preview. Lives in `vite/` and never enters the built
4
+ // blocks bundle (whose entry is `src/blocks/register.ts`), so there is no
5
+ // production cost — the Juo runtime provides the real services in production.
6
+ import {
7
+ provideContext,
8
+ effect,
9
+ untracked,
10
+ type ThemeState,
11
+ // product
12
+ ProductServiceContext,
13
+ createProductService,
14
+ createMockProductAdapter,
15
+ // customer
16
+ CustomerServiceContext,
17
+ createCustomerService,
18
+ createMockCustomerAdapter,
19
+ // orders
20
+ OrdersServiceContext,
21
+ createOrdersService,
22
+ createMockOrdersAdapter,
23
+ // subscription
24
+ SubscriptionServiceContext,
25
+ createSubscriptionService,
26
+ createMockSubscriptionAdapter,
27
+ // schedules
28
+ SchedulesServiceContext,
29
+ createSchedulesService,
30
+ createMockSchedulesAdapter,
31
+ // router
32
+ RouterServiceContext,
33
+ createMockRouterService,
34
+ // settings
35
+ SettingsServiceContext,
36
+ createSettingsService,
37
+ // shop
38
+ ShopServiceContext,
39
+ createShopService,
40
+ // login
41
+ LoginServiceContext,
42
+ // workflow
43
+ WorkflowServiceContext,
44
+ createWorkflowService,
45
+ createMockWorkflowApiAdapter,
46
+ } from "@juo/blocks";
47
+ import { createMockLoginService } from "./mock-login";
48
+ import { createWorkflowOverlay } from "./workflowOverlay";
49
+
50
+ /**
51
+ * Provide every available blocks service (backed by mock adapters) onto the
52
+ * dev preview's context root, and mount the workflow overlay.
53
+ */
54
+ export function provideMockServices(root: HTMLElement, themeState: ThemeState) {
55
+ provideContext(
56
+ root,
57
+ ProductServiceContext,
58
+ createProductService(createMockProductAdapter())
59
+ );
60
+
61
+ const customerService = createCustomerService({
62
+ adapter: createMockCustomerAdapter(),
63
+ getAccountOrdersUrl: () => "https://example.com/account",
64
+ });
65
+ // Eagerly populate `current` so name-aware blocks (e.g. Header) render in the
66
+ // dev preview. `createCustomerService` leaves `current` as `null` until
67
+ // `getCurrent()` is called — in the portal that population is done by
68
+ // `useCustomerService`'s watchEffect; here nothing else triggers it.
69
+ void customerService.getCurrent();
70
+ provideContext(root, CustomerServiceContext, customerService);
71
+
72
+ provideContext(
73
+ root,
74
+ OrdersServiceContext,
75
+ createOrdersService(createMockOrdersAdapter())
76
+ );
77
+
78
+ const subscriptionService = createSubscriptionService(
79
+ createMockSubscriptionAdapter()
80
+ );
81
+ provideContext(root, SubscriptionServiceContext, subscriptionService);
82
+
83
+ const schedulesService = createSchedulesService(createMockSchedulesAdapter(), {
84
+ // Share the billing-attempt signal so schedule-aware blocks stay in sync.
85
+ hasPendingBilling: subscriptionService.hasPendingBilling,
86
+ });
87
+ provideContext(root, SchedulesServiceContext, schedulesService);
88
+
89
+ provideContext(root, RouterServiceContext, createMockRouterService());
90
+
91
+ provideContext(root, SettingsServiceContext, createSettingsService());
92
+
93
+ provideContext(
94
+ root,
95
+ ShopServiceContext,
96
+ createShopService({ getContactEmail: () => "support@example.com" })
97
+ );
98
+
99
+ provideContext(root, LoginServiceContext, createMockLoginService());
100
+
101
+ // Note: OverlayServiceContext is provided by the theme shell's setup
102
+ // functions — editor mode must pass the same overlay service instance into
103
+ // `setupEditorMode`, so its lifecycle is owned there, not here.
104
+
105
+ const workflowService = createWorkflowService({
106
+ api: createMockWorkflowApiAdapter(),
107
+ themeState,
108
+ });
109
+ provideContext(root, WorkflowServiceContext, workflowService);
110
+
111
+ // Mount the workflow overlay UI so flows render in the dev preview.
112
+ createWorkflowOverlay(root, workflowService, themeState);
113
+
114
+ // Seed a focused subscription so subscription-scoped blocks render. There is
115
+ // no subscription id in the dev-preview URL, so use the mock adapter's default
116
+ // (mirrors the editor seed in customer-portal's useSubscriptionService).
117
+ void (async () => {
118
+ const idResult = await subscriptionService.getDefaultSubscriptionId();
119
+ const id = idResult._tag === "Success" ? idResult.data : null;
120
+ if (!id) return;
121
+ const subResult = await subscriptionService.getById(id);
122
+ if (subResult._tag === "Success") {
123
+ subscriptionService.current.value = subResult.data;
124
+ }
125
+ })();
126
+
127
+ // Cascade: load the upcoming order whenever the focused subscription changes
128
+ // (mirrors customer-portal's useSchedulesService watcher).
129
+ effect(() => {
130
+ const id = subscriptionService.current.value?.id ?? null;
131
+ untracked(() => {
132
+ if (!id) {
133
+ schedulesService.current.value = null;
134
+ schedulesService.currentLoading.value = false;
135
+ return;
136
+ }
137
+ void schedulesService.reloadCurrent(id);
138
+ });
139
+ });
140
+ }