@broadpaper/blocks 0.1.1
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/LICENSE +143 -0
- package/THIRD-PARTY-NOTICES.md +255 -0
- package/dist/index.d.ts +312 -0
- package/dist/index.js +1799 -0
- package/dist/index.js.map +1 -0
- package/package.json +35 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1799 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { BlockRegistry } from "@broadpaper/core";
|
|
3
|
+
|
|
4
|
+
// src/blocks/structure.ts
|
|
5
|
+
import { defineBlock, cssText } from "@broadpaper/core";
|
|
6
|
+
var sectionBlock = defineBlock({
|
|
7
|
+
type: "section",
|
|
8
|
+
name: "Section",
|
|
9
|
+
category: "structure",
|
|
10
|
+
icon: "rectangle-horizontal",
|
|
11
|
+
hidden: true,
|
|
12
|
+
defaultProps: { padding: 0 },
|
|
13
|
+
container: { accepts: ["@rows", "@blocks"] },
|
|
14
|
+
split: "children",
|
|
15
|
+
styleGroups: ["spacing", "background", "border", "flow"],
|
|
16
|
+
inspector: [
|
|
17
|
+
{
|
|
18
|
+
title: "Section",
|
|
19
|
+
fields: [
|
|
20
|
+
{ kind: "text", prop: "label", label: "Label", placeholder: "e.g. Executive summary", allowTokens: false },
|
|
21
|
+
{ kind: "number", prop: "padding", label: "Padding", min: 0, max: 120, unit: "px" }
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
canvas: { label: (p) => p.label ?? "Section" },
|
|
26
|
+
render: ({ h: h3, css, children, styles, props, mode, node }) => {
|
|
27
|
+
const pad = Number(props.padding) || 0;
|
|
28
|
+
const empty = children.length === 0;
|
|
29
|
+
return h3(
|
|
30
|
+
"div",
|
|
31
|
+
{ class: ["bp-section__inner", empty && "bp-section__inner--empty"], style: cssText({ ...css, padding: pad ? `${pad}px` : css["padding"] }) },
|
|
32
|
+
h3("div", { class: "bp-stack", style: { gap: `${styles.spacing.blockGap}px` }, "data-dropzone": node.nodeId }, empty && mode === "design" ? h3("div", { class: "bp-empty-section" }, h3("span", { class: "bp-empty-section__title" }, "Empty section"), h3("span", { class: "bp-empty-section__hint" }, "Drag blocks here, or pick one from the panel")) : children)
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
var rowBlock = defineBlock({
|
|
37
|
+
type: "row",
|
|
38
|
+
name: "Columns",
|
|
39
|
+
description: "A row of columns",
|
|
40
|
+
category: "layout",
|
|
41
|
+
icon: "columns-3",
|
|
42
|
+
keywords: ["row", "columns", "grid", "layout"],
|
|
43
|
+
defaultProps: { layout: [6, 6], allowSplit: false, verticalAlign: "top" },
|
|
44
|
+
container: { accepts: ["@columns"], direction: "row" },
|
|
45
|
+
split: "children",
|
|
46
|
+
styleGroups: ["spacing", "background", "border", "flow"],
|
|
47
|
+
inspector: [
|
|
48
|
+
{
|
|
49
|
+
title: "Columns",
|
|
50
|
+
fields: [
|
|
51
|
+
{
|
|
52
|
+
kind: "segmented",
|
|
53
|
+
prop: "verticalAlign",
|
|
54
|
+
label: "Vertical align",
|
|
55
|
+
options: [
|
|
56
|
+
{ value: "top", label: "Top", icon: "align-start-vertical" },
|
|
57
|
+
{ value: "middle", label: "Middle", icon: "align-center-vertical" },
|
|
58
|
+
{ value: "bottom", label: "Bottom", icon: "align-end-vertical" },
|
|
59
|
+
{ value: "stretch", label: "Stretch", icon: "stretch-vertical" }
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
{ kind: "toggle", prop: "allowSplit", label: "Allow splitting across pages", description: "When off, a row that does not fit moves to the next page whole." }
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
],
|
|
66
|
+
canvas: { label: (p) => `Columns ${p.layout.join(" / ")}` },
|
|
67
|
+
render: ({ h: h3, css, children, styles, props }) => {
|
|
68
|
+
const align = props.verticalAlign === "middle" ? "center" : props.verticalAlign === "bottom" ? "end" : props.verticalAlign === "stretch" ? "stretch" : "start";
|
|
69
|
+
return h3("div", { class: "bp-row__grid", style: cssText({ ...css, display: "grid", "grid-template-columns": "repeat(12, minmax(0, 1fr))", "column-gap": `${styles.spacing.columnGap}px`, "align-items": align }) }, children);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
var columnBlock = defineBlock({
|
|
73
|
+
type: "column",
|
|
74
|
+
name: "Column",
|
|
75
|
+
category: "structure",
|
|
76
|
+
icon: "rectangle-vertical",
|
|
77
|
+
hidden: true,
|
|
78
|
+
defaultProps: { span: 6 },
|
|
79
|
+
container: { accepts: ["@blocks", "@rows"] },
|
|
80
|
+
split: "children",
|
|
81
|
+
styleGroups: ["spacing", "background", "border"],
|
|
82
|
+
render: ({ h: h3, css, children, node, styles, mode }) => {
|
|
83
|
+
const empty = children.length === 0;
|
|
84
|
+
return h3(
|
|
85
|
+
"div",
|
|
86
|
+
{ class: ["bp-column__inner", empty && "bp-column__inner--empty"], style: cssText({ ...css, "min-width": "0", height: "100%" }) },
|
|
87
|
+
h3("div", { class: "bp-stack", style: { gap: `${styles.spacing.blockGap}px` }, "data-dropzone": node.nodeId }, empty && mode === "design" ? h3("div", { class: "bp-empty-column" }, "Drop here") : children)
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
var repeaterItemBlock = defineBlock({
|
|
92
|
+
type: "repeater-item",
|
|
93
|
+
name: "Repeater item",
|
|
94
|
+
category: "structure",
|
|
95
|
+
hidden: true,
|
|
96
|
+
defaultProps: { index: 0, count: 0 },
|
|
97
|
+
container: { accepts: ["@blocks", "@rows"] },
|
|
98
|
+
split: "children",
|
|
99
|
+
render: ({ h: h3, children, styles }) => h3("div", { class: "bp-stack bp-repeater-item__inner", style: { gap: `${styles.spacing.blockGap}px` } }, children)
|
|
100
|
+
});
|
|
101
|
+
function stack(h3, children, gap, extra = {}) {
|
|
102
|
+
return h3("div", { class: "bp-stack", style: { gap: `${gap}px` }, ...extra }, children);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/blocks/content.ts
|
|
106
|
+
import { defineBlock as defineBlock2, cssText as cssText2, createRichText, isSafeUrl, resolveValue, isDarkColor, formatDate } from "@broadpaper/core";
|
|
107
|
+
|
|
108
|
+
// src/shared/marks.ts
|
|
109
|
+
import { h } from "@broadpaper/core";
|
|
110
|
+
function svg(size, children) {
|
|
111
|
+
return h(
|
|
112
|
+
"svg",
|
|
113
|
+
{ width: size, height: size, viewBox: "0 0 12 12", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", style: { display: "block", "flex-shrink": "0" } },
|
|
114
|
+
children
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
function triangleMark(direction, size, color) {
|
|
118
|
+
const d = direction === "up" ? "M6 2 L11 9.5 L1 9.5 Z" : "M6 10 L1 2.5 L11 2.5 Z";
|
|
119
|
+
return svg(size, [h("path", { d, fill: color })]);
|
|
120
|
+
}
|
|
121
|
+
function checkMark(size, color) {
|
|
122
|
+
return svg(size, [h("path", { d: "M2 6.4 L4.8 9.2 L10 3.2", fill: "none", stroke: color, "stroke-width": 1.8, "stroke-linecap": "round", "stroke-linejoin": "round" })]);
|
|
123
|
+
}
|
|
124
|
+
function dotMark(size, color) {
|
|
125
|
+
return svg(size, [h("circle", { cx: 6, cy: 6, r: 2.6, fill: color })]);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/blocks/content.ts
|
|
129
|
+
var alignField = {
|
|
130
|
+
kind: "segmented",
|
|
131
|
+
prop: "align",
|
|
132
|
+
label: "Align",
|
|
133
|
+
options: [
|
|
134
|
+
{ value: "left", label: "Left", icon: "align-left" },
|
|
135
|
+
{ value: "center", label: "Centre", icon: "align-center" },
|
|
136
|
+
{ value: "right", label: "Right", icon: "align-right" }
|
|
137
|
+
]
|
|
138
|
+
};
|
|
139
|
+
var headingBlock = defineBlock2({
|
|
140
|
+
type: "heading",
|
|
141
|
+
name: "Heading",
|
|
142
|
+
description: "Section title using a document heading style",
|
|
143
|
+
category: "content",
|
|
144
|
+
icon: "heading",
|
|
145
|
+
keywords: ["title", "h1", "h2", "h3"],
|
|
146
|
+
defaultProps: { text: "Heading", level: "h2" },
|
|
147
|
+
defaultTextStyle: "h2",
|
|
148
|
+
defaultFlow: { keepWithNext: true },
|
|
149
|
+
split: "none",
|
|
150
|
+
styleGroups: ["textStyle", "typography", "alignment", "spacing", "flow"],
|
|
151
|
+
inspector: [
|
|
152
|
+
{
|
|
153
|
+
title: "Heading",
|
|
154
|
+
fields: [
|
|
155
|
+
{ kind: "text", prop: "text", label: "Text", placeholder: "Heading text or {{ token }}" },
|
|
156
|
+
{
|
|
157
|
+
kind: "segmented",
|
|
158
|
+
prop: "level",
|
|
159
|
+
label: "Level",
|
|
160
|
+
options: [
|
|
161
|
+
{ value: "h1", label: "H1" },
|
|
162
|
+
{ value: "h2", label: "H2" },
|
|
163
|
+
{ value: "h3", label: "H3" },
|
|
164
|
+
{ value: "h4", label: "H4" }
|
|
165
|
+
]
|
|
166
|
+
}
|
|
167
|
+
]
|
|
168
|
+
}
|
|
169
|
+
],
|
|
170
|
+
canvas: { inlineEdit: { prop: "text", kind: "text" }, label: (p) => p.text?.slice(0, 40) || "Heading" },
|
|
171
|
+
render: ({ h: h3, css, text, props, partAttrs, mode }) => {
|
|
172
|
+
const content = text("text");
|
|
173
|
+
const empty = content.length === 0 || content.length === 1 && content[0] === "";
|
|
174
|
+
return h3(props.level || "h2", { class: ["bp-heading", `bp-heading--${props.level}`], style: cssText2(css), ...partAttrs("text") }, empty && mode === "design" ? h3("span", { class: "bp-placeholder-text" }, "Heading") : content);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
var textBlock = defineBlock2({
|
|
178
|
+
type: "text",
|
|
179
|
+
name: "Text",
|
|
180
|
+
description: "Rich text with inline data tokens",
|
|
181
|
+
category: "content",
|
|
182
|
+
icon: "text",
|
|
183
|
+
keywords: ["paragraph", "rich text", "body", "copy"],
|
|
184
|
+
defaultProps: () => ({ content: createRichText("") }),
|
|
185
|
+
defaultTextStyle: "body",
|
|
186
|
+
split: "lines",
|
|
187
|
+
measure: "text",
|
|
188
|
+
styleGroups: ["textStyle", "typography", "alignment", "spacing", "background", "border", "flow"],
|
|
189
|
+
inspector: [{ title: "Text", fields: [{ kind: "richtext", prop: "content", label: "Content" }] }],
|
|
190
|
+
canvas: { inlineEdit: { prop: "content", kind: "richtext" }, label: () => "Text" },
|
|
191
|
+
render: ({ h: h3, css, richText, partAttrs, mode, node }) => {
|
|
192
|
+
const doc = node.richText.content ?? node.props.content;
|
|
193
|
+
const isEmpty = !doc || doc.content.every((b) => b.type === "paragraph" && b.content.length === 0);
|
|
194
|
+
return h3("div", { class: ["bp-text", isEmpty && "bp-text--empty"], style: cssText2(css), ...partAttrs("text") }, isEmpty && mode === "design" ? h3("p", { class: "bp-p bp-placeholder-text" }, "Type or drop a field here\u2026") : richText("content"));
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
var imageBlock = defineBlock2({
|
|
198
|
+
type: "image",
|
|
199
|
+
name: "Image",
|
|
200
|
+
description: "Static or data-bound image",
|
|
201
|
+
category: "content",
|
|
202
|
+
icon: "image",
|
|
203
|
+
keywords: ["picture", "photo", "graphic"],
|
|
204
|
+
defaultProps: { src: "", alt: "", fit: "contain", width: null, height: null, align: "left", radius: 0, link: "" },
|
|
205
|
+
split: "none",
|
|
206
|
+
styleGroups: ["spacing", "border", "flow"],
|
|
207
|
+
inspector: [
|
|
208
|
+
{
|
|
209
|
+
title: "Image",
|
|
210
|
+
fields: [
|
|
211
|
+
{ kind: "image", prop: "src", label: "Source", allowBinding: true },
|
|
212
|
+
{ kind: "text", prop: "alt", label: "Alt text", allowTokens: true },
|
|
213
|
+
{ kind: "text", prop: "link", label: "Link URL", allowTokens: true }
|
|
214
|
+
]
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
title: "Size",
|
|
218
|
+
fields: [
|
|
219
|
+
{ kind: "number", prop: "width", label: "Width", min: 8, max: 2e3, unit: "px" },
|
|
220
|
+
{ kind: "number", prop: "height", label: "Height", min: 8, max: 2e3, unit: "px" },
|
|
221
|
+
{
|
|
222
|
+
kind: "segmented",
|
|
223
|
+
prop: "fit",
|
|
224
|
+
label: "Fit",
|
|
225
|
+
options: [
|
|
226
|
+
{ value: "contain", label: "Contain" },
|
|
227
|
+
{ value: "cover", label: "Cover" }
|
|
228
|
+
]
|
|
229
|
+
},
|
|
230
|
+
alignField,
|
|
231
|
+
{ kind: "number", prop: "radius", label: "Corner radius", min: 0, max: 200, unit: "px" }
|
|
232
|
+
]
|
|
233
|
+
}
|
|
234
|
+
],
|
|
235
|
+
validate: ({ props }) => {
|
|
236
|
+
const src = String(props.src ?? "");
|
|
237
|
+
if (src && !src.includes("{{") && !isSafeUrl(src, { allowData: true })) return [{ severity: "warning", code: "invalid-url", message: `Image URL "${src.slice(0, 60)}" is not an http(s) or data:image URL`, prop: "src" }];
|
|
238
|
+
return [];
|
|
239
|
+
},
|
|
240
|
+
render: ({ h: h3, css, props, mode, node, width: avail }) => {
|
|
241
|
+
const src = typeof props.src === "string" ? props.src : "";
|
|
242
|
+
const safe = src && isSafeUrl(src, { allowData: true });
|
|
243
|
+
const w = props.width ?? void 0;
|
|
244
|
+
const hgt = props.height ?? void 0;
|
|
245
|
+
const justify = props.align === "center" ? "center" : props.align === "right" ? "flex-end" : "flex-start";
|
|
246
|
+
const imgStyle = {
|
|
247
|
+
width: w ? `${Math.min(w, avail)}px` : hgt ? "auto" : "100%",
|
|
248
|
+
height: hgt ? `${hgt}px` : "auto",
|
|
249
|
+
"max-width": "100%",
|
|
250
|
+
"object-fit": props.fit,
|
|
251
|
+
"border-radius": props.radius ? `${props.radius}px` : void 0,
|
|
252
|
+
display: "block"
|
|
253
|
+
};
|
|
254
|
+
let img;
|
|
255
|
+
if (safe) img = h3("img", { src, alt: props.alt || "", style: imgStyle, draggable: "false" });
|
|
256
|
+
else if (mode === "design") {
|
|
257
|
+
const bound = node.bindings.src;
|
|
258
|
+
img = h3("div", { class: "bp-image-placeholder", style: { width: w ? `${Math.min(w, avail)}px` : "100%", height: `${hgt ?? 140}px`, "border-radius": props.radius ? `${props.radius}px` : void 0 } }, h3("span", { class: "bp-image-placeholder__label" }, bound ? bound.label : src ? "Invalid image URL" : "Choose an image"));
|
|
259
|
+
} else img = h3("div", { style: { width: w ? `${w}px` : "100%", height: `${hgt ?? 0}px` } });
|
|
260
|
+
const linked = props.link && isSafeUrl(props.link) ? h3("a", { href: props.link, rel: "noopener", style: { display: "block", width: w ? `${Math.min(w, avail)}px` : "100%" } }, img) : img;
|
|
261
|
+
return h3("div", { class: "bp-image", style: cssText2({ ...css, display: "flex", "justify-content": justify }) }, linked);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
var logoBlock = defineBlock2({
|
|
265
|
+
type: "logo",
|
|
266
|
+
name: "Logo",
|
|
267
|
+
description: "The active brand's logo",
|
|
268
|
+
category: "content",
|
|
269
|
+
icon: "badge-check",
|
|
270
|
+
keywords: ["brand", "theme"],
|
|
271
|
+
defaultProps: { variant: "auto", height: 32, align: "left" },
|
|
272
|
+
split: "none",
|
|
273
|
+
styleGroups: ["spacing", "flow"],
|
|
274
|
+
inspector: [
|
|
275
|
+
{
|
|
276
|
+
title: "Logo",
|
|
277
|
+
fields: [
|
|
278
|
+
{ kind: "number", prop: "height", label: "Height", min: 8, max: 400, unit: "px" },
|
|
279
|
+
alignField,
|
|
280
|
+
{
|
|
281
|
+
kind: "select",
|
|
282
|
+
prop: "variant",
|
|
283
|
+
label: "Variant",
|
|
284
|
+
options: [
|
|
285
|
+
{ value: "auto", label: "Automatic" },
|
|
286
|
+
{ value: "light", label: "Standard" },
|
|
287
|
+
{ value: "dark", label: "For dark backgrounds" }
|
|
288
|
+
]
|
|
289
|
+
}
|
|
290
|
+
]
|
|
291
|
+
}
|
|
292
|
+
],
|
|
293
|
+
render: ({ h: h3, css, props, theme, mode, style }) => {
|
|
294
|
+
const onDark = props.variant === "dark" || props.variant === "auto" && !!style.background && isDarkColor(style.background);
|
|
295
|
+
const src = onDark && theme.logoDark || theme.logo;
|
|
296
|
+
const justify = props.align === "center" ? "center" : props.align === "right" ? "flex-end" : "flex-start";
|
|
297
|
+
const content = src && isSafeUrl(src, { allowData: true }) ? h3("img", { src, alt: theme.logoAlt ?? `${theme.name} logo`, style: { height: `${props.height}px`, width: "auto", "max-width": "100%", display: "block" }, draggable: "false" }) : mode === "design" ? h3("div", { class: "bp-logo-placeholder", style: { height: `${props.height}px` } }, theme.name || "Logo") : h3("div", { style: { height: `${props.height}px` } });
|
|
298
|
+
return h3("div", { class: "bp-logo", style: cssText2({ ...css, display: "flex", "justify-content": justify }) }, content);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
var dividerBlock = defineBlock2({
|
|
302
|
+
type: "divider",
|
|
303
|
+
name: "Divider",
|
|
304
|
+
category: "content",
|
|
305
|
+
icon: "minus",
|
|
306
|
+
keywords: ["rule", "line", "separator", "hr"],
|
|
307
|
+
defaultProps: { thickness: 1, color: "$colors.border", lineStyle: "solid", width: 100 },
|
|
308
|
+
split: "none",
|
|
309
|
+
styleGroups: ["spacing", "flow"],
|
|
310
|
+
inspector: [
|
|
311
|
+
{
|
|
312
|
+
title: "Divider",
|
|
313
|
+
fields: [
|
|
314
|
+
{ kind: "number", prop: "thickness", label: "Thickness", min: 1, max: 20, unit: "px" },
|
|
315
|
+
{ kind: "color", prop: "color", label: "Colour" },
|
|
316
|
+
{
|
|
317
|
+
kind: "segmented",
|
|
318
|
+
prop: "lineStyle",
|
|
319
|
+
label: "Style",
|
|
320
|
+
options: [
|
|
321
|
+
{ value: "solid", label: "Solid" },
|
|
322
|
+
{ value: "dashed", label: "Dashed" },
|
|
323
|
+
{ value: "dotted", label: "Dotted" }
|
|
324
|
+
]
|
|
325
|
+
},
|
|
326
|
+
{ kind: "number", prop: "width", label: "Width", min: 5, max: 100, unit: "%" }
|
|
327
|
+
]
|
|
328
|
+
}
|
|
329
|
+
],
|
|
330
|
+
render: ({ h: h3, css, props, theme }) => h3("div", { class: "bp-divider", style: cssText2({ ...css, display: "flex", "justify-content": "center" }) }, h3("div", { style: { width: `${props.width}%`, "border-top": `${props.thickness}px ${props.lineStyle} ${resolveValue(props.color, theme, theme.colors.border)}` } }))
|
|
331
|
+
});
|
|
332
|
+
var spacerBlock = defineBlock2({
|
|
333
|
+
type: "spacer",
|
|
334
|
+
name: "Spacer",
|
|
335
|
+
category: "content",
|
|
336
|
+
icon: "move-vertical",
|
|
337
|
+
keywords: ["gap", "space", "padding"],
|
|
338
|
+
defaultProps: { height: 24 },
|
|
339
|
+
split: "none",
|
|
340
|
+
styleGroups: ["flow"],
|
|
341
|
+
inspector: [{ title: "Spacer", fields: [{ kind: "number", prop: "height", label: "Height", min: 1, max: 600, unit: "px" }] }],
|
|
342
|
+
render: ({ h: h3, props, mode }) => h3("div", { class: ["bp-spacer", mode === "design" && "bp-spacer--design"], style: { height: `${props.height}px` } })
|
|
343
|
+
});
|
|
344
|
+
var buttonBlock = defineBlock2({
|
|
345
|
+
type: "button",
|
|
346
|
+
name: "Button",
|
|
347
|
+
description: "A hyperlink styled as a button",
|
|
348
|
+
category: "content",
|
|
349
|
+
icon: "mouse-pointer-click",
|
|
350
|
+
keywords: ["link", "cta", "url"],
|
|
351
|
+
defaultProps: { label: "View online", url: "https://", variant: "filled", color: "$colors.primary", align: "left", size: "md" },
|
|
352
|
+
split: "none",
|
|
353
|
+
styleGroups: ["typography", "spacing", "flow"],
|
|
354
|
+
inspector: [
|
|
355
|
+
{
|
|
356
|
+
title: "Button",
|
|
357
|
+
fields: [
|
|
358
|
+
{ kind: "text", prop: "label", label: "Label" },
|
|
359
|
+
{ kind: "text", prop: "url", label: "URL", placeholder: "https://\u2026 or {{ token }}" },
|
|
360
|
+
{
|
|
361
|
+
kind: "segmented",
|
|
362
|
+
prop: "variant",
|
|
363
|
+
label: "Style",
|
|
364
|
+
options: [
|
|
365
|
+
{ value: "filled", label: "Filled" },
|
|
366
|
+
{ value: "outline", label: "Outline" },
|
|
367
|
+
{ value: "link", label: "Link" }
|
|
368
|
+
]
|
|
369
|
+
},
|
|
370
|
+
{ kind: "color", prop: "color", label: "Colour" },
|
|
371
|
+
{
|
|
372
|
+
kind: "segmented",
|
|
373
|
+
prop: "size",
|
|
374
|
+
label: "Size",
|
|
375
|
+
options: [
|
|
376
|
+
{ value: "sm", label: "S" },
|
|
377
|
+
{ value: "md", label: "M" },
|
|
378
|
+
{ value: "lg", label: "L" }
|
|
379
|
+
]
|
|
380
|
+
},
|
|
381
|
+
alignField
|
|
382
|
+
]
|
|
383
|
+
}
|
|
384
|
+
],
|
|
385
|
+
validate: ({ props }) => {
|
|
386
|
+
const url = String(props.url ?? "");
|
|
387
|
+
return url && !url.includes("{{") && !isSafeUrl(url) ? [{ severity: "warning", code: "invalid-url", message: `Button URL "${url}" is not a valid http(s) or mailto link`, prop: "url" }] : [];
|
|
388
|
+
},
|
|
389
|
+
render: ({ h: h3, css, props, theme, text, style }) => {
|
|
390
|
+
const color = resolveValue(props.color, theme, theme.colors.primary);
|
|
391
|
+
const pad = props.size === "sm" ? "5px 10px" : props.size === "lg" ? "11px 22px" : "8px 16px";
|
|
392
|
+
const fontSize = props.size === "sm" ? style.fontSize - 1 : props.size === "lg" ? style.fontSize + 2 : style.fontSize;
|
|
393
|
+
const base = { display: "inline-block", padding: props.variant === "link" ? "0" : pad, "border-radius": `${theme.radius ?? 6}px`, "font-weight": "600", "font-size": `${fontSize}px`, "text-decoration": props.variant === "link" ? "underline" : "none", "font-family": style.fontFamily, "line-height": "1.2" };
|
|
394
|
+
if (props.variant === "filled") Object.assign(base, { background: color, color: isDarkColor(color) ? "#FFFFFF" : theme.colors.text });
|
|
395
|
+
else if (props.variant === "outline") Object.assign(base, { border: `1.5px solid ${color}`, color });
|
|
396
|
+
else Object.assign(base, { color });
|
|
397
|
+
const url = typeof props.url === "string" ? props.url : "";
|
|
398
|
+
const justify = props.align === "center" ? "center" : props.align === "right" ? "flex-end" : "flex-start";
|
|
399
|
+
return h3("div", { class: "bp-button", style: cssText2({ ...css, display: "flex", "justify-content": justify }) }, h3("a", { class: "bp-button__link", href: isSafeUrl(url) ? url : void 0, rel: "noopener", style: base }, text("label", "Button")));
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
var calloutBlock = defineBlock2({
|
|
403
|
+
type: "callout",
|
|
404
|
+
name: "Callout",
|
|
405
|
+
description: "Highlighted note or warning",
|
|
406
|
+
category: "content",
|
|
407
|
+
icon: "message-square-warning",
|
|
408
|
+
keywords: ["note", "warning", "alert", "notice", "info"],
|
|
409
|
+
defaultProps: () => ({ title: "Important", content: createRichText("Add the note here."), tone: "info", icon: true }),
|
|
410
|
+
defaultTextStyle: "body",
|
|
411
|
+
defaultFlow: { keepTogether: true },
|
|
412
|
+
split: "none",
|
|
413
|
+
styleGroups: ["typography", "spacing", "flow"],
|
|
414
|
+
inspector: [
|
|
415
|
+
{
|
|
416
|
+
title: "Callout",
|
|
417
|
+
fields: [
|
|
418
|
+
{ kind: "text", prop: "title", label: "Title" },
|
|
419
|
+
{ kind: "richtext", prop: "content", label: "Content" },
|
|
420
|
+
{
|
|
421
|
+
kind: "select",
|
|
422
|
+
prop: "tone",
|
|
423
|
+
label: "Tone",
|
|
424
|
+
options: [
|
|
425
|
+
{ value: "info", label: "Information" },
|
|
426
|
+
{ value: "success", label: "Success" },
|
|
427
|
+
{ value: "warning", label: "Warning" },
|
|
428
|
+
{ value: "danger", label: "Danger" },
|
|
429
|
+
{ value: "neutral", label: "Neutral" }
|
|
430
|
+
]
|
|
431
|
+
},
|
|
432
|
+
{ kind: "toggle", prop: "icon", label: "Show icon" }
|
|
433
|
+
]
|
|
434
|
+
}
|
|
435
|
+
],
|
|
436
|
+
canvas: { inlineEdit: { prop: "content", kind: "richtext" }, label: (p) => p.title || "Callout" },
|
|
437
|
+
render: ({ h: h3, css, props, theme, text, richText, partAttrs }) => {
|
|
438
|
+
const tones = { info: theme.colors.secondary, success: theme.colors.success ?? "#1E8E5A", warning: theme.colors.warning ?? "#C77D0A", danger: theme.colors.danger ?? "#C0392B", neutral: theme.colors.mutedText };
|
|
439
|
+
const accent = tones[props.tone] ?? theme.colors.secondary;
|
|
440
|
+
const glyphs = { info: "i", warning: "!", danger: "!" };
|
|
441
|
+
const icon = glyphs[props.tone] ?? (props.tone === "success" ? checkMark(11, "#fff") : props.tone === "neutral" ? dotMark(11, "#fff") : "i");
|
|
442
|
+
return h3(
|
|
443
|
+
"div",
|
|
444
|
+
{ class: ["bp-callout", `bp-callout--${props.tone}`], style: cssText2({ ...css, display: "flex", gap: "12px", padding: css["padding"] ?? "12px 14px", "border-left": `3px solid ${accent}`, background: css["background"] ?? mixWithWhite(accent), "border-radius": `${theme.radius ?? 6}px` }) },
|
|
445
|
+
props.icon ? h3("div", { class: "bp-callout__icon", style: { color: "#fff", background: accent, "font-weight": "700", "font-size": "11px", width: "18px", height: "18px", "border-radius": "9px", display: "flex", "align-items": "center", "justify-content": "center", "flex-shrink": "0", "margin-top": "1px" } }, icon) : null,
|
|
446
|
+
h3("div", { class: "bp-callout__body", style: { flex: "1", "min-width": "0" } }, props.title ? h3("div", { class: "bp-callout__title", style: { "font-weight": "600", "margin-bottom": "3px" } }, text("title")) : null, h3("div", { class: "bp-callout__content", ...partAttrs("text") }, richText("content")))
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
function mixWithWhite(hex, amount = 0.92) {
|
|
451
|
+
const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
|
|
452
|
+
if (!m) return "transparent";
|
|
453
|
+
const n = parseInt(m[1], 16);
|
|
454
|
+
const c = [n >> 16 & 255, n >> 8 & 255, n & 255].map((v) => Math.round(v + (255 - v) * amount));
|
|
455
|
+
return `#${c.map((v) => v.toString(16).padStart(2, "0")).join("")}`;
|
|
456
|
+
}
|
|
457
|
+
var quoteBlock = defineBlock2({
|
|
458
|
+
type: "quote",
|
|
459
|
+
name: "Quote",
|
|
460
|
+
category: "content",
|
|
461
|
+
icon: "quote",
|
|
462
|
+
keywords: ["blockquote", "testimonial", "citation"],
|
|
463
|
+
defaultProps: () => ({ content: createRichText("A memorable statement."), attribution: "" }),
|
|
464
|
+
defaultTextStyle: "lead",
|
|
465
|
+
defaultStyle: { fontStyle: "italic" },
|
|
466
|
+
defaultFlow: { keepTogether: true },
|
|
467
|
+
split: "none",
|
|
468
|
+
styleGroups: ["textStyle", "typography", "alignment", "spacing", "flow"],
|
|
469
|
+
inspector: [
|
|
470
|
+
{
|
|
471
|
+
title: "Quote",
|
|
472
|
+
fields: [
|
|
473
|
+
{ kind: "richtext", prop: "content", label: "Quote" },
|
|
474
|
+
{ kind: "text", prop: "attribution", label: "Attribution", placeholder: "\u2014 Name, role" }
|
|
475
|
+
]
|
|
476
|
+
}
|
|
477
|
+
],
|
|
478
|
+
canvas: { inlineEdit: { prop: "content", kind: "richtext" } },
|
|
479
|
+
render: ({ h: h3, css, props, theme, richText, text, partAttrs }) => h3(
|
|
480
|
+
"blockquote",
|
|
481
|
+
{ class: "bp-quote", style: cssText2({ ...css, margin: "0", "padding-left": "16px", "border-left": `3px solid ${theme.colors.accent}` }) },
|
|
482
|
+
h3("div", { class: "bp-quote__content", ...partAttrs("text") }, richText("content")),
|
|
483
|
+
props.attribution ? h3("div", { class: "bp-quote__attribution", style: { "font-style": "normal", "font-size": `${Math.max(8, Math.round(Number(css["font-size"]?.replace("px", "")) * 0.8))}px`, color: theme.colors.mutedText, "margin-top": "6px" } }, text("attribution")) : null
|
|
484
|
+
)
|
|
485
|
+
});
|
|
486
|
+
var fieldBlock = defineBlock2({
|
|
487
|
+
type: "field",
|
|
488
|
+
name: "Field",
|
|
489
|
+
description: "A labelled data value",
|
|
490
|
+
category: "data",
|
|
491
|
+
icon: "tag",
|
|
492
|
+
keywords: ["key value", "label", "dynamic text", "binding", "data"],
|
|
493
|
+
defaultProps: { label: "Label", value: "", layout: "stacked", fallback: "\u2014", labelWidth: 40 },
|
|
494
|
+
defaultTextStyle: "body",
|
|
495
|
+
split: "none",
|
|
496
|
+
styleGroups: ["typography", "alignment", "spacing", "flow"],
|
|
497
|
+
inspector: [
|
|
498
|
+
{
|
|
499
|
+
title: "Field",
|
|
500
|
+
fields: [
|
|
501
|
+
{ kind: "text", prop: "label", label: "Label" },
|
|
502
|
+
{ kind: "binding", prop: "value", label: "Value", placeholder: "Pick a field" },
|
|
503
|
+
{ kind: "text", prop: "fallback", label: "Fallback when empty", allowTokens: false },
|
|
504
|
+
{
|
|
505
|
+
kind: "segmented",
|
|
506
|
+
prop: "layout",
|
|
507
|
+
label: "Layout",
|
|
508
|
+
options: [
|
|
509
|
+
{ value: "stacked", label: "Stacked" },
|
|
510
|
+
{ value: "inline", label: "Inline" }
|
|
511
|
+
]
|
|
512
|
+
},
|
|
513
|
+
{ kind: "number", prop: "labelWidth", label: "Label width", min: 10, max: 80, unit: "%", when: (p) => p.layout === "inline" }
|
|
514
|
+
]
|
|
515
|
+
}
|
|
516
|
+
],
|
|
517
|
+
canvas: { label: (p) => p.label || "Field" },
|
|
518
|
+
render: ({ h: h3, css, props, theme, text, mode, node }) => {
|
|
519
|
+
const b = node.bindings.value;
|
|
520
|
+
const raw = props.value;
|
|
521
|
+
const display = raw === void 0 || raw === null || raw === "" ? props.fallback : String(raw);
|
|
522
|
+
const value = mode === "design" && b ? h3("span", { class: "bp-token", "data-expr": b.expr, title: b.expr }, b.label) : display;
|
|
523
|
+
const label = h3("div", { class: "bp-field__label", style: { color: theme.colors.mutedText, "font-size": "0.85em", "text-transform": "uppercase", "letter-spacing": "0.04em", "font-weight": "600" } }, text("label"));
|
|
524
|
+
const val = h3("div", { class: "bp-field__value", style: { "font-weight": "500" } }, value);
|
|
525
|
+
if (props.layout === "inline") return h3("div", { class: "bp-field bp-field--inline", style: cssText2({ ...css, display: "flex", gap: "8px", "align-items": "baseline" }) }, h3("div", { style: { flex: `0 0 ${props.labelWidth}%` } }, label), h3("div", { style: { flex: "1", "min-width": "0" } }, val));
|
|
526
|
+
return h3("div", { class: "bp-field", style: cssText2({ ...css, display: "flex", "flex-direction": "column", gap: "2px" }) }, label, val);
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
var kpiBlock = defineBlock2({
|
|
530
|
+
type: "kpi",
|
|
531
|
+
name: "Statistic",
|
|
532
|
+
description: "Big number with label and trend",
|
|
533
|
+
category: "data",
|
|
534
|
+
icon: "trending-up",
|
|
535
|
+
keywords: ["kpi", "metric", "number", "stat", "big number"],
|
|
536
|
+
defaultProps: { label: "Portfolio value", value: "", caption: "", trend: "", trendFormat: "percent", accent: "$colors.primary", align: "left", size: "md" },
|
|
537
|
+
defaultTextStyle: "body",
|
|
538
|
+
defaultFlow: { keepTogether: true },
|
|
539
|
+
split: "none",
|
|
540
|
+
styleGroups: ["typography", "spacing", "background", "border", "flow"],
|
|
541
|
+
inspector: [
|
|
542
|
+
{
|
|
543
|
+
title: "Statistic",
|
|
544
|
+
fields: [
|
|
545
|
+
{ kind: "text", prop: "label", label: "Label" },
|
|
546
|
+
{ kind: "binding", prop: "value", label: "Value", valueType: ["number", "currency", "percent", "string"] },
|
|
547
|
+
{ kind: "binding", prop: "trend", label: "Trend", valueType: ["number", "percent", "string"], placeholder: "e.g. portfolio.change" },
|
|
548
|
+
{
|
|
549
|
+
kind: "segmented",
|
|
550
|
+
prop: "trendFormat",
|
|
551
|
+
label: "Trend format",
|
|
552
|
+
options: [
|
|
553
|
+
{ value: "percent", label: "%" },
|
|
554
|
+
{ value: "number", label: "#" },
|
|
555
|
+
{ value: "text", label: "Aa" }
|
|
556
|
+
]
|
|
557
|
+
},
|
|
558
|
+
{ kind: "text", prop: "caption", label: "Caption" },
|
|
559
|
+
{ kind: "color", prop: "accent", label: "Accent" },
|
|
560
|
+
alignField,
|
|
561
|
+
{
|
|
562
|
+
kind: "segmented",
|
|
563
|
+
prop: "size",
|
|
564
|
+
label: "Size",
|
|
565
|
+
options: [
|
|
566
|
+
{ value: "sm", label: "S" },
|
|
567
|
+
{ value: "md", label: "M" },
|
|
568
|
+
{ value: "lg", label: "L" }
|
|
569
|
+
]
|
|
570
|
+
}
|
|
571
|
+
]
|
|
572
|
+
}
|
|
573
|
+
],
|
|
574
|
+
canvas: { label: (p) => p.label || "Statistic" },
|
|
575
|
+
render: ({ h: h3, css, props, theme, text, mode, node, format }) => {
|
|
576
|
+
const accent = resolveValue(props.accent, theme, theme.colors.primary);
|
|
577
|
+
const size = props.size === "sm" ? 20 : props.size === "lg" ? 36 : 28;
|
|
578
|
+
const b = node.bindings.value;
|
|
579
|
+
const valueNode = h3("span", { class: mode === "design" && b ? "bp-cell--bound" : void 0, title: b?.expr }, props.value === void 0 || props.value === null || props.value === "" ? "\u2014" : String(props.value));
|
|
580
|
+
let trendNode = null;
|
|
581
|
+
const tRaw = props.trend;
|
|
582
|
+
if (tRaw !== void 0 && tRaw !== null && tRaw !== "") {
|
|
583
|
+
const n = Number(tRaw);
|
|
584
|
+
const positive = Number.isFinite(n) ? n >= 0 : true;
|
|
585
|
+
const label = props.trendFormat === "percent" && Number.isFinite(n) ? `${n > 0 ? "+" : ""}${new Intl.NumberFormat(format.locale, { style: "percent", maximumFractionDigits: 1 }).format(n)}` : props.trendFormat === "number" && Number.isFinite(n) ? `${n > 0 ? "+" : ""}${new Intl.NumberFormat(format.locale, { maximumFractionDigits: 2 }).format(n)}` : String(tRaw);
|
|
586
|
+
const tb = node.bindings.trend;
|
|
587
|
+
const trendColor = positive ? theme.colors.success ?? "#1E8E5A" : theme.colors.danger ?? "#C0392B";
|
|
588
|
+
trendNode = h3(
|
|
589
|
+
"span",
|
|
590
|
+
{ class: ["bp-kpi__trend", positive ? "bp-kpi__trend--up" : "bp-kpi__trend--down", mode === "design" && tb ? "bp-cell--bound" : ""], title: tb?.expr, style: { color: trendColor, "font-weight": "600", "font-size": `${Math.round(size * 0.9)}px`, display: "inline-flex", "align-items": "center", gap: "4px", height: `${Math.round(size * 1.05)}px` } },
|
|
591
|
+
triangleMark(positive ? "up" : "down", Math.max(9, Math.round(size * 0.55)), trendColor),
|
|
592
|
+
h3("span", null, label)
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
return h3(
|
|
596
|
+
"div",
|
|
597
|
+
{ class: "bp-kpi", style: cssText2({ ...css, "text-align": props.align, "border-top": css["border"] ? void 0 : `3px solid ${accent}`, "padding-top": css["padding"] ? void 0 : "8px" }) },
|
|
598
|
+
h3("div", { class: "bp-kpi__label", style: { color: theme.colors.mutedText, "font-size": "0.9em", "text-transform": "uppercase", "letter-spacing": "0.05em", "font-weight": "600", display: "flex", gap: "6px", "align-items": "center", "justify-content": props.align === "center" ? "center" : props.align === "right" ? "flex-end" : "flex-start", "flex-wrap": "wrap" } }, text("label"), mode === "design" && b ? h3("span", { class: "bp-token", "data-expr": b.expr, style: { "text-transform": "none", "letter-spacing": "0", "font-weight": "500" } }, b.label) : null),
|
|
599
|
+
h3("div", { class: "bp-kpi__value", style: { "font-size": `${size}px`, "font-weight": "700", "line-height": "1.15", color: theme.colors.text, "font-family": theme.typography.headingFont, "letter-spacing": "-0.02em", "margin-top": "4px", display: "flex", "align-items": "baseline", gap: "8px", "justify-content": props.align === "center" ? "center" : props.align === "right" ? "flex-end" : "flex-start", "flex-wrap": "wrap" } }, valueNode, trendNode),
|
|
600
|
+
props.caption ? h3("div", { class: "bp-kpi__caption", style: { color: theme.colors.mutedText, "font-size": "0.9em", "margin-top": "3px" } }, text("caption")) : null
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
var signatureBlock = defineBlock2({
|
|
605
|
+
type: "signature",
|
|
606
|
+
name: "Signature",
|
|
607
|
+
description: "Signature line with name and date",
|
|
608
|
+
category: "content",
|
|
609
|
+
icon: "pen-line",
|
|
610
|
+
keywords: ["sign", "approval"],
|
|
611
|
+
defaultProps: { label: "Signed", name: "", role: "", showDate: true, lineWidth: 240 },
|
|
612
|
+
defaultTextStyle: "body",
|
|
613
|
+
defaultFlow: { keepTogether: true },
|
|
614
|
+
split: "none",
|
|
615
|
+
styleGroups: ["typography", "spacing", "flow"],
|
|
616
|
+
inspector: [
|
|
617
|
+
{
|
|
618
|
+
title: "Signature",
|
|
619
|
+
fields: [
|
|
620
|
+
{ kind: "text", prop: "label", label: "Label" },
|
|
621
|
+
{ kind: "text", prop: "name", label: "Name", placeholder: "{{ adviser.name }}" },
|
|
622
|
+
{ kind: "text", prop: "role", label: "Role" },
|
|
623
|
+
{ kind: "toggle", prop: "showDate", label: "Date line" },
|
|
624
|
+
{ kind: "number", prop: "lineWidth", label: "Line width", min: 80, max: 600, unit: "px" }
|
|
625
|
+
]
|
|
626
|
+
}
|
|
627
|
+
],
|
|
628
|
+
render: ({ h: h3, css, props, theme, text }) => {
|
|
629
|
+
const line = (label, content) => h3("div", { style: { width: `${props.lineWidth}px`, "max-width": "100%" } }, h3("div", { style: { height: "36px", "border-bottom": `1px solid ${theme.colors.text}` } }), h3("div", { style: { "font-size": "0.85em", color: theme.colors.mutedText, "margin-top": "4px" } }, label), content.length ? h3("div", { style: { "font-weight": "600", "margin-top": "2px" } }, content) : null);
|
|
630
|
+
return h3("div", { class: "bp-signature", style: cssText2({ ...css, display: "flex", gap: "32px", "flex-wrap": "wrap" }) }, line(props.label || "Signed", [...text("name"), ...props.role ? [h3("div", { style: { "font-weight": "400", color: theme.colors.mutedText } }, text("role"))] : []]), props.showDate ? line("Date", []) : null);
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
var dateBlock = defineBlock2({
|
|
634
|
+
type: "date",
|
|
635
|
+
name: "Date",
|
|
636
|
+
description: "Today's date or a bound date field",
|
|
637
|
+
category: "content",
|
|
638
|
+
icon: "calendar",
|
|
639
|
+
keywords: ["today", "generated", "date"],
|
|
640
|
+
defaultProps: { source: "now", value: "", pattern: "d MMMM yyyy", prefix: "" },
|
|
641
|
+
defaultTextStyle: "body",
|
|
642
|
+
split: "none",
|
|
643
|
+
styleGroups: ["textStyle", "typography", "alignment", "spacing", "flow"],
|
|
644
|
+
inspector: [
|
|
645
|
+
{
|
|
646
|
+
title: "Date",
|
|
647
|
+
fields: [
|
|
648
|
+
{
|
|
649
|
+
kind: "segmented",
|
|
650
|
+
prop: "source",
|
|
651
|
+
label: "Source",
|
|
652
|
+
options: [
|
|
653
|
+
{ value: "now", label: "Today" },
|
|
654
|
+
{ value: "binding", label: "Field" }
|
|
655
|
+
]
|
|
656
|
+
},
|
|
657
|
+
{ kind: "binding", prop: "value", label: "Date field", valueType: ["date", "datetime"], when: (p) => p.source === "binding" },
|
|
658
|
+
{ kind: "text", prop: "pattern", label: "Format", placeholder: "d MMMM yyyy", allowTokens: false },
|
|
659
|
+
{ kind: "text", prop: "prefix", label: "Prefix", placeholder: "Prepared on " }
|
|
660
|
+
]
|
|
661
|
+
}
|
|
662
|
+
],
|
|
663
|
+
render: ({ h: h3, css, props, format, text, mode, node }) => {
|
|
664
|
+
const d = props.source === "now" ? format.now ?? /* @__PURE__ */ new Date() : props.value;
|
|
665
|
+
const b = node.bindings.value;
|
|
666
|
+
const body = mode === "design" && props.source === "binding" && b ? h3("span", { class: "bp-token", "data-expr": b.expr }, b.label) : formatDate(d, props.pattern || void 0, format);
|
|
667
|
+
return h3("div", { class: "bp-date", style: cssText2(css) }, text("prefix"), body);
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
var pageNumberBlock = defineBlock2({
|
|
671
|
+
type: "pageNumber",
|
|
672
|
+
name: "Page number",
|
|
673
|
+
category: "content",
|
|
674
|
+
icon: "hash",
|
|
675
|
+
keywords: ["page", "of", "footer", "numbering"],
|
|
676
|
+
defaultProps: { format: "Page {{page.number}} of {{page.total}}", align: "right" },
|
|
677
|
+
defaultTextStyle: "caption",
|
|
678
|
+
split: "none",
|
|
679
|
+
styleGroups: ["textStyle", "typography", "spacing"],
|
|
680
|
+
inspector: [
|
|
681
|
+
{
|
|
682
|
+
title: "Page number",
|
|
683
|
+
fields: [
|
|
684
|
+
{ kind: "text", prop: "format", label: "Format", placeholder: "Page {{page.number}} of {{page.total}}" },
|
|
685
|
+
alignField
|
|
686
|
+
]
|
|
687
|
+
}
|
|
688
|
+
],
|
|
689
|
+
render: ({ h: h3, css, props, text }) => h3("div", { class: "bp-page-number", style: cssText2({ ...css, "text-align": props.align }) }, text("format"))
|
|
690
|
+
});
|
|
691
|
+
var timestampBlock = defineBlock2({
|
|
692
|
+
type: "timestamp",
|
|
693
|
+
name: "Generated at",
|
|
694
|
+
description: "Date and time the document was generated",
|
|
695
|
+
category: "content",
|
|
696
|
+
icon: "clock",
|
|
697
|
+
keywords: ["generated", "printed", "timestamp", "time"],
|
|
698
|
+
defaultProps: { prefix: "Generated ", pattern: "d MMM yyyy, HH:mm", align: "left" },
|
|
699
|
+
defaultTextStyle: "caption",
|
|
700
|
+
split: "none",
|
|
701
|
+
styleGroups: ["textStyle", "typography", "spacing"],
|
|
702
|
+
inspector: [
|
|
703
|
+
{
|
|
704
|
+
title: "Generated at",
|
|
705
|
+
fields: [
|
|
706
|
+
{ kind: "text", prop: "prefix", label: "Prefix" },
|
|
707
|
+
{ kind: "text", prop: "pattern", label: "Format", allowTokens: false },
|
|
708
|
+
alignField
|
|
709
|
+
]
|
|
710
|
+
}
|
|
711
|
+
],
|
|
712
|
+
render: ({ h: h3, css, props, format, text }) => h3("div", { class: "bp-timestamp", style: cssText2({ ...css, "text-align": props.align }) }, text("prefix"), formatDate(format.now ?? /* @__PURE__ */ new Date(), props.pattern || void 0, format, true))
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
// src/blocks/layout.ts
|
|
716
|
+
import { defineBlock as defineBlock3, cssText as cssText3, resolveValue as resolveValue2 } from "@broadpaper/core";
|
|
717
|
+
var containerBlock = defineBlock3({
|
|
718
|
+
type: "container",
|
|
719
|
+
name: "Container",
|
|
720
|
+
description: "Groups blocks with shared padding and background",
|
|
721
|
+
category: "layout",
|
|
722
|
+
icon: "square-dashed",
|
|
723
|
+
keywords: ["group", "box", "wrapper"],
|
|
724
|
+
defaultProps: { padding: 0, gap: null },
|
|
725
|
+
container: { accepts: ["@blocks", "@rows"] },
|
|
726
|
+
split: "children",
|
|
727
|
+
styleGroups: ["spacing", "background", "border", "flow"],
|
|
728
|
+
inspector: [
|
|
729
|
+
{
|
|
730
|
+
title: "Container",
|
|
731
|
+
fields: [
|
|
732
|
+
{ kind: "number", prop: "padding", label: "Padding", min: 0, max: 120, unit: "px" },
|
|
733
|
+
{ kind: "number", prop: "gap", label: "Block gap", min: 0, max: 120, unit: "px", description: "Overrides the document block gap" }
|
|
734
|
+
]
|
|
735
|
+
}
|
|
736
|
+
],
|
|
737
|
+
render: ({ h: h3, css, children, styles, props, node, mode }) => h3(
|
|
738
|
+
"div",
|
|
739
|
+
{ class: "bp-container", style: cssText3({ ...css, padding: props.padding ? `${props.padding}px` : css["padding"] }) },
|
|
740
|
+
h3("div", { class: "bp-stack", style: { gap: `${props.gap ?? styles.spacing.blockGap}px` }, "data-dropzone": node.nodeId }, children.length === 0 && mode === "design" ? h3("div", { class: "bp-empty-column" }, "Drop blocks here") : children)
|
|
741
|
+
)
|
|
742
|
+
});
|
|
743
|
+
var cardBlock = defineBlock3({
|
|
744
|
+
type: "card",
|
|
745
|
+
name: "Card",
|
|
746
|
+
description: "Panel with border, background and optional title",
|
|
747
|
+
category: "layout",
|
|
748
|
+
icon: "panel-top",
|
|
749
|
+
keywords: ["panel", "box", "surface"],
|
|
750
|
+
defaultProps: { title: "", padding: null, accent: false, accentColor: "$colors.primary" },
|
|
751
|
+
container: { accepts: ["@blocks", "@rows"] },
|
|
752
|
+
defaultFlow: { keepTogether: true },
|
|
753
|
+
split: "children",
|
|
754
|
+
styleGroups: ["spacing", "background", "border", "flow"],
|
|
755
|
+
inspector: [
|
|
756
|
+
{
|
|
757
|
+
title: "Card",
|
|
758
|
+
fields: [
|
|
759
|
+
{ kind: "text", prop: "title", label: "Title" },
|
|
760
|
+
{ kind: "number", prop: "padding", label: "Padding", min: 0, max: 120, unit: "px" },
|
|
761
|
+
{ kind: "toggle", prop: "accent", label: "Accent bar" },
|
|
762
|
+
{ kind: "color", prop: "accentColor", label: "Accent colour", when: (p) => !!p.accent }
|
|
763
|
+
]
|
|
764
|
+
}
|
|
765
|
+
],
|
|
766
|
+
canvas: { label: (p) => p.title || "Card" },
|
|
767
|
+
render: ({ h: h3, css, children, styles, props, theme, text, node, mode }) => {
|
|
768
|
+
const card = styles.card;
|
|
769
|
+
const pad = props.padding ?? card.padding ?? 16;
|
|
770
|
+
const style = {
|
|
771
|
+
...css,
|
|
772
|
+
background: css["background"] ?? resolveValue2(card.background, theme, theme.colors.surface),
|
|
773
|
+
border: css["border"] ?? `${card.borderWidth ?? 1}px solid ${resolveValue2(card.borderColor, theme, theme.colors.border)}`,
|
|
774
|
+
"border-radius": css["border-radius"] ?? `${card.borderRadius ?? theme.radius ?? 6}px`,
|
|
775
|
+
padding: `${pad}px`,
|
|
776
|
+
"box-shadow": card.shadow ? "0 1px 2px rgba(16,24,40,0.06), 0 1px 3px rgba(16,24,40,0.1)" : void 0,
|
|
777
|
+
"border-top": props.accent ? `3px solid ${resolveValue2(props.accentColor, theme, theme.colors.primary)}` : void 0
|
|
778
|
+
};
|
|
779
|
+
return h3(
|
|
780
|
+
"div",
|
|
781
|
+
{ class: "bp-card", style: cssText3(style) },
|
|
782
|
+
props.title ? h3("div", { class: "bp-card__title", style: { "font-family": theme.typography.headingFont, "font-weight": "600", "font-size": "1.1em", "margin-bottom": "8px", color: theme.colors.primary } }, text("title")) : null,
|
|
783
|
+
h3("div", { class: "bp-stack", style: { gap: `${styles.spacing.blockGap}px` }, "data-dropzone": node.nodeId }, children.length === 0 && mode === "design" ? h3("div", { class: "bp-empty-column" }, "Drop blocks here") : children)
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
var pageBreakBlock = defineBlock3({
|
|
788
|
+
type: "pageBreak",
|
|
789
|
+
name: "Page break",
|
|
790
|
+
description: "Start the following content on a new page",
|
|
791
|
+
category: "layout",
|
|
792
|
+
icon: "scissors",
|
|
793
|
+
keywords: ["break", "new page"],
|
|
794
|
+
defaultProps: {},
|
|
795
|
+
defaultFlow: { breakAfter: "page" },
|
|
796
|
+
split: "none",
|
|
797
|
+
styleGroups: [],
|
|
798
|
+
render: ({ h: h3, mode }) => mode === "design" ? h3("div", { class: "bp-page-break" }, h3("span", { class: "bp-page-break__label" }, "Page break")) : h3("div", { class: "bp-page-break bp-page-break--print", style: { height: "0" } })
|
|
799
|
+
});
|
|
800
|
+
var keepTogetherBlock = defineBlock3({
|
|
801
|
+
type: "keepTogether",
|
|
802
|
+
name: "Keep together",
|
|
803
|
+
description: "Blocks inside never split across pages",
|
|
804
|
+
category: "layout",
|
|
805
|
+
icon: "link",
|
|
806
|
+
keywords: ["group", "no split", "together"],
|
|
807
|
+
defaultProps: { label: "" },
|
|
808
|
+
container: { accepts: ["@blocks", "@rows"] },
|
|
809
|
+
defaultFlow: { keepTogether: true },
|
|
810
|
+
split: "children",
|
|
811
|
+
styleGroups: ["spacing", "flow"],
|
|
812
|
+
render: ({ h: h3, css, children, styles, node, mode }) => h3("div", { class: "bp-keep-together", style: cssText3(css) }, h3("div", { class: "bp-stack", style: { gap: `${styles.spacing.blockGap}px` }, "data-dropzone": node.nodeId }, children.length === 0 && mode === "design" ? h3("div", { class: "bp-empty-column" }, "Drop blocks here") : children))
|
|
813
|
+
});
|
|
814
|
+
var conditionalBlock = defineBlock3({
|
|
815
|
+
type: "conditional",
|
|
816
|
+
name: "Conditional",
|
|
817
|
+
description: "Show the blocks inside only when a rule is true",
|
|
818
|
+
category: "data",
|
|
819
|
+
icon: "git-branch",
|
|
820
|
+
keywords: ["if", "condition", "rule", "visibility", "show when"],
|
|
821
|
+
defaultProps: () => ({ condition: { op: "and", rules: [] }, label: "" }),
|
|
822
|
+
container: { accepts: ["@blocks", "@rows"], conditional: { conditionProp: "condition" } },
|
|
823
|
+
split: "children",
|
|
824
|
+
styleGroups: ["spacing", "flow"],
|
|
825
|
+
bindings: [{ prop: "condition", kind: "condition" }],
|
|
826
|
+
inspector: [{ title: "Show when", fields: [{ kind: "condition", prop: "condition", label: "Rule", fullWidth: true }] }],
|
|
827
|
+
canvas: { label: (p) => p.label || "Conditional" },
|
|
828
|
+
render: ({ h: h3, css, children, styles, props, node, mode }) => {
|
|
829
|
+
const result = props.__conditionResult !== false;
|
|
830
|
+
if (mode === "preview" && !result) return null;
|
|
831
|
+
return h3(
|
|
832
|
+
"div",
|
|
833
|
+
{ class: ["bp-conditional", !result && "bp-conditional--false"], style: cssText3(css) },
|
|
834
|
+
mode === "design" ? h3("div", { class: "bp-conditional__badge", contenteditable: "false" }, result ? "Shown when rule is true" : "Hidden with sample data") : null,
|
|
835
|
+
h3("div", { class: "bp-stack", style: { gap: `${styles.spacing.blockGap}px` }, "data-dropzone": node.nodeId }, children.length === 0 && mode === "design" ? h3("div", { class: "bp-empty-column" }, "Drop conditional blocks here") : children)
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
});
|
|
839
|
+
var repeaterBlock = defineBlock3({
|
|
840
|
+
type: "repeater",
|
|
841
|
+
name: "Repeater",
|
|
842
|
+
description: "Repeats the blocks inside for every item in a list",
|
|
843
|
+
category: "data",
|
|
844
|
+
icon: "repeat",
|
|
845
|
+
keywords: ["loop", "for each", "list", "items"],
|
|
846
|
+
defaultProps: { source: "", alias: "item", keepItemTogether: true, gap: null, limit: null, emptyMessage: "", separator: false },
|
|
847
|
+
container: { accepts: ["@blocks", "@rows"], repeat: { collectionProp: "source", aliasProp: "alias", keepItemTogetherProp: "keepItemTogether", limitProp: "limit" } },
|
|
848
|
+
split: "children",
|
|
849
|
+
styleGroups: ["spacing", "flow"],
|
|
850
|
+
bindings: [{ prop: "source", kind: "collection", alias: "alias" }, { prop: "emptyMessage", kind: "template" }],
|
|
851
|
+
inspector: [
|
|
852
|
+
{
|
|
853
|
+
title: "Repeat",
|
|
854
|
+
fields: [
|
|
855
|
+
{ kind: "collection", prop: "source", label: "For each item in", placeholder: "Pick a list" },
|
|
856
|
+
{ kind: "text", prop: "alias", label: "Item name", description: (p) => `The name each item goes by inside the repeater, e.g. {{ ${typeof p.alias === "string" && p.alias.trim() ? p.alias.trim() : "item"}.name }}`, allowTokens: false },
|
|
857
|
+
{ kind: "toggle", prop: "keepItemTogether", label: "Keep each item on one page" },
|
|
858
|
+
{ kind: "toggle", prop: "separator", label: "Divider between items" },
|
|
859
|
+
{ kind: "number", prop: "gap", label: "Gap between items", min: 0, max: 120, unit: "px" },
|
|
860
|
+
{ kind: "number", prop: "limit", label: "Maximum items", min: 1, max: 1e4 },
|
|
861
|
+
{ kind: "text", prop: "emptyMessage", label: "Message when empty", description: "Printed instead of the items when the list is empty.", fullWidth: true }
|
|
862
|
+
]
|
|
863
|
+
}
|
|
864
|
+
],
|
|
865
|
+
canvas: { label: (p) => p.source ? `Repeat ${p.source}` : "Repeater" },
|
|
866
|
+
render: ({ h: h3, css, children, styles, props, node, mode, theme, text }) => {
|
|
867
|
+
const count = Number(props.__itemCount ?? 0);
|
|
868
|
+
const b = node.bindings.source;
|
|
869
|
+
if (children.length === 0 && mode === "preview") {
|
|
870
|
+
return props.emptyMessage ? h3("div", { class: "bp-repeater__empty", style: cssText3({ ...css, color: theme.colors.mutedText }) }, text("emptyMessage")) : null;
|
|
871
|
+
}
|
|
872
|
+
const gap = props.gap ?? styles.spacing.blockGap;
|
|
873
|
+
const items = props.separator ? children.flatMap((c, i) => i === 0 ? [c] : [h3("div", { class: "bp-repeater__separator", style: { "border-top": `1px solid ${theme.colors.border}` } }), c]) : children;
|
|
874
|
+
return h3(
|
|
875
|
+
"div",
|
|
876
|
+
{ class: "bp-repeater", style: cssText3(css) },
|
|
877
|
+
// Design mode draws one iteration and the badge has to admit it, or a
|
|
878
|
+
// three-item repeater and a thirty-item one look identical on the canvas.
|
|
879
|
+
mode === "design" ? h3("div", { class: "bp-repeater__badge", contenteditable: "false" }, b ? `${b.label} \xB7 ${children.length >= count ? `repeats ${count} time${count === 1 ? "" : "s"}` : `showing ${children.length} of ${count} sample items`}` : "Choose a list to repeat over") : null,
|
|
880
|
+
h3("div", { class: "bp-stack bp-repeater__items", style: { gap: `${gap}px` }, "data-dropzone": node.nodeId }, items.length === 0 && mode === "design" ? h3("div", { class: "bp-empty-column" }, "Drop blocks to repeat here") : items)
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
// src/blocks/table.ts
|
|
886
|
+
import { defineBlock as defineBlock4, cssText as cssText4, resolveValue as resolveValue3, toNumber, toDisplayString, compareValues, getField, isDarkColor as isDarkColor2 } from "@broadpaper/core";
|
|
887
|
+
function defaultColumns() {
|
|
888
|
+
return [
|
|
889
|
+
{ id: "c1", header: "Name", value: "row.name", width: null },
|
|
890
|
+
{ id: "c2", header: "Value", value: "row.value | number", align: "right", width: 30, total: "sum" }
|
|
891
|
+
];
|
|
892
|
+
}
|
|
893
|
+
function aliasOf(props) {
|
|
894
|
+
const alias = props.alias;
|
|
895
|
+
return typeof alias === "string" && alias.trim() ? alias.trim() : "row";
|
|
896
|
+
}
|
|
897
|
+
function columnName(id, props) {
|
|
898
|
+
if (!id || id === "*") return "Every column";
|
|
899
|
+
const column = (props.columns ?? []).find((c) => c.id === id);
|
|
900
|
+
return column?.header || column?.value || id;
|
|
901
|
+
}
|
|
902
|
+
var tableBlock = defineBlock4({
|
|
903
|
+
type: "table",
|
|
904
|
+
name: "Table",
|
|
905
|
+
description: "Data table bound to a list, with totals, zebra rows and page-spanning headers",
|
|
906
|
+
category: "data",
|
|
907
|
+
icon: "table",
|
|
908
|
+
keywords: ["grid", "rows", "columns", "data", "list"],
|
|
909
|
+
defaultProps: () => ({
|
|
910
|
+
source: "",
|
|
911
|
+
alias: "row",
|
|
912
|
+
columns: defaultColumns(),
|
|
913
|
+
showHeader: true,
|
|
914
|
+
repeatHeader: true,
|
|
915
|
+
zebra: true,
|
|
916
|
+
borders: "inherit",
|
|
917
|
+
compact: false,
|
|
918
|
+
fontSize: null,
|
|
919
|
+
headerBackground: "",
|
|
920
|
+
headerColor: "",
|
|
921
|
+
sortBy: "",
|
|
922
|
+
sortDir: "asc",
|
|
923
|
+
emptyMessage: "No data",
|
|
924
|
+
totalsLabel: "Total",
|
|
925
|
+
conditionalFormats: [],
|
|
926
|
+
designRows: 3,
|
|
927
|
+
rowLimit: null
|
|
928
|
+
}),
|
|
929
|
+
defaultTextStyle: "body",
|
|
930
|
+
defaultFlow: { minOrphans: 2, minWidows: 2 },
|
|
931
|
+
split: "rows",
|
|
932
|
+
measure: "table",
|
|
933
|
+
styleGroups: ["typography", "spacing", "flow"],
|
|
934
|
+
bindings: [
|
|
935
|
+
{ prop: "source", kind: "collection", alias: "alias" },
|
|
936
|
+
{ prop: "columns", kind: "list", items: [{ prop: "value", kind: "value", perItem: true }, { prop: "header", kind: "template" }, { prop: "totalExpr", kind: "value" }] },
|
|
937
|
+
{ prop: "conditionalFormats", kind: "list", items: [{ prop: "condition", kind: "value", perItem: true }] },
|
|
938
|
+
{ prop: "emptyMessage", kind: "template" },
|
|
939
|
+
{ prop: "totalsLabel", kind: "template" }
|
|
940
|
+
],
|
|
941
|
+
inspector: [
|
|
942
|
+
{
|
|
943
|
+
title: "Data",
|
|
944
|
+
fields: [
|
|
945
|
+
{ kind: "collection", prop: "source", label: "Rows from", placeholder: "Pick a list" },
|
|
946
|
+
{ kind: "text", prop: "alias", label: "Row name", description: (p) => `The name each row goes by in column values, e.g. ${aliasOf(p)}.name`, allowTokens: false },
|
|
947
|
+
{ kind: "text", prop: "sortBy", label: "Sort by field", placeholder: "e.g. value", allowTokens: false },
|
|
948
|
+
{
|
|
949
|
+
kind: "segmented",
|
|
950
|
+
prop: "sortDir",
|
|
951
|
+
label: "Direction",
|
|
952
|
+
options: [
|
|
953
|
+
{ value: "asc", label: "Ascending" },
|
|
954
|
+
{ value: "desc", label: "Descending" }
|
|
955
|
+
],
|
|
956
|
+
when: (p) => !!p.sortBy
|
|
957
|
+
},
|
|
958
|
+
{ kind: "number", prop: "rowLimit", label: "Maximum rows", min: 1, max: 1e5 },
|
|
959
|
+
{ kind: "text", prop: "emptyMessage", label: "Message when empty", description: "Printed instead of the table when the list has no rows.", fullWidth: true }
|
|
960
|
+
]
|
|
961
|
+
},
|
|
962
|
+
{ title: "Columns", fields: [{ kind: "columns", prop: "columns", label: "Columns", fullWidth: true }] },
|
|
963
|
+
{
|
|
964
|
+
title: "Appearance",
|
|
965
|
+
collapsed: true,
|
|
966
|
+
fields: [
|
|
967
|
+
{ kind: "toggle", prop: "showHeader", label: "Header row" },
|
|
968
|
+
{ kind: "toggle", prop: "repeatHeader", label: "Repeat header on each page", when: (p) => p.showHeader !== false },
|
|
969
|
+
{ kind: "toggle", prop: "zebra", label: "Zebra rows" },
|
|
970
|
+
{ kind: "toggle", prop: "compact", label: "Compact rows" },
|
|
971
|
+
{
|
|
972
|
+
kind: "select",
|
|
973
|
+
prop: "borders",
|
|
974
|
+
label: "Borders",
|
|
975
|
+
options: [
|
|
976
|
+
{ value: "inherit", label: "Document default" },
|
|
977
|
+
{ value: "horizontal", label: "Horizontal lines" },
|
|
978
|
+
{ value: "all", label: "Full grid" },
|
|
979
|
+
{ value: "outer", label: "Outer only" },
|
|
980
|
+
{ value: "none", label: "None" }
|
|
981
|
+
]
|
|
982
|
+
},
|
|
983
|
+
{ kind: "color", prop: "headerBackground", label: "Header background", allowTransparent: true },
|
|
984
|
+
{ kind: "color", prop: "headerColor", label: "Header text" },
|
|
985
|
+
{ kind: "number", prop: "fontSize", label: "Font size", min: 6, max: 24, unit: "px" },
|
|
986
|
+
{ kind: "text", prop: "totalsLabel", label: "Totals label" }
|
|
987
|
+
]
|
|
988
|
+
},
|
|
989
|
+
{
|
|
990
|
+
title: "Conditional formatting",
|
|
991
|
+
collapsed: true,
|
|
992
|
+
fields: [
|
|
993
|
+
{
|
|
994
|
+
kind: "list",
|
|
995
|
+
prop: "conditionalFormats",
|
|
996
|
+
label: "Rules",
|
|
997
|
+
itemLabel: "Rule",
|
|
998
|
+
fullWidth: true,
|
|
999
|
+
// A rule reads as "Drift · when abs(a.drift) > 0.03" rather than "c5",
|
|
1000
|
+
// which is an internal id the designer never chose and cannot look up.
|
|
1001
|
+
itemSummary: (rule, i, props) => {
|
|
1002
|
+
const column = columnName(String(rule.columnId ?? ""), props);
|
|
1003
|
+
const when = String(rule.condition ?? "").trim();
|
|
1004
|
+
return when ? `${column} \xB7 when ${when}` : `${column} \xB7 Rule ${i + 1}`;
|
|
1005
|
+
},
|
|
1006
|
+
fields: [
|
|
1007
|
+
{
|
|
1008
|
+
kind: "select",
|
|
1009
|
+
prop: "columnId",
|
|
1010
|
+
label: "Column",
|
|
1011
|
+
optionsFrom: (props) => [{ value: "*", label: "Every column" }, ...(props.columns ?? []).map((c) => ({ value: c.id, label: c.header || c.id }))]
|
|
1012
|
+
},
|
|
1013
|
+
{ kind: "expression", prop: "condition", label: "When", placeholder: "row.value < 0", description: (p) => `Checked once per row, e.g. ${aliasOf(p)}.value < 0` },
|
|
1014
|
+
{ kind: "color", prop: "color", label: "Text colour" },
|
|
1015
|
+
{ kind: "color", prop: "background", label: "Background", allowTransparent: true },
|
|
1016
|
+
{ kind: "toggle", prop: "bold", label: "Bold" }
|
|
1017
|
+
]
|
|
1018
|
+
}
|
|
1019
|
+
]
|
|
1020
|
+
}
|
|
1021
|
+
],
|
|
1022
|
+
validate: ({ props, checkExpression }) => {
|
|
1023
|
+
const issues = [];
|
|
1024
|
+
if (!Array.isArray(props.columns) || props.columns.length === 0) issues.push({ severity: "warning", code: "empty-content", message: "Table has no columns", prop: "columns" });
|
|
1025
|
+
for (const c of props.columns ?? []) if (!c.value?.trim()) issues.push({ severity: "warning", code: "missing-required", message: `Column "${c.header || c.id}" has no value`, prop: `columns.${c.id}.value` });
|
|
1026
|
+
for (const c of props.columns ?? []) if (c.totalExpr?.trim()) issues.push(...checkExpression(c.totalExpr, `columns.${c.id}.totalExpr`));
|
|
1027
|
+
return issues;
|
|
1028
|
+
},
|
|
1029
|
+
canvas: { label: (p) => p.source ? `Table \xB7 ${p.source}` : "Table" },
|
|
1030
|
+
resolve: ({ props, mode, evaluateIn, evaluate: evaluate2, warn, theme }) => {
|
|
1031
|
+
const alias = props.alias || "row";
|
|
1032
|
+
let items = Array.isArray(props.source) ? props.source : [];
|
|
1033
|
+
if (props.sortBy) {
|
|
1034
|
+
const dir = props.sortDir === "desc" ? -1 : 1;
|
|
1035
|
+
items = [...items].sort((a, b) => compareValues(getField(a, props.sortBy), getField(b, props.sortBy)) * dir);
|
|
1036
|
+
}
|
|
1037
|
+
const total = items.length;
|
|
1038
|
+
if (props.rowLimit) items = items.slice(0, props.rowLimit);
|
|
1039
|
+
const allItems = items;
|
|
1040
|
+
if (mode === "design") items = items.slice(0, Math.max(1, props.designRows || 3));
|
|
1041
|
+
const columns = Array.isArray(props.columns) ? props.columns : [];
|
|
1042
|
+
const formats = Array.isArray(props.conditionalFormats) ? props.conditionalFormats : [];
|
|
1043
|
+
const rows = items.map((item, index) => {
|
|
1044
|
+
const vars = { [alias]: item, item, row: item, index: index + 1, first: index === 0, last: index === items.length - 1, count: items.length };
|
|
1045
|
+
const rowStyle = {};
|
|
1046
|
+
const cellStyles = {};
|
|
1047
|
+
for (const f of formats) {
|
|
1048
|
+
if (!f.condition?.trim()) continue;
|
|
1049
|
+
let ok = false;
|
|
1050
|
+
try {
|
|
1051
|
+
ok = !!evaluateIn(f.condition, vars);
|
|
1052
|
+
} catch {
|
|
1053
|
+
ok = false;
|
|
1054
|
+
}
|
|
1055
|
+
if (!ok) continue;
|
|
1056
|
+
const st = {};
|
|
1057
|
+
if (f.color) st["color"] = resolveValue3(f.color, theme, theme.colors.text);
|
|
1058
|
+
if (f.background) st["background"] = resolveValue3(f.background, theme, "transparent");
|
|
1059
|
+
if (f.bold) st["font-weight"] = "700";
|
|
1060
|
+
if (f.columnId === "*" || !f.columnId) Object.assign(rowStyle, st);
|
|
1061
|
+
else cellStyles[f.columnId] = { ...cellStyles[f.columnId] ?? {}, ...st };
|
|
1062
|
+
}
|
|
1063
|
+
const cells = columns.map((c) => {
|
|
1064
|
+
let text = "";
|
|
1065
|
+
if (c.value?.trim()) {
|
|
1066
|
+
try {
|
|
1067
|
+
text = toDisplayString(evaluateIn(c.value, vars));
|
|
1068
|
+
} catch (e) {
|
|
1069
|
+
warn(`Column "${c.header}" failed: ${e.message}`);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
return { text, style: cellStyles[c.id] };
|
|
1073
|
+
});
|
|
1074
|
+
return { cells, style: Object.keys(rowStyle).length ? rowStyle : void 0 };
|
|
1075
|
+
});
|
|
1076
|
+
let totals = null;
|
|
1077
|
+
if (columns.some((c) => c.total && c.total !== "none" || c.totalExpr?.trim())) {
|
|
1078
|
+
totals = columns.map((c, i) => {
|
|
1079
|
+
if (c.totalExpr?.trim()) {
|
|
1080
|
+
try {
|
|
1081
|
+
return { text: toDisplayString(evaluateIn(c.totalExpr, { rows: allItems, [alias + "s"]: allItems })) };
|
|
1082
|
+
} catch {
|
|
1083
|
+
return { text: "" };
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
if (!c.total || c.total === "none") return { text: i === 0 && !columns[0]?.total ? String(props.totalsLabel ?? "Total") : "" };
|
|
1087
|
+
const values = allItems.map((it) => toNumber(evaluateIn(stripPipes(c.value), { [alias]: it, item: it, row: it }))).filter((n2) => n2 !== null);
|
|
1088
|
+
let n = null;
|
|
1089
|
+
if (c.total === "sum") n = values.reduce((a, b) => a + b, 0);
|
|
1090
|
+
else if (c.total === "avg") n = values.length ? values.reduce((a, b) => a + b, 0) / values.length : null;
|
|
1091
|
+
else if (c.total === "count") n = allItems.length;
|
|
1092
|
+
else if (c.total === "min") n = values.length ? Math.min(...values) : null;
|
|
1093
|
+
else if (c.total === "max") n = values.length ? Math.max(...values) : null;
|
|
1094
|
+
const fmt2 = c.totalFormat?.trim() || extractPipes(c.value);
|
|
1095
|
+
const text = n === null ? "" : fmt2 ? toDisplayString(evaluate2(`(${n}) | ${fmt2}`)) : toDisplayString(n);
|
|
1096
|
+
return { text };
|
|
1097
|
+
});
|
|
1098
|
+
if (totals[0] && totals[0].text === "" && !(columns[0]?.total && columns[0].total !== "none")) totals[0] = { text: String(props.totalsLabel ?? "Total") };
|
|
1099
|
+
}
|
|
1100
|
+
return { __rows: rows, __totals: totals, __rowCount: total, __rowsShown: rows.length };
|
|
1101
|
+
},
|
|
1102
|
+
render: ({ h: h3, css, props, theme, styles, mode, text, node, style }) => {
|
|
1103
|
+
const p = props;
|
|
1104
|
+
const columns = Array.isArray(p.columns) ? p.columns : [];
|
|
1105
|
+
const ts = styles.table;
|
|
1106
|
+
const fontSize = p.fontSize ?? ts.fontSize ?? style.fontSize;
|
|
1107
|
+
const borders = p.borders === "inherit" ? ts.borders ?? "horizontal" : p.borders;
|
|
1108
|
+
const borderColor = resolveValue3(ts.borderColor, theme, theme.colors.border);
|
|
1109
|
+
const bw = ts.borderWidth ?? 1;
|
|
1110
|
+
const padY = p.compact ? Math.max(2, (ts.cellPaddingY ?? 6) - 3) : ts.cellPaddingY ?? 6;
|
|
1111
|
+
const padX = ts.cellPaddingX ?? 8;
|
|
1112
|
+
const headerBg = p.headerBackground ? resolveValue3(p.headerBackground, theme, "") : resolveValue3(ts.headerBackground, theme, theme.table?.headerBackground ?? "#F3F4F6");
|
|
1113
|
+
const headerColor = p.headerColor ? resolveValue3(p.headerColor, theme, "") : resolveValue3(ts.headerColor, theme, headerBg && isDarkColor2(headerBg) ? "#FFFFFF" : theme.colors.text);
|
|
1114
|
+
const zebraBg = resolveValue3(ts.zebraBackground, theme, theme.table?.zebraBackground ?? "#FAFAFB");
|
|
1115
|
+
const cellBorder = (isHeader, last) => {
|
|
1116
|
+
const line = `${bw}px solid ${borderColor}`;
|
|
1117
|
+
switch (borders) {
|
|
1118
|
+
case "all":
|
|
1119
|
+
return { border: line };
|
|
1120
|
+
case "horizontal":
|
|
1121
|
+
return { "border-bottom": last ? line : line, ...isHeader ? { "border-bottom": `${bw * 2}px solid ${borderColor}` } : {} };
|
|
1122
|
+
case "outer":
|
|
1123
|
+
return {};
|
|
1124
|
+
default:
|
|
1125
|
+
return {};
|
|
1126
|
+
}
|
|
1127
|
+
};
|
|
1128
|
+
const rows = p.__rows ?? [];
|
|
1129
|
+
const rowsVNodes = rows.map(
|
|
1130
|
+
(r, i) => h3(
|
|
1131
|
+
"tr",
|
|
1132
|
+
{ class: ["bp-table__row", p.zebra && i % 2 === 1 && "bp-table__row--alt"], style: { ...p.zebra && i % 2 === 1 ? { background: zebraBg } : {}, ...r.style ?? {} } },
|
|
1133
|
+
r.cells.map((c, ci) => {
|
|
1134
|
+
const col = columns[ci];
|
|
1135
|
+
return h3("td", { class: ["bp-table__cell", mode === "design" && "bp-cell--bound"], style: { padding: `${padY}px ${padX}px`, "text-align": col?.align ?? "left", "font-weight": col?.bold ? "600" : void 0, "vertical-align": "top", "white-space": col?.wrap === false ? "nowrap" : "normal", overflow: "hidden", "text-overflow": "ellipsis", ...cellBorder(false, i === rows.length - 1), ...c.style ?? {} } }, c.text);
|
|
1136
|
+
})
|
|
1137
|
+
)
|
|
1138
|
+
);
|
|
1139
|
+
const specified = columns.reduce((a, c) => a + (c.width ?? 0), 0);
|
|
1140
|
+
const autoCount = columns.filter((c) => !c.width).length;
|
|
1141
|
+
const autoWidth = autoCount ? Math.max(5, (100 - specified) / autoCount) : 0;
|
|
1142
|
+
const colgroup = h3("colgroup", null, columns.map((c) => h3("col", { style: { width: `${c.width ?? autoWidth}%` } })));
|
|
1143
|
+
const thead = p.showHeader !== false ? h3("thead", { class: "bp-table__head" }, h3("tr", null, columns.map((c) => h3("th", { class: "bp-table__th", style: { padding: `${padY}px ${padX}px`, "text-align": c.align ?? "left", background: headerBg || void 0, color: headerColor, "font-weight": String(ts.headerFontWeight ?? 600), "vertical-align": "bottom", ...cellBorder(true, false) } }, ...text(`columns.${columns.indexOf(c)}.header`, c.header))))) : null;
|
|
1144
|
+
const tfoot = p.__totals ? h3("tfoot", { class: "bp-table__foot" }, h3("tr", null, p.__totals.map((c, ci) => h3("td", { class: "bp-table__total", style: { padding: `${padY}px ${padX}px`, "text-align": columns[ci]?.align ?? "left", "font-weight": "700", "border-top": `${bw * 2}px solid ${borderColor}`, ...borders === "all" ? { border: `${bw}px solid ${borderColor}` } : {} } }, c.text)))) : null;
|
|
1145
|
+
const empty = rows.length === 0;
|
|
1146
|
+
const tableStyle = { width: "100%", "border-collapse": "collapse", "table-layout": "fixed", "font-size": `${fontSize}px`, ...borders === "outer" ? { border: `${bw}px solid ${borderColor}` } : {} };
|
|
1147
|
+
const b = node.bindings.source;
|
|
1148
|
+
return h3(
|
|
1149
|
+
"div",
|
|
1150
|
+
{ class: "bp-table-wrap", style: cssText4(css) },
|
|
1151
|
+
// Design mode draws a handful of rows to keep the canvas quick, which is
|
|
1152
|
+
// indistinguishable from a table that only has a handful of rows unless
|
|
1153
|
+
// the badge says so.
|
|
1154
|
+
mode === "design" ? h3("div", { class: "bp-table__badge", contenteditable: "false" }, b ? `${b.label} \xB7 ${rowSampleNote(p.__rowsShown ?? 0, p.__rowCount ?? 0)}` : "Choose a list") : null,
|
|
1155
|
+
h3("table", { class: "bp-table", style: tableStyle }, colgroup, thead, h3("tbody", null, empty ? h3("tr", null, h3("td", { colspan: String(Math.max(1, columns.length)), style: { padding: `${padY * 2}px ${padX}px`, color: theme.colors.mutedText, "text-align": "center", "font-style": "italic" } }, ...text("emptyMessage", "No data"))) : rowsVNodes), tfoot)
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
});
|
|
1159
|
+
function rowSampleNote(shown, total) {
|
|
1160
|
+
if (total === 0) return "no rows";
|
|
1161
|
+
if (shown >= total) return `${total} row${total === 1 ? "" : "s"}`;
|
|
1162
|
+
return `showing ${shown} of ${total} sample rows`;
|
|
1163
|
+
}
|
|
1164
|
+
function stripPipes(expr) {
|
|
1165
|
+
const idx = expr.indexOf("|");
|
|
1166
|
+
if (idx < 0) return expr;
|
|
1167
|
+
if (expr[idx + 1] === "|") return expr;
|
|
1168
|
+
return expr.slice(0, idx).trim();
|
|
1169
|
+
}
|
|
1170
|
+
function extractPipes(expr) {
|
|
1171
|
+
const idx = expr.indexOf("|");
|
|
1172
|
+
if (idx < 0 || expr[idx + 1] === "|") return "";
|
|
1173
|
+
return expr.slice(idx + 1).trim();
|
|
1174
|
+
}
|
|
1175
|
+
var keyValueListBlock = defineBlock4({
|
|
1176
|
+
type: "keyValueList",
|
|
1177
|
+
name: "Key / value list",
|
|
1178
|
+
description: "A list of labelled values",
|
|
1179
|
+
category: "data",
|
|
1180
|
+
icon: "list",
|
|
1181
|
+
keywords: ["details", "properties", "definition list", "summary"],
|
|
1182
|
+
defaultProps: () => ({
|
|
1183
|
+
items: [
|
|
1184
|
+
{ id: "k1", label: "Name", value: "" },
|
|
1185
|
+
{ id: "k2", label: "Reference", value: "" }
|
|
1186
|
+
],
|
|
1187
|
+
layout: "inline",
|
|
1188
|
+
labelWidth: 35,
|
|
1189
|
+
dividers: true,
|
|
1190
|
+
columns: 1
|
|
1191
|
+
}),
|
|
1192
|
+
defaultTextStyle: "body",
|
|
1193
|
+
split: "none",
|
|
1194
|
+
styleGroups: ["typography", "spacing", "background", "border", "flow"],
|
|
1195
|
+
bindings: [{ prop: "items", kind: "list", items: [{ prop: "label", kind: "template" }, { prop: "value", kind: "template" }] }],
|
|
1196
|
+
inspector: [
|
|
1197
|
+
{
|
|
1198
|
+
title: "Items",
|
|
1199
|
+
fields: [
|
|
1200
|
+
{ kind: "keyValues", prop: "items", label: "Items", fullWidth: true },
|
|
1201
|
+
{
|
|
1202
|
+
kind: "segmented",
|
|
1203
|
+
prop: "layout",
|
|
1204
|
+
label: "Layout",
|
|
1205
|
+
options: [
|
|
1206
|
+
{ value: "inline", label: "Inline" },
|
|
1207
|
+
{ value: "stacked", label: "Stacked" },
|
|
1208
|
+
{ value: "grid", label: "Grid" }
|
|
1209
|
+
]
|
|
1210
|
+
},
|
|
1211
|
+
{ kind: "number", prop: "columns", label: "Columns", min: 1, max: 4, when: (p) => p.layout === "grid" },
|
|
1212
|
+
{ kind: "number", prop: "labelWidth", label: "Label width", min: 10, max: 80, unit: "%", when: (p) => p.layout === "inline" },
|
|
1213
|
+
{ kind: "toggle", prop: "dividers", label: "Dividers" }
|
|
1214
|
+
]
|
|
1215
|
+
}
|
|
1216
|
+
],
|
|
1217
|
+
render: ({ h: h3, css, props, theme, text }) => {
|
|
1218
|
+
const items = Array.isArray(props.items) ? props.items : [];
|
|
1219
|
+
const divider = props.dividers ? `1px solid ${theme.colors.border}` : void 0;
|
|
1220
|
+
if (props.layout === "grid") {
|
|
1221
|
+
return h3(
|
|
1222
|
+
"div",
|
|
1223
|
+
{ class: "bp-kv bp-kv--grid", style: cssText4({ ...css, display: "grid", "grid-template-columns": `repeat(${props.columns || 2}, minmax(0, 1fr))`, gap: "10px 16px" }) },
|
|
1224
|
+
items.map((it, i) => h3("div", { class: "bp-kv__item" }, h3("div", { class: "bp-kv__label", style: { color: theme.colors.mutedText, "font-size": "0.85em", "text-transform": "uppercase", "letter-spacing": "0.04em", "font-weight": "600" } }, text(`items.${i}.label`, it.label)), h3("div", { class: "bp-kv__value", style: { "font-weight": "500" } }, text(`items.${i}.value`, it.value || "\u2014"))))
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
1227
|
+
return h3(
|
|
1228
|
+
"div",
|
|
1229
|
+
{ class: ["bp-kv", `bp-kv--${props.layout}`], style: cssText4(css) },
|
|
1230
|
+
items.map(
|
|
1231
|
+
(it, i) => props.layout === "inline" ? h3("div", { class: "bp-kv__row", style: { display: "flex", gap: "8px", padding: "5px 0", "border-bottom": i < items.length - 1 ? divider : void 0 } }, h3("div", { class: "bp-kv__label", style: { flex: `0 0 ${props.labelWidth}%`, color: theme.colors.mutedText } }, text(`items.${i}.label`, it.label)), h3("div", { class: "bp-kv__value", style: { flex: "1", "min-width": "0", "font-weight": "500" } }, text(`items.${i}.value`, it.value || "\u2014"))) : h3("div", { class: "bp-kv__row", style: { padding: "5px 0", "border-bottom": i < items.length - 1 ? divider : void 0 } }, h3("div", { class: "bp-kv__label", style: { color: theme.colors.mutedText, "font-size": "0.85em" } }, text(`items.${i}.label`, it.label)), h3("div", { class: "bp-kv__value", style: { "font-weight": "500" } }, text(`items.${i}.value`, it.value || "\u2014")))
|
|
1232
|
+
)
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
// src/blocks/charts.ts
|
|
1238
|
+
import { defineBlock as defineBlock5, cssText as cssText5, resolveValue as resolveValue4, toNumber as toNumber2, toDisplayString as toDisplayString2, evaluate, Scope } from "@broadpaper/core";
|
|
1239
|
+
|
|
1240
|
+
// src/charts/chart-svg.ts
|
|
1241
|
+
import { scaleBand, scaleLinear, scalePoint } from "d3-scale";
|
|
1242
|
+
import { arc as d3arc, area as d3area, line as d3line, curveMonotoneX, curveLinear, pie as d3pie } from "d3-shape";
|
|
1243
|
+
import { h as h2 } from "@broadpaper/core";
|
|
1244
|
+
var FONT_SIZE = 9;
|
|
1245
|
+
var AXIS_COLOR = "#9AA1AB";
|
|
1246
|
+
function hit(opts, entry) {
|
|
1247
|
+
if (!opts.hits) return {};
|
|
1248
|
+
opts.hits.push(entry);
|
|
1249
|
+
return { "data-bp-hit": String(opts.hits.length - 1) };
|
|
1250
|
+
}
|
|
1251
|
+
function fmt(n) {
|
|
1252
|
+
return Math.round(n * 100) / 100 + "";
|
|
1253
|
+
}
|
|
1254
|
+
function legend(data, opts, y, width) {
|
|
1255
|
+
if (!opts.showLegend || data.series.length === 0) return { node: null, height: 0 };
|
|
1256
|
+
const items = opts.type === "pie" || opts.type === "donut" ? data.categories.map((c, i) => ({ label: c, color: colorAt(data, i) })) : data.series.map((s) => ({ label: s.label, color: s.color }));
|
|
1257
|
+
const perRow = Math.max(1, Math.floor(width / 120));
|
|
1258
|
+
const rows = Math.ceil(items.length / perRow);
|
|
1259
|
+
const node = h2(
|
|
1260
|
+
"g",
|
|
1261
|
+
{ class: "bp-chart__legend", transform: `translate(0, ${y})` },
|
|
1262
|
+
items.map((it, i) => {
|
|
1263
|
+
const col = i % perRow;
|
|
1264
|
+
const row = Math.floor(i / perRow);
|
|
1265
|
+
const x = width / perRow * col;
|
|
1266
|
+
return h2("g", { transform: `translate(${fmt(x)}, ${row * 16})` }, h2("rect", { x: 0, y: 2, width: 10, height: 10, rx: 2, fill: it.color }), h2("text", { x: 15, y: 11, "font-size": FONT_SIZE + 1, "font-family": opts.fontFamily, fill: opts.theme.colors.text }, truncate(it.label, Math.floor(width / perRow / 6))));
|
|
1267
|
+
})
|
|
1268
|
+
);
|
|
1269
|
+
return { node, height: rows * 16 + 4 };
|
|
1270
|
+
}
|
|
1271
|
+
function truncate(s, n) {
|
|
1272
|
+
return s.length > n ? s.slice(0, Math.max(1, n - 1)) + "\u2026" : s;
|
|
1273
|
+
}
|
|
1274
|
+
function colorAt(data, i) {
|
|
1275
|
+
const s = data.series[0];
|
|
1276
|
+
const palette = data.series.map((x) => x.color);
|
|
1277
|
+
return palette[i % Math.max(1, palette.length)] ?? s?.color ?? "#888";
|
|
1278
|
+
}
|
|
1279
|
+
function renderChart(data, opts) {
|
|
1280
|
+
const { width, height } = opts;
|
|
1281
|
+
if (opts.type === "pie" || opts.type === "donut") return renderPie(data, opts);
|
|
1282
|
+
const horizontal = opts.type === "horizontalBar";
|
|
1283
|
+
const lg = legend(data, opts, 0, width);
|
|
1284
|
+
const marginTop = lg.height + 6;
|
|
1285
|
+
const marginRight = 8;
|
|
1286
|
+
const marginBottom = 24;
|
|
1287
|
+
const allValues = data.series.flatMap((s) => s.values.map((v) => v ?? 0));
|
|
1288
|
+
const stackedTotals = data.categories.map((_, i) => data.series.reduce((a, s) => a + Math.max(0, s.values[i] ?? 0), 0));
|
|
1289
|
+
const stackedNeg = data.categories.map((_, i) => data.series.reduce((a, s) => a + Math.min(0, s.values[i] ?? 0), 0));
|
|
1290
|
+
const maxV = Math.max(0, ...opts.stacked ? stackedTotals : allValues);
|
|
1291
|
+
const minV = Math.min(0, ...opts.stacked ? stackedNeg : allValues);
|
|
1292
|
+
const vScale = scaleLinear().domain([minV, maxV === minV ? maxV + 1 : maxV]).nice(5);
|
|
1293
|
+
const ticks = vScale.ticks(5);
|
|
1294
|
+
const tickLabels = ticks.map(opts.formatValue);
|
|
1295
|
+
const labelWidth = Math.max(...tickLabels.map((t) => t.length * FONT_SIZE * 0.6), 18);
|
|
1296
|
+
const marginLeft = horizontal ? Math.min(width * 0.35, Math.max(...data.categories.map((c) => c.length)) * FONT_SIZE * 0.6 + 8) : labelWidth + 8;
|
|
1297
|
+
const plotW = Math.max(10, width - marginLeft - marginRight);
|
|
1298
|
+
const plotH = Math.max(10, height - marginTop - marginBottom);
|
|
1299
|
+
const children = [];
|
|
1300
|
+
if (lg.node) children.push(lg.node);
|
|
1301
|
+
const g = (x, y, ...kids) => h2("g", { transform: `translate(${fmt(x)}, ${fmt(y)})` }, kids);
|
|
1302
|
+
if (horizontal) {
|
|
1303
|
+
vScale.range([0, plotW]);
|
|
1304
|
+
const band = scaleBand().domain(data.categories.map((_, i) => String(i))).range([0, plotH]).paddingInner(0.25).paddingOuter(0.1);
|
|
1305
|
+
const inner = scaleBand().domain(data.series.map((_, i) => String(i))).range([0, band.bandwidth()]).padding(0.1);
|
|
1306
|
+
const grid2 = opts.showGrid ? ticks.map((t) => h2("line", { x1: vScale(t), x2: vScale(t), y1: 0, y2: plotH, stroke: opts.theme.colors.border, "stroke-width": 1 })) : [];
|
|
1307
|
+
const axis = ticks.map((t) => h2("text", { x: vScale(t), y: plotH + 14, "text-anchor": "middle", "font-size": FONT_SIZE, "font-family": opts.fontFamily, fill: AXIS_COLOR }, opts.formatValue(t)));
|
|
1308
|
+
const cats = data.categories.map((c, i) => h2("text", { x: -6, y: (band(String(i)) ?? 0) + band.bandwidth() / 2 + 3, "text-anchor": "end", "font-size": FONT_SIZE, "font-family": opts.fontFamily, fill: opts.theme.colors.text }, truncate(c, Math.floor(marginLeft / (FONT_SIZE * 0.6)))));
|
|
1309
|
+
const bars = [];
|
|
1310
|
+
data.categories.forEach((_, ci) => {
|
|
1311
|
+
let posAcc = 0;
|
|
1312
|
+
let negAcc = 0;
|
|
1313
|
+
data.series.forEach((s, si) => {
|
|
1314
|
+
const v = s.values[ci] ?? 0;
|
|
1315
|
+
let x0, x1;
|
|
1316
|
+
if (opts.stacked) {
|
|
1317
|
+
if (v >= 0) {
|
|
1318
|
+
x0 = vScale(posAcc);
|
|
1319
|
+
x1 = vScale(posAcc + v);
|
|
1320
|
+
posAcc += v;
|
|
1321
|
+
} else {
|
|
1322
|
+
x0 = vScale(negAcc + v);
|
|
1323
|
+
x1 = vScale(negAcc);
|
|
1324
|
+
negAcc += v;
|
|
1325
|
+
}
|
|
1326
|
+
} else {
|
|
1327
|
+
x0 = vScale(Math.min(0, v));
|
|
1328
|
+
x1 = vScale(Math.max(0, v));
|
|
1329
|
+
}
|
|
1330
|
+
const y = (band(String(ci)) ?? 0) + (opts.stacked ? 0 : inner(String(si)) ?? 0);
|
|
1331
|
+
const bh = opts.stacked ? band.bandwidth() : inner.bandwidth();
|
|
1332
|
+
bars.push(h2("rect", { x: fmt(x0), y: fmt(y), width: fmt(Math.max(0, x1 - x0)), height: fmt(bh), fill: s.color, rx: 1.5, ...hit(opts, { categoryIndex: ci, category: data.categories[ci] ?? "", seriesIndex: si, series: s.label, value: s.values[ci] ?? null, formatted: opts.formatValue(v), color: s.color }) }));
|
|
1333
|
+
if (opts.showValues && !opts.stacked) bars.push(h2("text", { x: fmt(x1 + 4), y: fmt(y + bh / 2 + 3), "font-size": FONT_SIZE, "font-family": opts.fontFamily, fill: opts.theme.colors.text }, opts.formatValue(v)));
|
|
1334
|
+
});
|
|
1335
|
+
});
|
|
1336
|
+
children.push(g(marginLeft, marginTop, ...grid2, h2("line", { x1: vScale(0), x2: vScale(0), y1: 0, y2: plotH, stroke: AXIS_COLOR }), ...bars, ...axis, ...cats));
|
|
1337
|
+
return svg2(width, height, children);
|
|
1338
|
+
}
|
|
1339
|
+
vScale.range([plotH, 0]);
|
|
1340
|
+
const grid = opts.showGrid ? ticks.map((t) => h2("line", { x1: 0, x2: plotW, y1: fmt(vScale(t)), y2: fmt(vScale(t)), stroke: opts.theme.colors.border, "stroke-width": 1 })) : [];
|
|
1341
|
+
const yAxis = ticks.map((t) => h2("text", { x: -6, y: fmt(vScale(t) + 3), "text-anchor": "end", "font-size": FONT_SIZE, "font-family": opts.fontFamily, fill: AXIS_COLOR }, opts.formatValue(t)));
|
|
1342
|
+
const maxCatLabels = Math.max(1, Math.floor(plotW / 48));
|
|
1343
|
+
const every = Math.ceil(data.categories.length / maxCatLabels);
|
|
1344
|
+
if (opts.type === "bar") {
|
|
1345
|
+
const band = scaleBand().domain(data.categories.map((_, i) => String(i))).range([0, plotW]).paddingInner(0.25).paddingOuter(0.1);
|
|
1346
|
+
const inner = scaleBand().domain(data.series.map((_, i) => String(i))).range([0, band.bandwidth()]).padding(0.1);
|
|
1347
|
+
const bars = [];
|
|
1348
|
+
data.categories.forEach((_, ci) => {
|
|
1349
|
+
let posAcc = 0;
|
|
1350
|
+
let negAcc = 0;
|
|
1351
|
+
data.series.forEach((s, si) => {
|
|
1352
|
+
const v = s.values[ci] ?? 0;
|
|
1353
|
+
let y0, y1;
|
|
1354
|
+
if (opts.stacked) {
|
|
1355
|
+
if (v >= 0) {
|
|
1356
|
+
y1 = vScale(posAcc);
|
|
1357
|
+
y0 = vScale(posAcc + v);
|
|
1358
|
+
posAcc += v;
|
|
1359
|
+
} else {
|
|
1360
|
+
y1 = vScale(negAcc + v);
|
|
1361
|
+
y0 = vScale(negAcc);
|
|
1362
|
+
negAcc += v;
|
|
1363
|
+
}
|
|
1364
|
+
} else {
|
|
1365
|
+
y0 = vScale(Math.max(0, v));
|
|
1366
|
+
y1 = vScale(Math.min(0, v));
|
|
1367
|
+
}
|
|
1368
|
+
const x = (band(String(ci)) ?? 0) + (opts.stacked ? 0 : inner(String(si)) ?? 0);
|
|
1369
|
+
const bw = opts.stacked ? band.bandwidth() : inner.bandwidth();
|
|
1370
|
+
bars.push(h2("rect", { x: fmt(x), y: fmt(y0), width: fmt(bw), height: fmt(Math.max(0, y1 - y0)), fill: s.color, rx: 1.5, ...hit(opts, { categoryIndex: ci, category: data.categories[ci] ?? "", seriesIndex: si, series: s.label, value: s.values[ci] ?? null, formatted: opts.formatValue(v), color: s.color }) }));
|
|
1371
|
+
if (opts.showValues && !opts.stacked) bars.push(h2("text", { x: fmt(x + bw / 2), y: fmt(y0 - 3), "text-anchor": "middle", "font-size": FONT_SIZE - 1, "font-family": opts.fontFamily, fill: opts.theme.colors.text }, opts.formatValue(v)));
|
|
1372
|
+
});
|
|
1373
|
+
});
|
|
1374
|
+
const xAxis2 = data.categories.map((c, i) => i % every === 0 ? h2("text", { x: fmt((band(String(i)) ?? 0) + band.bandwidth() / 2), y: plotH + 14, "text-anchor": "middle", "font-size": FONT_SIZE, "font-family": opts.fontFamily, fill: AXIS_COLOR }, truncate(c, 10)) : null);
|
|
1375
|
+
children.push(g(marginLeft, marginTop, ...grid, ...bars, h2("line", { x1: 0, x2: plotW, y1: fmt(vScale(0)), y2: fmt(vScale(0)), stroke: AXIS_COLOR }), ...yAxis, ...xAxis2));
|
|
1376
|
+
return svg2(width, height, children);
|
|
1377
|
+
}
|
|
1378
|
+
const xScale = scalePoint().domain(data.categories.map((_, i) => String(i))).range([0, plotW]).padding(0.15);
|
|
1379
|
+
const curve = opts.smooth ? curveMonotoneX : curveLinear;
|
|
1380
|
+
const shapes = [];
|
|
1381
|
+
const stackBase = data.categories.map(() => 0);
|
|
1382
|
+
data.series.forEach((s, si) => {
|
|
1383
|
+
const pts = s.values.map((v, i) => ({ i, v: v ?? 0 }));
|
|
1384
|
+
const base = [...stackBase];
|
|
1385
|
+
if (opts.stacked) pts.forEach((p) => stackBase[p.i] = (stackBase[p.i] ?? 0) + p.v);
|
|
1386
|
+
const yOf = (p) => vScale(opts.stacked ? (base[p.i] ?? 0) + p.v : p.v);
|
|
1387
|
+
const y0Of = (p) => vScale(opts.stacked ? base[p.i] ?? 0 : 0);
|
|
1388
|
+
const l = d3line().x((p) => xScale(String(p.i)) ?? 0).y(yOf).curve(curve);
|
|
1389
|
+
if (opts.type === "area") {
|
|
1390
|
+
const a = d3area().x((p) => xScale(String(p.i)) ?? 0).y0(y0Of).y1(yOf).curve(curve);
|
|
1391
|
+
shapes.push(h2("path", { d: a(pts) ?? "", fill: s.color, "fill-opacity": 0.18 }));
|
|
1392
|
+
}
|
|
1393
|
+
shapes.push(h2("path", { d: l(pts) ?? "", fill: "none", stroke: s.color, "stroke-width": 2, "stroke-linejoin": "round", "stroke-linecap": "round" }));
|
|
1394
|
+
pts.forEach((p) => {
|
|
1395
|
+
const attrs = hit(opts, { categoryIndex: p.i, category: data.categories[p.i] ?? "", seriesIndex: si, series: s.label, value: s.values[p.i] ?? null, formatted: opts.formatValue(p.v), color: s.color });
|
|
1396
|
+
if (pts.length <= 40) shapes.push(h2("circle", { cx: fmt(xScale(String(p.i)) ?? 0), cy: fmt(yOf(p)), r: 2.5, fill: "#fff", stroke: s.color, "stroke-width": 1.5, ...attrs }));
|
|
1397
|
+
else if (opts.hits) opts.hits[opts.hits.length - 1].value = s.values[p.i] ?? null;
|
|
1398
|
+
});
|
|
1399
|
+
if (opts.showValues) pts.forEach((p) => shapes.push(h2("text", { x: fmt(xScale(String(p.i)) ?? 0), y: fmt(yOf(p) - 6), "text-anchor": "middle", "font-size": FONT_SIZE - 1, "font-family": opts.fontFamily, fill: opts.theme.colors.text }, opts.formatValue(p.v))));
|
|
1400
|
+
});
|
|
1401
|
+
const xAxis = data.categories.map((c, i) => i % every === 0 ? h2("text", { x: fmt(xScale(String(i)) ?? 0), y: plotH + 14, "text-anchor": "middle", "font-size": FONT_SIZE, "font-family": opts.fontFamily, fill: AXIS_COLOR }, truncate(c, 10)) : null);
|
|
1402
|
+
children.push(g(marginLeft, marginTop, ...grid, h2("line", { x1: 0, x2: plotW, y1: fmt(vScale(0)), y2: fmt(vScale(0)), stroke: AXIS_COLOR }), ...shapes, ...yAxis, ...xAxis));
|
|
1403
|
+
return svg2(width, height, children);
|
|
1404
|
+
}
|
|
1405
|
+
function renderPie(data, opts) {
|
|
1406
|
+
const { width, height } = opts;
|
|
1407
|
+
const series = data.series[0];
|
|
1408
|
+
const values = data.categories.map((_, i) => Math.max(0, series?.values[i] ?? 0));
|
|
1409
|
+
const total = values.reduce((a, b) => a + b, 0);
|
|
1410
|
+
const legendW = opts.showLegend ? Math.min(160, width * 0.45) : 0;
|
|
1411
|
+
const size = Math.min(height, width - legendW);
|
|
1412
|
+
const r = Math.max(10, size / 2 - 6);
|
|
1413
|
+
const cx = (width - legendW) / 2;
|
|
1414
|
+
const cy = height / 2;
|
|
1415
|
+
const inner = opts.type === "donut" ? r * 0.62 : 0;
|
|
1416
|
+
const pie = d3pie().sort(null).value((d) => d);
|
|
1417
|
+
const arcs = pie(values);
|
|
1418
|
+
const arcGen = d3arc().innerRadius(inner).outerRadius(r).padAngle(0.01).cornerRadius(2);
|
|
1419
|
+
const labelArc = d3arc().innerRadius((inner + r) / 2).outerRadius((inner + r) / 2);
|
|
1420
|
+
const slices = arcs.map((a, i) => {
|
|
1421
|
+
const frac = total > 0 ? values[i] / total : 0;
|
|
1422
|
+
const [lx, ly] = labelArc.centroid(a);
|
|
1423
|
+
const attrs = hit(opts, {
|
|
1424
|
+
categoryIndex: i,
|
|
1425
|
+
category: data.categories[i] ?? "",
|
|
1426
|
+
seriesIndex: 0,
|
|
1427
|
+
series: series?.label ?? "",
|
|
1428
|
+
value: series?.values[i] ?? null,
|
|
1429
|
+
formatted: opts.formatValue(values[i] ?? 0),
|
|
1430
|
+
color: colorAt(data, i),
|
|
1431
|
+
share: frac
|
|
1432
|
+
});
|
|
1433
|
+
return h2("g", null, h2("path", { d: arcGen(a) ?? "", fill: colorAt(data, i), ...attrs }), opts.showValues && frac >= 0.06 ? h2("text", { x: fmt(lx), y: fmt(ly + 3), "text-anchor": "middle", "font-size": FONT_SIZE, "font-family": opts.fontFamily, fill: "#fff", "font-weight": "600" }, opts.formatPercent(frac)) : null);
|
|
1434
|
+
});
|
|
1435
|
+
const children = [h2("g", { transform: `translate(${fmt(cx)}, ${fmt(cy)})` }, total === 0 ? h2("circle", { r, fill: opts.theme.colors.border }) : slices, opts.type === "donut" && opts.innerLabel ? h2("text", { "text-anchor": "middle", y: 4, "font-size": FONT_SIZE + 3, "font-weight": "700", "font-family": opts.fontFamily, fill: opts.theme.colors.text }, opts.innerLabel) : null)];
|
|
1436
|
+
if (opts.showLegend) {
|
|
1437
|
+
const items = data.categories.map((c, i) => ({ label: c, color: colorAt(data, i), frac: total > 0 ? values[i] / total : 0 }));
|
|
1438
|
+
const lineH = 15;
|
|
1439
|
+
const startY = Math.max(0, cy - items.length * lineH / 2);
|
|
1440
|
+
children.push(
|
|
1441
|
+
h2(
|
|
1442
|
+
"g",
|
|
1443
|
+
{ transform: `translate(${fmt(width - legendW)}, ${fmt(startY)})` },
|
|
1444
|
+
items.map((it, i) => h2("g", { transform: `translate(0, ${i * lineH})` }, h2("rect", { x: 0, y: 2, width: 9, height: 9, rx: 2, fill: it.color }), h2("text", { x: 14, y: 10, "font-size": FONT_SIZE, "font-family": opts.fontFamily, fill: opts.theme.colors.text }, `${truncate(it.label, Math.floor(legendW / 6) - 6)} ${opts.formatPercent(it.frac)}`)))
|
|
1445
|
+
)
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1448
|
+
return svg2(width, height, children);
|
|
1449
|
+
}
|
|
1450
|
+
function svg2(width, height, children) {
|
|
1451
|
+
return h2("svg", { xmlns: "http://www.w3.org/2000/svg", width: fmt(width), height: fmt(height), viewBox: `0 0 ${fmt(width)} ${fmt(height)}`, class: "bp-chart__svg", role: "img", style: { display: "block", overflow: "visible" } }, children);
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
// src/blocks/charts.ts
|
|
1455
|
+
var chartBlock = defineBlock5({
|
|
1456
|
+
type: "chart",
|
|
1457
|
+
name: "Chart",
|
|
1458
|
+
description: "Bar, line, area, pie or donut chart bound to a list",
|
|
1459
|
+
category: "visualisation",
|
|
1460
|
+
icon: "bar-chart-3",
|
|
1461
|
+
keywords: ["graph", "bar", "line", "pie", "donut", "area", "visualisation"],
|
|
1462
|
+
defaultProps: () => ({
|
|
1463
|
+
chartType: "bar",
|
|
1464
|
+
source: "",
|
|
1465
|
+
alias: "item",
|
|
1466
|
+
label: "item.label",
|
|
1467
|
+
series: [{ id: "s1", label: "Value", value: "item.value" }],
|
|
1468
|
+
height: 220,
|
|
1469
|
+
title: "",
|
|
1470
|
+
showLegend: true,
|
|
1471
|
+
showValues: false,
|
|
1472
|
+
showGrid: true,
|
|
1473
|
+
stacked: false,
|
|
1474
|
+
smooth: true,
|
|
1475
|
+
valueFormat: "",
|
|
1476
|
+
percentDecimals: 0,
|
|
1477
|
+
innerLabel: "",
|
|
1478
|
+
limit: null
|
|
1479
|
+
}),
|
|
1480
|
+
defaultTextStyle: "body",
|
|
1481
|
+
defaultFlow: { keepTogether: true },
|
|
1482
|
+
split: "none",
|
|
1483
|
+
styleGroups: ["spacing", "background", "border", "flow"],
|
|
1484
|
+
bindings: [
|
|
1485
|
+
{ prop: "source", kind: "collection", alias: "alias" },
|
|
1486
|
+
{ prop: "label", kind: "value", perItem: true },
|
|
1487
|
+
{ prop: "series", kind: "list", items: [{ prop: "value", kind: "value", perItem: true }, { prop: "label", kind: "template" }] },
|
|
1488
|
+
{ prop: "title", kind: "template" },
|
|
1489
|
+
{ prop: "innerLabel", kind: "template" }
|
|
1490
|
+
],
|
|
1491
|
+
inspector: [
|
|
1492
|
+
{
|
|
1493
|
+
title: "Chart",
|
|
1494
|
+
fields: [
|
|
1495
|
+
{
|
|
1496
|
+
kind: "segmented",
|
|
1497
|
+
prop: "chartType",
|
|
1498
|
+
label: "Type",
|
|
1499
|
+
options: [
|
|
1500
|
+
{ value: "bar", label: "Bar", icon: "bar-chart-3" },
|
|
1501
|
+
{ value: "horizontalBar", label: "Horizontal", icon: "bar-chart-horizontal" },
|
|
1502
|
+
{ value: "line", label: "Line", icon: "line-chart" },
|
|
1503
|
+
{ value: "area", label: "Area", icon: "area-chart" },
|
|
1504
|
+
{ value: "pie", label: "Pie", icon: "pie-chart" },
|
|
1505
|
+
{ value: "donut", label: "Donut", icon: "circle-dot" }
|
|
1506
|
+
]
|
|
1507
|
+
},
|
|
1508
|
+
{ kind: "text", prop: "title", label: "Title" },
|
|
1509
|
+
{ kind: "number", prop: "height", label: "Height", min: 80, max: 800, unit: "px" }
|
|
1510
|
+
]
|
|
1511
|
+
},
|
|
1512
|
+
{
|
|
1513
|
+
title: "Data",
|
|
1514
|
+
fields: [
|
|
1515
|
+
{ kind: "collection", prop: "source", label: "Data from", placeholder: "Pick a list" },
|
|
1516
|
+
{ kind: "text", prop: "alias", label: "Item name", description: (p) => `The name each data point goes by, e.g. ${typeof p.alias === "string" && p.alias.trim() ? p.alias.trim() : "item"}.value`, allowTokens: false },
|
|
1517
|
+
{ kind: "expression", prop: "label", label: "Category label", placeholder: "item.name" },
|
|
1518
|
+
{ kind: "series", prop: "series", label: "Series", fullWidth: true },
|
|
1519
|
+
{ kind: "number", prop: "limit", label: "Maximum items", min: 1, max: 1e3 },
|
|
1520
|
+
{ kind: "text", prop: "valueFormat", label: "Value format", placeholder: 'e.g. currency:"GBP":0 or percent', allowTokens: false }
|
|
1521
|
+
]
|
|
1522
|
+
},
|
|
1523
|
+
{
|
|
1524
|
+
title: "Options",
|
|
1525
|
+
fields: [
|
|
1526
|
+
{ kind: "toggle", prop: "showLegend", label: "Legend" },
|
|
1527
|
+
{ kind: "toggle", prop: "showValues", label: "Value labels" },
|
|
1528
|
+
{ kind: "toggle", prop: "showGrid", label: "Grid lines", when: (p) => p.chartType !== "pie" && p.chartType !== "donut" },
|
|
1529
|
+
{ kind: "toggle", prop: "stacked", label: "Stacked", when: (p) => ["bar", "horizontalBar", "line", "area"].includes(String(p.chartType)) },
|
|
1530
|
+
{ kind: "toggle", prop: "smooth", label: "Smooth lines", when: (p) => p.chartType === "line" || p.chartType === "area" },
|
|
1531
|
+
{ kind: "number", prop: "percentDecimals", label: "Percent decimals", min: 0, max: 3, when: (p) => p.chartType === "pie" || p.chartType === "donut" },
|
|
1532
|
+
{ kind: "text", prop: "innerLabel", label: "Centre label", when: (p) => p.chartType === "donut" }
|
|
1533
|
+
]
|
|
1534
|
+
}
|
|
1535
|
+
],
|
|
1536
|
+
validate: ({ props }) => {
|
|
1537
|
+
const issues = [];
|
|
1538
|
+
if (!Array.isArray(props.series) || props.series.length === 0) issues.push({ severity: "warning", code: "empty-content", message: "Chart has no series", prop: "series" });
|
|
1539
|
+
return issues;
|
|
1540
|
+
},
|
|
1541
|
+
canvas: { label: (p) => p.title ? `Chart \xB7 ${p.title}` : "Chart" },
|
|
1542
|
+
resolve: ({ props, evaluateIn, theme, warn }) => {
|
|
1543
|
+
const alias = props.alias || "item";
|
|
1544
|
+
let items = Array.isArray(props.source) ? props.source : [];
|
|
1545
|
+
if (props.limit) items = items.slice(0, props.limit);
|
|
1546
|
+
const palette = theme.colors.chart ?? [theme.colors.primary, theme.colors.secondary, theme.colors.accent];
|
|
1547
|
+
const categories = items.map((it, index) => {
|
|
1548
|
+
try {
|
|
1549
|
+
return toDisplayString2(evaluateIn(props.label || `${alias}.label`, { [alias]: it, item: it, index: index + 1 }));
|
|
1550
|
+
} catch {
|
|
1551
|
+
return String(index + 1);
|
|
1552
|
+
}
|
|
1553
|
+
});
|
|
1554
|
+
const series = (Array.isArray(props.series) ? props.series : []).map((s, si) => ({
|
|
1555
|
+
label: s.label || `Series ${si + 1}`,
|
|
1556
|
+
color: s.color ? resolveValue4(s.color, theme, palette[si % palette.length]) : palette[si % palette.length],
|
|
1557
|
+
values: items.map((it, index) => {
|
|
1558
|
+
try {
|
|
1559
|
+
return toNumber2(evaluateIn(s.value, { [alias]: it, item: it, index: index + 1 }));
|
|
1560
|
+
} catch (e) {
|
|
1561
|
+
warn(`Series "${s.label}" failed: ${e.message}`);
|
|
1562
|
+
return null;
|
|
1563
|
+
}
|
|
1564
|
+
})
|
|
1565
|
+
}));
|
|
1566
|
+
if (props.chartType === "pie" || props.chartType === "donut") {
|
|
1567
|
+
const s0 = series[0];
|
|
1568
|
+
if (s0) {
|
|
1569
|
+
const colored = items.map((_, i) => ({ ...s0, color: palette[i % palette.length] }));
|
|
1570
|
+
return { __data: { categories, series: colored.length ? [{ ...s0 }, ...colored.slice(1)] : [] }, __palette: palette };
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
return { __data: { categories, series }, __palette: palette };
|
|
1574
|
+
},
|
|
1575
|
+
render: ({ h: h3, css, props, theme, width, mode, node, format, text, style }) => {
|
|
1576
|
+
const p = props;
|
|
1577
|
+
const data = p.__data ?? { categories: [], series: [] };
|
|
1578
|
+
const isPie = p.chartType === "pie" || p.chartType === "donut";
|
|
1579
|
+
if (isPie && data.series.length) {
|
|
1580
|
+
const palette = p.__palette ?? theme.colors.chart ?? [];
|
|
1581
|
+
data.series = data.categories.map((_, i) => ({ ...data.series[0], color: palette[i % Math.max(1, palette.length)] ?? data.series[0].color }));
|
|
1582
|
+
}
|
|
1583
|
+
const formatValue = (n) => {
|
|
1584
|
+
const f = (p.valueFormat || "").trim();
|
|
1585
|
+
if (!f) return new Intl.NumberFormat(format.locale, { notation: Math.abs(n) >= 1e4 ? "compact" : "standard", maximumFractionDigits: 2 }).format(n);
|
|
1586
|
+
try {
|
|
1587
|
+
return toDisplayString2(evaluate(`(${n}) | ${f}`, { scope: new Scope({}), format }));
|
|
1588
|
+
} catch {
|
|
1589
|
+
return String(n);
|
|
1590
|
+
}
|
|
1591
|
+
};
|
|
1592
|
+
const formatPercent = (fr) => new Intl.NumberFormat(format.locale, { style: "percent", maximumFractionDigits: p.percentDecimals ?? 0, minimumFractionDigits: p.percentDecimals ?? 0 }).format(fr);
|
|
1593
|
+
const pad = style.padding;
|
|
1594
|
+
const innerWidth = Math.max(40, width - pad.left - pad.right - (style.border ? style.border.width * 2 : 0));
|
|
1595
|
+
const b = node.bindings.source;
|
|
1596
|
+
const hits = [];
|
|
1597
|
+
const svg3 = data.categories.length ? renderChart(data, { type: p.chartType, width: innerWidth, height: p.height, theme, fontFamily: style.fontFamily, showLegend: p.showLegend, showValues: p.showValues, showGrid: p.showGrid, stacked: p.stacked, smooth: p.smooth, formatValue, formatPercent, innerLabel: typeof p.innerLabel === "string" && p.innerLabel ? p.innerLabel : void 0, hits }) : h3("div", { class: "bp-chart__empty", style: { height: `${p.height}px`, display: "flex", "align-items": "center", "justify-content": "center", color: theme.colors.mutedText, border: `1px dashed ${theme.colors.border}`, "border-radius": `${theme.radius ?? 6}px` } }, mode === "design" ? b ? "No data in the list" : "Choose a list to chart" : "");
|
|
1598
|
+
return h3(
|
|
1599
|
+
"div",
|
|
1600
|
+
{
|
|
1601
|
+
class: ["bp-chart", `bp-chart--${p.chartType}`],
|
|
1602
|
+
style: cssText5(css),
|
|
1603
|
+
...hits.length ? { "data-bp-chart": JSON.stringify({ type: p.chartType, title: typeof p.title === "string" ? p.title : "", hits }) } : {}
|
|
1604
|
+
},
|
|
1605
|
+
p.title ? h3("div", { class: "bp-chart__title", style: { "font-family": theme.typography.headingFont, "font-weight": "600", "margin-bottom": "6px", color: theme.colors.text } }, text("title")) : null,
|
|
1606
|
+
mode === "design" && b ? h3("div", { class: "bp-chart__badge", contenteditable: "false" }, b.label) : null,
|
|
1607
|
+
svg3
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
});
|
|
1611
|
+
var progressBlock = defineBlock5({
|
|
1612
|
+
type: "progress",
|
|
1613
|
+
name: "Progress / gauge",
|
|
1614
|
+
description: "A value against a maximum, as a bar or gauge",
|
|
1615
|
+
category: "visualisation",
|
|
1616
|
+
icon: "gauge",
|
|
1617
|
+
keywords: ["score", "meter", "gauge", "progress", "percentage"],
|
|
1618
|
+
defaultProps: () => ({ value: "", max: 100, label: "Score", caption: "", variant: "bar", color: "$colors.primary", trackColor: "$colors.border", showValue: true, format: "percent", thresholds: [], height: 10 }),
|
|
1619
|
+
defaultTextStyle: "body",
|
|
1620
|
+
defaultFlow: { keepTogether: true },
|
|
1621
|
+
split: "none",
|
|
1622
|
+
styleGroups: ["typography", "spacing", "background", "border", "flow"],
|
|
1623
|
+
inspector: [
|
|
1624
|
+
{
|
|
1625
|
+
title: "Value",
|
|
1626
|
+
fields: [
|
|
1627
|
+
{ kind: "binding", prop: "value", label: "Value", valueType: ["number", "percent", "currency"] },
|
|
1628
|
+
{ kind: "number", prop: "max", label: "Maximum", min: 1e-4, max: 1e12 },
|
|
1629
|
+
{ kind: "text", prop: "label", label: "Label" },
|
|
1630
|
+
{ kind: "text", prop: "caption", label: "Caption" }
|
|
1631
|
+
]
|
|
1632
|
+
},
|
|
1633
|
+
{
|
|
1634
|
+
title: "Appearance",
|
|
1635
|
+
fields: [
|
|
1636
|
+
{
|
|
1637
|
+
kind: "segmented",
|
|
1638
|
+
prop: "variant",
|
|
1639
|
+
label: "Style",
|
|
1640
|
+
options: [
|
|
1641
|
+
{ value: "bar", label: "Bar" },
|
|
1642
|
+
{ value: "gauge", label: "Gauge" }
|
|
1643
|
+
]
|
|
1644
|
+
},
|
|
1645
|
+
{
|
|
1646
|
+
kind: "segmented",
|
|
1647
|
+
prop: "format",
|
|
1648
|
+
label: "Show as",
|
|
1649
|
+
options: [
|
|
1650
|
+
{ value: "percent", label: "%" },
|
|
1651
|
+
{ value: "value", label: "Value" },
|
|
1652
|
+
{ value: "fraction", label: "x / max" }
|
|
1653
|
+
]
|
|
1654
|
+
},
|
|
1655
|
+
{ kind: "toggle", prop: "showValue", label: "Show value" },
|
|
1656
|
+
{ kind: "color", prop: "color", label: "Colour" },
|
|
1657
|
+
{ kind: "color", prop: "trackColor", label: "Track" },
|
|
1658
|
+
{ kind: "number", prop: "height", label: "Bar height", min: 2, max: 40, unit: "px", when: (p) => p.variant === "bar" },
|
|
1659
|
+
{ kind: "list", prop: "thresholds", label: "Colour by range", itemLabel: "Range", fullWidth: true, fields: [
|
|
1660
|
+
{ kind: "number", prop: "upTo", label: "Up to" },
|
|
1661
|
+
{ kind: "color", prop: "color", label: "Colour" }
|
|
1662
|
+
] }
|
|
1663
|
+
]
|
|
1664
|
+
}
|
|
1665
|
+
],
|
|
1666
|
+
render: ({ h: h3, css, props, theme, text, mode, node, format }) => {
|
|
1667
|
+
const raw = toNumber2(props.value);
|
|
1668
|
+
const max = Number(props.max) || 100;
|
|
1669
|
+
const value = raw ?? 0;
|
|
1670
|
+
const frac = Math.max(0, Math.min(1, max > 0 ? value / max : 0));
|
|
1671
|
+
let color = resolveValue4(props.color, theme, theme.colors.primary);
|
|
1672
|
+
const sorted = [...props.thresholds ?? []].sort((a, b2) => a.upTo - b2.upTo);
|
|
1673
|
+
for (const t of sorted) {
|
|
1674
|
+
if (value <= t.upTo) {
|
|
1675
|
+
color = resolveValue4(t.color, theme, color);
|
|
1676
|
+
break;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
const track = resolveValue4(props.trackColor, theme, theme.colors.border);
|
|
1680
|
+
const b = node.bindings.value;
|
|
1681
|
+
const display = props.format === "percent" ? new Intl.NumberFormat(format.locale, { style: "percent", maximumFractionDigits: 0 }).format(frac) : props.format === "fraction" ? `${new Intl.NumberFormat(format.locale).format(value)} / ${new Intl.NumberFormat(format.locale).format(max)}` : new Intl.NumberFormat(format.locale, { maximumFractionDigits: 2 }).format(value);
|
|
1682
|
+
const header = h3("div", { style: { display: "flex", "justify-content": "space-between", "align-items": "baseline", gap: "8px", "margin-bottom": "4px" } }, h3("span", { style: { "font-weight": "600" } }, text("label")), props.showValue ? h3("span", { class: ["bp-progress__value", mode === "design" && b ? "bp-cell--bound" : ""], title: b?.expr, style: { color: theme.colors.mutedText, "font-variant-numeric": "tabular-nums" } }, display) : null);
|
|
1683
|
+
if (props.variant === "gauge") {
|
|
1684
|
+
const w = 160;
|
|
1685
|
+
const r = 62;
|
|
1686
|
+
const cx = w / 2;
|
|
1687
|
+
const cy = 70;
|
|
1688
|
+
const arcPath = (f) => {
|
|
1689
|
+
const a0 = Math.PI;
|
|
1690
|
+
const sweep = Math.PI * f;
|
|
1691
|
+
const a1 = a0 + sweep;
|
|
1692
|
+
const x0 = cx + r * Math.cos(a0);
|
|
1693
|
+
const y0 = cy + r * Math.sin(a0);
|
|
1694
|
+
const x1 = cx + r * Math.cos(a1);
|
|
1695
|
+
const y1 = cy + r * Math.sin(a1);
|
|
1696
|
+
const large = sweep > Math.PI ? 1 : 0;
|
|
1697
|
+
return `M ${x0} ${y0} A ${r} ${r} 0 ${large} 1 ${x1} ${y1}`;
|
|
1698
|
+
};
|
|
1699
|
+
return h3(
|
|
1700
|
+
"div",
|
|
1701
|
+
{ class: "bp-progress bp-progress--gauge", style: cssText5({ ...css, display: "flex", "flex-direction": "column", "align-items": "center" }) },
|
|
1702
|
+
h3("svg", { width: w, height: 80, viewBox: `0 0 ${w} 80`, style: { display: "block" } }, h3("path", { d: arcPath(1), fill: "none", stroke: track, "stroke-width": 12, "stroke-linecap": "round" }), frac > 0 ? h3("path", { d: arcPath(frac), fill: "none", stroke: color, "stroke-width": 12, "stroke-linecap": "round" }) : null, h3("text", { x: cx, y: cy - 4, "text-anchor": "middle", "font-size": 20, "font-weight": "700", "font-family": theme.typography.headingFont, fill: theme.colors.text }, display)),
|
|
1703
|
+
h3("div", { style: { "font-weight": "600", "margin-top": "2px" } }, text("label")),
|
|
1704
|
+
props.caption ? h3("div", { style: { color: theme.colors.mutedText, "font-size": "0.9em" } }, text("caption")) : null
|
|
1705
|
+
);
|
|
1706
|
+
}
|
|
1707
|
+
return h3(
|
|
1708
|
+
"div",
|
|
1709
|
+
{ class: "bp-progress bp-progress--bar", style: cssText5(css) },
|
|
1710
|
+
header,
|
|
1711
|
+
// The fill's height is stated in pixels rather than as `100%`: a
|
|
1712
|
+
// percentage height needs a parent the layout engine considers to have a
|
|
1713
|
+
// definite one, and the PDF engine does not, so the fill came out a
|
|
1714
|
+
// hairline in the file while looking right in the browser. The track's
|
|
1715
|
+
// own height is the number to use, and it is right here.
|
|
1716
|
+
h3("div", { class: "bp-progress__track", style: { height: `${props.height}px`, background: track, "border-radius": `${props.height}px`, overflow: "hidden" } }, h3("div", { class: "bp-progress__fill", style: { width: `${frac * 100}%`, height: `${props.height}px`, background: color, "border-radius": `${props.height}px` } })),
|
|
1717
|
+
props.caption ? h3("div", { style: { color: theme.colors.mutedText, "font-size": "0.9em", "margin-top": "4px" } }, text("caption")) : null
|
|
1718
|
+
);
|
|
1719
|
+
}
|
|
1720
|
+
});
|
|
1721
|
+
|
|
1722
|
+
// src/index.ts
|
|
1723
|
+
var builtInBlocks = [
|
|
1724
|
+
// structure
|
|
1725
|
+
sectionBlock,
|
|
1726
|
+
rowBlock,
|
|
1727
|
+
columnBlock,
|
|
1728
|
+
repeaterItemBlock,
|
|
1729
|
+
// content
|
|
1730
|
+
headingBlock,
|
|
1731
|
+
textBlock,
|
|
1732
|
+
imageBlock,
|
|
1733
|
+
logoBlock,
|
|
1734
|
+
dividerBlock,
|
|
1735
|
+
spacerBlock,
|
|
1736
|
+
buttonBlock,
|
|
1737
|
+
calloutBlock,
|
|
1738
|
+
quoteBlock,
|
|
1739
|
+
signatureBlock,
|
|
1740
|
+
dateBlock,
|
|
1741
|
+
pageNumberBlock,
|
|
1742
|
+
timestampBlock,
|
|
1743
|
+
// layout
|
|
1744
|
+
containerBlock,
|
|
1745
|
+
cardBlock,
|
|
1746
|
+
pageBreakBlock,
|
|
1747
|
+
keepTogetherBlock,
|
|
1748
|
+
// data
|
|
1749
|
+
fieldBlock,
|
|
1750
|
+
kpiBlock,
|
|
1751
|
+
keyValueListBlock,
|
|
1752
|
+
conditionalBlock,
|
|
1753
|
+
repeaterBlock,
|
|
1754
|
+
tableBlock,
|
|
1755
|
+
// visualisation
|
|
1756
|
+
chartBlock,
|
|
1757
|
+
progressBlock
|
|
1758
|
+
];
|
|
1759
|
+
function createRegistry(custom = [], opts = {}) {
|
|
1760
|
+
return new BlockRegistry([...builtInBlocks, ...custom], opts);
|
|
1761
|
+
}
|
|
1762
|
+
export {
|
|
1763
|
+
builtInBlocks,
|
|
1764
|
+
buttonBlock,
|
|
1765
|
+
calloutBlock,
|
|
1766
|
+
cardBlock,
|
|
1767
|
+
chartBlock,
|
|
1768
|
+
colorAt,
|
|
1769
|
+
columnBlock,
|
|
1770
|
+
conditionalBlock,
|
|
1771
|
+
containerBlock,
|
|
1772
|
+
createRegistry,
|
|
1773
|
+
dateBlock,
|
|
1774
|
+
dividerBlock,
|
|
1775
|
+
fieldBlock,
|
|
1776
|
+
headingBlock,
|
|
1777
|
+
imageBlock,
|
|
1778
|
+
keepTogetherBlock,
|
|
1779
|
+
keyValueListBlock,
|
|
1780
|
+
kpiBlock,
|
|
1781
|
+
logoBlock,
|
|
1782
|
+
mixWithWhite,
|
|
1783
|
+
pageBreakBlock,
|
|
1784
|
+
pageNumberBlock,
|
|
1785
|
+
progressBlock,
|
|
1786
|
+
quoteBlock,
|
|
1787
|
+
renderChart,
|
|
1788
|
+
repeaterBlock,
|
|
1789
|
+
repeaterItemBlock,
|
|
1790
|
+
rowBlock,
|
|
1791
|
+
sectionBlock,
|
|
1792
|
+
signatureBlock,
|
|
1793
|
+
spacerBlock,
|
|
1794
|
+
stack,
|
|
1795
|
+
tableBlock,
|
|
1796
|
+
textBlock,
|
|
1797
|
+
timestampBlock
|
|
1798
|
+
};
|
|
1799
|
+
//# sourceMappingURL=index.js.map
|