cabloy 5.1.134 → 5.1.136
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/.cabloy-version +1 -1
- package/.claude/skills/cabloy-backend-scaffold/SKILL.md +9 -0
- package/.claude/skills/cabloy-zova-source-reading/SKILL.md +24 -1
- package/.claude/skills/cabloy-zova-source-reading/references/analysis-modes.md +12 -0
- package/.claude/skills/cabloy-zova-source-reading/references/core-reading-paths.md +28 -0
- package/.github/workflows/playwright-e2e.yml +2 -2
- package/.github/workflows/vona-test-pg.yml +1 -1
- package/.github/workflows/vona-test-sqlite3.yml +1 -1
- package/CHANGELOG.md +38 -0
- package/CLAUDE.md +1 -0
- package/cabloy-docs/.vitepress/config.mjs +7 -0
- package/cabloy-docs/backend/markdown-guide.md +189 -0
- package/cabloy-docs/frontend/component-guide.md +6 -0
- package/cabloy-docs/frontend/css-in-js-guide.md +6 -0
- package/cabloy-docs/frontend/markdown-guide.md +258 -0
- package/cabloy-docs/frontend/page-guide.md +48 -0
- package/cabloy-docs/frontend/page-params-guide.md +4 -2
- package/cabloy-docs/frontend/page-route-guide.md +2 -2
- package/cabloy-docs/frontend/zova-router-under-the-hood.md +2 -0
- package/cabloy-docs/frontend/zova-source-reading-map.md +53 -9
- package/cabloy-docs/fullstack/a-pay-payment-suite.md +333 -0
- package/cabloy-docs/fullstack/payment-sandbox-configuration.md +285 -0
- package/e2e/specs/a-commerce/commerce.spec.ts +503 -47
- package/e2e/specs/cabloy-basic/basic.spec.ts +30 -0
- package/package.json +1 -1
- package/vona/env/.env +2 -0
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/cli/templates/init/error/boilerplate/config/errors.ts +6 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/entity/{{resourceName}}.tsx_ +2 -0
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/entity/{{resourceName}}.tsx_ +2 -0
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/packages-utils/localeutil/package.json +1 -1
- package/vona/packages-utils/localeutil/src/util.ts +1 -1
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/packages-vona/vona-core/package.json +1 -1
- package/vona/packages-vona/vona-mock/package.json +1 -1
- package/vona/patches/{zova-core@5.1.82.patch → zova-core@5.1.84.patch} +2 -2
- package/vona/pnpm-lock.yaml +826 -270
- package/vona/pnpm-workspace.yaml +1 -1
- package/vona/src/suite/a-commerce/modules/commerce-catalog/package.json +1 -1
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/.metadata/index.ts +99 -1
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/bean/meta.index.ts +1 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/bean/meta.version.ts +19 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/en-us.ts +3 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/zh-cn.ts +3 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/controller/product.ts +3 -3
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productContentUpdate.tsx +14 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productContentView.tsx +19 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productCreate.tsx +48 -3
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productPublicDetail.tsx +15 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productUpdate.tsx +48 -3
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productView.tsx +52 -3
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/entity/productContent.tsx +36 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/model/product.ts +6 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/model/productContent.ts +22 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/service/product.ts +82 -6
- package/vona/src/suite/a-commerce/modules/commerce-catalog/test/productContent.test.ts +271 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/test/productPresentation.test.ts +81 -2
- package/vona/src/suite/a-commerce/modules/commerce-payment/package.json +4 -1
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/payScene.commerceOrder.ts +32 -15
- package/vona/src/suite/a-commerce/modules/commerce-payment/test/paymentAttempt.test.ts +8 -2
- package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/.metadata/index.ts +8 -2
- package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/.metadata/locales.ts +18 -0
- package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/bean/ssrMenu.home.ts +3 -1
- package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/config/locale/en-us.ts +3 -0
- package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/config/locale/zh-cn.ts +3 -0
- package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/index.ts +1 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/checkout.ts +13 -2
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/lib/providerReference.ts +18 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/order.ts +43 -8
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/paymentOutcome.test.ts +74 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/providerReference.test.ts +33 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/refundLifecycle.test.ts +73 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/reservationExpiry.test.ts +2 -0
- package/vona/src/suite/a-home/modules/home-user/src/.metadata/index.ts +30 -0
- package/vona/src/suite/a-home/modules/home-user/src/bean/meta.version.ts +7 -2
- package/vona/src/suite/a-home/modules/home-user/src/config/locale/en-us.ts +4 -1
- package/vona/src/suite/a-home/modules/home-user/src/config/locale/zh-cn.ts +4 -1
- package/vona/src/suite/a-home/modules/home-user/src/config/roles.ts +4 -4
- package/vona/src/suite/a-home/modules/home-user/src/entity/role.ts +2 -2
- package/vona/src/suite/a-home/modules/home-user/src/entity/user.ts +8 -1
- package/vona/src/suite/a-home/modules/home-user/src/service/userAdapter.ts +6 -2
- package/vona/src/suite/a-home/modules/home-user/test/passportTest.test.ts +1 -0
- package/vona/src/suite/a-home/modules/home-user/test/role.test.ts +4 -4
- package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/package.json +64 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/src/.metadata/index.ts +54 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/src/.metadata/this.ts +2 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/src/bean/bean.markdown.ts +137 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/src/index.ts +1 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/tsconfig.build.json +11 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/tsconfig.json +7 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/package.json +1 -1
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/bean/bean.ssr.ts +17 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/lib/beanSsrSiteBase.ts +11 -3
- package/vona/src/suite-vendor/a-cabloy/package.json +2 -1
- package/vona/src/suite-vendor/a-cabloy/tsconfig.json +3 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/package.json +1 -1
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.version.ts +1 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/providerOperation.tsx +8 -1
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/lib/index.ts +1 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/lib/providerOperationFailure.ts +8 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/providerOperation.ts +187 -29
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/types/payment.ts +4 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/package.json +1 -1
- package/vona/src/suite-vendor/a-pay/modules/pay-paypal/package.json +1 -1
- package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/bean/payProvider.paypal.ts +0 -6
- package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/index.ts +1 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-stripe/package.json +2 -1
- package/vona/src/suite-vendor/a-pay/modules/pay-stripe/src/bean/payProvider.stripe.ts +458 -22
- package/vona/src/suite-vendor/a-pay/modules/pay-stripe/src/index.ts +1 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-stripe/src/lib/stripeGateway.ts +85 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-stripe/src/types/index.ts +2 -0
- package/vona/src/suite-vendor/a-pay/package.json +5 -5
- package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-permission/src/bean/bean.permission.ts +1 -0
- package/vona/src/suite-vendor/a-vona/modules/a-user/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-user/src/bean/bean.passport.ts +14 -0
- package/vona/src/suite-vendor/a-vona/modules/a-user/src/bean/guard.passport.ts +4 -0
- package/vona/src/suite-vendor/a-vona/modules/a-user/src/types/role.ts +3 -2
- package/vona/src/suite-vendor/a-vona/modules/a-user/src/types/user.ts +4 -0
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/packages-utils/logger/package.json +2 -2
- package/zova/packages-utils/zova-jsx/package.json +2 -2
- package/zova/packages-zova/zova/package.json +3 -3
- package/zova/packages-zova/zova-core/package.json +3 -3
- package/zova/packages-zova/zova-core/src/core/sys/resource.ts +1 -1
- package/zova/packages-zova/zova-core/src/core/sys/util.ts +8 -0
- package/zova/pnpm-lock.yaml +570 -11
- package/zova/src/front/config/config/config.cabloyCommerce.ts +0 -6
- package/zova/src/suite/a-commerce/modules/commerce-catalog/package.json +3 -1
- package/zova/src/suite/a-commerce/modules/commerce-catalog/src/api/openapi/schemas.ts +236 -36
- package/zova/src/suite/a-commerce/modules/commerce-catalog/src/api/openapi/types.ts +4962 -2402
- package/zova/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/en-us.ts +1 -0
- package/zova/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/zh-cn.ts +1 -0
- package/zova/src/suite/a-commerce/modules/commerce-catalog/src/page/product/controller.tsx +8 -3
- package/zova/src/suite/a-commerce/modules/commerce-member/src/.metadata/index.ts +6 -7
- package/zova/src/suite/a-commerce/modules/commerce-member/src/page/address/controller.tsx +1 -3
- package/zova/src/suite/a-commerce/modules/commerce-member/src/routes.ts +2 -3
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +13 -20
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/cart/controller.tsx +3 -9
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/checkout/controller.tsx +9 -11
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/order/controller.tsx +0 -1
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/orders/controller.tsx +6 -4
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/payment/controller.tsx +78 -17
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/routes.ts +10 -13
- package/zova/src/suite/a-home/modules/home-api/mock/passport.fake.ts +3 -3
- package/zova/src/suite/a-home/modules/home-api/src/.metadata/index.ts +31 -1
- package/zova/src/suite/a-home/modules/home-api/src/api/homeBaseSiteCatalog.ts +24 -0
- package/zova/src/suite/a-home/modules/home-api/src/api/openapi/schemas.ts +329 -17
- package/zova/src/suite/a-home/modules/home-api/src/api/openapi/types.ts +5866 -1829
- package/zova/src/suite/a-home/modules/home-api/src/apiSchema/homeBaseSiteCatalog.ts +13 -0
- package/zova/src/suite/a-home/modules/home-base/src/component/page/controller.tsx +1 -0
- package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/render.header.tsx +0 -1
- package/zova/src/suite/cabloy-basic/modules/basic-details/src/types/detail.ts +1 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/package.json +61 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/.metadata/component/formFieldMarkdown.ts +36 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/.metadata/component/markdownHtml.ts +36 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/.metadata/index.ts +147 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/.metadata/this.ts +2 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/component/formFieldMarkdown/controller.tsx +111 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/component/formFieldMarkdown/render.tsx +40 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/component/formFieldMarkdown/style.ts +13 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/component/markdownHtml/controller.tsx +17 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/component/markdownHtml/render.tsx +9 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/component/markdownHtml/style.ts +13 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/index.ts +1 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/lib/richTextContentStyle.ts +142 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/tsconfig.build.json +13 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/tsconfig.json +5 -0
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockPage/controller.tsx +25 -3
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/types/page.ts +1 -0
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/types/pageEntry.ts +1 -0
- package/zova/src/suite/cabloy-basic/package.json +1 -0
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +7 -0
- package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/modules/a-logger/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/modules/a-router/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-router/src/bean/sys.router.ts +3 -2
- package/zova/src/suite-vendor/a-zova/modules/a-ssr/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-ssr/test/lib/ssrProfile.test.ts +88 -0
- package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/package.json +5 -5
- package/.claude/scheduled_tasks.lock +0 -1
package/.cabloy-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
5.1.
|
|
1
|
+
5.1.136
|
|
@@ -117,6 +117,15 @@ Typical backend thread pieces include:
|
|
|
117
117
|
|
|
118
118
|
Do not throw away the generated structure and rewrite it from scratch unless the generator clearly does not match the task.
|
|
119
119
|
|
|
120
|
+
### Generated renderer decision
|
|
121
|
+
|
|
122
|
+
When refining generated entity fields, choose form and table controls from business semantics, not only from the primitive TypeScript type:
|
|
123
|
+
|
|
124
|
+
- keep ordinary Cabloy Basic text fields, such as generated `name` and `description`, on the implicit default `Input` renderer; do not mechanically add `basic-input:formFieldInput`
|
|
125
|
+
- add explicit `ZovaRender.field(...)` when semantics require a specialized control, such as an enum/select, resource relation, date/time, boolean choice, money, image, or file
|
|
126
|
+
- add `ZovaRender.cell(...)` when that field also needs specialized table presentation
|
|
127
|
+
- reuse a shared renderer first, configure it with field-level options next, and create a custom renderer only when the shared surface cannot express the required behavior; follow `cabloy-resource-field-update` for the edition-aware renderer branch
|
|
128
|
+
|
|
120
129
|
## Step 5: Apply backend follow-up logic deliberately
|
|
121
130
|
|
|
122
131
|
Backend scaffolding is rarely complete after file generation alone. Treat this follow-up review as mandatory.
|
|
@@ -82,6 +82,9 @@ Start from the public frontend reading docs in `cabloy-docs/frontend/`.
|
|
|
82
82
|
- `cabloy-docs/frontend/zova-vs-vue3-comparison.md`
|
|
83
83
|
- `cabloy-docs/frontend/zova-reactivity-under-the-hood.md`
|
|
84
84
|
- `cabloy-docs/frontend/zova-source-reading-map.md`
|
|
85
|
+
- `cabloy-docs/frontend/page-guide.md`
|
|
86
|
+
- `cabloy-docs/frontend/component-guide.md`
|
|
87
|
+
- `cabloy-docs/frontend/css-in-js-guide.md`
|
|
85
88
|
|
|
86
89
|
For compact procedural summaries inside the skill bundle, also use:
|
|
87
90
|
|
|
@@ -142,7 +145,27 @@ Do **not** lead with statements like:
|
|
|
142
145
|
|
|
143
146
|
Those translations can erase the actual Zova architecture.
|
|
144
147
|
|
|
145
|
-
For component-wrapper questions specifically, remember that `controllerRef` exposes the controller instance, not a generic DOM ref, and should not be treated as a generic Vue component-ref substitute without checking the current wrapper/controller source path.
|
|
148
|
+
For component-wrapper questions specifically, remember that `controllerRef` exposes the controller instance to the component consumer, not a generic DOM ref, and should not be treated as a generic Vue component-ref substitute without checking the current wrapper/controller source path.
|
|
149
|
+
|
|
150
|
+
### Split Controller, Render, and Style access
|
|
151
|
+
|
|
152
|
+
When explaining a split page or component, establish role ownership before discussing access:
|
|
153
|
+
|
|
154
|
+
- Controller owns state, actions, and lifecycle work.
|
|
155
|
+
- Render owns TSX composition.
|
|
156
|
+
- Style owns scoped CSS-in-JS setup.
|
|
157
|
+
|
|
158
|
+
The source-confirmed direct member lookup order is:
|
|
159
|
+
|
|
160
|
+
| Current bean | Direct member lookup order |
|
|
161
|
+
| --- | --- |
|
|
162
|
+
| Controller | Controller only |
|
|
163
|
+
| Style | Style, then Controller |
|
|
164
|
+
| Render | Render, then Controller, then Style |
|
|
165
|
+
|
|
166
|
+
State the shadowing caveat: an own member on the current bean takes precedence over each fallback surface. Recommend `this.member` for ordinary same-component companion access, such as Render reading Controller state or a Style-generated class.
|
|
167
|
+
|
|
168
|
+
Distinguish this normal fallback from explicit `@Use()` or bean/container access. Use explicit access only when the task needs a named or specific instance, selector or scope boundary, identity passing, lifecycle control, or other interop. Do not confuse generated type augmentation with runtime behavior: generated interfaces expose the type surface, while proxy fallback supplies missing companion members at runtime.
|
|
146
169
|
|
|
147
170
|
## Step 6: Distinguish source-confirmed behavior from interpretive comparison
|
|
148
171
|
|
|
@@ -46,6 +46,7 @@ Use this mode when the user is mainly asking:
|
|
|
46
46
|
- model
|
|
47
47
|
- behavior
|
|
48
48
|
- SSR
|
|
49
|
+
- split Controller/Render/Style delegation
|
|
49
50
|
3. trace the smallest current-source path that confirms the runtime behavior
|
|
50
51
|
4. distinguish source-confirmed behavior from interpretation
|
|
51
52
|
5. only after the Zova-native explanation is clear, add Vue analogy if helpful
|
|
@@ -57,6 +58,17 @@ Use this mode when the user is mainly asking:
|
|
|
57
58
|
- what triggers the behavior
|
|
58
59
|
- what is source-confirmed
|
|
59
60
|
|
|
61
|
+
### Companion delegation response sequence
|
|
62
|
+
|
|
63
|
+
For questions about a split Controller, Render, and Style implementation:
|
|
64
|
+
|
|
65
|
+
1. establish role ownership: Controller for state/actions/lifecycle, Render for TSX composition, Style for scoped CSS-in-JS
|
|
66
|
+
2. give the direct lookup order: Controller only; Style then Controller; Render then Controller then Style
|
|
67
|
+
3. state that an own member shadows the next fallback surface
|
|
68
|
+
4. recommend direct `this.member` access for ordinary same-component access
|
|
69
|
+
5. distinguish generated type augmentation from proxy-based runtime fallback
|
|
70
|
+
6. reserve `@Use()` or explicit bean/container access for named/specific identity, selectors/scopes, lifecycle control, or interop
|
|
71
|
+
|
|
60
72
|
## Mode C: Vue-vs-Zova comparison mode
|
|
61
73
|
|
|
62
74
|
Use this mode when the user is mainly asking:
|
|
@@ -37,6 +37,34 @@ Use when the question is about:
|
|
|
37
37
|
- `controllerRef`
|
|
38
38
|
- component-local controller behavior
|
|
39
39
|
|
|
40
|
+
## Split Controller, Render, and Style companion path
|
|
41
|
+
|
|
42
|
+
Read in this order:
|
|
43
|
+
|
|
44
|
+
1. `home-layoutadmin/src/.metadata/component/layoutAdmin.ts`
|
|
45
|
+
2. `zova-core/src/composables/useController.ts`
|
|
46
|
+
3. `zova-core/src/bean/beanStyleBase.ts`
|
|
47
|
+
4. `zova-core/src/bean/beanControllerLike.ts`
|
|
48
|
+
5. `zova-core/src/bean/beanRenderLike.ts`
|
|
49
|
+
6. `zova-core/src/bean/beanContainer.ts`
|
|
50
|
+
7. `zova-core/src/core/context/component.ts`
|
|
51
|
+
|
|
52
|
+
Use when the question is about:
|
|
53
|
+
|
|
54
|
+
- Render reading Controller state or actions through `this.member`
|
|
55
|
+
- Style reading Controller state through `this.member`
|
|
56
|
+
- Render consuming a Style-generated class such as `this.cClass`
|
|
57
|
+
- generated typing versus runtime fallback
|
|
58
|
+
- companion-member shadowing or explicit `@Use()` boundaries
|
|
59
|
+
|
|
60
|
+
Expected conclusion:
|
|
61
|
+
|
|
62
|
+
- generated metadata types Style as Controller and Render as Style, but that type composition is not the runtime mechanism
|
|
63
|
+
- `useController(...)` creates Controller, Style, then Render in one context
|
|
64
|
+
- Controller has no companion fallback; Style falls back to Controller; Render falls back to Controller, then Style
|
|
65
|
+
- a locally declared member wins before fallback, so same-name members shadow companion members
|
|
66
|
+
- use direct `this.member` for normal companion access; reserve explicit identity/container access for named instances, selector/scope boundaries, lifecycle control, or interop
|
|
67
|
+
|
|
40
68
|
## Bean lifecycle and helper API path
|
|
41
69
|
|
|
42
70
|
Read in this order:
|
|
@@ -30,7 +30,7 @@ jobs:
|
|
|
30
30
|
run: npm run test:e2e:basic:clean
|
|
31
31
|
- name: upload Playwright artifacts
|
|
32
32
|
if: failure()
|
|
33
|
-
uses: actions/upload-artifact@
|
|
33
|
+
uses: actions/upload-artifact@v7
|
|
34
34
|
with:
|
|
35
35
|
name: playwright-artifacts-basic
|
|
36
36
|
path: |
|
|
@@ -68,7 +68,7 @@ jobs:
|
|
|
68
68
|
run: npm run test:e2e:commerce:clean
|
|
69
69
|
- name: upload Playwright artifacts
|
|
70
70
|
if: failure()
|
|
71
|
-
uses: actions/upload-artifact@
|
|
71
|
+
uses: actions/upload-artifact@v7
|
|
72
72
|
with:
|
|
73
73
|
name: playwright-artifacts-commerce
|
|
74
74
|
path: |
|
|
@@ -35,7 +35,7 @@ jobs:
|
|
|
35
35
|
- name: init
|
|
36
36
|
run: npm run init
|
|
37
37
|
- name: run A-Pay focused gate
|
|
38
|
-
run: DATABASE_DEFAULT_CLIENT=pg npm run vona :bin:test -- test-pay/test/paymentSession.test.ts test-pay/test/payMock.test.ts test-pay/test/paypalProvider.test.ts test-pay/test/paypalLifecycle.test.ts test-pay/test/outbox.test.ts test-pay/test/webhook.test.ts --flavor=normal
|
|
38
|
+
run: DATABASE_DEFAULT_CLIENT=pg npm run vona :bin:test -- test-pay/test/paymentSession.test.ts test-pay/test/payMock.test.ts test-pay/test/paypalProvider.test.ts test-pay/test/paypalLifecycle.test.ts test-pay/test/stripeProvider.test.ts test-pay/test/stripeLifecycle.test.ts test-pay/test/outbox.test.ts test-pay/test/webhook.test.ts --flavor=normal
|
|
39
39
|
working-directory: vona
|
|
40
40
|
- name: run Commerce stock contention gate
|
|
41
41
|
run: |
|
|
@@ -28,7 +28,7 @@ jobs:
|
|
|
28
28
|
- name: init
|
|
29
29
|
run: npm run init
|
|
30
30
|
- name: run A-Pay focused gate
|
|
31
|
-
run: DATABASE_DEFAULT_CLIENT=sqlite3 npm run vona :bin:test -- test-pay/test/paymentSession.test.ts test-pay/test/payMock.test.ts test-pay/test/paypalProvider.test.ts test-pay/test/paypalLifecycle.test.ts test-pay/test/outbox.test.ts test-pay/test/webhook.test.ts --flavor=normal
|
|
31
|
+
run: DATABASE_DEFAULT_CLIENT=sqlite3 npm run vona :bin:test -- test-pay/test/paymentSession.test.ts test-pay/test/payMock.test.ts test-pay/test/paypalProvider.test.ts test-pay/test/paypalLifecycle.test.ts test-pay/test/stripeProvider.test.ts test-pay/test/stripeLifecycle.test.ts test-pay/test/outbox.test.ts test-pay/test/webhook.test.ts --flavor=normal
|
|
32
32
|
working-directory: vona
|
|
33
33
|
- name: run Commerce payment, shipment, and refund lifecycle gate
|
|
34
34
|
run: DATABASE_DEFAULT_CLIENT=sqlite3 npm run vona :bin:test -- commerce-trade/test/paymentOutcome.test.ts commerce-trade/test/reservationExpiry.test.ts commerce-trade/test/shipment.test.ts commerce-trade/test/refundLifecycle.test.ts --flavor=normal
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.1.136
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- Add route name support.
|
|
8
|
+
- Support YouTube embeds in Markdown.
|
|
9
|
+
- Update retryable behavior.
|
|
10
|
+
- Add `getPagePath`.
|
|
11
|
+
- Add block page support.
|
|
12
|
+
- Render fields with improved support.
|
|
13
|
+
- Add commerce locale support.
|
|
14
|
+
- Add Markdown task lists, HTML, and CSS support.
|
|
15
|
+
- Split Controller, Render, and Style companion access.
|
|
16
|
+
- Add payment and Stripe integrations.
|
|
17
|
+
- Add Stripe-hosted checkout payments.
|
|
18
|
+
- Support disabled users.
|
|
19
|
+
- Add locale support for SSR profiles and sessions.
|
|
20
|
+
- Add locale support for role titles.
|
|
21
|
+
|
|
22
|
+
### Bug Fixes
|
|
23
|
+
|
|
24
|
+
- Fix end-to-end test issues.
|
|
25
|
+
- Fix test error handling.
|
|
26
|
+
- Resolve route resolution issues.
|
|
27
|
+
|
|
28
|
+
### Improvements
|
|
29
|
+
|
|
30
|
+
- Refactor Markdown handling.
|
|
31
|
+
- Remove trailing whitespace from generated metadata.
|
|
32
|
+
- Update Playwright workflows and dependencies.
|
|
33
|
+
|
|
34
|
+
## 5.1.135
|
|
35
|
+
|
|
36
|
+
### Improvements
|
|
37
|
+
|
|
38
|
+
- Refresh the Zova lockfile after compensation rerun preparation.
|
|
39
|
+
- Refresh the Vona Zova Core patch for v5.1.84.
|
|
40
|
+
|
|
3
41
|
## 5.1.134
|
|
4
42
|
|
|
5
43
|
### Features
|
package/CLAUDE.md
CHANGED
|
@@ -54,6 +54,7 @@ Before inventing a custom implementation path:
|
|
|
54
54
|
- For frontend work, assume Cabloy Basic and Cabloy Start share a frontend engineering layer but may diverge in UI layer, frontend flavors, suite/module availability, SSR site baselines, project assets, and generated outputs.
|
|
55
55
|
- For Zova frontend analysis, do not default to generic Vue reinterpretation first. Read the code through Zova’s controller / bean / IoC architecture before mapping it to Vue concepts.
|
|
56
56
|
- For Zova source-reading or Vue-vs-Zova explanation tasks, start from the frontend reading guides and source-reading map in `cabloy-docs/frontend/` before doing framework-neutral reinterpretation.
|
|
57
|
+
- In Zova page routes, any route with dynamic `params` must define `route.name`; do not rely on unnamed path-keyed routes for typed `$params`, and regenerate page metadata after route changes.
|
|
57
58
|
- For frontend async state that affects rendering or interaction across consumers, prefer model-owned `$useStateData(...)` over controller-managed fetch/cache state.
|
|
58
59
|
- Default to establishing such query state during render. Use `disableSuspenseOnInit: true` only for relatively stable query-backed state when you want to skip the init-time `query.suspense()` kick; it does not prevent query creation, fetches, or hydration-time rendering. If strict readiness is needed later, wait explicitly at the interaction boundary.
|
|
59
60
|
- In SSR, keep server HTML and the client's hydration-time initial render equivalent. When server rendering intentionally omits private, cookie-unavailable, or browser-only state, keep the same neutral shell or placeholder through hydration and defer its query/load/render branch to an explicit post-hydration, admission, mounted, or interaction boundary.
|
|
@@ -92,6 +92,11 @@ const fullstackGroups = [
|
|
|
92
92
|
},
|
|
93
93
|
{ text: 'Framework Performance', link: '/fullstack/framework-performance' },
|
|
94
94
|
{ text: 'Vona + Zova Integration', link: '/fullstack/vona-zova-integration' },
|
|
95
|
+
{ text: 'A-Pay Payment Suite', link: '/fullstack/a-pay-payment-suite' },
|
|
96
|
+
{
|
|
97
|
+
text: 'Payment Provider Sandbox Configuration',
|
|
98
|
+
link: '/fullstack/payment-sandbox-configuration',
|
|
99
|
+
},
|
|
95
100
|
{ text: 'Contract Loop Playbook', link: '/fullstack/contract-loop-playbook' },
|
|
96
101
|
{
|
|
97
102
|
text: 'Admin Resource and Web Self-Service',
|
|
@@ -254,6 +259,7 @@ export default defineConfig({
|
|
|
254
259
|
{ text: 'Upload Guide', link: '/backend/upload-guide' },
|
|
255
260
|
{ text: 'Image Guide', link: '/backend/image-guide' },
|
|
256
261
|
{ text: 'File Guide', link: '/backend/file-guide' },
|
|
262
|
+
{ text: 'Markdown Guide', link: '/backend/markdown-guide' },
|
|
257
263
|
{ text: 'Mail Guide', link: '/backend/mail-guide' },
|
|
258
264
|
{ text: 'Serialization Guide', link: '/backend/serialization-guide' },
|
|
259
265
|
],
|
|
@@ -445,6 +451,7 @@ export default defineConfig({
|
|
|
445
451
|
{ text: 'Form Layout Guide', link: '/frontend/form-layout-guide' },
|
|
446
452
|
{ text: 'Image Guide', link: '/frontend/image-guide' },
|
|
447
453
|
{ text: 'File Guide', link: '/frontend/file-guide' },
|
|
454
|
+
{ text: 'Markdown Guide', link: '/frontend/markdown-guide' },
|
|
448
455
|
{
|
|
449
456
|
text: 'Zova Form Under the Hood',
|
|
450
457
|
link: '/frontend/zova-form-under-the-hood',
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Backend Markdown Guide
|
|
2
|
+
|
|
3
|
+
This guide documents the Vona `a-markdown` module in Cabloy Basic. It is a small server-side rendering and sanitization module, not a content-management product or an HTTP API by itself.
|
|
4
|
+
|
|
5
|
+
Use this page together with:
|
|
6
|
+
|
|
7
|
+
- [Frontend Markdown Guide](/frontend/markdown-guide)
|
|
8
|
+
- [Validation Guide](/backend/validation-guide)
|
|
9
|
+
- [Serialization Guide](/backend/serialization-guide)
|
|
10
|
+
|
|
11
|
+
## What `a-markdown` provides
|
|
12
|
+
|
|
13
|
+
The module registers one global Vona bean:
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
this.bean.markdown.renderHtml(markdown);
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The public method is:
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
renderHtml(markdown?: string): string
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Its behavior is intentionally narrow:
|
|
26
|
+
|
|
27
|
+
- execution is synchronous;
|
|
28
|
+
- the return type is always `string`;
|
|
29
|
+
- missing, empty, or whitespace-only input returns `''`;
|
|
30
|
+
- non-empty input is parsed, rendered to HTML, and sanitized;
|
|
31
|
+
- parsing and rendering errors are not swallowed by a local fallback.
|
|
32
|
+
|
|
33
|
+
The module itself has no HTTP routes, controllers, DTOs, entities, database tables, migrations, locale resources, runtime configuration, or business persistence policy. A business module decides whether to store Markdown, store a derived HTML projection, return one of those values to each consumer, or delete an empty content record.
|
|
34
|
+
|
|
35
|
+
For example, Commerce Catalog treats blank Markdown as absent product content and deletes its content record. That is a catalog service decision; `renderHtml()` only returns an empty string for blank input.
|
|
36
|
+
|
|
37
|
+
## Rendering pipeline
|
|
38
|
+
|
|
39
|
+
`BeanMarkdown.renderHtml()` follows this sequence:
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
Markdown input
|
|
43
|
+
→ MarkdownManager.parse(...)
|
|
44
|
+
→ Tiptap static renderer
|
|
45
|
+
→ sanitize-html with the module policy
|
|
46
|
+
→ sanitized HTML string
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The parser and static renderer are initialized once at module load. The configured extension set is:
|
|
50
|
+
|
|
51
|
+
- `Markdown`
|
|
52
|
+
- `StarterKit`
|
|
53
|
+
- `TaskList`
|
|
54
|
+
- nested `TaskItem`
|
|
55
|
+
- `Image`
|
|
56
|
+
- `TableKit`
|
|
57
|
+
- `Highlight`
|
|
58
|
+
|
|
59
|
+
The Markdown manager enables GitHub-flavored Markdown and uses two-space indentation:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
const markdownManager = new MarkdownManager({
|
|
63
|
+
extensions,
|
|
64
|
+
markedOptions: { gfm: true },
|
|
65
|
+
indentation: { style: 'space', size: 2 },
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The module therefore supports the configured rich-text structures—such as headings, emphasis, lists, task lists, code, images, and tables—but it should not be documented as preserving arbitrary raw HTML or every feature of a general Markdown product. The extension list and sanitizer allowlist are the current contract.
|
|
70
|
+
|
|
71
|
+
## Sanitization is part of the API contract
|
|
72
|
+
|
|
73
|
+
The rendered HTML is always passed through a fixed `sanitize-html` policy. The policy is defined in source and is not a runtime configuration option that callers can replace or extend.
|
|
74
|
+
|
|
75
|
+
### Allowed document structures
|
|
76
|
+
|
|
77
|
+
The allowlist retains the structures needed by the configured rich-text output:
|
|
78
|
+
|
|
79
|
+
- document blocks: `p`, `br`, `hr`, `div`;
|
|
80
|
+
- headings: `h1` through `h6`;
|
|
81
|
+
- text marks: `em`, `strong`, `del`, `s`;
|
|
82
|
+
- links: `a`;
|
|
83
|
+
- lists and quotes: `ol`, `ul`, `li`, `blockquote`;
|
|
84
|
+
- code: `code`, `pre`;
|
|
85
|
+
- images: `img`;
|
|
86
|
+
- task-list support: `input`, `label`, `span`;
|
|
87
|
+
- tables: `table`, `thead`, `tbody`, `tr`, `th`, `td`.
|
|
88
|
+
|
|
89
|
+
Scripts, iframes, style attributes, event-handler attributes, and other non-allowlisted elements are removed rather than preserved as arbitrary HTML.
|
|
90
|
+
|
|
91
|
+
### Allowed attributes and classes
|
|
92
|
+
|
|
93
|
+
Only the following attribute groups are retained:
|
|
94
|
+
|
|
95
|
+
- links: `href`, `rel`, `target`, `title`;
|
|
96
|
+
- code: `class`;
|
|
97
|
+
- images: `alt`, `height`, `src`, `title`, `width`;
|
|
98
|
+
- task inputs: `checked`, `disabled`, `type`;
|
|
99
|
+
- task list items: `data-type`, `data-checked`;
|
|
100
|
+
- task list containers: `data-type`;
|
|
101
|
+
- spans: `class`.
|
|
102
|
+
|
|
103
|
+
Class values are restricted by tag:
|
|
104
|
+
|
|
105
|
+
- code classes must match `language-*`;
|
|
106
|
+
- span classes must match `hljs-*`.
|
|
107
|
+
|
|
108
|
+
Arbitrary classes and inline CSS do not become part of the output contract.
|
|
109
|
+
|
|
110
|
+
### URL policy
|
|
111
|
+
|
|
112
|
+
The sanitizer applies scheme checks to `href` and `src`:
|
|
113
|
+
|
|
114
|
+
- links allow `http`, `https`, and `mailto`;
|
|
115
|
+
- images allow only `http` and `https`;
|
|
116
|
+
- protocol-relative URLs are disabled;
|
|
117
|
+
- unsupported schemes such as `javascript:` do not survive as active link or image attributes.
|
|
118
|
+
|
|
119
|
+
This is a content-safety rule, not an authorization rule. It does not decide whether a user may publish content, whether an external image is acceptable for privacy or availability reasons, or which CSP a consuming application should deploy.
|
|
120
|
+
|
|
121
|
+
### Transformations
|
|
122
|
+
|
|
123
|
+
The sanitizer also normalizes selected rendered elements:
|
|
124
|
+
|
|
125
|
+
- every surviving anchor receives `rel="noopener noreferrer"`;
|
|
126
|
+
- a checkbox input keeps its checked state when present but is always forced to `disabled="disabled"`;
|
|
127
|
+
- a non-checkbox `input` is converted to an attribute-free `span`;
|
|
128
|
+
- a task `li` keeps `data-type="taskItem"` only when valid, and keeps `data-checked` only when it is exactly `true` or `false`;
|
|
129
|
+
- a task `ul` keeps `data-type="taskList"` only when valid;
|
|
130
|
+
- unrelated attributes on task list elements are discarded.
|
|
131
|
+
|
|
132
|
+
Task checkboxes are therefore presentation elements, not interactive controls.
|
|
133
|
+
|
|
134
|
+
## The authoritative server projection pattern
|
|
135
|
+
|
|
136
|
+
Commerce Catalog demonstrates the recommended source/projection boundary for a business resource:
|
|
137
|
+
|
|
138
|
+
1. An Admin form submits `descriptionMarkdown`.
|
|
139
|
+
2. The product service trims the Markdown.
|
|
140
|
+
3. The service calls `this.bean.markdown.renderHtml(markdown)`.
|
|
141
|
+
4. The service persists the Markdown source and generated HTML projection together.
|
|
142
|
+
5. The Admin DTO returns the editable Markdown to the editor.
|
|
143
|
+
6. The public detail DTO returns `descriptionHtml` without the editable Markdown.
|
|
144
|
+
7. The Web page passes that server-derived HTML to `ZMarkdownHtml`.
|
|
145
|
+
|
|
146
|
+
The core service operation is equivalent to:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
const markdown = descriptionMarkdown?.trim();
|
|
150
|
+
if (!markdown) {
|
|
151
|
+
// The business module decides whether to remove empty content.
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const descriptionHtml = this.bean.markdown.renderHtml(markdown);
|
|
156
|
+
await productContent.save({
|
|
157
|
+
descriptionMarkdown: markdown,
|
|
158
|
+
descriptionHtml,
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The actual Commerce Catalog service uses a row-locking lookup and either updates or inserts its content record. Its important security property is that it does not trust a caller-supplied `descriptionHtml`; it derives the value from Markdown on the server.
|
|
163
|
+
|
|
164
|
+
The integration tests verify that:
|
|
165
|
+
|
|
166
|
+
- headings, strong text, tables, code, images, and nested task lists are rendered;
|
|
167
|
+
- task inputs are disabled;
|
|
168
|
+
- `javascript:` links are not retained;
|
|
169
|
+
- a submitted forged value such as `<script>forged</script>` is ignored in favor of newly rendered HTML;
|
|
170
|
+
- blank Markdown removes the product content record in that business module;
|
|
171
|
+
- the public detail exposes HTML while the Admin form receives Markdown.
|
|
172
|
+
|
|
173
|
+
Every create, update, import, migration/backfill, or other write path that can change Markdown must apply the same server-side projection rule. Rendering only in one Admin endpoint leaves other write paths able to create stale or unsafe derived HTML.
|
|
174
|
+
|
|
175
|
+
## Adopting Markdown in another module
|
|
176
|
+
|
|
177
|
+
Use this checklist when adding Markdown-backed content:
|
|
178
|
+
|
|
179
|
+
1. Decide whether the resource needs only Markdown or both Markdown and a derived HTML projection.
|
|
180
|
+
2. Keep Markdown as the editable/source field.
|
|
181
|
+
3. Call `this.bean.markdown.renderHtml(...)` on every authoritative write path.
|
|
182
|
+
4. Define a business policy for blank Markdown; do not assume `renderHtml('')` deletes records.
|
|
183
|
+
5. Expose the source and projection separately according to the consumer's authority.
|
|
184
|
+
6. Use `basic-markdown:formFieldMarkdown` for the Markdown form field.
|
|
185
|
+
7. Use `ZMarkdownHtml` only for HTML produced by the trusted server rendering path.
|
|
186
|
+
8. Add tests for blank values, invalid URL schemes, task-list output, derived HTML consistency, and forged output fields.
|
|
187
|
+
9. Treat external-image policy, CSP, authorization, and content lifecycle cleanup as application concerns beyond this renderer.
|
|
188
|
+
|
|
189
|
+
The matching frontend component and SSR behavior are documented in [Frontend Markdown Guide](/frontend/markdown-guide).
|
|
@@ -103,6 +103,12 @@ class RenderPageCounter {
|
|
|
103
103
|
}
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
+
## `controllerRef` and internal companion access
|
|
107
|
+
|
|
108
|
+
`controllerRef` is a consumer-side mechanism: it exposes this component's Controller instance to the component's caller. It is distinct from the internal companion-member access used by a split Controller, Render, and Style implementation.
|
|
109
|
+
|
|
110
|
+
Inside one component, use direct `this.member` access for ordinary companion state, actions, and generated style classes. For the lookup order, shadowing rule, and cases that require explicit identity or interop access instead, see [Page Guide: Companion-member access](/frontend/page-guide#companion-member-access-in-a-split-page).
|
|
111
|
+
|
|
106
112
|
## Practical implications for component implementation
|
|
107
113
|
|
|
108
114
|
When working on Zova components, do not automatically fall back to generic Vue component habits.
|
|
@@ -56,6 +56,12 @@ A practical rule is:
|
|
|
56
56
|
|
|
57
57
|
This is an organizational split, not a different styling engine. Zova still uses the same CSS-in-JS model; it simply gives larger pages and components a better place to hold their style concerns.
|
|
58
58
|
|
|
59
|
+
### Style beans in a split Controller/Render/Style component
|
|
60
|
+
|
|
61
|
+
A Style Bean owns generated class setup. It can directly read its companion Controller's state through `this.member`; a Render Bean can consume the Style Bean's generated class through the same direct access surface, for example `this.cClass`. Keep Style class names distinct from Controller state and actions, because a same-name member on the current bean takes precedence.
|
|
62
|
+
|
|
63
|
+
For the complete lookup order and the boundary between ordinary companion access and explicit bean identity/interop access, see [Page Guide: Companion-member access](/frontend/page-guide#companion-member-access-in-a-split-page).
|
|
64
|
+
|
|
59
65
|
## Shared/global styles with `@Css()` and `$cssBase`
|
|
60
66
|
|
|
61
67
|
Not every style should stay local.
|