@lowdefy/blocks-basic 0.0.0-experimental-20260928105334 → 0.0.0-experimental-20260929101721

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/blocks/Anchor/Anchor.js +1 -1
  2. package/dist/blocks/Anchor/gallery.yaml +14 -14
  3. package/dist/blocks/Anchor/meta.js +4 -2
  4. package/dist/blocks/Anchor/tests/Anchor.e2e.yaml +1 -1
  5. package/dist/blocks/Anchor/tests.yaml +6 -6
  6. package/dist/blocks/Box/Box.js +9 -5
  7. package/dist/blocks/Box/gallery.yaml +9 -9
  8. package/dist/blocks/Box/tests/Box.e2e.yaml +62 -0
  9. package/dist/blocks/ClickableHtml/ClickableHtml.js +9 -28
  10. package/dist/blocks/ClickableHtml/gallery.yaml +86 -0
  11. package/dist/blocks/ClickableHtml/meta.js +57 -1
  12. package/dist/blocks/ClickableHtml/tests/ClickableHtml.e2e.yaml +119 -0
  13. package/dist/blocks/ClickableHtml/tests.yaml +13 -0
  14. package/dist/blocks/DangerousHtml/DangerousHtml.js +19 -19
  15. package/dist/blocks/DangerousHtml/meta.js +13 -1
  16. package/dist/blocks/Dynamic/Dynamic.js +5 -3
  17. package/dist/blocks/Dynamic/meta.js +4 -0
  18. package/dist/blocks/Html/gallery.yaml +28 -0
  19. package/dist/blocks/Html/meta.js +23 -1
  20. package/dist/blocks/Html/tests/Html.e2e.yaml +12 -0
  21. package/dist/blocks/Icon/Icon.js +21 -1
  22. package/dist/blocks/Icon/gallery.yaml +158 -64
  23. package/dist/blocks/Icon/meta.js +11 -4
  24. package/dist/blocks/Icon/tests/Icon.e2e.yaml +3 -3
  25. package/dist/blocks/Icon/tests.yaml +7 -7
  26. package/dist/blocks/Img/meta.js +2 -0
  27. package/dist/blocks/List/gallery.yaml +11 -11
  28. package/dist/blocks/PollingTimer/PollingTimer.js +120 -0
  29. package/dist/blocks/PollingTimer/e2e.js +21 -0
  30. package/dist/blocks/PollingTimer/gallery.yaml +190 -0
  31. package/dist/blocks/PollingTimer/meta.js +61 -0
  32. package/dist/blocks/PollingTimer/validateInterval.js +21 -0
  33. package/dist/blocks/Span/Span.js +5 -3
  34. package/dist/blocks/Span/gallery.yaml +7 -7
  35. package/dist/blocks.js +1 -0
  36. package/dist/e2e.js +1 -0
  37. package/dist/metas.js +1 -0
  38. package/package.json +8 -5
@@ -18,6 +18,9 @@ blocks:
18
18
  - id: clickable_basic
19
19
  type: ClickableHtml
20
20
  properties:
21
+ dataEvents:
22
+ - onGreetClick
23
+ - onWaveClick
21
24
  html: '<p>Hello <button data-event="onGreetClick" data-user-id="u_1">World</button> <button data-event="onWaveClick" data-user-id="u_2">Wave</button></p>'
22
25
  events:
23
26
  onGreetClick:
@@ -67,3 +70,119 @@ blocks:
67
70
  type: ClickableHtml
68
71
  properties:
69
72
  html: <p>Safe content</p><script>alert('xss')</script>
73
+
74
+ - id: clickable_confirm
75
+ type: ClickableHtml
76
+ properties:
77
+ dataEvents:
78
+ - onDelete
79
+ html: '<button id="confirm_delete" data-event="onDelete" data-row-id="7" data-confirm="Delete row 7?">Delete</button> <button id="proto_event" data-event="__proto__">Proto</button>'
80
+ events:
81
+ onDelete:
82
+ - id: count_delete
83
+ type: SetState
84
+ params:
85
+ delete_count:
86
+ _sum:
87
+ - _state:
88
+ key: delete_count
89
+ default: 0
90
+ - 1
91
+
92
+ - id: clickable_confirm_result
93
+ type: Html
94
+ properties:
95
+ html:
96
+ _string.concat:
97
+ - '<span id="delete_count">'
98
+ - _json.stringify:
99
+ - _state:
100
+ key: delete_count
101
+ default: 0
102
+ - '</span>'
103
+
104
+ - id: clickable_allowlist
105
+ type: ClickableHtml
106
+ properties:
107
+ dataEvents:
108
+ - onListedClick
109
+ html: '<span id="listed_target" data-event="onListedClick">Listed</span> <span id="unlisted_target" data-event="onUnlistedClick">Unlisted</span>'
110
+ events:
111
+ onListedClick:
112
+ - id: set_listed
113
+ type: SetState
114
+ params:
115
+ allowlist_fired: listed
116
+ onUnlistedClick:
117
+ - id: set_unlisted
118
+ type: SetState
119
+ params:
120
+ allowlist_fired: unlisted
121
+
122
+ - id: clickable_declared_confirm
123
+ type: ClickableHtml
124
+ properties:
125
+ dataEvents:
126
+ - name: onRemove
127
+ confirm: Remove row 3?
128
+ html: '<button id="declared_remove" data-event="onRemove" data-row-id="3">Remove</button>'
129
+ events:
130
+ onRemove:
131
+ - id: set_removed
132
+ type: SetState
133
+ params:
134
+ removed_row:
135
+ _event: row_id
136
+
137
+ - id: clickable_refresh
138
+ type: ClickableHtml
139
+ properties:
140
+ dataEvents:
141
+ - onRefreshSoon
142
+ - onRefreshDelete
143
+ html:
144
+ _string.concat:
145
+ - '<button id="refresh_soon" data-event="onRefreshSoon">Refresh soon</button><p id="refresh_count">Refreshed '
146
+ - _state:
147
+ key: refresh_count
148
+ default: 0
149
+ - '</p><button id="refresh_delete" data-event="onRefreshDelete" data-id="5" data-confirm="Delete 5?">Delete</button> <span id="refresh_more" data-popover="more">More</span><div data-popover-content="more" hidden><span class="refresh-menu">Menu after '
150
+ - _state:
151
+ key: refresh_count
152
+ default: 0
153
+ - '</span></div>'
154
+ events:
155
+ onRefreshSoon:
156
+ - id: wait
157
+ type: Wait
158
+ params:
159
+ ms: 1000
160
+ - id: refresh
161
+ type: SetState
162
+ params:
163
+ refresh_count: 1
164
+ onRefreshDelete:
165
+ - id: set_refresh_deleted
166
+ type: SetState
167
+ params:
168
+ refresh_deleted:
169
+ _event: id
170
+
171
+ - id: clickable_events_result
172
+ type: Html
173
+ properties:
174
+ html:
175
+ _string.concat:
176
+ - '<span id="allowlist_fired">'
177
+ - _state:
178
+ key: allowlist_fired
179
+ default: ''
180
+ - '</span><span id="removed_row">'
181
+ - _state:
182
+ key: removed_row
183
+ default: ''
184
+ - '</span><span id="refresh_deleted">'
185
+ - _state:
186
+ key: refresh_deleted
187
+ default: ''
188
+ - '</span>'
@@ -16,6 +16,8 @@
16
16
  - id: properties.html
17
17
  type: ClickableHtml
18
18
  properties:
19
+ dataEvents:
20
+ - onGoClick
19
21
  html: |
20
22
  <p>Plain markup <button data-event="onGoClick" data-item-id="1">Go</button></p>
21
23
  - id: properties.html-no-events
@@ -26,6 +28,8 @@
26
28
  - id: properties.html-bad-code
27
29
  type: ClickableHtml
28
30
  properties:
31
+ dataEvents:
32
+ - onOkClick
29
33
  html: |
30
34
  <button data-event="onOkClick" onclick="alert('inline handler stripped')">Ok</button>
31
35
  <script>alert('script tag');</script>
@@ -33,3 +37,12 @@
33
37
  type: ClickableHtml
34
38
  properties:
35
39
  html: 0
40
+ - id: properties.dataEvents
41
+ type: ClickableHtml
42
+ properties:
43
+ dataEvents:
44
+ - onEditClick
45
+ - name: onDeleteClick
46
+ confirm: Delete this row?
47
+ html: |
48
+ <p><button data-event="onEditClick">Edit</button> <button data-event="onDeleteClick">Delete</button></p>
@@ -13,39 +13,39 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import React from 'react';
16
- import DOMPurify from 'dompurify';
17
- import { withBlockDefaults } from '@lowdefy/block-utils';
18
- import { type } from '@lowdefy/helpers';
16
+ import { HtmlComponent, withBlockDefaults } from '@lowdefy/block-utils';
19
17
  let DangerousHtml = class DangerousHtml extends React.Component {
18
+ warnOnOptionsChange() {
19
+ const { DOMPurifyOptions } = this.props.properties;
20
+ if (!this.warnedOptionsChange && JSON.stringify(DOMPurifyOptions) !== JSON.stringify(this.DOMPurifyOptions)) {
21
+ this.warnedOptionsChange = true;
22
+ console.warn(`DangerousHtml block "${this.props.blockId}": DOMPurifyOptions changed after mount and the change is ignored. The sanitizer options are fixed at the first render; write them as a literal, not from state.`);
23
+ }
24
+ }
20
25
  componentDidMount() {
21
- const htmlString = type.isNone(this.props.properties.html) ? '' : this.props.properties.html.toString();
22
- this.div.innerHTML = DOMPurify.sanitize(htmlString, this.DOMPurifyOptions);
26
+ this.warnOnOptionsChange();
23
27
  }
24
28
  componentDidUpdate() {
25
- const htmlString = type.isNone(this.props.properties.html) ? '' : this.props.properties.html.toString();
26
- this.div.innerHTML = DOMPurify.sanitize(htmlString, this.DOMPurifyOptions);
29
+ this.warnOnOptionsChange();
27
30
  }
28
31
  render() {
29
- const { blockId, classNames, styles } = this.props;
30
- return /*#__PURE__*/ React.createElement("div", {
32
+ const { blockId, classNames, properties, styles } = this.props;
33
+ return /*#__PURE__*/ React.createElement(HtmlComponent, {
34
+ div: true,
35
+ html: properties.html,
31
36
  id: blockId,
32
- "data-testid": blockId,
33
- ref: (el)=>{
34
- if (el) {
35
- this.div = el;
36
- }
37
- },
38
37
  className: classNames?.element,
38
+ sanitizeOptions: this.DOMPurifyOptions,
39
39
  style: styles?.element
40
40
  });
41
41
  }
42
42
  constructor(props){
43
43
  super(props);
44
- this.div = {
45
- innerHTML: ''
46
- };
47
- // we do not revaluate DOMPurifyOptions improve options safety by not making options dynamic.
44
+ // The sanitizer options are fixed at mount on purpose: a later, operator-driven
45
+ // value could loosen the sanitizer from state. A change is refused loudly
46
+ // rather than ignored silently.
48
47
  this.DOMPurifyOptions = props.properties.DOMPurifyOptions;
48
+ this.warnedOptionsChange = false;
49
49
  }
50
50
  };
51
51
  export default withBlockDefaults(DangerousHtml);
@@ -19,13 +19,25 @@
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,
25
37
  properties: {
26
38
  html: {
27
39
  type: 'string',
28
- description: 'Content to be rendered as Html.',
40
+ description: 'Content to be rendered as Html. The Html block attributes (data-icon, data-tooltip, data-popover, data-page-id links, data-new-tab, data-tag, data-status, data-time, data-format, data-avatar, data-copy, data-truncate, data-tone) work here too, unless DOMPurifyOptions removes data attributes.',
29
41
  docs: {
30
42
  displayType: 'text-area'
31
43
  }
@@ -13,7 +13,7 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import React from 'react';
16
- import { withBlockDefaults } from '@lowdefy/block-utils';
16
+ import { isEventFromDomDescendant, withBlockDefaults } from '@lowdefy/block-utils';
17
17
  // Server-resolved container. The server fills the content slot at page get by
18
18
  // calling the configured api endpoint — by the time config reaches the client,
19
19
  // content holds the resolved blocks (or the fallback blocks on failure).
@@ -21,9 +21,11 @@ const Dynamic = ({ blockId, classNames, content, events, methods, styles })=>{
21
21
  return /*#__PURE__*/ React.createElement("div", {
22
22
  id: blockId,
23
23
  "data-testid": blockId,
24
- onClick: ()=>methods.triggerEvent({
24
+ onClick: (event)=>{
25
+ if (isEventFromDomDescendant(event)) methods.triggerEvent({
25
26
  name: 'onClick'
26
- }),
27
+ });
28
+ },
27
29
  className: classNames?.element,
28
30
  style: {
29
31
  outline: 'none',
@@ -38,6 +38,10 @@
38
38
  type: 'object',
39
39
  description: 'Static values passed to the endpoint payload as params. Operators are not allowed — read runtime values in the endpoint routine with _payload, _user or _secret.'
40
40
  },
41
+ policy: {
42
+ type: 'string',
43
+ description: 'Id of a policies.dynamicBlocks entry in lowdefy.yaml. The resolved content must pass the policy on every page get. The endpoint returns stored content as config by reading the blocks of a ValidateDynamic step with the same policy. Replaces types: the policy lists are bundled.'
44
+ },
41
45
  required: {
42
46
  type: 'boolean',
43
47
  default: false,
@@ -237,3 +237,31 @@
237
237
  border: 1px solid var(--ant-color-border)
238
238
  properties:
239
239
  html: '<h4 style="margin: 0 0 8px 0;">Elevated Block</h4><p style="margin: 0;">Uses bg-elevated token for a surface that adapts to light and dark mode.</p>'
240
+
241
+ - title: Icons, tooltips and popovers
242
+ blocks:
243
+ - id: html_icons
244
+ type: Html
245
+ properties:
246
+ html: '<p><i data-icon="success" style="color: var(--ant-color-success)"></i> Saved <span data-tooltip="Saved to the cloud 2 minutes ago" style="text-decoration: underline dotted">just now</span></p><p><i data-icon="warning" style="color: var(--ant-color-warning)"></i> 3 fields need attention. <span data-popover="fields" style="color: var(--ant-color-primary); cursor: pointer">Which? <i data-icon="chevron-down"></i></span></p><div data-popover-content="fields" hidden><ul style="margin: 0; padding-left: 16px"><li>Email</li><li>Phone</li><li>Address</li></ul></div>'
247
+
248
+ - title: Links and status tags
249
+ blocks:
250
+ - id: html_links_tags
251
+ type: Html
252
+ properties:
253
+ html: '<p><a data-page-id="Button">Button</a> moved to <span data-tag="success">Released</span> · <a data-page-id="theming" data-url-query="section=icons">Theming</a> is <span data-tag="warning">In review</span> · <a href="https://ant.design" data-new-tab>Ant Design</a> <i data-icon="external-link"></i></p><p><span data-status="success">Online</span> &nbsp; <span data-status="processing">Syncing</span> &nbsp; <span data-status="error">Offline</span></p><p><span data-tag>Design</span> <span data-tag>Engineering</span> <span data-tag>Support</span> <span data-tag="purple">Beta</span> <span data-tag="#0f766e">Custom colour</span></p>'
254
+
255
+ - title: Dates, numbers, avatars and copy
256
+ blocks:
257
+ - id: html_time_format
258
+ type: Html
259
+ properties:
260
+ html: '<p><span data-avatar="Jane Doe"></span> Jane Doe updated the invoice <time datetime="2026-01-05T09:30:00Z" data-time="relative"></time> (<time datetime="2026-01-05T09:30:00Z" data-time="D MMM YYYY"></time>) · <span data-format="currency" data-currency="USD">1234.5</span> · <span data-format="percent" data-decimals="1">0.123</span> of target · <span data-format="bytes">1536000</span> attached</p><p>Invoice number <code data-copy>INV-2026-0042</code> · owner <img data-avatar="Sam Smith" src="" style="--lf-avatar-size: 32px"></p>'
261
+
262
+ - title: Truncation and text tone
263
+ blocks:
264
+ - id: html_truncate_tone
265
+ type: Html
266
+ properties:
267
+ html: '<div style="max-width: 320px"><div data-truncate><b>Quarterly report for the northern region, including every branch and its targets</b></div><div data-truncate="2" data-tone="secondary">The report covers revenue, costs and headcount for each branch, compares them with the same quarter last year, and lists the actions each branch manager agreed for the next quarter.</div><div><span data-tone="tertiary">Updated 3 days ago</span> · <span data-tone="success">On track</span> · <span data-tone="quaternary">—</span></div></div>'
@@ -19,6 +19,28 @@
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
+ {
29
+ id: 'html-no-click-events',
30
+ message: 'Html fires no events from its markup: data-event attributes do nothing here. Use ClickableHtml for clickable elements, including icons.',
31
+ see: 'display-blocks/clickablehtml'
32
+ },
33
+ {
34
+ id: 'html-hardcoded-links',
35
+ message: 'Link to app pages with <a data-page-id="page-id" data-url-query="key=value">, not <a href="/page-id?key=value">. A hard-coded href reloads the whole app on click, ignores basePath and is not checked by the build. Use data-new-tab, not target="_blank", which sanitising removes.',
36
+ see: 'concepts/html-attributes'
37
+ },
38
+ {
39
+ id: 'html-inline-status-pills',
40
+ message: 'Show statuses with <span data-tag="success">Approved</span> or <span data-status="warning">Syncing</span>, not inline-styled pills or hex status colours. data-tag and data-status follow the theme and dark mode and match the ag-grid tag cells.',
41
+ see: 'concepts/html-attributes'
42
+ }
43
+ ],
22
44
  events: {
23
45
  onTextSelection: 'Trigger action when text is selected and pass selected text to the event object.'
24
46
  },
@@ -28,7 +50,7 @@
28
50
  properties: {
29
51
  html: {
30
52
  type: 'string',
31
- description: 'Content to be rendered as Html.',
53
+ description: 'Content to be rendered as Html. It understands these attributes: data-icon="edit" renders an icon (a semantic name like edit, delete or warning, or a Lucide icon name like Pencil); data-tooltip="Text" shows a tooltip on hover and focus; data-popover="id" toggles a popover showing the element with data-popover-content="id" (mark it hidden); <a data-page-id="page-id" data-url-query="key=value"> links to an app page without reloading (never hard-code href="/page"); data-link opts an existing app-relative <a href="/path"> into the same navigation; data-new-tab opens a link in a new tab (target is stripped by sanitising); data-tag="success" renders a tinted status tag and data-status="success" a status dot (tones success, processing, info, warning, error, default, an antd preset colour, a CSS colour, or any other value to seed a stable colour); <time datetime="ISO" data-time="relative"> renders a date as relative, date, datetime, time or a dayjs format; data-format="currency" data-currency="USD" formats the element\'s number as number, currency, percent, compact or bytes; data-avatar="Jane Doe" renders initials (on an <img>, a fallback for a broken image; never use an avatar image service); data-copy adds a copy button for the element\'s text or the attribute\'s value; data-truncate="2" clamps block text to 1–6 lines with the full text in a tooltip when cut off; data-tone="secondary" colours text with a theme tone (secondary, tertiary, quaternary, success, warning, error, info). See the HTML attributes docs page.',
32
54
  docs: {
33
55
  displayType: 'text-area'
34
56
  }
@@ -54,3 +54,15 @@ blocks:
54
54
  params:
55
55
  selected_text:
56
56
  _event: selectedText
57
+
58
+ - id: html_enhanced
59
+ type: Html
60
+ properties:
61
+ html: >-
62
+ <i id="enh_icon" data-icon="edit"></i>
63
+ <span id="enh_tooltip" data-tooltip="&lt;img src=x onerror=&quot;window.tooltipXss=1&quot;&gt;">Hover</span>
64
+ <span id="enh_popover" data-popover="more">More</span>
65
+ <div data-popover-content="more" hidden><b class="enh-popover-body">Popover body</b></div>
66
+ <a id="enh_page_link" data-page-id="clickablehtml" data-url-query="tab=a">Clickable</a>
67
+ <a id="enh_dot_link" href="/.//evil.example/x" data-link>Dot link</a>
68
+ <span id="enh_copy_long" data-copy="npm install lowdefy-package-with-a-long-name --save-dev">Install</span>
@@ -14,5 +14,25 @@
14
14
  limitations under the License.
15
15
  */ import React from 'react';
16
16
  import { withBlockDefaults } from '@lowdefy/block-utils';
17
- const IconBlock = ({ components: { Icon }, ...props })=>/*#__PURE__*/ React.createElement(Icon, props);
17
+ // An Icon block with an onClick event is a control, so it takes keyboard focus
18
+ // and Enter/Space fire the event like a click. The icon component other blocks
19
+ // render (inside a Button, an Alert) stays untouched: their block is the control.
20
+ const IconBlock = ({ components: { Icon }, events, methods, ...props })=>{
21
+ const keyboardProps = events.onClick ? {
22
+ tabIndex: 0,
23
+ onKeyDown: (event)=>{
24
+ if (event.key !== 'Enter' && event.key !== ' ') return;
25
+ event.preventDefault();
26
+ methods.triggerEvent({
27
+ name: 'onClick'
28
+ });
29
+ }
30
+ } : {};
31
+ return /*#__PURE__*/ React.createElement(Icon, {
32
+ events: events,
33
+ methods: methods,
34
+ ...keyboardProps,
35
+ ...props
36
+ });
37
+ };
18
38
  export default withBlockDefaults(IconBlock);