@lowdefy/blocks-basic 0.0.0-experimental-20260829192306 → 0.0.0-experimental-20260830202815

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.
@@ -21,8 +21,11 @@ const Box = ({ blockId, classNames, content, events, methods, properties, styles
21
21
  onClick: ()=>methods.triggerEvent({
22
22
  name: 'onClick'
23
23
  }),
24
- onPaste: ()=>methods.triggerEvent({
25
- name: 'onPaste'
24
+ onPaste: (e)=>methods.triggerEvent({
25
+ name: 'onPaste',
26
+ event: {
27
+ text: e.clipboardData.getData('text')
28
+ }
26
29
  }),
27
30
  className: classNames?.element,
28
31
  style: {
@@ -24,7 +24,19 @@
24
24
  },
25
25
  events: {
26
26
  onClick: 'Trigger actions when the Box is clicked.',
27
- onPaste: 'Trigger actions when the element is focused and a paste event is triggered.'
27
+ onPaste: {
28
+ description: 'Trigger actions when the element is focused and a paste event is triggered.',
29
+ payload: {
30
+ type: 'object',
31
+ additionalProperties: false,
32
+ properties: {
33
+ text: {
34
+ type: 'string',
35
+ description: 'The pasted plain text.'
36
+ }
37
+ }
38
+ }
39
+ }
28
40
  },
29
41
  properties: {
30
42
  type: 'object',
@@ -19,6 +19,18 @@
19
19
  cssKeys: {
20
20
  element: 'The DangerousHtml element.'
21
21
  },
22
+ hazards: [
23
+ {
24
+ id: 'dangerous-html-unsanitised',
25
+ message: 'Content is rendered without sanitising, so never pass caller-supplied or database HTML through it. Use Html for anything a user could have written.',
26
+ see: 'display-blocks/dangeroushtml'
27
+ },
28
+ {
29
+ id: 'dompurify-options-first-render',
30
+ message: 'DOMPurifyOptions is read once when the block mounts, so a value that changes after the first render has no effect. Set it statically or remount the block.',
31
+ see: 'display-blocks/dangeroushtml'
32
+ }
33
+ ],
22
34
  properties: {
23
35
  type: 'object',
24
36
  additionalProperties: false,
@@ -19,8 +19,27 @@
19
19
  cssKeys: {
20
20
  element: 'The Html element.'
21
21
  },
22
+ hazards: [
23
+ {
24
+ id: 'html-style-stripped',
25
+ message: 'Content is sanitised with DOMPurify, so <style>, <script> and inline event handlers are removed before render. Use DangerousHtml for a trusted <style> block, or style the block through its style and class properties.',
26
+ see: 'display-blocks/html'
27
+ }
28
+ ],
22
29
  events: {
23
- onTextSelection: 'Trigger action when text is selected and pass selected text to the event object.'
30
+ onTextSelection: {
31
+ description: 'Trigger action when text is selected.',
32
+ payload: {
33
+ type: 'object',
34
+ additionalProperties: false,
35
+ properties: {
36
+ selection: {
37
+ type: 'string',
38
+ description: 'The selected text.'
39
+ }
40
+ }
41
+ }
42
+ }
24
43
  },
25
44
  properties: {
26
45
  type: 'object',
@@ -0,0 +1,64 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import React, { useLayoutEffect, useRef, useState } from 'react';
16
+ import { createPortal } from 'react-dom';
17
+ import DOMPurify from 'dompurify';
18
+ import { withBlockDefaults } from '@lowdefy/block-utils';
19
+ import { type } from '@lowdefy/helpers';
20
+ import { createTemplateFunction } from '@lowdefy/nunjucks';
21
+ function renderTemplateHtml({ template, context }) {
22
+ if (type.isNone(template)) return '';
23
+ const render = createTemplateFunction(template);
24
+ const value = type.isObject(context) ? context : {
25
+ value: context
26
+ };
27
+ // DOMPurify keeps data-* attributes, so the {% slot %} markers survive; <style> and <script>
28
+ // do not, which is why CSS goes through properties.css.
29
+ return DOMPurify.sanitize(render(value));
30
+ }
31
+ const TemplateBlock = ({ blockId, classNames, content = {}, properties, styles })=>{
32
+ const containerRef = useRef(null);
33
+ const [slotNodes, setSlotNodes] = useState({});
34
+ const html = renderTemplateHtml({
35
+ template: properties.template,
36
+ context: properties.context
37
+ });
38
+ // Assigning innerHTML destroys the previous slot containers, so the portal targets are
39
+ // re-read from the new DOM on every change of the rendered html.
40
+ useLayoutEffect(()=>{
41
+ const el = containerRef.current;
42
+ el.innerHTML = html;
43
+ const nodes = {};
44
+ el.querySelectorAll('[data-ldf-slot]').forEach((node)=>{
45
+ nodes[node.getAttribute('data-ldf-slot')] = node;
46
+ });
47
+ setSlotNodes(nodes);
48
+ }, [
49
+ html
50
+ ]);
51
+ const hasCss = type.isString(properties.css) && properties.css.trim() !== '';
52
+ return /*#__PURE__*/ React.createElement("div", {
53
+ id: blockId,
54
+ "data-testid": blockId,
55
+ className: classNames?.element,
56
+ style: styles?.element
57
+ }, hasCss && /*#__PURE__*/ React.createElement("style", null, `#bl-${blockId} { ${properties.css} }`), /*#__PURE__*/ React.createElement("div", {
58
+ ref: containerRef
59
+ }), Object.keys(slotNodes).map((name)=>{
60
+ if (!content[name]) return null;
61
+ return /*#__PURE__*/ createPortal(content[name](), slotNodes[name], name);
62
+ }));
63
+ };
64
+ export default withBlockDefaults(TemplateBlock);
@@ -0,0 +1,23 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import { createBlockHelper, escapeId } from '@lowdefy/e2e-utils';
16
+ import { expect } from '@playwright/test';
17
+ const locator = (page, blockId)=>page.locator(`#${escapeId(blockId)}`);
18
+ export default createBlockHelper({
19
+ locator,
20
+ expect: {
21
+ containsText: (page, blockId, text)=>expect(locator(page, blockId)).toContainText(text)
22
+ }
23
+ });
@@ -0,0 +1,143 @@
1
+ # Copyright 2020-2026 Lowdefy, Inc
2
+
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ - title: Basic Template
16
+ blocks:
17
+ - id: basic_plain
18
+ type: Template
19
+ properties:
20
+ context:
21
+ name: Lowdefy
22
+ template: '<p>Hello <strong>{{ name }}</strong> from a Template block.</p>'
23
+ - id: basic_primitive_context
24
+ type: Template
25
+ properties:
26
+ context: 42
27
+ template: '<p>A primitive context is available as <code>value</code>: {{ value }}</p>'
28
+ - id: basic_conditional
29
+ type: Template
30
+ properties:
31
+ context:
32
+ status: active
33
+ template: |
34
+ {% if status == "active" %}<p>Status is <strong>active</strong>.</p>{% else %}<p>Inactive.</p>{% endif %}
35
+
36
+ - title: Escaping
37
+ blocks:
38
+ - id: escaping_default
39
+ type: Template
40
+ properties:
41
+ context:
42
+ untrusted: '<img src=x onerror=alert(1)><b>bold?</b>'
43
+ template: '<p>Escaped by default: {{ untrusted }}</p>'
44
+ - id: escaping_safe
45
+ type: Template
46
+ properties:
47
+ context:
48
+ markup: 'Trusted <b>bold</b> and <em>italic</em> markup'
49
+ template: '<p>With <code>| safe</code>: {{ markup | safe }}</p>'
50
+
51
+ - title: Loops
52
+ blocks:
53
+ - id: loop_rows
54
+ type: Template
55
+ properties:
56
+ context:
57
+ rows:
58
+ - label: Name
59
+ value: Ada Lovelace
60
+ - label: Born
61
+ value: 1815
62
+ - label: Field
63
+ value: Mathematics
64
+ css: |
65
+ .row { display: flex; gap: 8px; padding: 4px 0; border-bottom: 1px solid var(--ant-color-border); }
66
+ .row .label { color: var(--ant-color-text-secondary); min-width: 60px; }
67
+ template: |
68
+ {% for row in rows %}
69
+ <div class="row"><span class="label">{{ row.label }}</span><span>{{ row.value }}</span></div>
70
+ {% endfor %}
71
+ - id: loop_list
72
+ type: Template
73
+ properties:
74
+ context:
75
+ items: [alpha, beta, gamma]
76
+ template: |
77
+ <ol>{% for item in items %}<li>{{ loop.index }}. {{ item }}</li>{% endfor %}</ol>
78
+
79
+ - title: Scoped Style
80
+ blocks:
81
+ - id: scoped_style_red
82
+ type: Template
83
+ properties:
84
+ css: |
85
+ color: var(--ant-color-error);
86
+ .tag { background: var(--ant-color-error-bg); padding: 2px 6px; border-radius: 4px; }
87
+ template: '<p>This block has a red <span class="tag">tag</span> style.</p>'
88
+ - id: scoped_style_sibling
89
+ type: Template
90
+ properties:
91
+ template: '<p>This sibling block shares the <span class="tag">tag</span> class but is not styled by the block above.</p>'
92
+
93
+ - title: Tailwind Classes
94
+ blocks:
95
+ - id: tailwind_card
96
+ type: Template
97
+ properties:
98
+ context:
99
+ title: Tailwind in a template
100
+ template: |
101
+ <div class="rounded-lg border border-gray-300 p-4 shadow-sm">
102
+ <h3 class="text-lg font-semibold">{{ title }}</h3>
103
+ <p class="text-sm text-gray-500">Classes written in the template are scanned by Tailwind at build.</p>
104
+ </div>
105
+
106
+ - title: Slots
107
+ blocks:
108
+ - id: slot_card
109
+ type: Template
110
+ properties:
111
+ context:
112
+ title: Answer card
113
+ summary: A templated card with a real Button in its footer slot.
114
+ css: |
115
+ .card { border: 1px solid var(--ant-color-border); border-radius: 8px; padding: 16px; }
116
+ .footer { margin-top: 12px; display: flex; justify-content: flex-end; }
117
+ template: |
118
+ <div class="card">
119
+ <h3>{{ title }}</h3>
120
+ <p>{{ summary }}</p>
121
+ <div class="footer">{% slot "footer" %}</div>
122
+ </div>
123
+ slots:
124
+ footer:
125
+ blocks:
126
+ - id: slot_card_approve
127
+ type: Button
128
+ properties:
129
+ title: Approve
130
+ events:
131
+ onClick:
132
+ - id: set_approved
133
+ type: SetState
134
+ params:
135
+ slot_card_approved: true
136
+ - id: slot_card_result
137
+ type: Template
138
+ properties:
139
+ context:
140
+ approved:
141
+ _state: slot_card_approved
142
+ template: |
143
+ <p>Approved: <strong>{{ approved | default(false, true) }}</strong></p>
@@ -0,0 +1,62 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ export default {
16
+ category: 'container',
17
+ icons: [],
18
+ valueType: null,
19
+ // Slot names are declared by the template's {% slot %} tags, so the meta cannot enumerate them.
20
+ slots: false,
21
+ cssKeys: {
22
+ element: 'The Template element.'
23
+ },
24
+ hazards: [
25
+ {
26
+ id: 'template-safe-filter-unescaped',
27
+ message: 'Every {{ value }} is HTML-escaped. The "| safe" filter turns escaping off for that value, so never apply it to caller-supplied or database strings - only to markup the app itself produced.',
28
+ see: 'container-blocks/template'
29
+ },
30
+ {
31
+ id: 'template-css-is-text',
32
+ message: 'properties.css is CSS text scoped to this block (nested under #bl-<blockId>). The style key stays an object of CSS properties per cssKey; a <style> tag inside the template is stripped.',
33
+ see: 'container-blocks/template'
34
+ }
35
+ ],
36
+ properties: {
37
+ type: 'object',
38
+ additionalProperties: false,
39
+ properties: {
40
+ template: {
41
+ type: 'string',
42
+ description: 'Nunjucks template source. {{ value }} is escaped, {{ value | safe }} is not, and {% slot "name" %} places the blocks configured under slots.name.',
43
+ docs: {
44
+ displayType: 'text-area'
45
+ }
46
+ },
47
+ context: {
48
+ description: 'Values available to the template. Objects are spread into the template scope; a primitive is available as "value".',
49
+ docs: {
50
+ displayType: 'yaml'
51
+ }
52
+ },
53
+ css: {
54
+ type: 'string',
55
+ description: 'CSS text applied to this block only. Rules are nested under the block wrapper selector, so ".row { ... }" styles only .row elements inside this block.',
56
+ docs: {
57
+ displayType: 'text-area'
58
+ }
59
+ }
60
+ }
61
+ }
62
+ };
@@ -0,0 +1,82 @@
1
+ # Copyright 2020-2026 Lowdefy, Inc
2
+
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ id: template
16
+ type: Box
17
+
18
+ blocks:
19
+ - id: template_basic
20
+ type: Template
21
+ properties:
22
+ context:
23
+ name: World
24
+ template: '<p>Hello {{ name }}</p>'
25
+
26
+ - id: template_escaped
27
+ type: Template
28
+ properties:
29
+ context:
30
+ html: '<b>bold</b>'
31
+ template: '<p>{{ html }}</p>'
32
+
33
+ - id: template_safe
34
+ type: Template
35
+ properties:
36
+ context:
37
+ html: '<b>bold</b>'
38
+ template: '<p>{{ html | safe }}</p>'
39
+
40
+ - id: template_loop
41
+ type: Template
42
+ properties:
43
+ context:
44
+ items: [one, two, three]
45
+ template: '<ul>{% for item in items %}<li>{{ item }}</li>{% endfor %}</ul>'
46
+
47
+ - id: template_scoped_style
48
+ type: Template
49
+ properties:
50
+ css: '.row { color: rgb(255, 0, 0); }'
51
+ template: '<p class="row">Red row</p>'
52
+
53
+ - id: template_scoped_style_sibling
54
+ type: Template
55
+ properties:
56
+ template: '<p class="row">Not red row</p>'
57
+
58
+ - id: template_slot
59
+ type: Template
60
+ properties:
61
+ template: '<div class="card"><p>Card</p>{% slot "footer" %}</div>'
62
+ slots:
63
+ footer:
64
+ blocks:
65
+ - id: template_slot_button
66
+ type: Button
67
+ properties:
68
+ title: Approve
69
+ events:
70
+ onClick:
71
+ - id: set_clicked
72
+ type: SetState
73
+ params:
74
+ template_clicked: yes
75
+
76
+ - id: template_slot_result
77
+ type: Template
78
+ properties:
79
+ context:
80
+ clicked:
81
+ _state: template_clicked
82
+ template: '<p>Clicked: {{ clicked | default("no", true) }}</p>'
package/dist/blocks.js CHANGED
@@ -21,4 +21,5 @@ export { default as Icon } from './blocks/Icon/Icon.js';
21
21
  export { default as Img } from './blocks/Img/Img.js';
22
22
  export { default as List } from './blocks/List/List.js';
23
23
  export { default as Span } from './blocks/Span/Span.js';
24
+ export { default as Template } from './blocks/Template/Template.js';
24
25
  export { default as Throw } from './blocks/Throw/Throw.js';
package/dist/e2e.js CHANGED
@@ -21,4 +21,5 @@ export { default as Icon } from './blocks/Icon/e2e.js';
21
21
  export { default as Img } from './blocks/Img/e2e.js';
22
22
  export { default as List } from './blocks/List/e2e.js';
23
23
  export { default as Span } from './blocks/Span/e2e.js';
24
+ export { default as Template } from './blocks/Template/e2e.js';
24
25
  export { default as Throw } from './blocks/Throw/e2e.js';
package/dist/metas.js CHANGED
@@ -21,4 +21,5 @@ export { default as Icon } from './blocks/Icon/meta.js';
21
21
  export { default as Img } from './blocks/Img/meta.js';
22
22
  export { default as List } from './blocks/List/meta.js';
23
23
  export { default as Span } from './blocks/Span/meta.js';
24
+ export { default as Template } from './blocks/Template/meta.js';
24
25
  export { default as Throw } from './blocks/Throw/meta.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lowdefy/blocks-basic",
3
- "version": "0.0.0-experimental-20260829192306",
3
+ "version": "0.0.0-experimental-20260830202815",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Basic html Lowdefy blocks.",
6
6
  "homepage": "https://lowdefy.com",
@@ -42,8 +42,9 @@
42
42
  "dist/*"
43
43
  ],
44
44
  "dependencies": {
45
- "@lowdefy/block-utils": "0.0.0-experimental-20260829192306",
46
- "@lowdefy/helpers": "0.0.0-experimental-20260829192306",
45
+ "@lowdefy/block-utils": "0.0.0-experimental-20260830202815",
46
+ "@lowdefy/helpers": "0.0.0-experimental-20260830202815",
47
+ "@lowdefy/nunjucks": "0.0.0-experimental-20260830202815",
47
48
  "dompurify": "3.4.2"
48
49
  },
49
50
  "peerDependencies": {
@@ -51,8 +52,8 @@
51
52
  "react-dom": ">=18"
52
53
  },
53
54
  "devDependencies": {
54
- "@lowdefy/block-dev-e2e": "0.0.0-experimental-20260829192306",
55
- "@lowdefy/e2e-utils": "0.0.0-experimental-20260829192306",
55
+ "@lowdefy/block-dev-e2e": "0.0.0-experimental-20260830202815",
56
+ "@lowdefy/e2e-utils": "0.0.0-experimental-20260830202815",
56
57
  "@playwright/test": "1.59.1",
57
58
  "@swc/cli": "0.8.1",
58
59
  "@swc/core": "1.15.32",