@lowdefy/blocks-basic 0.0.0-experimental-20260925135632 → 0.0.0-experimental-20260925210809

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.
@@ -65,7 +65,7 @@
65
65
  'string',
66
66
  'object'
67
67
  ],
68
- description: "Name of an React-Icon (See <a href='https://react-icons.github.io/react-icons/'>all icons</a>) or properties of an Icon block for anchor icon.",
68
+ description: "Icon name (a semantic name like edit, or a <a href='https://react-icons.github.io/react-icons/'>React Icons</a> name like LuPencil) or properties of an Icon block for anchor icon.",
69
69
  docs: {
70
70
  displayType: 'icon'
71
71
  }
@@ -14,32 +14,12 @@
14
14
  limitations under the License.
15
15
  */ import React from 'react';
16
16
  import { withBlockDefaults, HtmlComponent } from '@lowdefy/block-utils';
17
- // dataset keys are camelCase (data-record-id → recordId); config authors
18
- // write the attributes in kebab-case, so hand them back as snake_case.
19
- function toSnakeCase(key) {
20
- return key.replace(/[A-Z]/g, (match)=>`_${match.toLowerCase()}`);
21
- }
22
- const ClickableHtml = ({ blockId, classNames, events, properties, methods, styles })=>{
23
- // Each clickable element names the event it fires in its data-event
24
- // attribute (data-event="onEditClick" → events.onEditClick), so every
25
- // target in the markup has its own action chain. Its other data-*
26
- // attributes are the event object.
27
- function onClick(clickEvent) {
28
- const target = clickEvent.target.closest('[data-event]');
29
- if (!target || !clickEvent.currentTarget.contains(target)) return;
30
- const { event: name, ...data } = target.dataset;
31
- if (!name) return;
32
- clickEvent.preventDefault();
33
- const event = {};
34
- Object.keys(data).forEach((key)=>{
35
- event[toSnakeCase(key)] = data[key];
36
- });
37
- methods.triggerEvent({
38
- name,
39
- event
40
- });
41
- }
42
- return /*#__PURE__*/ React.createElement(HtmlComponent, {
17
+ // Each clickable element names the event it fires in its data-event attribute
18
+ // (data-event="onEditClick" → events.onEditClick), so every target in the
19
+ // markup has its own action chain. Its other data-* attributes are the event
20
+ // object. HtmlComponent finds the target, including inside data-popover
21
+ // content, and prevents the element's default action.
22
+ const ClickableHtml = ({ blockId, classNames, events, properties, methods, styles })=>/*#__PURE__*/ React.createElement(HtmlComponent, {
43
23
  div: true,
44
24
  events: events,
45
25
  html: properties.html,
@@ -47,7 +27,6 @@ const ClickableHtml = ({ blockId, classNames, events, properties, methods, style
47
27
  methods: methods,
48
28
  className: classNames?.element,
49
29
  style: styles?.element,
50
- onClick: onClick
30
+ onDataEvent: methods.triggerEvent
51
31
  });
52
- };
53
32
  export default withBlockDefaults(ClickableHtml);
@@ -111,3 +111,70 @@
111
111
  type: SetState
112
112
  params:
113
113
  style_card_action: remove
114
+
115
+ - title: Clickable icons and a popover menu
116
+ blocks:
117
+ - id: row_actions
118
+ type: ClickableHtml
119
+ properties:
120
+ html: '<p>Invoice INV-042 <i data-icon="edit" data-event="onEdit" data-id="42" data-tooltip="Edit" style="cursor: pointer"></i> <span data-popover="more" style="cursor: pointer"><i data-icon="more-vertical"></i></span></p><div data-popover-content="more" hidden><p data-event="onDuplicate" data-id="42" style="cursor: pointer; margin: 0 0 8px"><i data-icon="copy"></i> Duplicate</p><p data-event="onDelete" data-id="42" style="cursor: pointer; margin: 0; color: var(--ant-color-error)"><i data-icon="delete"></i> Delete</p></div>'
121
+ events:
122
+ onEdit:
123
+ - id: set_edit
124
+ type: SetState
125
+ params:
126
+ row_actions_last: { _string.concat: ['edit ', { _event: id }] }
127
+ onDuplicate:
128
+ - id: set_duplicate
129
+ type: SetState
130
+ params:
131
+ row_actions_last: { _string.concat: ['duplicate ', { _event: id }] }
132
+ onDelete:
133
+ - id: set_delete
134
+ type: SetState
135
+ params:
136
+ row_actions_last: { _string.concat: ['delete ', { _event: id }] }
137
+ - id: row_actions_result
138
+ type: Html
139
+ properties:
140
+ html:
141
+ _string.concat:
142
+ - '<p>Last action: <code>'
143
+ - _state:
144
+ key: row_actions_last
145
+ default: 'none yet'
146
+ - '</code></p>'
147
+
148
+ - title: Confirm before an event
149
+ blocks:
150
+ - id: confirm_actions
151
+ type: ClickableHtml
152
+ properties:
153
+ html: '<p>INV-2026-0042 <i data-icon="delete" data-event="onDelete" data-id="42" data-tooltip="Delete" data-confirm="Delete invoice INV-2026-0042?" style="cursor: pointer; color: var(--ant-color-error)"></i> <span data-event="onArchive" data-id="42" data-confirm style="cursor: pointer; color: var(--ant-color-primary)">Archive</span></p>'
154
+ events:
155
+ onDelete:
156
+ - id: deleted
157
+ type: SetState
158
+ params:
159
+ confirm_last:
160
+ _string.concat:
161
+ - 'deleted '
162
+ - _event: id
163
+ onArchive:
164
+ - id: archived
165
+ type: SetState
166
+ params:
167
+ confirm_last:
168
+ _string.concat:
169
+ - 'archived '
170
+ - _event: id
171
+ - id: confirm_result
172
+ type: Html
173
+ properties:
174
+ html:
175
+ _string.concat:
176
+ - '<p>Last action: <code>'
177
+ - _state:
178
+ key: confirm_last
179
+ default: 'none yet'
180
+ - '</code></p>'
@@ -24,6 +24,11 @@
24
24
  id: 'html-style-stripped',
25
25
  message: 'Content is sanitised with DOMPurify, so <style>, <script> and inline event handlers are removed before render. Clicks are wired through data-event attributes instead of inline handlers.',
26
26
  see: 'display-blocks/clickablehtml'
27
+ },
28
+ {
29
+ id: 'clickable-html-confirm-destructive',
30
+ message: 'Add data-confirm="Delete this row?" to a data-event element whose event deletes or changes data, so a stray click cannot fire it. Only OK fires the event.',
31
+ see: 'concepts/html-attributes'
27
32
  }
28
33
  ],
29
34
  // Each clickable element names its event in a data-event attribute, so the
@@ -38,7 +43,7 @@
38
43
  properties: {
39
44
  html: {
40
45
  type: 'string',
41
- description: 'Content to be rendered as Html. An element with a data-event attribute fires the event it names when clicked (data-event="onEditClick" fires events.onEditClick), and its default browser action is prevented. The event object holds the element\'s other data-* attributes with snake_case keys, so data-event="onEditClick" data-record-id="42" gives { record_id: "42" }.',
46
+ description: 'Content to be rendered as Html. An element with a data-event attribute fires the event it names when clicked (data-event="onEditClick" fires events.onEditClick), and its default browser action is prevented. The event object holds the element\'s other data-* attributes with snake_case keys, so data-event="onEditClick" data-record-id="42" gives { record_id: "42" }. Targets that are not links or buttons become keyboard focusable, and Enter or Space clicks them. A data-event inside popover content fires too, then closes the popover. A link with data-event fires the event and does not navigate. data-confirm="Delete this row?" on a data-event element asks first: only OK fires the event (bare data-confirm asks "Are you sure?"). All Html block attributes work too: 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 and data-tone. See the HTML attributes docs page.',
42
47
  docs: {
43
48
  displayType: 'text-area'
44
49
  }
@@ -13,44 +13,34 @@
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 {
20
- sanitize() {
21
- const { html, DOMPurifyOptions } = this.props.properties;
18
+ warnOnOptionsChange() {
19
+ const { DOMPurifyOptions } = this.props.properties;
22
20
  if (!this.warnedOptionsChange && JSON.stringify(DOMPurifyOptions) !== JSON.stringify(this.DOMPurifyOptions)) {
23
21
  this.warnedOptionsChange = true;
24
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.`);
25
23
  }
26
- const htmlString = type.isNone(html) ? '' : html.toString();
27
- this.div.innerHTML = DOMPurify.sanitize(htmlString, this.DOMPurifyOptions);
28
24
  }
29
25
  componentDidMount() {
30
- this.sanitize();
26
+ this.warnOnOptionsChange();
31
27
  }
32
28
  componentDidUpdate() {
33
- this.sanitize();
29
+ this.warnOnOptionsChange();
34
30
  }
35
31
  render() {
36
- const { blockId, classNames, styles } = this.props;
37
- 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,
38
36
  id: blockId,
39
- "data-testid": blockId,
40
- ref: (el)=>{
41
- if (el) {
42
- this.div = el;
43
- }
44
- },
45
37
  className: classNames?.element,
38
+ sanitizeOptions: this.DOMPurifyOptions,
46
39
  style: styles?.element
47
40
  });
48
41
  }
49
42
  constructor(props){
50
43
  super(props);
51
- this.div = {
52
- innerHTML: ''
53
- };
54
44
  // The sanitizer options are fixed at mount on purpose: a later, operator-driven
55
45
  // value could loosen the sanitizer from state. A change is refused loudly
56
46
  // rather than ignored silently.
@@ -37,7 +37,7 @@
37
37
  properties: {
38
38
  html: {
39
39
  type: 'string',
40
- 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.',
41
41
  docs: {
42
42
  displayType: 'text-area'
43
43
  }
@@ -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>'
@@ -24,6 +24,21 @@
24
24
  id: 'html-style-stripped',
25
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
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'
27
42
  }
28
43
  ],
29
44
  events: {
@@ -35,7 +50,7 @@
35
50
  properties: {
36
51
  html: {
37
52
  type: 'string',
38
- 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, warning, or a React Icons name); 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.',
39
54
  docs: {
40
55
  displayType: 'text-area'
41
56
  }
@@ -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);
@@ -12,6 +12,65 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ - title: Semantic icon names
16
+ blocks:
17
+ - id: semantic_add
18
+ type: Icon
19
+ layout:
20
+ flex: 0 0 auto
21
+ properties:
22
+ name: add
23
+ size: 24
24
+ - id: semantic_edit
25
+ type: Icon
26
+ layout:
27
+ flex: 0 0 auto
28
+ properties:
29
+ name: edit
30
+ size: 24
31
+ - id: semantic_delete
32
+ type: Icon
33
+ layout:
34
+ flex: 0 0 auto
35
+ properties:
36
+ name: delete
37
+ size: 24
38
+ - id: semantic_search
39
+ type: Icon
40
+ layout:
41
+ flex: 0 0 auto
42
+ properties:
43
+ name: search
44
+ size: 24
45
+ - id: semantic_settings
46
+ type: Icon
47
+ layout:
48
+ flex: 0 0 auto
49
+ properties:
50
+ name: settings
51
+ size: 24
52
+ - id: semantic_warning
53
+ type: Icon
54
+ layout:
55
+ flex: 0 0 auto
56
+ properties:
57
+ name: warning
58
+ size: 24
59
+ - id: semantic_info
60
+ type: Icon
61
+ layout:
62
+ flex: 0 0 auto
63
+ properties:
64
+ name: info
65
+ size: 24
66
+ - id: semantic_success
67
+ type: Icon
68
+ layout:
69
+ flex: 0 0 auto
70
+ properties:
71
+ name: success
72
+ size: 24
73
+
15
74
  - title: Common Icons
16
75
  blocks:
17
76
  - id: icon_home
@@ -22,7 +22,7 @@
22
22
  element: 'The Icon element.'
23
23
  },
24
24
  events: {
25
- onClick: 'Trigger actions when icon is clicked.'
25
+ onClick: 'Trigger actions when icon is clicked. The icon then takes keyboard focus, and Enter or Space trigger it too.'
26
26
  },
27
27
  properties: {
28
28
  type: 'object',
@@ -38,7 +38,7 @@
38
38
  name: {
39
39
  type: 'string',
40
40
  default: 'AiOutlineCloseCircle',
41
- description: 'Name of icon to be displayed.'
41
+ description: 'Name of the icon: a semantic name like edit, delete or warning (add your own with theme.icons.aliases), or a React Icons name like LuPencil.'
42
42
  },
43
43
  rotate: {
44
44
  type: 'number',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lowdefy/blocks-basic",
3
- "version": "0.0.0-experimental-20260925135632",
3
+ "version": "0.0.0-experimental-20260925210809",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Basic html Lowdefy blocks.",
6
6
  "homepage": "https://lowdefy.com",
@@ -45,8 +45,8 @@
45
45
  "dist/*"
46
46
  ],
47
47
  "dependencies": {
48
- "@lowdefy/block-utils": "0.0.0-experimental-20260925135632",
49
- "@lowdefy/helpers": "0.0.0-experimental-20260925135632",
48
+ "@lowdefy/block-utils": "0.0.0-experimental-20260925210809",
49
+ "@lowdefy/helpers": "0.0.0-experimental-20260925210809",
50
50
  "dompurify": "3.4.13"
51
51
  },
52
52
  "peerDependencies": {
@@ -54,8 +54,8 @@
54
54
  "react-dom": ">=18"
55
55
  },
56
56
  "devDependencies": {
57
- "@lowdefy/block-dev-e2e": "0.0.0-experimental-20260925135632",
58
- "@lowdefy/e2e-utils": "0.0.0-experimental-20260925135632",
57
+ "@lowdefy/block-dev-e2e": "0.0.0-experimental-20260925210809",
58
+ "@lowdefy/e2e-utils": "0.0.0-experimental-20260925210809",
59
59
  "@playwright/test": "1.59.1",
60
60
  "@swc/cli": "0.8.1",
61
61
  "@swc/core": "1.15.32",