@standhigher/puck-page-builder 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -0
- package/dist/chunk-72MFV6J7.js +15 -0
- package/dist/chunk-G2LNCKYO.js +107 -0
- package/dist/chunk-RGB53WE6.js +180 -0
- package/dist/extensions.d.ts +148 -0
- package/dist/extensions.js +10 -0
- package/dist/index.d.ts +182 -0
- package/dist/index.js +911 -0
- package/dist/renderer.d.ts +12 -0
- package/dist/renderer.js +6 -0
- package/dist/schema.d.ts +59 -0
- package/dist/schema.js +10 -0
- package/package.json +68 -0
- package/src/styles.css +91 -0
- package/src/ui/builder/tokens.css +23 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ExtensionRegistry } from './extensions.js';
|
|
3
|
+
import { PageDocument } from './schema.js';
|
|
4
|
+
|
|
5
|
+
type WebRendererProps = {
|
|
6
|
+
document: PageDocument;
|
|
7
|
+
className?: string;
|
|
8
|
+
registry?: ExtensionRegistry;
|
|
9
|
+
};
|
|
10
|
+
declare function WebRenderer({ document, className, registry }: WebRendererProps): react.JSX.Element;
|
|
11
|
+
|
|
12
|
+
export { WebRenderer, type WebRendererProps };
|
package/dist/renderer.js
ADDED
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
type JsonPrimitive = string | number | boolean | null;
|
|
2
|
+
type JsonValue = JsonPrimitive | JsonValue[] | {
|
|
3
|
+
[key: string]: JsonValue;
|
|
4
|
+
};
|
|
5
|
+
type RenderTarget = "web" | "email";
|
|
6
|
+
type PageSettings = {
|
|
7
|
+
locale: string;
|
|
8
|
+
seoTitle?: string;
|
|
9
|
+
};
|
|
10
|
+
type DataBinding = {
|
|
11
|
+
source: string;
|
|
12
|
+
params?: Record<string, JsonValue>;
|
|
13
|
+
};
|
|
14
|
+
type BlockNode = {
|
|
15
|
+
id: string;
|
|
16
|
+
type: string;
|
|
17
|
+
version: number;
|
|
18
|
+
props: Record<string, JsonValue>;
|
|
19
|
+
slots?: Record<string, BlockNode[]>;
|
|
20
|
+
binding?: DataBinding;
|
|
21
|
+
};
|
|
22
|
+
type PageDocument = {
|
|
23
|
+
schemaVersion: 1;
|
|
24
|
+
pageId: string;
|
|
25
|
+
target: RenderTarget;
|
|
26
|
+
templateId?: string;
|
|
27
|
+
root: Record<string, JsonValue>;
|
|
28
|
+
blocks: BlockNode[];
|
|
29
|
+
settings: PageSettings;
|
|
30
|
+
};
|
|
31
|
+
type PageDocumentIssue = {
|
|
32
|
+
path: string;
|
|
33
|
+
message: string;
|
|
34
|
+
};
|
|
35
|
+
type PageDocumentValidation = {
|
|
36
|
+
success: true;
|
|
37
|
+
data: PageDocument;
|
|
38
|
+
} | {
|
|
39
|
+
success: false;
|
|
40
|
+
issues: PageDocumentIssue[];
|
|
41
|
+
};
|
|
42
|
+
type PageDocumentMigration = {
|
|
43
|
+
success: true;
|
|
44
|
+
data: PageDocument;
|
|
45
|
+
migrated: boolean;
|
|
46
|
+
} | {
|
|
47
|
+
success: false;
|
|
48
|
+
issues: PageDocumentIssue[];
|
|
49
|
+
};
|
|
50
|
+
declare function createPageDocument(input: Partial<PageDocument> & Pick<PageDocument, "pageId">): PageDocument;
|
|
51
|
+
declare function validatePageDocument(value: unknown): PageDocumentValidation;
|
|
52
|
+
/**
|
|
53
|
+
* Accept the pre-versioned shape produced by the early Demo and normalize it
|
|
54
|
+
* to the first persisted PageDocument schema. Future schema migrations belong
|
|
55
|
+
* here so storage callers have one validation boundary.
|
|
56
|
+
*/
|
|
57
|
+
declare function migratePageDocument(value: unknown): PageDocumentMigration;
|
|
58
|
+
|
|
59
|
+
export { type BlockNode, type DataBinding, type JsonPrimitive, type JsonValue, type PageDocument, type PageDocumentIssue, type PageDocumentMigration, type PageDocumentValidation, type PageSettings, type RenderTarget, createPageDocument, migratePageDocument, validatePageDocument };
|
package/dist/schema.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@standhigher/puck-page-builder",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React PageDocument editor, extension registry, and web renderer for BestTrack page experiences.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/standhigher/puck-page-builder.git"
|
|
9
|
+
},
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/standhigher/puck-page-builder/issues"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/standhigher/puck-page-builder#readme",
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"src/styles.css",
|
|
17
|
+
"src/ui/builder/tokens.css",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"sideEffects": [
|
|
21
|
+
"./src/styles.css",
|
|
22
|
+
"./src/ui/builder/tokens.css"
|
|
23
|
+
],
|
|
24
|
+
"main": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"import": "./dist/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./renderer": {
|
|
32
|
+
"types": "./dist/renderer.d.ts",
|
|
33
|
+
"import": "./dist/renderer.js"
|
|
34
|
+
},
|
|
35
|
+
"./extensions": {
|
|
36
|
+
"types": "./dist/extensions.d.ts",
|
|
37
|
+
"import": "./dist/extensions.js"
|
|
38
|
+
},
|
|
39
|
+
"./schema": {
|
|
40
|
+
"types": "./dist/schema.d.ts",
|
|
41
|
+
"import": "./dist/schema.js"
|
|
42
|
+
},
|
|
43
|
+
"./styles.css": "./src/styles.css"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"@puckeditor/core": "^0.23.0",
|
|
50
|
+
"@shopify/polaris": "^13.9.5",
|
|
51
|
+
"@shopify/polaris-icons": "^9.3.1",
|
|
52
|
+
"react": "^18.0.0",
|
|
53
|
+
"react-dom": "^18.0.0"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@puckeditor/core": "^0.23.0",
|
|
57
|
+
"@shopify/polaris": "^13.9.5",
|
|
58
|
+
"@shopify/polaris-icons": "^9.3.1",
|
|
59
|
+
"react": "^18.3.1",
|
|
60
|
+
"react-dom": "^18.3.1",
|
|
61
|
+
"tsup": "^8.5.1",
|
|
62
|
+
"typescript": "^5.9.3"
|
|
63
|
+
},
|
|
64
|
+
"scripts": {
|
|
65
|
+
"build": "tsup src/index.ts src/renderer.ts src/extensions.ts src/schema.ts --format esm --dts --clean",
|
|
66
|
+
"typecheck": "tsc --noEmit"
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/styles.css
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
@import "./ui/builder/tokens.css";
|
|
2
|
+
|
|
3
|
+
:root { font-family: var(--builder-font); color: var(--builder-text); background: var(--builder-canvas-bg); }
|
|
4
|
+
* { box-sizing: border-box; }
|
|
5
|
+
body { margin: 0; min-width: 320px; }
|
|
6
|
+
button:focus-visible, [role="button"]:focus-visible { outline: 2px solid var(--builder-accent); outline-offset: 2px; }
|
|
7
|
+
.pb-shell { min-height: 100vh; color: var(--builder-text); background: var(--builder-canvas-bg); }
|
|
8
|
+
.pb-header { position: sticky; top: 0; z-index: 20; min-height: 56px; padding: var(--builder-space-2) var(--builder-space-4); border-bottom: 1px solid var(--builder-border); background: var(--builder-surface); }
|
|
9
|
+
.pb-page-title { display: grid; gap: 0; min-width: 0; }
|
|
10
|
+
.pb-page-title p { margin: 0; }
|
|
11
|
+
.pb-workspace { display: grid; grid-template-columns: 64px minmax(260px, 300px) minmax(640px, 1fr) minmax(300px, 320px); min-height: calc(100vh - 56px); }
|
|
12
|
+
.pb-tool-rail { display: flex; flex-direction: column; align-items: center; gap: var(--builder-space-2); padding: var(--builder-space-2); border-right: 1px solid var(--builder-border); background: var(--builder-surface-subtle); }
|
|
13
|
+
.pb-tool-rail .Polaris-Button { min-width: 44px; min-height: 44px; }
|
|
14
|
+
.pb-left-panel, .pb-right-panel { display: flex; flex-direction: column; gap: var(--builder-space-4); min-width: 0; padding: var(--builder-space-4); overflow: auto; background: var(--builder-surface); }
|
|
15
|
+
.pb-left-panel { border-right: 1px solid var(--builder-border); }
|
|
16
|
+
.pb-right-panel { border-left: 1px solid var(--builder-border); }
|
|
17
|
+
.pb-panel--closed { display: none; }
|
|
18
|
+
.pb-block-list { display: flex; flex: 1; flex-direction: column; gap: var(--builder-space-2); min-height: 0; }
|
|
19
|
+
.pb-block-row { display: flex; align-items: center; gap: var(--builder-space-2); padding: var(--builder-space-2); border: 1px solid transparent; border-radius: var(--builder-panel-radius); background: transparent; transition: background-color 120ms ease, border-color 120ms ease; }
|
|
20
|
+
.pb-block-row:hover { background: var(--builder-surface-subtle); }
|
|
21
|
+
.pb-block-row--selected { border-color: var(--builder-accent); background: var(--builder-accent-subtle); }
|
|
22
|
+
.pb-block-handle { display: inline-flex; flex: 0 0 auto; width: 18px; color: var(--builder-text-muted); cursor: grab; }
|
|
23
|
+
.pb-block-handle svg { width: 16px; height: 16px; }
|
|
24
|
+
.pb-block-select { display: flex; flex: 1; flex-direction: column; gap: var(--builder-space-1); min-width: 0; padding: var(--builder-space-1); border: 0; border-radius: var(--builder-control-radius); text-align: left; color: inherit; background: transparent; cursor: pointer; }
|
|
25
|
+
.pb-block-actions { display: none; align-items: center; gap: var(--builder-space-1); }
|
|
26
|
+
.pb-block-row:hover .pb-block-actions, .pb-block-row--selected .pb-block-actions, .pb-block-row:focus-within .pb-block-actions { display: flex; }
|
|
27
|
+
.pb-canvas-area { display: flex; flex-direction: column; gap: var(--builder-space-3); min-width: 0; padding: var(--builder-space-4); overflow: auto; background: var(--builder-canvas-bg); }
|
|
28
|
+
.pb-canvas-toolbar { padding: var(--builder-space-2); border: 1px solid var(--builder-border); border-radius: var(--builder-panel-radius); background: var(--builder-surface); }
|
|
29
|
+
.pb-canvas-stage { position: relative; display: flex; justify-content: center; min-height: 38rem; padding: var(--builder-space-6); }
|
|
30
|
+
.pb-canvas-frame { width: 100%; min-height: 38rem; margin: 0 auto; overflow: hidden; border: 1px solid var(--builder-border); border-radius: var(--builder-canvas-radius); background: var(--builder-surface); box-shadow: var(--builder-canvas-shadow); transition: width 160ms ease, transform 160ms ease; }
|
|
31
|
+
.pb-canvas-drop-target { position: absolute; inset: var(--builder-space-6); z-index: 2; display: grid; place-items: center; border: 2px dashed var(--builder-accent); border-radius: var(--builder-canvas-radius); color: var(--builder-accent); background: color-mix(in srgb, var(--builder-accent-subtle) 88%, transparent); font-weight: 600; pointer-events: auto; }
|
|
32
|
+
.pb-canvas-frame--tablet { width: min(100%, 768px); }
|
|
33
|
+
.pb-canvas-frame--mobile { width: min(100%, 390px); }
|
|
34
|
+
.pb-canvas-frame--full { width: 100%; }
|
|
35
|
+
.pb-canvas-frame--zoom-50 { transform: scale(.5); transform-origin: top center; margin-bottom: -19rem; }
|
|
36
|
+
.pb-canvas-frame--zoom-70 { transform: scale(.7); transform-origin: top center; margin-bottom: -11rem; }
|
|
37
|
+
.pb-canvas-frame--zoom-100 { transform: scale(1); }
|
|
38
|
+
.pb-canvas-overlay { position: absolute; top: calc(var(--builder-space-6) + var(--builder-space-2)); left: 50%; display: flex; gap: var(--builder-space-2); padding: var(--builder-space-1) var(--builder-space-2); border: 2px solid var(--builder-accent); border-radius: var(--builder-control-radius); color: var(--builder-accent); background: var(--builder-surface); font-size: 12px; font-weight: 600; transform: translateX(-50%); box-shadow: var(--builder-canvas-shadow); pointer-events: none; }
|
|
39
|
+
.pb-canvas-overlay span:last-child { color: var(--builder-text-muted); font-weight: 500; }
|
|
40
|
+
.pb-collapsed-actions { display: flex; gap: var(--builder-space-2); }
|
|
41
|
+
.pb-inspector-tabs { display: grid; grid-template-columns: repeat(3, 1fr); padding: var(--builder-space-1); border: 1px solid var(--builder-border); border-radius: var(--builder-control-radius); background: var(--builder-surface-subtle); }
|
|
42
|
+
.pb-inspector-tabs button { min-height: 32px; border: 0; border-radius: var(--builder-control-radius); color: var(--builder-text-muted); background: transparent; font: inherit; font-size: 13px; cursor: pointer; }
|
|
43
|
+
.pb-inspector-tabs .pb-inspector-tab--selected { color: var(--builder-text); background: var(--builder-surface); box-shadow: 0 1px 2px rgba(26, 26, 26, .12); }
|
|
44
|
+
.pb-document-block { display: grid; grid-template-columns: 20px minmax(0, 1fr); gap: var(--builder-space-1) var(--builder-space-2); width: 100%; padding: var(--builder-space-3); border: 1px solid transparent; border-radius: var(--builder-panel-radius); text-align: left; color: var(--builder-text); background: transparent; cursor: pointer; }
|
|
45
|
+
.pb-document-block:hover { background: var(--builder-surface-subtle); }
|
|
46
|
+
.pb-document-block--selected { border-color: var(--builder-accent); background: var(--builder-accent-subtle); }
|
|
47
|
+
.pb-document-block svg { width: 18px; height: 18px; color: var(--builder-text-muted); }
|
|
48
|
+
.pb-document-block span { font-size: 14px; font-weight: 600; }
|
|
49
|
+
.pb-document-block small { grid-column: 2; color: var(--builder-text-muted); }
|
|
50
|
+
.pb-document-block-row { display: flex; align-items: center; gap: var(--builder-space-2); padding: var(--builder-space-2); border: 1px solid transparent; border-radius: var(--builder-panel-radius); background: transparent; }
|
|
51
|
+
.pb-document-block-row--library { width: 100%; flex-direction: column; align-items: flex-start; text-align: left; color: var(--builder-text); font: inherit; cursor: grab; }
|
|
52
|
+
.pb-document-block-row--library:active { cursor: grabbing; }
|
|
53
|
+
.pb-library-block-title { display: flex; align-items: center; justify-content: space-between; width: 100%; gap: var(--builder-space-2); }
|
|
54
|
+
.pb-library-block-drag-hint { display: inline-flex; color: var(--builder-text-muted); pointer-events: none; }
|
|
55
|
+
.pb-library-block-drag-hint svg { width: 16px; height: 16px; }
|
|
56
|
+
.pb-document-block-row:hover { background: var(--builder-surface-subtle); }
|
|
57
|
+
.pb-document-block-row--selected { border-color: var(--builder-accent); background: var(--builder-accent-subtle); }
|
|
58
|
+
.pb-editor-status { display: grid; place-items: center; min-height: min(50vh, 420px); padding: var(--builder-space-6); }
|
|
59
|
+
.pb-shell--v04 > .Polaris-Banner { margin: var(--builder-space-3) var(--builder-space-4) 0; }
|
|
60
|
+
.pb-document-canvas__text, .pb-document-canvas__image { max-width: 760px; margin: 0 auto; padding: 48px; color: #1d2939; background: #fff; cursor: pointer; }
|
|
61
|
+
.pb-document-canvas__text { font-size: clamp(32px, 4vw, 52px); font-weight: 650; line-height: 1.1; letter-spacing: -.03em; }
|
|
62
|
+
.pb-document-canvas__text p { margin: 0; }
|
|
63
|
+
.pb-document-canvas__text p[contenteditable="true"] { outline: 1px dashed var(--builder-accent); outline-offset: 8px; cursor: text; }
|
|
64
|
+
.pb-document-canvas__image { padding-top: 0; }
|
|
65
|
+
.pb-document-canvas__image img { display: block; width: 100%; border-radius: 12px; }
|
|
66
|
+
.pb-document-canvas__image input { width: 100%; margin-top: var(--builder-space-3); padding: var(--builder-space-2); border: 1px solid var(--builder-accent); border-radius: var(--builder-control-radius); color: var(--builder-text); background: var(--builder-surface); font: inherit; }
|
|
67
|
+
.pb-document-export { max-height: 420px; padding: var(--builder-space-3); overflow: auto; border-radius: var(--builder-control-radius); color: var(--builder-text); background: var(--builder-surface-subtle); font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre-wrap; }
|
|
68
|
+
.pb-web-renderer { min-height: 100vh; padding: 72px clamp(24px, 10vw, 180px); color: #1d2939; background: #fff; font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
69
|
+
.pb-web-renderer__text { max-width: 760px; margin: 0 auto 40px; font-size: clamp(32px, 5vw, 64px); font-weight: 650; line-height: 1.08; letter-spacing: -.04em; }
|
|
70
|
+
.pb-web-renderer__text p { margin: 0; }
|
|
71
|
+
.pb-web-renderer__image { max-width: 900px; margin: 0 auto; }
|
|
72
|
+
.pb-web-renderer__image img { display: block; width: 100%; border-radius: 18px; }
|
|
73
|
+
|
|
74
|
+
/* Storefront-only presentation rendered inside the Puck iframe. */
|
|
75
|
+
.bt-storefront { min-height: 100%; padding: 48px clamp(24px, 7vw, 96px); color: #18212b; background: #fffdf9; font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
76
|
+
.bt-storefront__header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 32px; border-bottom: 1px solid #e6e1da; font-size: 14px; }
|
|
77
|
+
.bt-storefront__brand { font-weight: 700; letter-spacing: .04em; }
|
|
78
|
+
.bt-storefront__link { color: #495968; }
|
|
79
|
+
.bt-storefront__section { max-width: 720px; margin: 0 auto; padding: 56px 0; }
|
|
80
|
+
.bt-storefront__eyebrow { margin: 0 0 12px; color: #657786; font-size: 12px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; }
|
|
81
|
+
.bt-storefront h1, .bt-storefront h2 { max-width: 620px; margin: 0 0 16px; letter-spacing: -.03em; }
|
|
82
|
+
.bt-storefront h1 { font-size: clamp(36px, 5vw, 58px); line-height: 1.04; }
|
|
83
|
+
.bt-storefront h2 { font-size: 28px; line-height: 1.2; }
|
|
84
|
+
.bt-storefront p { max-width: 560px; margin: 0; color: #495968; font-size: 17px; line-height: 1.6; }
|
|
85
|
+
.bt-storefront__tracking { padding: 32px; border: 1px solid #d8dfe4; border-radius: 16px; background: #ffffff; box-shadow: 0 16px 40px rgba(32, 45, 57, .08); }
|
|
86
|
+
.bt-storefront__form { display: flex; gap: 12px; margin-top: 24px; }
|
|
87
|
+
.bt-storefront__form input { flex: 1; min-width: 0; padding: 13px 14px; border: 1px solid #b8c3cc; border-radius: 8px; font: inherit; }
|
|
88
|
+
.bt-storefront__form button { padding: 13px 18px; border: 0; border-radius: 8px; color: white; background: #203e54; font: inherit; font-weight: 600; }
|
|
89
|
+
@media (max-width: 1180px) { .pb-workspace { grid-template-columns: 64px minmax(260px, 300px) minmax(0, 1fr); } .pb-right-panel { grid-column: 2 / -1; border-top: 1px solid var(--builder-border); border-left: 0; } }
|
|
90
|
+
@media (max-width: 1023px) { .pb-header { position: static; overflow: auto; } .pb-workspace { grid-template-columns: 64px minmax(0, 1fr); } .pb-left-panel, .pb-right-panel { grid-column: 2; border-left: 0; border-right: 0; border-bottom: 1px solid var(--builder-border); } .pb-canvas-area { grid-column: 2; } }
|
|
91
|
+
@media (max-width: 640px) { .pb-header, .pb-canvas-area { padding: var(--builder-space-2); } .pb-header > .Polaris-InlineStack { min-width: 680px; } .pb-canvas-stage { padding: var(--builder-space-3) 0; } .pb-canvas-frame--tablet, .pb-canvas-frame--mobile { width: 100%; } .bt-storefront { padding: 28px 20px; } .bt-storefront__form { flex-direction: column; } }
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--builder-canvas-bg: #f5f6f7;
|
|
3
|
+
--builder-surface: #ffffff;
|
|
4
|
+
--builder-surface-subtle: #f7f7f8;
|
|
5
|
+
--builder-border: #e1e3e5;
|
|
6
|
+
--builder-text: #1a1a1a;
|
|
7
|
+
--builder-text-muted: #616161;
|
|
8
|
+
--builder-accent: #2563eb;
|
|
9
|
+
--builder-accent-subtle: #eff6ff;
|
|
10
|
+
--builder-action-primary: #1f1f1f;
|
|
11
|
+
--builder-danger: #d72c0d;
|
|
12
|
+
--builder-font: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
13
|
+
--builder-space-1: 4px;
|
|
14
|
+
--builder-space-2: 8px;
|
|
15
|
+
--builder-space-3: 12px;
|
|
16
|
+
--builder-space-4: 16px;
|
|
17
|
+
--builder-space-6: 24px;
|
|
18
|
+
--builder-space-8: 32px;
|
|
19
|
+
--builder-control-radius: 6px;
|
|
20
|
+
--builder-panel-radius: 8px;
|
|
21
|
+
--builder-canvas-radius: 12px;
|
|
22
|
+
--builder-canvas-shadow: 0 12px 32px rgba(26, 26, 26, 0.12);
|
|
23
|
+
}
|